From 2be584108e9adad2214dc60dd78acb21640a265e Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 12 Mar 2026 19:29:49 -0700 Subject: [PATCH 1/3] test: add pre-push gate regression coverage --- CHANGELOG.md | 1 + ROADMAP.md | 18 +++++++-------- docs/ROADMAP/COMPLETED.md | 1 + scripts/hooks/pre-push | 2 +- test/unit/scripts/pre-push-hook.test.js | 30 +++++++++++++++++++++++++ 5 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 test/unit/scripts/pre-push-hook.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d89bfd4..14a8b147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Seeded tree-construction determinism fuzzer** — Added property-based coverage for patch and checkpoint tree construction, proving stable tree OIDs across internal content-anchor permutations in `PatchBuilderV2` and shuffled content-property insertion order in `CheckpointService.createV5()`. - **Focused markdownlint gate** — Added `npm run lint:md` backed by `markdownlint-cli` and a repo config that enforces fenced code-block languages (`MD040`) across Markdown files. - **Markdown JS/TS code-sample linter** — Added `npm run lint:md:code`, which scans fenced JavaScript and TypeScript blocks in Markdown and syntax-checks them with the TypeScript parser for file/line-accurate diagnostics. +- **Pre-push hook regression harness** — Added a focused Vitest source-level regression suite for `scripts/hooks/pre-push` that locks the “seven gates in parallel” header, quick-mode skip messaging, and all Gate 1–8 failure labels to the checked-in hook source. ### Changed diff --git a/ROADMAP.md b/ROADMAP.md index c4f5b426..c2d24206 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,7 +1,7 @@ # ROADMAP — @git-stunts/git-warp > **Current version:** v14.0.0 -> **Last reconciled:** 2026-03-12 (main after PR #66 merge; 26 active standalone items remain after trust/serve hardening, type-surface cleanup, large-graph traversal work, test-infra extraction, the constructor-default lint cleanup, checkpoint content-anchor batching, tree-construction determinism fuzzing, CI gate dedupe, the explicit type-only export manifest split, and the merged Markdown code-sample lint gate) +> **Last reconciled:** 2026-03-12 (feature branch after B168 pre-push gate regression coverage; 25 active standalone items remain after trust/serve hardening, type-surface cleanup, large-graph traversal work, test-infra extraction, the constructor-default lint cleanup, checkpoint content-anchor batching, tree-construction determinism fuzzing, CI gate dedupe, the explicit type-only export manifest split, the merged Markdown code-sample lint gate, and the pre-push hook regression harness) > **Completed milestones:** [docs/ROADMAP/COMPLETED.md](docs/ROADMAP/COMPLETED.md) --- @@ -204,7 +204,7 @@ P1 is complete on `v15`: B36 and B37 landed as the shared test-foundation pass, ### P2 — CI & Tooling (one batch PR) -`B83`, `B85`, `B57`, `B86`, and `B87` are now merged on `main`. The repo now runs both markdownlint and the Markdown JS/TS code-sample linter in the CI fast gate and the local `scripts/hooks/pre-push` firewall. Remaining P2 work starts at B88. This merge also promoted one follow-up item, B168, so the local hook's gate labels and quick-mode messaging get their own regression coverage. B123 is still the largest item and may need to split out if the PR gets too big. +`B83`, `B85`, `B57`, `B86`, and `B87` are now merged on `main`, and `B168` is complete in this branch. The repo now runs both markdownlint and the Markdown JS/TS code-sample linter in the CI fast gate and the local `scripts/hooks/pre-push` firewall, with regression coverage locking the hook’s header, gate labels, quick-mode messaging, and Gate 8 failure label directly in source. Remaining P2 work starts at B88. B123 is still the largest item and may need to split out if the PR gets too big. | ID | Item | Depends on | Effort | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | ------ | @@ -219,7 +219,7 @@ P1 is complete on `v15`: B36 and B37 landed as the shared test-foundation pass, | B128 | **DOCS CONSISTENCY PREFLIGHT** — automated pass in `release:preflight` verifying changelog/readme/guide updates for behavior changes in hot paths (materialize, checkpoint, sync). From BACKLOG 2026-02-28. | — | S | | B12 | **DOCS-VERSION-SYNC PRE-COMMIT CHECK** — grep version literals in .md files against `package.json` | — | S | | B43 | **VITEST EXPLICIT RUNTIME EXCLUDES** — prevent accidental local runs of Docker-only suites | — | S | -| B168 | **PRE-PUSH GATE LABEL REGRESSION TEST** — add a lightweight regression test or shared source for `scripts/hooks/pre-push` gate numbering and quick-mode messaging so local hook text cannot drift from the actual gate layout or CI ordering. From PR #66 review follow-up. | — | S | +| B168 | ✅ **PRE-PUSH GATE LABEL REGRESSION TEST** — Added a focused Vitest source-level regression suite for `scripts/hooks/pre-push`, locking the “seven gates in parallel” header, quick-mode skip messaging, and all eight gate failure labels so hook text cannot drift silently. From PR #66 review follow-up. | — | S | ### P3 — Type Safety & Surface @@ -337,9 +337,9 @@ Complete on `v15`: **B80** and **B99**. #### Wave 2: CI & Tooling (P2, one batch PR) -3. **B88, B119, B123, B128, B12, B43, B168** +3. **B88, B119, B123, B128, B12, B43** -Internal chain: **B97 already resolved** → B85 → B57. That chain is complete on `main`, and B87 now ships on top of the existing B86 markdown gate to cover JS/TS sample syntax. B168 captures the remaining hook-message drift follow-up from the B87 review cycle. B123 remains the largest remaining item and may need to split out. +Internal chain: **B97 already resolved** → B85 → B57. That chain is complete on `main`, B87 now ships on top of the existing B86 markdown gate to cover JS/TS sample syntax, and B168 closes the remaining hook-message drift follow-up from the B87 review cycle. B123 remains the largest remaining item and may need to split out. #### Wave 3: Type Surface (P3) @@ -397,11 +397,11 @@ B158 (P7) ──→ B159 (P7) CDC seek cache | **Milestone (M12)** | 18 | B66, B67, B70, B73, B75, B105–B115, B117, B118 | | **Milestone (M13)** | 1 | B116 (internal: DONE; wire-format: DEFERRED) | | **Milestone (M14)** | 16 | B130–B145 | -| **Standalone** | 26 | B12, B28, B34–B35, B43, B53, B54, B76, B79, B88, B96, B98, B102–B104, B119, B123, B127–B129, B147, B152, B155–B156, B168–B169 | -| **Standalone (done)** | 61 | B19, B22, B26, B36–B37, B44, B46, B47, B48–B52, B55, B57, B71, B72, B77, B78, B80–B87, B89–B95, B97, B99–B100, B120–B122, B124, B125, B126, B146, B148–B151, B153, B154, B157–B165, B167 | +| **Standalone** | 25 | B12, B28, B34–B35, B43, B53, B54, B76, B79, B88, B96, B98, B102–B104, B119, B123, B127–B129, B147, B152, B155–B156, B169 | +| **Standalone (done)** | 62 | B19, B22, B26, B36–B37, B44, B46, B47, B48–B52, B55, B57, B71, B72, B77, B78, B80–B87, B89–B95, B97, B99–B100, B120–B122, B124, B125, B126, B146, B148–B151, B153, B154, B157–B165, B167–B168 | | **Deferred** | 7 | B4, B7, B16, B20, B21, B27, B101 | | **Rejected** | 7 | B5, B6, B13, B17, B18, B25, B45 | -| **Total tracked** | **146** total; 61 standalone done | | +| **Total tracked** | **146** total; 62 standalone done | | ### STANK.md Cross-Reference @@ -505,7 +505,7 @@ B158 (P7) ──→ B159 (P7) CDC seek cache Every milestone has a hard gate. No milestone blurs into the next. All milestones are complete: M10 → M12 → M13 (internal) → M11 → M14. M13 wire-format cutover remains deferred by ADR 3 readiness gates. -The active backlog is **26 standalone items** sorted into **8 priority tiers** (P0–P7) with **6 execution waves**. Wave 1 is complete, and Wave 2 now starts at B88 in the CI & Tooling pack, with B168 and B169 added from the PR #66 review loop. See [Execution Order](#execution-order) for the full sequence. +The active backlog is **25 standalone items** sorted into **8 priority tiers** (P0–P7) with **6 execution waves**. Wave 1 is complete, and Wave 2 now starts at B88 in the CI & Tooling pack, with B169 remaining from the PR #66 review loop. See [Execution Order](#execution-order) for the full sequence. Rejected items live in `GRAVEYARD.md`. Resurrections require an RFC. `BACKLOG.md` retired — all intake goes directly into this file (policy in `CLAUDE.md`). diff --git a/docs/ROADMAP/COMPLETED.md b/docs/ROADMAP/COMPLETED.md index 11329dd8..d92d4ce1 100644 --- a/docs/ROADMAP/COMPLETED.md +++ b/docs/ROADMAP/COMPLETED.md @@ -397,6 +397,7 @@ Investigation revealed the correct approach is a two-phase split: | B85 | ~~**TYPE-ONLY EXPORT MANIFEST SECTION**~~ — **DONE (`v15`).** Added explicit `typeExports` to `type-surface.m8.json` and taught `check-dts-surface` to fail on misplaced or duplicate entries across `exports` and `typeExports`. | | B86 | ~~**MARKDOWNLINT CI GATE**~~ — **DONE (`v15`).** Added `npm run lint:md` with focused `MD040` enforcement and wired it into CI, then labeled the repo’s bare fenced code blocks so Markdown docs/examples pass the new gate immediately. | | B87 | ~~**CODE SAMPLE LINTER**~~ — **DONE.** Added `scripts/lint-markdown-code-samples.js` and `npm run lint:md:code`, which extract fenced JavaScript/TypeScript samples from Markdown and syntax-check them with line-accurate diagnostics. Wired into the CI fast gate and local pre-push firewall alongside markdownlint, with failures for malformed mixed-marker fences and unterminated JS/TS blocks. | +| B168 | ~~**PRE-PUSH GATE LABEL REGRESSION TEST**~~ — **DONE.** Added a focused Vitest source-level regression suite for `scripts/hooks/pre-push`, locking the “seven gates in parallel” header, quick-mode skip messaging, and every Gate 1–8 failure label so hook text cannot drift silently. | | B95 | ~~**NAMESPACE EXPORT SUPPORT**~~ — **DONE (`v15`).** The declaration surface validator now handles namespace exports in `.d.ts` parsing. | | B97 | ~~**AUDIT MANIFEST vs `index.js` DRIFT**~~ — **DONE (`v15`).** Runtime exports were reconciled with the public surface, and the surface checker now distinguishes runtime-backed vs type-only manifest entries. | | B99 | ~~**DETERMINISM FUZZER FOR TREE CONSTRUCTION**~~ — **DONE (`v15`).** Added seeded property tests that prove stable tree OIDs when `PatchBuilderV2` content anchor order is permuted internally and when `CheckpointService.createV5()` sees the same content properties in different insertion orders. | diff --git a/scripts/hooks/pre-push b/scripts/hooks/pre-push index a15cf115..e91b3e7f 100755 --- a/scripts/hooks/pre-push +++ b/scripts/hooks/pre-push @@ -66,7 +66,7 @@ if [ "$QUICK" = "1" ]; then echo "[Gate 8] Skipped (WARP_QUICK_PUSH quick mode)" else echo "[Gate 8] Running unit tests..." - npm run test:local + npm run test:local || { echo ""; echo "BLOCKED — Gate 8 FAILED: Unit tests"; exit 1; } fi echo "══════════════════════════════════════════════════════════" diff --git a/test/unit/scripts/pre-push-hook.test.js b/test/unit/scripts/pre-push-hook.test.js new file mode 100644 index 00000000..9b6a478e --- /dev/null +++ b/test/unit/scripts/pre-push-hook.test.js @@ -0,0 +1,30 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const hookPath = fileURLToPath(new URL('../../../scripts/hooks/pre-push', import.meta.url)); +const hookSource = readFileSync(hookPath, 'utf8'); + +describe('scripts/hooks/pre-push', () => { + it('documents seven parallel gates before unit tests', () => { + expect(hookSource).toContain('# Seven gates in parallel, then unit tests. ALL must pass or push is blocked.'); + expect(hookSource).toContain('echo "[Gates 1-7] Running lint + typecheck + policy + consumer type test + surface validator + markdown gates..."'); + expect(hookSource).toContain('echo "[Gate 8] Running unit tests..."'); + }); + + it('keeps quick mode tied to Gate 8', () => { + expect(hookSource).toContain('echo "WARP_QUICK_PUSH: quick mode active — Gate 8 (unit tests) will be skipped"'); + expect(hookSource).toContain('echo "[Gate 8] Skipped (WARP_QUICK_PUSH quick mode)"'); + }); + + it('keeps explicit failure labels aligned for every gate', () => { + expect(hookSource).toContain('BLOCKED — Gate 1 FAILED: TypeScript compiler (strict mode)'); + expect(hookSource).toContain('BLOCKED — Gate 2 FAILED: IRONCLAD policy (any/wildcard/ts-ignore ban)'); + expect(hookSource).toContain('BLOCKED — Gate 3 FAILED: Consumer type surface test'); + expect(hookSource).toContain('BLOCKED — Gate 4 FAILED: ESLint (includes no-explicit-any, no-unsafe-*)'); + expect(hookSource).toContain('BLOCKED — Gate 5 FAILED: Declaration surface validator'); + expect(hookSource).toContain('BLOCKED — Gate 6 FAILED: Markdown lint'); + expect(hookSource).toContain('BLOCKED — Gate 7 FAILED: Markdown JS/TS code-sample syntax check'); + expect(hookSource).toContain('BLOCKED — Gate 8 FAILED: Unit tests'); + }); +}); From 11cfee52b20e1d6e963edf9944c7dda79afcb042 Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 12 Mar 2026 19:57:16 -0700 Subject: [PATCH 2/3] test: exercise pre-push hook behavior --- CHANGELOG.md | 2 +- ROADMAP.md | 18 +-- docs/ROADMAP/COMPLETED.md | 1 - test/unit/scripts/pre-push-hook.test.js | 199 +++++++++++++++++++++--- 4 files changed, 190 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a8b147..466d1c2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Seeded tree-construction determinism fuzzer** — Added property-based coverage for patch and checkpoint tree construction, proving stable tree OIDs across internal content-anchor permutations in `PatchBuilderV2` and shuffled content-property insertion order in `CheckpointService.createV5()`. - **Focused markdownlint gate** — Added `npm run lint:md` backed by `markdownlint-cli` and a repo config that enforces fenced code-block languages (`MD040`) across Markdown files. - **Markdown JS/TS code-sample linter** — Added `npm run lint:md:code`, which scans fenced JavaScript and TypeScript blocks in Markdown and syntax-checks them with the TypeScript parser for file/line-accurate diagnostics. -- **Pre-push hook regression harness** — Added a focused Vitest source-level regression suite for `scripts/hooks/pre-push` that locks the “seven gates in parallel” header, quick-mode skip messaging, and all Gate 1–8 failure labels to the checked-in hook source. +- **Pre-push hook regression harness** — Added a focused Vitest behavioral harness for `scripts/hooks/pre-push` that exercises the real shell hook with stubbed commands, proves quick mode skips Gate 8, and verifies Gate 1–8 failure labels at runtime. ### Changed diff --git a/ROADMAP.md b/ROADMAP.md index c2d24206..d8ff691e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,7 +1,7 @@ # ROADMAP — @git-stunts/git-warp > **Current version:** v14.0.0 -> **Last reconciled:** 2026-03-12 (feature branch after B168 pre-push gate regression coverage; 25 active standalone items remain after trust/serve hardening, type-surface cleanup, large-graph traversal work, test-infra extraction, the constructor-default lint cleanup, checkpoint content-anchor batching, tree-construction determinism fuzzing, CI gate dedupe, the explicit type-only export manifest split, the merged Markdown code-sample lint gate, and the pre-push hook regression harness) +> **Last reconciled:** 2026-03-12 (main after PR #66 merge; 26 active standalone items remain after trust/serve hardening, type-surface cleanup, large-graph traversal work, test-infra extraction, the constructor-default lint cleanup, checkpoint content-anchor batching, tree-construction determinism fuzzing, CI gate dedupe, the explicit type-only export manifest split, and the merged Markdown code-sample lint gate) > **Completed milestones:** [docs/ROADMAP/COMPLETED.md](docs/ROADMAP/COMPLETED.md) --- @@ -204,7 +204,7 @@ P1 is complete on `v15`: B36 and B37 landed as the shared test-foundation pass, ### P2 — CI & Tooling (one batch PR) -`B83`, `B85`, `B57`, `B86`, and `B87` are now merged on `main`, and `B168` is complete in this branch. The repo now runs both markdownlint and the Markdown JS/TS code-sample linter in the CI fast gate and the local `scripts/hooks/pre-push` firewall, with regression coverage locking the hook’s header, gate labels, quick-mode messaging, and Gate 8 failure label directly in source. Remaining P2 work starts at B88. B123 is still the largest item and may need to split out if the PR gets too big. +`B83`, `B85`, `B57`, `B86`, and `B87` are now merged on `main`. The repo now runs both markdownlint and the Markdown JS/TS code-sample linter in the CI fast gate and the local `scripts/hooks/pre-push` firewall. Remaining P2 work starts at B88. That merge also promoted one follow-up item, B168, so the local hook's gate labels and quick-mode messaging now have their own regression-coverage task. B123 is still the largest item and may need to split out if the PR gets too big. | ID | Item | Depends on | Effort | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | ------ | @@ -219,7 +219,7 @@ P1 is complete on `v15`: B36 and B37 landed as the shared test-foundation pass, | B128 | **DOCS CONSISTENCY PREFLIGHT** — automated pass in `release:preflight` verifying changelog/readme/guide updates for behavior changes in hot paths (materialize, checkpoint, sync). From BACKLOG 2026-02-28. | — | S | | B12 | **DOCS-VERSION-SYNC PRE-COMMIT CHECK** — grep version literals in .md files against `package.json` | — | S | | B43 | **VITEST EXPLICIT RUNTIME EXCLUDES** — prevent accidental local runs of Docker-only suites | — | S | -| B168 | ✅ **PRE-PUSH GATE LABEL REGRESSION TEST** — Added a focused Vitest source-level regression suite for `scripts/hooks/pre-push`, locking the “seven gates in parallel” header, quick-mode skip messaging, and all eight gate failure labels so hook text cannot drift silently. From PR #66 review follow-up. | — | S | +| B168 | **PRE-PUSH GATE LABEL REGRESSION TEST** — add a lightweight regression test or shared source for `scripts/hooks/pre-push` gate numbering and quick-mode messaging so local hook text cannot drift from the actual gate layout or CI ordering. From PR #66 review follow-up. | — | S | ### P3 — Type Safety & Surface @@ -337,9 +337,9 @@ Complete on `v15`: **B80** and **B99**. #### Wave 2: CI & Tooling (P2, one batch PR) -3. **B88, B119, B123, B128, B12, B43** +3. **B88, B119, B123, B128, B12, B43, B168** -Internal chain: **B97 already resolved** → B85 → B57. That chain is complete on `main`, B87 now ships on top of the existing B86 markdown gate to cover JS/TS sample syntax, and B168 closes the remaining hook-message drift follow-up from the B87 review cycle. B123 remains the largest remaining item and may need to split out. +Internal chain: **B97 already resolved** → B85 → B57. That chain is complete on `main`, and B168 remains the hook-message drift follow-up from the B87 review cycle. B123 remains the largest remaining item and may need to split out. #### Wave 3: Type Surface (P3) @@ -397,11 +397,11 @@ B158 (P7) ──→ B159 (P7) CDC seek cache | **Milestone (M12)** | 18 | B66, B67, B70, B73, B75, B105–B115, B117, B118 | | **Milestone (M13)** | 1 | B116 (internal: DONE; wire-format: DEFERRED) | | **Milestone (M14)** | 16 | B130–B145 | -| **Standalone** | 25 | B12, B28, B34–B35, B43, B53, B54, B76, B79, B88, B96, B98, B102–B104, B119, B123, B127–B129, B147, B152, B155–B156, B169 | -| **Standalone (done)** | 62 | B19, B22, B26, B36–B37, B44, B46, B47, B48–B52, B55, B57, B71, B72, B77, B78, B80–B87, B89–B95, B97, B99–B100, B120–B122, B124, B125, B126, B146, B148–B151, B153, B154, B157–B165, B167–B168 | +| **Standalone** | 26 | B12, B28, B34–B35, B43, B53, B54, B76, B79, B88, B96, B98, B102–B104, B119, B123, B127–B129, B147, B152, B155–B156, B168–B169 | +| **Standalone (done)** | 61 | B19, B22, B26, B36–B37, B44, B46, B47, B48–B52, B55, B57, B71, B72, B77, B78, B80–B87, B89–B95, B97, B99–B100, B120–B122, B124, B125, B126, B146, B148–B151, B153, B154, B157–B165, B167 | | **Deferred** | 7 | B4, B7, B16, B20, B21, B27, B101 | | **Rejected** | 7 | B5, B6, B13, B17, B18, B25, B45 | -| **Total tracked** | **146** total; 62 standalone done | | +| **Total tracked** | **146** total; 61 standalone done | | ### STANK.md Cross-Reference @@ -505,7 +505,7 @@ B158 (P7) ──→ B159 (P7) CDC seek cache Every milestone has a hard gate. No milestone blurs into the next. All milestones are complete: M10 → M12 → M13 (internal) → M11 → M14. M13 wire-format cutover remains deferred by ADR 3 readiness gates. -The active backlog is **25 standalone items** sorted into **8 priority tiers** (P0–P7) with **6 execution waves**. Wave 1 is complete, and Wave 2 now starts at B88 in the CI & Tooling pack, with B169 remaining from the PR #66 review loop. See [Execution Order](#execution-order) for the full sequence. +The active backlog is **26 standalone items** sorted into **8 priority tiers** (P0–P7) with **6 execution waves**. Wave 1 is complete, and Wave 2 now starts at B88 in the CI & Tooling pack, with B168 still active as the remaining hook-message drift follow-up. See [Execution Order](#execution-order) for the full sequence. Rejected items live in `GRAVEYARD.md`. Resurrections require an RFC. `BACKLOG.md` retired — all intake goes directly into this file (policy in `CLAUDE.md`). diff --git a/docs/ROADMAP/COMPLETED.md b/docs/ROADMAP/COMPLETED.md index d92d4ce1..11329dd8 100644 --- a/docs/ROADMAP/COMPLETED.md +++ b/docs/ROADMAP/COMPLETED.md @@ -397,7 +397,6 @@ Investigation revealed the correct approach is a two-phase split: | B85 | ~~**TYPE-ONLY EXPORT MANIFEST SECTION**~~ — **DONE (`v15`).** Added explicit `typeExports` to `type-surface.m8.json` and taught `check-dts-surface` to fail on misplaced or duplicate entries across `exports` and `typeExports`. | | B86 | ~~**MARKDOWNLINT CI GATE**~~ — **DONE (`v15`).** Added `npm run lint:md` with focused `MD040` enforcement and wired it into CI, then labeled the repo’s bare fenced code blocks so Markdown docs/examples pass the new gate immediately. | | B87 | ~~**CODE SAMPLE LINTER**~~ — **DONE.** Added `scripts/lint-markdown-code-samples.js` and `npm run lint:md:code`, which extract fenced JavaScript/TypeScript samples from Markdown and syntax-check them with line-accurate diagnostics. Wired into the CI fast gate and local pre-push firewall alongside markdownlint, with failures for malformed mixed-marker fences and unterminated JS/TS blocks. | -| B168 | ~~**PRE-PUSH GATE LABEL REGRESSION TEST**~~ — **DONE.** Added a focused Vitest source-level regression suite for `scripts/hooks/pre-push`, locking the “seven gates in parallel” header, quick-mode skip messaging, and every Gate 1–8 failure label so hook text cannot drift silently. | | B95 | ~~**NAMESPACE EXPORT SUPPORT**~~ — **DONE (`v15`).** The declaration surface validator now handles namespace exports in `.d.ts` parsing. | | B97 | ~~**AUDIT MANIFEST vs `index.js` DRIFT**~~ — **DONE (`v15`).** Runtime exports were reconciled with the public surface, and the surface checker now distinguishes runtime-backed vs type-only manifest entries. | | B99 | ~~**DETERMINISM FUZZER FOR TREE CONSTRUCTION**~~ — **DONE (`v15`).** Added seeded property tests that prove stable tree OIDs when `PatchBuilderV2` content anchor order is permuted internally and when `CheckpointService.createV5()` sees the same content properties in different insertion orders. | diff --git a/test/unit/scripts/pre-push-hook.test.js b/test/unit/scripts/pre-push-hook.test.js index 9b6a478e..04e6ef2b 100644 --- a/test/unit/scripts/pre-push-hook.test.js +++ b/test/unit/scripts/pre-push-hook.test.js @@ -1,30 +1,191 @@ -import { readFileSync } from 'node:fs'; +import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it } from 'vitest'; +const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)); const hookPath = fileURLToPath(new URL('../../../scripts/hooks/pre-push', import.meta.url)); -const hookSource = readFileSync(hookPath, 'utf8'); + +/** @type {string[]} */ +const tempDirs = []; + +afterEach(() => { + while (tempDirs.length > 0) { + rmSync(/** @type {string} */ (tempDirs.pop()), { force: true, recursive: true }); + } +}); + +/** + * @returns {string} + */ +function createTempDir() { + const dir = mkdtempSync(join(tmpdir(), 'git-warp-pre-push-')); + tempDirs.push(dir); + return dir; +} + +/** + * @param {string} filePath + * @param {string} source + */ +function writeExecutable(filePath, source) { + writeFileSync(filePath, source, 'utf8'); + chmodSync(filePath, 0o755); +} + +/** + * @param {string} filePath + * @returns {string[]} + */ +function readLog(filePath) { + try { + return readFileSync(filePath, 'utf8') + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + } catch { + return []; + } +} + +/** + * @param {{ quick?: boolean, failCommand?: string|null }} [options] + */ +function runPrePushHook(options = {}) { + const { quick = false, failCommand = null } = options; + const binDir = createTempDir(); + const npmLog = join(binDir, 'npm.log'); + const lycheeLog = join(binDir, 'lychee.log'); + + writeExecutable( + join(binDir, 'npm'), + [ + '#!/bin/sh', + 'set -eu', + 'cmd="$1"', + 'if [ "$cmd" = "run" ]; then', + ' cmd="$2"', + 'fi', + "printf '%s\\n' \"$cmd\" >> \"$WARP_NPM_LOG\"", + 'if [ "${WARP_FAIL_NPM_CMD:-}" = "$cmd" ]; then', + ' echo "stub npm failing for $cmd" >&2', + ' exit 1', + 'fi', + 'exit 0', + '', + ].join('\n') + ); + + writeExecutable( + join(binDir, 'lychee'), + [ + '#!/bin/sh', + 'set -eu', + "printf '%s\\n' \"$*\" >> \"$WARP_LYCHEE_LOG\"", + 'exit 0', + '', + ].join('\n') + ); + + const env = { + ...process.env, + PATH: `${binDir}:${process.env.PATH}`, + WARP_NPM_LOG: npmLog, + WARP_LYCHEE_LOG: lycheeLog, + }; + + if (quick) { + env.WARP_QUICK_PUSH = '1'; + } + + if (failCommand) { + env.WARP_FAIL_NPM_CMD = failCommand; + } + + const result = spawnSync('sh', [hookPath], { + cwd: repoRoot, + env, + encoding: 'utf8', + timeout: 3000, + }); + + if (result.error) { + throw result.error; + } + + return { + status: result.status, + output: `${result.stdout}${result.stderr}`, + commands: readLog(npmLog), + lycheeCalls: readLog(lycheeLog), + }; +} describe('scripts/hooks/pre-push', () => { - it('documents seven parallel gates before unit tests', () => { - expect(hookSource).toContain('# Seven gates in parallel, then unit tests. ALL must pass or push is blocked.'); - expect(hookSource).toContain('echo "[Gates 1-7] Running lint + typecheck + policy + consumer type test + surface validator + markdown gates..."'); - expect(hookSource).toContain('echo "[Gate 8] Running unit tests..."'); + it('keeps the checked-in header aligned with the runtime gate layout', () => { + const source = readFileSync(hookPath, 'utf8'); + + expect(source).toContain('# Seven gates in parallel, then unit tests. ALL must pass or push is blocked.'); }); - it('keeps quick mode tied to Gate 8', () => { - expect(hookSource).toContain('echo "WARP_QUICK_PUSH: quick mode active — Gate 8 (unit tests) will be skipped"'); - expect(hookSource).toContain('echo "[Gate 8] Skipped (WARP_QUICK_PUSH quick mode)"'); + it('skips Gate 8 in quick mode without running unit tests', () => { + const result = runPrePushHook({ quick: true }); + + expect(result.status).toBe(0); + expect(result.output).toContain('WARP_QUICK_PUSH: quick mode active — Gate 8 (unit tests) will be skipped'); + expect(result.output).toContain('[Gates 1-7] Running lint + typecheck + policy + consumer type test + surface validator + markdown gates...'); + expect(result.output).toContain('[Gate 8] Skipped (WARP_QUICK_PUSH quick mode)'); + expect([...result.commands].sort()).toEqual([ + 'lint', + 'lint:md', + 'lint:md:code', + 'typecheck', + 'typecheck:consumer', + 'typecheck:policy', + 'typecheck:surface', + ]); + expect(result.lycheeCalls).toEqual(['--config .lychee.toml **/*.md']); }); - it('keeps explicit failure labels aligned for every gate', () => { - expect(hookSource).toContain('BLOCKED — Gate 1 FAILED: TypeScript compiler (strict mode)'); - expect(hookSource).toContain('BLOCKED — Gate 2 FAILED: IRONCLAD policy (any/wildcard/ts-ignore ban)'); - expect(hookSource).toContain('BLOCKED — Gate 3 FAILED: Consumer type surface test'); - expect(hookSource).toContain('BLOCKED — Gate 4 FAILED: ESLint (includes no-explicit-any, no-unsafe-*)'); - expect(hookSource).toContain('BLOCKED — Gate 5 FAILED: Declaration surface validator'); - expect(hookSource).toContain('BLOCKED — Gate 6 FAILED: Markdown lint'); - expect(hookSource).toContain('BLOCKED — Gate 7 FAILED: Markdown JS/TS code-sample syntax check'); - expect(hookSource).toContain('BLOCKED — Gate 8 FAILED: Unit tests'); + it('runs Gate 8 in normal mode', () => { + const result = runPrePushHook(); + + expect(result.status).toBe(0); + expect(result.output).toContain('[Gate 8] Running unit tests...'); + expect([...result.commands].sort()).toEqual([ + 'lint', + 'lint:md', + 'lint:md:code', + 'test:local', + 'typecheck', + 'typecheck:consumer', + 'typecheck:policy', + 'typecheck:surface', + ]); }); + + const failureCases = [ + ['typecheck', 'BLOCKED — Gate 1 FAILED: TypeScript compiler (strict mode)'], + ['typecheck:policy', 'BLOCKED — Gate 2 FAILED: IRONCLAD policy (any/wildcard/ts-ignore ban)'], + ['typecheck:consumer', 'BLOCKED — Gate 3 FAILED: Consumer type surface test'], + ['lint', 'BLOCKED — Gate 4 FAILED: ESLint (includes no-explicit-any, no-unsafe-*)'], + ['typecheck:surface', 'BLOCKED — Gate 5 FAILED: Declaration surface validator'], + ['lint:md', 'BLOCKED — Gate 6 FAILED: Markdown lint'], + ['lint:md:code', 'BLOCKED — Gate 7 FAILED: Markdown JS/TS code-sample syntax check'], + ['test:local', 'BLOCKED — Gate 8 FAILED: Unit tests'], + ]; + + for (const [failCommand, expectedMessage] of failureCases) { + it(`reports ${expectedMessage}`, () => { + const result = runPrePushHook({ + quick: failCommand !== 'test:local', + failCommand, + }); + + expect(result.status).toBe(1); + expect(result.output).toContain(expectedMessage); + }); + } }); From 9d417f15d7b3c9bff46510881e13d859acf10500 Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 12 Mar 2026 19:58:15 -0700 Subject: [PATCH 3/3] test: fix pre-push hook env typing --- test/unit/scripts/pre-push-hook.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/scripts/pre-push-hook.test.js b/test/unit/scripts/pre-push-hook.test.js index 04e6ef2b..2adc0dba 100644 --- a/test/unit/scripts/pre-push-hook.test.js +++ b/test/unit/scripts/pre-push-hook.test.js @@ -89,6 +89,7 @@ function runPrePushHook(options = {}) { ].join('\n') ); + /** @type {Record} */ const env = { ...process.env, PATH: `${binDir}:${process.env.PATH}`,