From 3f962259ed2cc5f35d90184a107f6d1e9661497f Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:49:14 +0200 Subject: [PATCH 1/9] feat(signing): add opt-in isolated-worktree exact-tree typecheck verification S3b Part 2b (closes the plan's remaining gap: whether ci-prepush-lowend.mjs's local checks running against whatever's on disk actually correspond to the exact committed tree about to be pushed). New scripts/verify-exact-tree.mjs, `pnpm run verify:exact-tree [ref...]` (default HEAD): creates an isolated `git worktree add --detach` at the exact commit, symlinks in the real node_modules, and runs the same single-checker `tsgo --noEmit` there. Deliberately opt-in, not wired into the default ci:prepush path -- an isolated worktree plus a real tsgo compile roughly doubles local typecheck cost, unacceptable as an always-on default on this repo's constrained development hardware. Mirrors the existing --full opt-in-escalation pattern rather than the automatic pipeline. Design departures from the original plan's Part 2, reasoned through fresh against current main (recorded in the plan doc): - node_modules via symlink, not a real reinstall per run -- unbounded local cost is exactly what this program has repeatedly avoided elsewhere. - The symlink is only sound when Part 2a's dependencyState === 'MATCHES' for that update -- a precondition for a trustworthy answer, not a skip gate (the check always runs; it honestly reports UNKNOWN rather than a silently wrong PASS/FAIL when the precondition isn't met). - New, distinct state vocabulary (PASS | FAIL | NOT_APPLICABLE | UNKNOWN) rather than reusing MATCHES | DIVERGED -- this is an absolute correctness check, not a comparison to a baseline. - No new evidence-transport mechanism: localSha is already present on every PushEvidenceUpdate since #494, and tsgo takes no file list, so the frozen branch's WORLD_SCRIPT_PREPUSH_EXACT_FILES scoping idea turns out to be unnecessary entirely once retired rather than reconstructed. - Multi-ref handling revised from blocking-fail-fast to non-blocking aggregate-all, consistent with this being an opt-in diagnostic. Fail-closed worktree lifecycle reusing #500's runBounded/ runLocalBinaryDetailed exclusively (no raw spawnSync): proactive `git worktree prune` before creating one, try/finally cleanup, `git worktree remove --force` with a `rm -rf` + prune fallback. Isolated trees live under os.tmpdir(), never inside the project's own .worktrees/. Tested with real git worktree + real tsgo runs against a tiny fixture project (proving the actual mechanism, not just injected fakes) plus DI-based fail-closed-lifecycle and precondition/aggregation coverage. --- CLAUDE.md | 2 + package.json | 1 + scripts/verify-exact-tree.d.mts | 38 ++++ scripts/verify-exact-tree.mjs | 156 ++++++++++++++ tests/unit/tooling/verify-exact-tree.test.mjs | 191 ++++++++++++++++++ 5 files changed, 388 insertions(+) create mode 100644 scripts/verify-exact-tree.d.mts create mode 100644 scripts/verify-exact-tree.mjs create mode 100644 tests/unit/tooling/verify-exact-tree.test.mjs diff --git a/CLAUDE.md b/CLAUDE.md index aa420ca0a..4ec3dd739 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,8 @@ pnpm run token:audit # audit-tokens.mjs — design-token usage gate (CI b **Quality gate (local pre-push subset):** `pnpm run ci:prepush` runs dependency-state/docs/CSP/native-readiness checks unconditionally (never full-repository lint — see the pre-push gate note above for what runs lint locally), and the single-checker local typecheck and i18n/content-guard checks only for changes the classifier marks as potentially impacting them (fail-closed to "run everything conditional" when evidence is incomplete); CI additionally runs full-repository lint, the 4-checker typecheck, full-suite coverage, and heavy jobs regardless of what the local gate ran. Locally use only the targeted form `pnpm exec vitest run --coverage` when debugging coverage. Full pipeline graph: [`docs/CI.md`](docs/CI.md). Coverage thresholds: lines 74, branches 60, functions 67, statements 72 (see `vitest.config.ts`). +**Exact-tree typecheck verification (opt-in, not part of `ci:prepush`):** `pnpm run verify:exact-tree [ref...]` (default `HEAD`) creates an isolated `git worktree` at the exact commit, symlinks in the real `node_modules` (skipped — reports `UNKNOWN` — when `scripts/dependency-state.mjs`'s reconciled fingerprint doesn't match that commit's manifests, since the symlink would then misrepresent the commit's real dependency graph), and runs the same single-checker `tsgo --noEmit` there. This closes the gap where `ci-prepush-lowend.mjs`'s normal typecheck runs against whatever is currently on disk, not necessarily the exact tree about to be pushed. Deliberately **not** wired into the default `ci:prepush` path — it roughly doubles local typecheck cost, unacceptable as an always-on default on this hardware; run it manually before a risky push or when investigating a CI/local typecheck mismatch. Diagnostic-only: reports `PASS | FAIL | NOT_APPLICABLE | UNKNOWN` and never blocks a push on its own; required CI remains the sole merge-safety authority regardless of its result. + **CI pipeline order:** `security` → `quality` (Biome + tsgo + Vitest matrix) → `build` / `e2e` / `storybook` (parallel) → `lighthouse` (after build) → `deploy` on `main`. `ci-success` is a required-status aggregator (`needs: [security, quality, build]`) so branch protection can require one context instead of three/four individual ones — see `docs/CI.md`. Two additional jobs run in parallel with `quality`, both path-scoped via the `changes` job (legitimately `skipping` on PRs that don't touch their directory, which `ci-success` treats as a pass for that job only): `rust-tauri` (`src-tauri/**` — fmt/check/clippy/test, needs the GTK/WebKit apt-get steps) and `core-rust` (`crates/**` — same fmt/check/clippy/test for the renderer-neutral Rust Core, no GUI deps so no apt-get steps needed). **CI-cloud-first workflow (constrained local hardware only):** On low-end hardware, run only `ci:prepush` locally before pushing. Coverage, E2E, Lighthouse, and Stryker are CI-gate jobs. After each push, update README.md badges and AUDIT.md quality-gate line with CI-reported numbers. Local CI simulation: `act pull_request --job quality` (Docker + `act`; see `infra/low-end-ci/DAILY-DRIVER.md`). diff --git a/package.json b/package.json index ddbc47b7b..8cce1c2f6 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "deps:verify": "node scripts/dependency-state.mjs verify", "deps:reconcile": "node scripts/dependency-state.mjs reconcile", "ci:prepush": "node scripts/ci-prepush-lowend.mjs", + "verify:exact-tree": "node scripts/verify-exact-tree.mjs", "ci:local:full": "pnpm run lint && pnpm run typecheck && pnpm run i18n:check && pnpm run guardrail:desktop-imports && pnpm run native-readiness:check", "predev": "node scripts/sync-csp.mjs && node scripts/sync-sw-version.mjs && node scripts/sync-tauri-version.mjs && node scripts/sync-readme-metrics.mjs && node scripts/build-i18n.mjs && node scripts/copy-duckdb-assets.mjs", "dev": "vite", diff --git a/scripts/verify-exact-tree.d.mts b/scripts/verify-exact-tree.d.mts new file mode 100644 index 000000000..4f7816051 --- /dev/null +++ b/scripts/verify-exact-tree.d.mts @@ -0,0 +1,38 @@ +import type { GitOptions, GitResult } from './signing/signing-core.d.mts'; + +// QNBS-v3: absolute correctness check, not a comparison -- distinct vocabulary from WorkingTreeState. +export type ExactTreeState = 'PASS' | 'FAIL' | 'NOT_APPLICABLE' | 'UNKNOWN'; + +export interface VerifyExactTreeDependencies { + runBounded?: ( + command: string, + args: string[], + options?: GitOptions & { timeoutMs?: number }, + ) => Promise; + runLocalBinaryDetailed?: ( + binary: string, + args: string[], + options?: { root?: string; cwd?: string; timeoutMs?: number }, + ) => Promise; + mkdtempFn?: () => Promise; + rmFn?: (path: string) => Promise; + symlinkFn?: (target: string, linkPath: string) => void; + dependencyStateForRef?: (sha: string) => string; + nodeModulesSource?: string; + tsgoArgs?: string[]; + runGitSync?: (args: string[]) => { status: number | null; stdout: string }; +} + +export function verifyExactTreeTypecheck( + sha: string, + repoRoot?: string, + dependencies?: VerifyExactTreeDependencies, +): Promise; + +export function verifyExactTreeForShas( + shas: string[], + repoRoot?: string, + dependencies?: VerifyExactTreeDependencies, +): Promise; + +export function main(argv?: string[]): Promise; diff --git a/scripts/verify-exact-tree.mjs b/scripts/verify-exact-tree.mjs new file mode 100644 index 000000000..fa8309f77 --- /dev/null +++ b/scripts/verify-exact-tree.mjs @@ -0,0 +1,156 @@ +import { spawnSync } from 'node:child_process'; +import { symlinkSync } from 'node:fs'; +import { mkdtemp as mkdtempAsync, rm as rmAsync } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import process from 'node:process'; +import { isMainModule } from './ci-prepush-range-resolver.mjs'; +import { computeDependencyState } from './dependency-state.mjs'; +import { runBounded, runLocalBinaryDetailed } from './hooks/shared.mjs'; + +const DEFAULT_TSGO_ARGS = ['--project', 'tsconfig.tsgo.json', '--noEmit', '--checkers', '1']; + +// QNBS-v3: sweeps entries orphaned by a prior crashed/killed run before creating a new one. +async function pruneStaleWorktrees(repoRoot, dependencies) { + const runGit = dependencies.runBounded ?? runBounded; + await runGit('git', ['worktree', 'prune'], { cwd: repoRoot }); +} + +async function createIsolatedWorktree(sha, repoRoot, dependencies) { + const runGit = dependencies.runBounded ?? runBounded; + const makeTempDir = + dependencies.mkdtempFn ?? (() => mkdtempAsync(join(tmpdir(), 'worldscript-exact-tree-'))); + await pruneStaleWorktrees(repoRoot, dependencies); + let path; + try { + path = await makeTempDir(); + } catch { + return { ok: false, path: undefined }; + } + const result = await runGit('git', ['worktree', 'add', '--detach', path, sha], { cwd: repoRoot }); + if (result.error || result.timedOut || result.interrupted || result.status !== 0) { + return { ok: false, path }; + } + return { ok: true, path }; +} + +// QNBS-v3: fail-closed -- git's own removal failing falls back to a raw sweep plus a metadata prune. +async function removeIsolatedWorktree(worktreePath, repoRoot, dependencies) { + if (!worktreePath) return; + const runGit = dependencies.runBounded ?? runBounded; + const removeDir = dependencies.rmFn ?? ((path) => rmAsync(path, { recursive: true, force: true })); + const result = await runGit('git', ['worktree', 'remove', '--force', worktreePath], { + cwd: repoRoot, + }); + if (result.error || result.timedOut || result.interrupted || result.status !== 0) { + try { + await removeDir(worktreePath); + } catch { + // Best-effort: nothing more can be done from here; the directory is under os.tmpdir(). + } + await runGit('git', ['worktree', 'prune'], { cwd: repoRoot }); + } +} + +// QNBS-v3: real node_modules install per verification is exactly the unbounded cost this avoids. +function linkNodeModules(worktreePath, source, dependencies) { + const symlinkFn = + dependencies.symlinkFn ?? + ((target, linkPath) => symlinkSync(target, linkPath, process.platform === 'win32' ? 'junction' : 'dir')); + try { + symlinkFn(source, join(worktreePath, 'node_modules')); + return true; + } catch { + return false; + } +} + +// QNBS-v3: precedence shape mirrors aggregateDiagnosticState, kept separate -- different vocabulary. +function aggregateExactTreeState(states) { + const relevant = states.filter((state) => state !== 'NOT_APPLICABLE'); + if (relevant.length === 0) return 'NOT_APPLICABLE'; + if (relevant.includes('FAIL')) return 'FAIL'; + if (relevant.includes('UNKNOWN')) return 'UNKNOWN'; + return 'PASS'; +} + +// QNBS-v3: never throws -- an opt-in diagnostic tool must fail closed to UNKNOWN, not crash the caller. +export async function verifyExactTreeTypecheck(sha, repoRoot = process.cwd(), dependencies = {}) { + try { + const dependencyStateForRef = + dependencies.dependencyStateForRef ?? ((ref) => computeDependencyState(ref, repoRoot)); + // QNBS-v3: precondition for a trustworthy symlink, not a skip gate -- the check always runs. + if (dependencyStateForRef(sha) !== 'MATCHES') return 'UNKNOWN'; + + const created = await createIsolatedWorktree(sha, repoRoot, dependencies); + if (!created.ok) { + await removeIsolatedWorktree(created.path, repoRoot, dependencies); + return 'UNKNOWN'; + } + + try { + const nodeModulesSource = dependencies.nodeModulesSource ?? join(repoRoot, 'node_modules'); + if (!linkNodeModules(created.path, nodeModulesSource, dependencies)) return 'UNKNOWN'; + + const runDetailed = dependencies.runLocalBinaryDetailed ?? runLocalBinaryDetailed; + const tsgoArgs = dependencies.tsgoArgs ?? DEFAULT_TSGO_ARGS; + const result = await runDetailed('tsgo', tsgoArgs, { + root: created.path, + cwd: created.path, + }); + if (result.error || result.timedOut || result.interrupted) return 'UNKNOWN'; + return result.status === 0 ? 'PASS' : 'FAIL'; + } finally { + await removeIsolatedWorktree(created.path, repoRoot, dependencies); + } + } catch { + return 'UNKNOWN'; + } +} + +export async function verifyExactTreeForShas(shas, repoRoot = process.cwd(), dependencies = {}) { + const unique = [...new Set(shas)]; + if (unique.length === 0) return 'NOT_APPLICABLE'; + const states = []; + for (const sha of unique) { + // QNBS-v3: sequential, never parallel -- bounded resource use on constrained developer hardware. + states.push(await verifyExactTreeTypecheck(sha, repoRoot, dependencies)); + } + return aggregateExactTreeState(states); +} + +function resolveRef(ref, dependencies) { + const runGitSync = + dependencies.runGitSync ?? ((args) => spawnSync('git', args, { encoding: 'utf8' })); + const result = runGitSync(['rev-parse', '--verify', `${ref}^{commit}`]); + if (result.status !== 0) return null; + return result.stdout.trim(); +} + +export async function main(argv = process.argv.slice(2)) { + const refs = argv.length > 0 ? argv : ['HEAD']; + const shas = []; + for (const ref of refs) { + const sha = resolveRef(ref); + if (!sha) { + console.error(`[verify-exact-tree] could not resolve ref: ${ref}`); + process.exitCode = 1; + return; + } + shas.push(sha); + } + console.log(`[verify-exact-tree] verifying ${shas.length} commit(s) in isolated worktree(s)...`); + const state = await verifyExactTreeForShas(shas); + console.log(`[verify-exact-tree] result: ${state}`); + if (state === 'UNKNOWN') { + console.log( + '[verify-exact-tree] could not be established (dependencies not reconciled/matched locally, or a worktree/tsgo step failed); required CI remains authoritative.', + ); + } else if (state === 'FAIL') { + console.log('[verify-exact-tree] the exact committed tree does not typecheck in isolation.'); + process.exitCode = 1; + } +} + +// QNBS-v3: guard execution so this module can be imported for testing without running the CLI. +if (isMainModule(process.argv[1], import.meta.url)) await main(); diff --git a/tests/unit/tooling/verify-exact-tree.test.mjs b/tests/unit/tooling/verify-exact-tree.test.mjs new file mode 100644 index 000000000..f79b30a2d --- /dev/null +++ b/tests/unit/tooling/verify-exact-tree.test.mjs @@ -0,0 +1,191 @@ +import { strict as assert } from 'node:assert'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { afterEach, describe, it } from 'node:test'; +import { + verifyExactTreeForShas, + verifyExactTreeTypecheck, +} from '../../../scripts/verify-exact-tree.mjs'; + +const temporaryRoots = []; + +afterEach(() => { + for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +function makeTinyTsRepo(source) { + const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-')); + temporaryRoots.push(root); + const git = (args) => execFileSync('git', args, { cwd: root, encoding: 'utf8' }); + git(['init', '--quiet', '--initial-branch=main']); + git(['config', 'user.email', 'test@example.com']); + git(['config', 'user.name', 'test']); + writeFileSync( + join(root, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + strict: true, + noEmit: true, + module: 'esnext', + target: 'es2022', + moduleResolution: 'bundler', + skipLibCheck: true, + }, + include: ['index.ts'], + }), + ); + writeFileSync(join(root, 'index.ts'), source); + git(['add', '-A']); + git(['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'test']); + const sha = git(['rev-parse', 'HEAD']).trim(); + return { root, sha }; +} + +const realProjectNodeModules = join(process.cwd(), 'node_modules'); +const tinyTsgoArgs = ['--project', 'tsconfig.json', '--noEmit', '--checkers', '1']; + +describe('verifyExactTreeTypecheck (precondition -- not a skip gate)', () => { + it('reports UNKNOWN without creating a worktree when dependencyState is not MATCHES', async () => { + const state = await verifyExactTreeTypecheck('deadbeef', '/repo', { + dependencyStateForRef: () => 'DIVERGED', + runBounded: () => { + throw new Error('must not be called when the precondition is not met'); + }, + }); + assert.equal(state, 'UNKNOWN'); + }); + + it('reports UNKNOWN rather than propagating a throw from an injected dependency', async () => { + const state = await verifyExactTreeTypecheck('deadbeef', '/repo', { + dependencyStateForRef: () => { + throw new Error('boom'); + }, + }); + assert.equal(state, 'UNKNOWN'); + }); +}); + +describe('verifyExactTreeTypecheck (real git worktree + real tsgo, small fixture)', () => { + it('reports PASS for a commit that typechecks cleanly in isolation', async () => { + const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); + const state = await verifyExactTreeTypecheck(sha, root, { + dependencyStateForRef: () => 'MATCHES', + nodeModulesSource: realProjectNodeModules, + tsgoArgs: tinyTsgoArgs, + }); + assert.equal(state, 'PASS'); + }); + + it('reports FAIL for a commit with a genuine type error in isolation', async () => { + const { root, sha } = makeTinyTsRepo('const x: number = "not a number";\n'); + const state = await verifyExactTreeTypecheck(sha, root, { + dependencyStateForRef: () => 'MATCHES', + nodeModulesSource: realProjectNodeModules, + tsgoArgs: tinyTsgoArgs, + }); + assert.equal(state, 'FAIL'); + }); + + it('leaves no worktree registered after a successful run (cleanup ran)', async () => { + const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); + await verifyExactTreeTypecheck(sha, root, { + dependencyStateForRef: () => 'MATCHES', + nodeModulesSource: realProjectNodeModules, + tsgoArgs: tinyTsgoArgs, + }); + const list = execFileSync('git', ['worktree', 'list', '--porcelain'], { + cwd: root, + encoding: 'utf8', + }); + assert.equal(list.trim().split('\n\n').length, 1, `expected only the main worktree: ${list}`); + }); +}); + +describe('verifyExactTreeTypecheck (fail-closed worktree lifecycle, injected failures)', () => { + it('reports UNKNOWN when git worktree add fails', async () => { + const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + dependencyStateForRef: () => 'MATCHES', + runBounded: (_command, args) => { + if (args.includes('add')) return { status: 1, error: null, timedOut: false, interrupted: false }; + return { status: 0, error: null, timedOut: false, interrupted: false }; + }, + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + }); + assert.equal(state, 'UNKNOWN'); + }); + + it('falls back to a raw directory sweep plus prune when git worktree remove fails', async () => { + let removeCalled = false; + let pruneCallCount = 0; + let rmCalled = false; + const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + dependencyStateForRef: () => 'MATCHES', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('prune')) { + pruneCallCount += 1; + return { status: 0, error: null, timedOut: false, interrupted: false }; + } + if (args.includes('add')) return { status: 0, error: null, timedOut: false, interrupted: false }; + if (args.includes('remove')) { + removeCalled = true; + return { status: 1, error: null, timedOut: false, interrupted: false }; + } + return { status: 0, error: null, timedOut: false, interrupted: false }; + }, + symlinkFn: () => { + throw new Error('force UNKNOWN before any real tsgo invocation is attempted'); + }, + rmFn: async () => { + rmCalled = true; + }, + }); + assert.equal(state, 'UNKNOWN'); + assert.equal(removeCalled, true); + assert.equal(rmCalled, true); + // QNBS-v3: proactive prune before creating the worktree, plus the fallback prune after removal fails. + assert.equal(pruneCallCount, 2); + }); +}); + +describe('verifyExactTreeForShas (dedup, sequential, aggregation)', () => { + it('deduplicates identical SHAs so the underlying check runs once', async () => { + const calls = []; + const state = await verifyExactTreeForShas(['a'.repeat(40), 'a'.repeat(40)], '/repo', { + dependencyStateForRef: (sha) => { + calls.push(sha); + return 'MATCHES'; + }, + runBounded: () => ({ status: 0, error: null, timedOut: false, interrupted: false }), + symlinkFn: () => { + throw new Error('force UNKNOWN quickly -- this test only cares about call count'); + }, + }); + assert.equal(calls.length, 1); + assert.equal(state, 'UNKNOWN'); + }); + + it('returns NOT_APPLICABLE for an empty list', async () => { + assert.equal(await verifyExactTreeForShas([], '/repo'), 'NOT_APPLICABLE'); + }); + + it('aggregates with FAIL outranking UNKNOWN', async () => { + const shaFail = 'a'.repeat(40); + const shaUnknown = 'b'.repeat(40); + const state = await verifyExactTreeForShas([shaFail, shaUnknown], '/repo', { + dependencyStateForRef: (sha) => (sha === shaUnknown ? 'DIVERGED' : 'MATCHES'), + runBounded: () => ({ status: 0, error: null, timedOut: false, interrupted: false }), + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + symlinkFn: () => {}, + runLocalBinaryDetailed: async () => ({ + status: 1, + error: null, + timedOut: false, + interrupted: false, + }), + }); + // QNBS-v3: shaFail -> real FAIL via the mocked tsgo run; shaUnknown -> UNKNOWN via the precondition. + assert.equal(state, 'FAIL'); + }); +}); From feb850417827b5626ede7638b2ad25a66adc5e64 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 25 Aug 2026 22:55:05 +0200 Subject: [PATCH 2/9] fix(signing): replace live node_modules reuse with real pnpm materialization Consolidated remediation for the #503 review epoch. Reconstructs the isolated-worktree dependency environment via a real `pnpm install --frozen-lockfile --offline` instead of symlinking the live checkout's node_modules. Root cluster A -- exact-tree dependency-resolution soundness: The original design symlinked the real checkout's node_modules into the isolated worktree, reasoning a full reinstall was the unbounded cost this program had repeatedly avoided. Review, and the empirical investigation it prompted, found this unsound: pnpm workspace-package symlinks (node_modules/@domain/) are relative, so a whole-directory symlink transitively resolves them back into the live, possibly- uncommitted packages/* -- confirmed directly (readlink -f resolved into the live checkout, not the isolated tree). A second, deeper leak was found nested at node_modules/.pnpm/node_modules/@domain/desktop-contracts, proving a hand-patched subset of the link graph could never be trusted exhaustive; package-local packages/*/node_modules directories would also simply be absent from a fresh worktree under that approach. Fixed by running a real, frozen-lockfile, offline pnpm install from inside the isolated worktree -- pnpm's own algorithm, not a reconstruction, so it is structurally correct for every link class (top-level, nested .pnpm, package-local, transitive), not just the ones this round happened to find. --offline never touches the network; a package missing from the local store fails the install, mapped to UNKNOWN, never a silently-wrong PASS. Verified end-to-end against this actual repository (real commit, real isolated worktree, real install, real tsgo): PASS, with the install completing in ~1m22s-2m20s on this hardware using the warm local store -- fully acceptable given Part 2b is opt-in. The now-unnecessary dependencyState precondition (it only ever guarded the symlink design's soundness) is removed rather than kept as inert logic; #502's dependencyState remains the sole authority for its own distinct question, unchanged. Root cluster B -- bounded process/result semantics: - resolveRef used a raw, timeout-less spawnSync, conflicting with the #500 bounded-subprocess authority, and the real CLI path crashed outright (resolveRef(ref) called without its dependencies argument, which the function then dereferenced unconditionally). Fixed by reusing signing-core's already-bounded (5s timeout), output-capturing runGit -- a distinct, pre-existing (#494) wrapper for exactly this job, since runBounded's own contract is inherit-stdio-only and structurally cannot capture stdout. main() now threads a real dependencies parameter through to resolveRef and the verification path, closing the exact gap that let the crash ship uncaught by helper-only tests. - Split the single bounded-result check into two correctly-scoped predicates: boundedCommandFailed (worktree/install lifecycle -- any non-zero or unreadable exit fails the step outright) and tsgoResultUnknown (only the tsgo result -- a genuine numeric non-zero status is a real FAIL; error/timeout/interrupt/signal/non-numeric- status mean UNKNOWN, so a signal, including an external OOM kill, can never read as a false FAIL). The prior single conflated helper broke lifecycle-failure detection for numeric non-zero exits. - verify-exact-tree.d.mts now reuses shared.d.mts's real BoundedResult type instead of an inaccurate GitResult-shaped declaration. - repoRoot is canonicalized to an absolute path before any git/install call, so a relative caller-supplied root can't produce an ambiguous cwd. - Worktree cleanup now unconditionally sweeps the mkdtemp-created directory after a successful git worktree remove, not only on failure -- git deregisters and clears the worktree's own content but leaves the pre-existing (now-empty) directory in place, which was accumulating empty leftovers under os.tmpdir() across runs. Root cluster C -- routine regression admission: .mjs node:test tooling suites (dependency-state.test.mjs, verify-exact-tree.test.mjs) fall outside Vitest's .ts/.tsx-only include glob and were never run in CI at all. Added a `test:node` script and a dedicated CI step in the quality matrix job -- the one authoritative place these run routinely, not ad-hoc local-only invocation. This retroactively covers Part 2a's dependency-state.test.mjs too, with zero touch to that file. Tests: three deliberate tiers. Fast DI-based unit coverage (bounded- result semantics, fail-closed cleanup including the leftover-directory fix, aggregation, ref resolution, and the real main() CLI entry point with realistic injected dependencies -- specifically because the resolveRef bug was invisible to helper-only tests). A real git worktree + real, zero-external-dependency pnpm workspace fixture (root -> demo-pkg -> inner-pkg) proving root, package-local, and transitive workspace-link resolution lands inside the isolated tree via direct file-content comparison against a deliberately live-mutated checkout -- the load-bearing regression proof, strong by construction (a leak would read the live, wrong value). A manual, one-time, real-repository smoke proof against this actual repo and a real commit, documented as evidence rather than run routinely (the real install alone measures over a minute on this hardware). False-positive note: CodeRabbit's "duplicate `shared` declaration" claim on the earlier #502 epoch did not match its exact-HEAD code (verified via grep + a clean typecheck/test run) and was resolved with that evidence at the time; unrelated to this remediation. --- .github/workflows/ci.yml | 5 + biome.json | 2 +- package.json | 1 + scripts/verify-exact-tree.d.mts | 48 +- scripts/verify-exact-tree.mjs | 95 ++-- tests/unit/tooling/verify-exact-tree.test.mjs | 455 +++++++++++++----- 6 files changed, 446 insertions(+), 160 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 818c72f25..75ef61bf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -212,6 +212,11 @@ jobs: NODE_OPTIONS: "--no-experimental-webstorage --max-old-space-size=4096" CI: "true" + # QNBS-v3: node:test tooling scripts (.mjs) fall outside Vitest's include glob (.ts/.tsx only) -- + # this is the one authoritative step admitting them to routine CI, not ad-hoc scattered invocation. + - name: Unit tests (node:test, tooling scripts) + run: pnpm run test:node + - name: Coverage ratchet check (informational, non-blocking) if: always() continue-on-error: true diff --git a/biome.json b/biome.json index 66be3be5a..2c1dd1fef 100644 --- a/biome.json +++ b/biome.json @@ -135,7 +135,7 @@ } }, { - "includes": ["tests/**/*.ts", "tests/**/*.tsx"], + "includes": ["tests/**/*.ts", "tests/**/*.tsx", "tests/**/*.mjs"], "linter": { "rules": { "suspicious": { diff --git a/package.json b/package.json index 8cce1c2f6..7875922e8 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "test:turbo": "turbo run test", "test:run": "vitest run", "test:coverage": "vitest run --coverage", + "test:node": "node --test tests/unit/tooling/*.test.mjs", "bench": "vitest bench --run tests/bench", "test:e2e": "node -e \"if (process.env.CI !== 'true') { console.error('E2E tests are CI-only. Set CI=true or run via GitHub Actions.'); process.exit(1); }\" && playwright test", "test:e2e:ui": "node -e \"if (process.env.CI !== 'true') { console.error('E2E tests are CI-only. Set CI=true or run via GitHub Actions.'); process.exit(1); }\" && playwright test --ui", diff --git a/scripts/verify-exact-tree.d.mts b/scripts/verify-exact-tree.d.mts index 4f7816051..e98eb1d51 100644 --- a/scripts/verify-exact-tree.d.mts +++ b/scripts/verify-exact-tree.d.mts @@ -1,3 +1,4 @@ +import type { BoundedResult } from './hooks/shared.d.mts'; import type { GitOptions, GitResult } from './signing/signing-core.d.mts'; // QNBS-v3: absolute correctness check, not a comparison -- distinct vocabulary from WorkingTreeState. @@ -7,22 +8,47 @@ export interface VerifyExactTreeDependencies { runBounded?: ( command: string, args: string[], - options?: GitOptions & { timeoutMs?: number }, - ) => Promise; + options?: { + timeoutMs?: number; + cwd?: string; + env?: NodeJS.ProcessEnv; + input?: string; + shell?: boolean; + root?: string; + detached?: boolean; + }, + ) => Promise; runLocalBinaryDetailed?: ( binary: string, args: string[], options?: { root?: string; cwd?: string; timeoutMs?: number }, - ) => Promise; + ) => Promise; + // QNBS-v3: a distinct, pre-existing (#494) synchronous/output-capturing wrapper -- not BoundedResult. + runGit?: (args: string[], options?: GitOptions) => GitResult; mkdtempFn?: () => Promise; rmFn?: (path: string) => Promise; - symlinkFn?: (target: string, linkPath: string) => void; - dependencyStateForRef?: (sha: string) => string; - nodeModulesSource?: string; + installTimeoutMs?: number; tsgoArgs?: string[]; - runGitSync?: (args: string[]) => { status: number | null; stdout: string }; + repoRoot?: string; } +export function createIsolatedWorktree( + sha: string, + repoRoot: string, + dependencies?: VerifyExactTreeDependencies, +): Promise<{ ok: boolean; path: string | undefined }>; + +export function removeIsolatedWorktree( + worktreePath: string | undefined, + repoRoot: string, + dependencies?: VerifyExactTreeDependencies, +): Promise; + +export function installDependencies( + worktreePath: string, + dependencies?: VerifyExactTreeDependencies, +): Promise; + export function verifyExactTreeTypecheck( sha: string, repoRoot?: string, @@ -35,4 +61,10 @@ export function verifyExactTreeForShas( dependencies?: VerifyExactTreeDependencies, ): Promise; -export function main(argv?: string[]): Promise; +export function resolveRef( + ref: string, + repoRoot: string, + dependencies?: VerifyExactTreeDependencies, +): string | null; + +export function main(argv?: string[], dependencies?: VerifyExactTreeDependencies): Promise; diff --git a/scripts/verify-exact-tree.mjs b/scripts/verify-exact-tree.mjs index fa8309f77..a0c446cd2 100644 --- a/scripts/verify-exact-tree.mjs +++ b/scripts/verify-exact-tree.mjs @@ -1,14 +1,28 @@ -import { spawnSync } from 'node:child_process'; -import { symlinkSync } from 'node:fs'; import { mkdtemp as mkdtempAsync, rm as rmAsync } from 'node:fs/promises'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { join, resolve } from 'node:path'; import process from 'node:process'; import { isMainModule } from './ci-prepush-range-resolver.mjs'; -import { computeDependencyState } from './dependency-state.mjs'; import { runBounded, runLocalBinaryDetailed } from './hooks/shared.mjs'; +import { runGit as defaultRunGit } from './signing/signing-core.mjs'; const DEFAULT_TSGO_ARGS = ['--project', 'tsconfig.tsgo.json', '--noEmit', '--checkers', '1']; +// QNBS-v3: measured ~2m20s for the full project on this hardware with a warm store; 5min gives margin. +const DEFAULT_INSTALL_TIMEOUT_MS = 300_000; + +// QNBS-v3: lifecycle commands (worktree/install) -- any non-zero or unreadable exit fails outright. +function boundedCommandFailed(result) { + return Boolean( + result.error || result.timedOut || result.interrupted || result.signal || result.status !== 0, + ); +} + +// QNBS-v3: tsgo only -- a genuine numeric non-zero status is a real FAIL; a signal/timeout/OOM is UNKNOWN. +function tsgoResultUnknown(result) { + return Boolean( + result.error || result.timedOut || result.interrupted || result.signal || typeof result.status !== 'number', + ); +} // QNBS-v3: sweeps entries orphaned by a prior crashed/killed run before creating a new one. async function pruneStaleWorktrees(repoRoot, dependencies) { @@ -16,7 +30,7 @@ async function pruneStaleWorktrees(repoRoot, dependencies) { await runGit('git', ['worktree', 'prune'], { cwd: repoRoot }); } -async function createIsolatedWorktree(sha, repoRoot, dependencies) { +export async function createIsolatedWorktree(sha, repoRoot, dependencies) { const runGit = dependencies.runBounded ?? runBounded; const makeTempDir = dependencies.mkdtempFn ?? (() => mkdtempAsync(join(tmpdir(), 'worldscript-exact-tree-'))); @@ -28,43 +42,46 @@ async function createIsolatedWorktree(sha, repoRoot, dependencies) { return { ok: false, path: undefined }; } const result = await runGit('git', ['worktree', 'add', '--detach', path, sha], { cwd: repoRoot }); - if (result.error || result.timedOut || result.interrupted || result.status !== 0) { - return { ok: false, path }; - } + if (boundedCommandFailed(result)) return { ok: false, path }; return { ok: true, path }; } // QNBS-v3: fail-closed -- git's own removal failing falls back to a raw sweep plus a metadata prune. -async function removeIsolatedWorktree(worktreePath, repoRoot, dependencies) { +export async function removeIsolatedWorktree(worktreePath, repoRoot, dependencies) { if (!worktreePath) return; const runGit = dependencies.runBounded ?? runBounded; const removeDir = dependencies.rmFn ?? ((path) => rmAsync(path, { recursive: true, force: true })); const result = await runGit('git', ['worktree', 'remove', '--force', worktreePath], { cwd: repoRoot, }); - if (result.error || result.timedOut || result.interrupted || result.status !== 0) { + if (boundedCommandFailed(result)) { try { await removeDir(worktreePath); } catch { // Best-effort: nothing more can be done from here; the directory is under os.tmpdir(). } await runGit('git', ['worktree', 'prune'], { cwd: repoRoot }); + return; } -} - -// QNBS-v3: real node_modules install per verification is exactly the unbounded cost this avoids. -function linkNodeModules(worktreePath, source, dependencies) { - const symlinkFn = - dependencies.symlinkFn ?? - ((target, linkPath) => symlinkSync(target, linkPath, process.platform === 'win32' ? 'junction' : 'dir')); + // QNBS-v3: git leaves the now-empty mkdtemp-created dir in place after remove -- sweep it too. try { - symlinkFn(source, join(worktreePath, 'node_modules')); - return true; + await removeDir(worktreePath); } catch { - return false; + // Best-effort: nothing more can be done from here; the directory is under os.tmpdir(). } } +// QNBS-v3: real pnpm install, not a hand-reconstructed symlink graph -- offline, fails to UNKNOWN below. +export async function installDependencies(worktreePath, dependencies) { + const runPnpm = dependencies.runBounded ?? runBounded; + const timeoutMs = dependencies.installTimeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS; + const result = await runPnpm('pnpm', ['install', '--frozen-lockfile', '--offline'], { + cwd: worktreePath, + timeoutMs, + }); + return !boundedCommandFailed(result); +} + // QNBS-v3: precedence shape mirrors aggregateDiagnosticState, kept separate -- different vocabulary. function aggregateExactTreeState(states) { const relevant = states.filter((state) => state !== 'NOT_APPLICABLE'); @@ -76,21 +93,17 @@ function aggregateExactTreeState(states) { // QNBS-v3: never throws -- an opt-in diagnostic tool must fail closed to UNKNOWN, not crash the caller. export async function verifyExactTreeTypecheck(sha, repoRoot = process.cwd(), dependencies = {}) { + // QNBS-v3: a relative repoRoot would produce ambiguous git-cwd and install-cwd semantics. + const absoluteRepoRoot = resolve(repoRoot); try { - const dependencyStateForRef = - dependencies.dependencyStateForRef ?? ((ref) => computeDependencyState(ref, repoRoot)); - // QNBS-v3: precondition for a trustworthy symlink, not a skip gate -- the check always runs. - if (dependencyStateForRef(sha) !== 'MATCHES') return 'UNKNOWN'; - - const created = await createIsolatedWorktree(sha, repoRoot, dependencies); + const created = await createIsolatedWorktree(sha, absoluteRepoRoot, dependencies); if (!created.ok) { - await removeIsolatedWorktree(created.path, repoRoot, dependencies); + await removeIsolatedWorktree(created.path, absoluteRepoRoot, dependencies); return 'UNKNOWN'; } try { - const nodeModulesSource = dependencies.nodeModulesSource ?? join(repoRoot, 'node_modules'); - if (!linkNodeModules(created.path, nodeModulesSource, dependencies)) return 'UNKNOWN'; + if (!(await installDependencies(created.path, dependencies))) return 'UNKNOWN'; const runDetailed = dependencies.runLocalBinaryDetailed ?? runLocalBinaryDetailed; const tsgoArgs = dependencies.tsgoArgs ?? DEFAULT_TSGO_ARGS; @@ -98,10 +111,11 @@ export async function verifyExactTreeTypecheck(sha, repoRoot = process.cwd(), de root: created.path, cwd: created.path, }); - if (result.error || result.timedOut || result.interrupted) return 'UNKNOWN'; + // QNBS-v3: a signal (including an external OOM kill) must yield UNKNOWN, never a false FAIL. + if (tsgoResultUnknown(result)) return 'UNKNOWN'; return result.status === 0 ? 'PASS' : 'FAIL'; } finally { - await removeIsolatedWorktree(created.path, repoRoot, dependencies); + await removeIsolatedWorktree(created.path, absoluteRepoRoot, dependencies); } } catch { return 'UNKNOWN'; @@ -119,19 +133,20 @@ export async function verifyExactTreeForShas(shas, repoRoot = process.cwd(), dep return aggregateExactTreeState(states); } -function resolveRef(ref, dependencies) { - const runGitSync = - dependencies.runGitSync ?? ((args) => spawnSync('git', args, { encoding: 'utf8' })); - const result = runGitSync(['rev-parse', '--verify', `${ref}^{commit}`]); - if (result.status !== 0) return null; +// QNBS-v3: reuses signing-core's bounded, output-capturing runGit -- runBounded can't capture stdout. +export function resolveRef(ref, repoRoot, dependencies = {}) { + const runGit = dependencies.runGit ?? defaultRunGit; + const result = runGit(['rev-parse', '--verify', `${ref}^{commit}`], { cwd: repoRoot }); + if (result.error || result.status !== 0) return null; return result.stdout.trim(); } -export async function main(argv = process.argv.slice(2)) { +export async function main(argv = process.argv.slice(2), dependencies = {}) { + const repoRoot = resolve(dependencies.repoRoot ?? process.cwd()); const refs = argv.length > 0 ? argv : ['HEAD']; const shas = []; for (const ref of refs) { - const sha = resolveRef(ref); + const sha = resolveRef(ref, repoRoot, dependencies); if (!sha) { console.error(`[verify-exact-tree] could not resolve ref: ${ref}`); process.exitCode = 1; @@ -140,11 +155,11 @@ export async function main(argv = process.argv.slice(2)) { shas.push(sha); } console.log(`[verify-exact-tree] verifying ${shas.length} commit(s) in isolated worktree(s)...`); - const state = await verifyExactTreeForShas(shas); + const state = await verifyExactTreeForShas(shas, repoRoot, dependencies); console.log(`[verify-exact-tree] result: ${state}`); if (state === 'UNKNOWN') { console.log( - '[verify-exact-tree] could not be established (dependencies not reconciled/matched locally, or a worktree/tsgo step failed); required CI remains authoritative.', + '[verify-exact-tree] could not be established (offline dependency materialization failed, or a worktree/tsgo step failed); required CI remains authoritative.', ); } else if (state === 'FAIL') { console.log('[verify-exact-tree] the exact committed tree does not typecheck in isolation.'); diff --git a/tests/unit/tooling/verify-exact-tree.test.mjs b/tests/unit/tooling/verify-exact-tree.test.mjs index f79b30a2d..6214707c7 100644 --- a/tests/unit/tooling/verify-exact-tree.test.mjs +++ b/tests/unit/tooling/verify-exact-tree.test.mjs @@ -1,9 +1,14 @@ import { strict as assert } from 'node:assert'; import { execFileSync } from 'node:child_process'; -import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; import { afterEach, describe, it } from 'node:test'; import { + createIsolatedWorktree, + installDependencies, + main, + removeIsolatedWorktree, + resolveRef, verifyExactTreeForShas, verifyExactTreeTypecheck, } from '../../../scripts/verify-exact-tree.mjs'; @@ -14,155 +19,294 @@ afterEach(() => { for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true }); }); +function git(cwd, args) { + return execFileSync('git', args, { cwd, encoding: 'utf8' }); +} + function makeTinyTsRepo(source) { const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-')); temporaryRoots.push(root); - const git = (args) => execFileSync('git', args, { cwd: root, encoding: 'utf8' }); - git(['init', '--quiet', '--initial-branch=main']); - git(['config', 'user.email', 'test@example.com']); - git(['config', 'user.name', 'test']); + git(root, ['init', '--quiet', '--initial-branch=main']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); writeFileSync( join(root, 'tsconfig.json'), JSON.stringify({ - compilerOptions: { - strict: true, - noEmit: true, - module: 'esnext', - target: 'es2022', - moduleResolution: 'bundler', - skipLibCheck: true, - }, + compilerOptions: { strict: true, noEmit: true, module: 'esnext', target: 'es2022' }, include: ['index.ts'], }), ); writeFileSync(join(root, 'index.ts'), source); - git(['add', '-A']); - git(['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'test']); - const sha = git(['rev-parse', 'HEAD']).trim(); + git(root, ['add', '-A']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'test']); + const sha = git(root, ['rev-parse', 'HEAD']).trim(); return { root, sha }; } -const realProjectNodeModules = join(process.cwd(), 'node_modules'); -const tinyTsgoArgs = ['--project', 'tsconfig.json', '--noEmit', '--checkers', '1']; +// QNBS-v3: root/package-local/transitive links, zero external deps (offline metadata can't resolve fresh). +function makeWorkspaceFixture(innerContent) { + const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-ws-')); + temporaryRoots.push(root); + mkdirSync(join(root, 'packages', 'demo-pkg'), { recursive: true }); + mkdirSync(join(root, 'packages', 'inner-pkg'), { recursive: true }); + writeFileSync( + join(root, 'package.json'), + '{"name":"fixture-root","private":true,"dependencies":{"@fixture/demo-pkg":"workspace:*"}}\n', + ); + writeFileSync(join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n'); + writeFileSync( + join(root, 'packages', 'demo-pkg', 'package.json'), + '{"name":"@fixture/demo-pkg","version":"1.0.0","dependencies":{"@fixture/inner-pkg":"workspace:*"}}\n', + ); + writeFileSync(join(root, 'packages', 'demo-pkg', 'index.js'), "export { INNER } from '@fixture/inner-pkg';\n"); + writeFileSync( + join(root, 'packages', 'inner-pkg', 'package.json'), + '{"name":"@fixture/inner-pkg","version":"1.0.0"}\n', + ); + writeFileSync(join(root, 'packages', 'inner-pkg', 'index.js'), `export const INNER = '${innerContent}';\n`); + // QNBS-v3: generates a real, valid lockfile for this fixture -- workspace-only, no network needed. + execFileSync('pnpm', ['install', '--offline'], { cwd: root, stdio: 'ignore' }); + git(root, ['init', '--quiet', '--initial-branch=main']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + git(root, ['add', '-A']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'test']); + const sha = git(root, ['rev-parse', 'HEAD']).trim(); + return { root, sha }; +} -describe('verifyExactTreeTypecheck (precondition -- not a skip gate)', () => { - it('reports UNKNOWN without creating a worktree when dependencyState is not MATCHES', async () => { - const state = await verifyExactTreeTypecheck('deadbeef', '/repo', { - dependencyStateForRef: () => 'DIVERGED', - runBounded: () => { - throw new Error('must not be called when the precondition is not met'); - }, - }); - assert.equal(state, 'UNKNOWN'); +describe('createIsolatedWorktree / removeIsolatedWorktree (real git, no pnpm)', () => { + it('creates a worktree at the exact commit and leaves nothing registered after cleanup', async () => { + const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); + const created = await createIsolatedWorktree(sha, root, {}); + assert.equal(created.ok, true); + assert.equal(readFileSync(join(created.path, 'index.ts'), 'utf8'), 'const x: number = 1;\n'); + await removeIsolatedWorktree(created.path, root, {}); + const list = git(root, ['worktree', 'list', '--porcelain']); + assert.equal(list.trim().split('\n\n').length, 1, `expected only the main worktree: ${list}`); }); - it('reports UNKNOWN rather than propagating a throw from an injected dependency', async () => { - const state = await verifyExactTreeTypecheck('deadbeef', '/repo', { - dependencyStateForRef: () => { - throw new Error('boom'); + it('falls back to a raw directory sweep plus prune when git worktree remove fails', async () => { + let removeCalled = false; + let pruneCallCount = 0; + let rmCalled = false; + await removeIsolatedWorktree('/tmp/worldscript-exact-tree-fake', '/repo', { + runBounded: (_command, args) => { + if (args.includes('prune')) { + pruneCallCount += 1; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + } + if (args.includes('remove')) { + removeCalled = true; + return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; + } + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + rmFn: async () => { + rmCalled = true; }, }); - assert.equal(state, 'UNKNOWN'); + assert.equal(removeCalled, true); + assert.equal(rmCalled, true); + assert.equal(pruneCallCount, 1); }); }); -describe('verifyExactTreeTypecheck (real git worktree + real tsgo, small fixture)', () => { - it('reports PASS for a commit that typechecks cleanly in isolation', async () => { - const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); - const state = await verifyExactTreeTypecheck(sha, root, { - dependencyStateForRef: () => 'MATCHES', - nodeModulesSource: realProjectNodeModules, - tsgoArgs: tinyTsgoArgs, - }); - assert.equal(state, 'PASS'); - }); +describe('installDependencies (workspace-package soundness -- the core regression)', () => { + it('resolves workspace packages to the isolated worktree\'s committed source, never the live checkout, including a transitive package-local link', async () => { + const { root, sha } = makeWorkspaceFixture('committed'); - it('reports FAIL for a commit with a genuine type error in isolation', async () => { - const { root, sha } = makeTinyTsRepo('const x: number = "not a number";\n'); - const state = await verifyExactTreeTypecheck(sha, root, { - dependencyStateForRef: () => 'MATCHES', - nodeModulesSource: realProjectNodeModules, - tsgoArgs: tinyTsgoArgs, - }); - assert.equal(state, 'FAIL'); + // QNBS-v3: mutate the LIVE checkout AFTER committing -- a leak would read this instead of committed. + writeFileSync( + join(root, 'packages', 'inner-pkg', 'index.js'), + "export const INNER = 'LEAKED-live-checkout-value';\n", + ); + + const created = await createIsolatedWorktree(sha, root, {}); + assert.equal(created.ok, true); + try { + const installed = await installDependencies(created.path, {}); + assert.equal(installed, true); + + // QNBS-v3: root workspace link (node_modules/@fixture/demo-pkg) resolving into the isolated tree. + const demoPkgContent = readFileSync( + join(created.path, 'node_modules', '@fixture', 'demo-pkg', 'index.js'), + 'utf8', + ); + assert.match(demoPkgContent, /@fixture\/inner-pkg/); + + // QNBS-v3: the critical assertion -- package-local, transitive workspace link resolves committed. + const innerContent = readFileSync( + join(created.path, 'packages', 'demo-pkg', 'node_modules', '@fixture', 'inner-pkg', 'index.js'), + 'utf8', + ); + assert.match(innerContent, /'committed'/); + assert.doesNotMatch(innerContent, /LEAKED/); + + // QNBS-v3: also verify via Node's own resolution, wherever pnpm actually placed the hoisted link. + const resolved = execFileSync('node', ['-e', "process.stdout.write(require('fs').readFileSync(require.resolve('@fixture/inner-pkg'), 'utf8'))"], { + cwd: join(created.path, 'packages', 'demo-pkg'), + encoding: 'utf8', + }); + assert.match(resolved, /'committed'/); + assert.doesNotMatch(resolved, /LEAKED/); + } finally { + await removeIsolatedWorktree(created.path, root, {}); + } }); - it('leaves no worktree registered after a successful run (cleanup ran)', async () => { - const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); - await verifyExactTreeTypecheck(sha, root, { - dependencyStateForRef: () => 'MATCHES', - nodeModulesSource: realProjectNodeModules, - tsgoArgs: tinyTsgoArgs, - }); - const list = execFileSync('git', ['worktree', 'list', '--porcelain'], { - cwd: root, - encoding: 'utf8', + it('returns false (mapped to UNKNOWN by callers) when the frozen-lockfile install fails', async () => { + const installed = await installDependencies('/tmp/worldscript-exact-tree-fake', { + runBounded: async () => ({ + status: 1, + error: null, + signal: null, + timedOut: false, + interrupted: false, + }), }); - assert.equal(list.trim().split('\n\n').length, 1, `expected only the main worktree: ${list}`); + assert.equal(installed, false); }); }); -describe('verifyExactTreeTypecheck (fail-closed worktree lifecycle, injected failures)', () => { +describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semantics -- DI only)', () => { it('reports UNKNOWN when git worktree add fails', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - dependencyStateForRef: () => 'MATCHES', runBounded: (_command, args) => { - if (args.includes('add')) return { status: 1, error: null, timedOut: false, interrupted: false }; - return { status: 0, error: null, timedOut: false, interrupted: false }; + if (args.includes('add')) return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; }, mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', }); assert.equal(state, 'UNKNOWN'); }); - it('falls back to a raw directory sweep plus prune when git worktree remove fails', async () => { - let removeCalled = false; - let pruneCallCount = 0; - let rmCalled = false; + it('reports UNKNOWN, never a false PASS/FAIL, when the install fails', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - dependencyStateForRef: () => 'MATCHES', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: (_command, args) => { - if (args.includes('prune')) { - pruneCallCount += 1; - return { status: 0, error: null, timedOut: false, interrupted: false }; - } - if (args.includes('add')) return { status: 0, error: null, timedOut: false, interrupted: false }; - if (args.includes('remove')) { - removeCalled = true; - return { status: 1, error: null, timedOut: false, interrupted: false }; - } - return { status: 0, error: null, timedOut: false, interrupted: false }; + if (args.includes('worktree')) return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + // QNBS-v3: the pnpm install call -- fails, must map to UNKNOWN, and tsgo must never run. + return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; }, - symlinkFn: () => { - throw new Error('force UNKNOWN before any real tsgo invocation is attempted'); - }, - rmFn: async () => { - rmCalled = true; + runLocalBinaryDetailed: async () => { + throw new Error('must not be called -- install already failed'); }, }); assert.equal(state, 'UNKNOWN'); - assert.equal(removeCalled, true); - assert.equal(rmCalled, true); - // QNBS-v3: proactive prune before creating the worktree, plus the fallback prune after removal fails. - assert.equal(pruneCallCount, 2); + }); + + it('reports PASS/FAIL correctly from a genuine tsgo exit status once install succeeds (DI)', async () => { + const runBounded = () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }); + const pass = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded, + runLocalBinaryDetailed: async () => ({ + status: 0, + error: null, + signal: null, + timedOut: false, + interrupted: false, + }), + }); + assert.equal(pass, 'PASS'); + + const fail = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded, + runLocalBinaryDetailed: async () => ({ + status: 2, + error: null, + signal: null, + timedOut: false, + interrupted: false, + }), + }); + assert.equal(fail, 'FAIL'); + }); + + it('reports UNKNOWN, not FAIL, when tsgo is terminated by a signal (e.g. external OOM kill)', async () => { + const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), + runLocalBinaryDetailed: async () => ({ + status: null, + signal: 'SIGKILL', + error: null, + timedOut: false, + interrupted: false, + }), + }); + assert.equal(state, 'UNKNOWN'); + }); + + it('reports UNKNOWN, not FAIL, on a null status with no signal (defensive)', async () => { + const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), + runLocalBinaryDetailed: async () => ({ + status: null, + signal: null, + error: null, + timedOut: false, + interrupted: false, + }), + }); + assert.equal(state, 'UNKNOWN'); + }); + + it('canonicalizes a relative repoRoot to an absolute path before any git/install call', async () => { + const seenCwds = []; + await verifyExactTreeTypecheck('a'.repeat(40), '.', { + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, _args, options) => { + seenCwds.push(options?.cwd); + return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.ok(seenCwds.length > 0); + for (const cwd of seenCwds) assert.equal(cwd, resolve('.'), `expected absolute cwd, got ${cwd}`); + }); +}); + +describe('resolveRef (bounded, output-capturing ref resolution)', () => { + it('resolves a valid ref to its full commit SHA via the injected runGit', () => { + const sha = resolveRef('HEAD', '/repo', { + runGit: (args) => { + assert.deepEqual(args, ['rev-parse', '--verify', 'HEAD^{commit}']); + return { status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }; + }, + }); + assert.equal(sha, 'a'.repeat(40)); + }); + + it('returns null for a ref that does not resolve', () => { + const sha = resolveRef('not-a-ref', '/repo', { + runGit: () => ({ status: 128, stdout: '', stderr: 'fatal: bad revision', error: undefined }), + }); + assert.equal(sha, null); + }); + + // QNBS-v3: regression -- the CLI path must not crash when no dependencies argument is passed. + it('does not throw when called with only (ref, repoRoot), matching a bare call site', () => { + assert.doesNotThrow(() => resolveRef('HEAD', process.cwd())); }); }); describe('verifyExactTreeForShas (dedup, sequential, aggregation)', () => { it('deduplicates identical SHAs so the underlying check runs once', async () => { - const calls = []; + let calls = 0; const state = await verifyExactTreeForShas(['a'.repeat(40), 'a'.repeat(40)], '/repo', { - dependencyStateForRef: (sha) => { - calls.push(sha); - return 'MATCHES'; + mkdtempFn: async () => { + calls += 1; + return '/tmp/worldscript-exact-tree-fake'; }, - runBounded: () => ({ status: 0, error: null, timedOut: false, interrupted: false }), - symlinkFn: () => { - throw new Error('force UNKNOWN quickly -- this test only cares about call count'); + runBounded: (_command, args) => { + if (args.includes('add')) return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; }, }); - assert.equal(calls.length, 1); + assert.equal(calls, 1); assert.equal(state, 'UNKNOWN'); }); @@ -171,21 +315,110 @@ describe('verifyExactTreeForShas (dedup, sequential, aggregation)', () => { }); it('aggregates with FAIL outranking UNKNOWN', async () => { - const shaFail = 'a'.repeat(40); - const shaUnknown = 'b'.repeat(40); - const state = await verifyExactTreeForShas([shaFail, shaUnknown], '/repo', { - dependencyStateForRef: (sha) => (sha === shaUnknown ? 'DIVERGED' : 'MATCHES'), - runBounded: () => ({ status: 0, error: null, timedOut: false, interrupted: false }), + // QNBS-v3: sequential processing -- alternate by call order for one genuine FAIL, one UNKNOWN. + let tsgoCallCount = 0; + const state = await verifyExactTreeForShas(['a'.repeat(40), 'b'.repeat(40)], '/repo', { mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', - symlinkFn: () => {}, - runLocalBinaryDetailed: async () => ({ - status: 1, - error: null, - timedOut: false, - interrupted: false, - }), + runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), + runLocalBinaryDetailed: async () => { + tsgoCallCount += 1; + // QNBS-v3: first sha -> genuine FAIL (numeric non-zero exit); second sha -> UNKNOWN (signaled). + return tsgoCallCount === 1 + ? { status: 2, error: null, signal: null, timedOut: false, interrupted: false } + : { status: null, error: null, signal: 'SIGKILL', timedOut: false, interrupted: false }; + }, }); - // QNBS-v3: shaFail -> real FAIL via the mocked tsgo run; shaUnknown -> UNKNOWN via the precondition. + assert.equal(tsgoCallCount, 2); assert.equal(state, 'FAIL'); }); }); + +describe('main (real CLI entry path, realistic DI)', () => { + it('resolves HEAD by default, verifies it, and prints the result without crashing', async () => { + const logs = []; + const originalLog = console.log; + console.log = (message) => logs.push(message); + try { + await main([], { + repoRoot: '/repo', + runGit: () => ({ status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }), + runBounded: (_command, args) => { + if (args.includes('add')) return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + }); + } finally { + console.log = originalLog; + } + assert.ok(logs.some((line) => line.includes('result: UNKNOWN')), logs.join('\n')); + }); + + it('resolves and verifies multiple explicit refs', async () => { + const resolvedRefs = []; + const logs = []; + const originalLog = console.log; + console.log = (message) => logs.push(message); + try { + await main(['main', 'feature-branch'], { + repoRoot: '/repo', + runGit: (args) => { + resolvedRefs.push(args[2]); + return { status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }; + }, + runBounded: (_command, args) => { + if (args.includes('add')) return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + }); + } finally { + console.log = originalLog; + } + assert.deepEqual(resolvedRefs, ['main^{commit}', 'feature-branch^{commit}']); + assert.ok(logs.some((line) => line.includes('verifying 2 commit(s)')), logs.join('\n')); + }); + + // QNBS-v3: regression -- the real resolveRef(ref) call-site crash; helper-only tests missed main(). + it('reports a clear error and a non-zero exit code for an unresolvable ref, without crashing', async () => { + const errors = []; + const originalError = console.error; + console.error = (message) => errors.push(message); + const originalExitCode = process.exitCode; + process.exitCode = 0; + try { + await main(['not-a-real-ref'], { + repoRoot: '/repo', + runGit: () => ({ status: 128, stdout: '', stderr: 'fatal: bad revision', error: undefined }), + }); + assert.equal(process.exitCode, 1); + } finally { + console.error = originalError; + process.exitCode = originalExitCode; + } + assert.ok(errors.some((line) => line.includes('could not resolve ref: not-a-real-ref')), errors.join('\n')); + }); + + it('sets a non-zero exit code when the exact tree fails to typecheck', async () => { + const originalExitCode = process.exitCode; + process.exitCode = 0; + try { + await main(['HEAD'], { + repoRoot: '/repo', + runGit: () => ({ status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }), + runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runLocalBinaryDetailed: async () => ({ + status: 2, + error: null, + signal: null, + timedOut: false, + interrupted: false, + }), + }); + assert.equal(process.exitCode, 1); + } finally { + process.exitCode = originalExitCode; + } + }); +}); From 3f952e58885c36f66abfea7b69a43999e62bff89 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:30:00 +0200 Subject: [PATCH 3/9] fix(signing): harden exact-tree install against scripts, corepack, and a fixture flaw Consolidated remediation for the fresh #503 review epoch on feb85041. Security -- lifecycle scripts during arbitrary-ref verification (P1): pnpm install --frozen-lockfile --offline can still execute lifecycle scripts declared by the checked-out ref, with the developer's OS permissions. Since this tool exists specifically to verify refs a developer may not already trust (a colleague's branch, a suspicious commit), that's a real risk. Added --ignore-scripts. Verified empirically against this actual repository: the real tsgo proof still completes with a clean PASS (typechecking needs declared types, not built native binaries) -- confirmed via a real end-to-end run before applying the flag, and again after. Corepack offline gap: pnpm in this environment is itself a Corepack shim (confirmed: `which pnpm` resolves through corepack/dist/pnpm.js). Corepack can reach the network to resolve the requested package-manager version before handing off to pnpm, before pnpm's own --offline has any effect. Set COREPACK_ENABLE_NETWORK=0 in the bounded child's env to close that gap at the shim layer too; verified `pnpm --version` still works correctly with it set (the required version is already resolved locally). Windows pnpm launcher: added shell: process.platform === 'win32' to the pnpm invocation, matching the existing platform-aware pattern already used by runLocalBinaryDetailed for the same class of npm-installed Windows command shim -- no new process-runner authority, just the narrow platform adaptation on the existing bounded call. Bare-call defaults: createIsolatedWorktree, removeIsolatedWorktree, installDependencies, and pruneStaleWorktrees now default dependencies = {} like every other exported function in this module, closing the same class of bug as the original resolveRef crash for these now-exported lifecycle primitives. Critical test-validity fix -- the workspace-leak regression fixture was weakened: makeWorkspaceFixture() ran `pnpm install --offline` before `git init` + `git add -A` with no .gitignore, so the generated node_modules symlinks (both the root workspace link and the package-local, nested one) got committed into the fixture's own git history. Verified empirically: with that flaw, `git worktree add` alone -- with installDependencies() never actually invoked -- already produced a correctly-resolving symlink, via git's own relative-symlink preservation, completely independent of whether the code under test did anything. Fixed by adding a .gitignore (node_modules) to the fixture before staging, matching this real repository's own convention. Reverified: the fixture now commits only source, manifests, and pnpm-lock.yaml; the isolated worktree checkout starts with zero dependency links, so installDependencies() is now the only possible source of the correctly-resolved links the test asserts on -- the regression is now genuinely load-bearing. Housekeeping: - .gitignore: added .worldscript-* (matching the existing .storycraft-* precedent) -- test fixture temp roots created under cwd by dependency-state/signing/verify-exact-tree tests were not previously ignored, so an interrupted run could leave them visible to git status and local admission tooling. - CLAUDE.md: the exact-tree verification paragraph still described the original, superseded live-node_modules-symlink design; rewritten to match the actual pnpm-materialization architecture, including the --ignore-scripts/Corepack-offline rationale and the test:node CI step. - package.json: test:node now lists both node:test files explicitly instead of a shell glob (tests/unit/tooling/*.test.mjs), which is not reliably expanded by every shell (notably cmd.exe on Windows). Validation: full test:node suite (35/35), 4-checker typecheck, lint, ci:prepush, and a second real end-to-end smoke run against this actual repository with all of the above applied together -- genuine PASS, confirmed leftover-free cleanup (no registered worktree, no temp directory) afterward. --- .gitignore | 3 +++ CLAUDE.md | 2 +- package.json | 2 +- scripts/verify-exact-tree.mjs | 24 ++++++++++++------- tests/unit/tooling/verify-exact-tree.test.mjs | 2 ++ 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 2f9167382..057f61fa3 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,9 @@ voice-spike/ *.crt *.der .storycraft-* +# Test fixture temp roots created under cwd by dependency-state/signing/verify-exact-tree tests -- +# normally cleaned up in afterEach, but an interrupted run can leave these visible to git status. +.worldscript-* ~/ .playwright-mcp/ diff --git a/CLAUDE.md b/CLAUDE.md index 4ec3dd739..15f9b9ec4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,7 +45,7 @@ pnpm run token:audit # audit-tokens.mjs — design-token usage gate (CI b **Quality gate (local pre-push subset):** `pnpm run ci:prepush` runs dependency-state/docs/CSP/native-readiness checks unconditionally (never full-repository lint — see the pre-push gate note above for what runs lint locally), and the single-checker local typecheck and i18n/content-guard checks only for changes the classifier marks as potentially impacting them (fail-closed to "run everything conditional" when evidence is incomplete); CI additionally runs full-repository lint, the 4-checker typecheck, full-suite coverage, and heavy jobs regardless of what the local gate ran. Locally use only the targeted form `pnpm exec vitest run --coverage` when debugging coverage. Full pipeline graph: [`docs/CI.md`](docs/CI.md). Coverage thresholds: lines 74, branches 60, functions 67, statements 72 (see `vitest.config.ts`). -**Exact-tree typecheck verification (opt-in, not part of `ci:prepush`):** `pnpm run verify:exact-tree [ref...]` (default `HEAD`) creates an isolated `git worktree` at the exact commit, symlinks in the real `node_modules` (skipped — reports `UNKNOWN` — when `scripts/dependency-state.mjs`'s reconciled fingerprint doesn't match that commit's manifests, since the symlink would then misrepresent the commit's real dependency graph), and runs the same single-checker `tsgo --noEmit` there. This closes the gap where `ci-prepush-lowend.mjs`'s normal typecheck runs against whatever is currently on disk, not necessarily the exact tree about to be pushed. Deliberately **not** wired into the default `ci:prepush` path — it roughly doubles local typecheck cost, unacceptable as an always-on default on this hardware; run it manually before a risky push or when investigating a CI/local typecheck mismatch. Diagnostic-only: reports `PASS | FAIL | NOT_APPLICABLE | UNKNOWN` and never blocks a push on its own; required CI remains the sole merge-safety authority regardless of its result. +**Exact-tree typecheck verification (opt-in, not part of `ci:prepush`):** `pnpm run verify:exact-tree [ref...]` (default `HEAD`) creates an isolated `git worktree` at the exact commit, runs a real `pnpm install --frozen-lockfile --offline --ignore-scripts` there (`COREPACK_ENABLE_NETWORK=0` set too, since `pnpm` itself is a Corepack shim that could otherwise reach the network before pnpm's own `--offline` takes effect), and runs the same single-checker `tsgo --noEmit` inside that isolated tree. Reconstructs pnpm's own real dependency-resolution graph (root, package-local, and transitive workspace links included) rather than symlinking the live checkout's `node_modules` — an earlier symlink-based design was found, in review, to leak uncommitted/live workspace-package source back into the "isolated" result. `--ignore-scripts`: verifying an arbitrary ref must never execute that ref's lifecycle scripts with developer OS permissions; confirmed empirically that this repo's real typecheck still passes without them. A missing package in the local offline store fails the install and reports `UNKNOWN`, never a silent wrong answer — this tool never falls back to the network. Closes the gap where `ci-prepush-lowend.mjs`'s normal typecheck runs against whatever is currently on disk, not necessarily the exact tree about to be pushed. Deliberately **not** wired into the default `ci:prepush` path — the real install alone measures over a minute on this hardware, unacceptable as an always-on default; run it manually before a risky push or when investigating a CI/local typecheck mismatch. Diagnostic-only: reports `PASS | FAIL | NOT_APPLICABLE | UNKNOWN` and never blocks a push on its own; required CI remains the sole merge-safety authority regardless of its result. Its `node:test` suite (`scripts/verify-exact-tree.mjs`'s own tooling test, alongside `scripts/dependency-state.mjs`'s) runs via `pnpm run test:node`, wired into CI's quality job separately from Vitest (whose include glob is `.ts`/`.tsx`-only). **CI pipeline order:** `security` → `quality` (Biome + tsgo + Vitest matrix) → `build` / `e2e` / `storybook` (parallel) → `lighthouse` (after build) → `deploy` on `main`. `ci-success` is a required-status aggregator (`needs: [security, quality, build]`) so branch protection can require one context instead of three/four individual ones — see `docs/CI.md`. Two additional jobs run in parallel with `quality`, both path-scoped via the `changes` job (legitimately `skipping` on PRs that don't touch their directory, which `ci-success` treats as a pass for that job only): `rust-tauri` (`src-tauri/**` — fmt/check/clippy/test, needs the GTK/WebKit apt-get steps) and `core-rust` (`crates/**` — same fmt/check/clippy/test for the renderer-neutral Rust Core, no GUI deps so no apt-get steps needed). diff --git a/package.json b/package.json index 7875922e8..49862381e 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "test:turbo": "turbo run test", "test:run": "vitest run", "test:coverage": "vitest run --coverage", - "test:node": "node --test tests/unit/tooling/*.test.mjs", + "test:node": "node --test tests/unit/tooling/dependency-state.test.mjs tests/unit/tooling/verify-exact-tree.test.mjs", "bench": "vitest bench --run tests/bench", "test:e2e": "node -e \"if (process.env.CI !== 'true') { console.error('E2E tests are CI-only. Set CI=true or run via GitHub Actions.'); process.exit(1); }\" && playwright test", "test:e2e:ui": "node -e \"if (process.env.CI !== 'true') { console.error('E2E tests are CI-only. Set CI=true or run via GitHub Actions.'); process.exit(1); }\" && playwright test --ui", diff --git a/scripts/verify-exact-tree.mjs b/scripts/verify-exact-tree.mjs index a0c446cd2..cd1d9dbd6 100644 --- a/scripts/verify-exact-tree.mjs +++ b/scripts/verify-exact-tree.mjs @@ -25,12 +25,12 @@ function tsgoResultUnknown(result) { } // QNBS-v3: sweeps entries orphaned by a prior crashed/killed run before creating a new one. -async function pruneStaleWorktrees(repoRoot, dependencies) { +async function pruneStaleWorktrees(repoRoot, dependencies = {}) { const runGit = dependencies.runBounded ?? runBounded; await runGit('git', ['worktree', 'prune'], { cwd: repoRoot }); } -export async function createIsolatedWorktree(sha, repoRoot, dependencies) { +export async function createIsolatedWorktree(sha, repoRoot, dependencies = {}) { const runGit = dependencies.runBounded ?? runBounded; const makeTempDir = dependencies.mkdtempFn ?? (() => mkdtempAsync(join(tmpdir(), 'worldscript-exact-tree-'))); @@ -47,7 +47,7 @@ export async function createIsolatedWorktree(sha, repoRoot, dependencies) { } // QNBS-v3: fail-closed -- git's own removal failing falls back to a raw sweep plus a metadata prune. -export async function removeIsolatedWorktree(worktreePath, repoRoot, dependencies) { +export async function removeIsolatedWorktree(worktreePath, repoRoot, dependencies = {}) { if (!worktreePath) return; const runGit = dependencies.runBounded ?? runBounded; const removeDir = dependencies.rmFn ?? ((path) => rmAsync(path, { recursive: true, force: true })); @@ -72,13 +72,21 @@ export async function removeIsolatedWorktree(worktreePath, repoRoot, dependencie } // QNBS-v3: real pnpm install, not a hand-reconstructed symlink graph -- offline, fails to UNKNOWN below. -export async function installDependencies(worktreePath, dependencies) { +export async function installDependencies(worktreePath, dependencies = {}) { const runPnpm = dependencies.runBounded ?? runBounded; const timeoutMs = dependencies.installTimeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS; - const result = await runPnpm('pnpm', ['install', '--frozen-lockfile', '--offline'], { - cwd: worktreePath, - timeoutMs, - }); + const result = await runPnpm( + 'pnpm', + // QNBS-v3: verifying an arbitrary ref must never run that ref's lifecycle scripts as the developer. + ['install', '--frozen-lockfile', '--offline', '--ignore-scripts'], + { + cwd: worktreePath, + timeoutMs, + shell: process.platform === 'win32', + // QNBS-v3: pnpm here is a Corepack shim, which can itself reach the network before --offline applies. + env: { COREPACK_ENABLE_NETWORK: '0' }, + }, + ); return !boundedCommandFailed(result); } diff --git a/tests/unit/tooling/verify-exact-tree.test.mjs b/tests/unit/tooling/verify-exact-tree.test.mjs index 6214707c7..25f4dc1c9 100644 --- a/tests/unit/tooling/verify-exact-tree.test.mjs +++ b/tests/unit/tooling/verify-exact-tree.test.mjs @@ -64,6 +64,8 @@ function makeWorkspaceFixture(innerContent) { '{"name":"@fixture/inner-pkg","version":"1.0.0"}\n', ); writeFileSync(join(root, 'packages', 'inner-pkg', 'index.js'), `export const INNER = '${innerContent}';\n`); + // QNBS-v3: load-bearing -- without this, git add -A commits the symlinks and a bare checkout alone would pass. + writeFileSync(join(root, '.gitignore'), 'node_modules\n'); // QNBS-v3: generates a real, valid lockfile for this fixture -- workspace-only, no network needed. execFileSync('pnpm', ['install', '--offline'], { cwd: root, stdio: 'ignore' }); git(root, ['init', '--quiet', '--initial-branch=main']); From b09591f9b789087f1b8b063a995f0b4cc00444f3 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:37:53 +0200 Subject: [PATCH 4/9] test(signing): add bare-call regressions for the exact-tree lifecycle helpers Follow-up requested by both CodeRabbit and chatgpt-codex-connector on the dependencies = {} default fix in 3f952e58: add regression tests proving createIsolatedWorktree, removeIsolatedWorktree, and installDependencies (and pruneStaleWorktrees, exercised transitively through createIsolatedWorktree) don't throw when called without a dependencies argument, mirroring the existing resolveRef bare-call test. --- tests/unit/tooling/verify-exact-tree.test.mjs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/unit/tooling/verify-exact-tree.test.mjs b/tests/unit/tooling/verify-exact-tree.test.mjs index 25f4dc1c9..4aca9ebad 100644 --- a/tests/unit/tooling/verify-exact-tree.test.mjs +++ b/tests/unit/tooling/verify-exact-tree.test.mjs @@ -114,6 +114,34 @@ describe('createIsolatedWorktree / removeIsolatedWorktree (real git, no pnpm)', }); }); +describe('bare-call regressions (dependencies argument omitted, matching resolveRef)', () => { + it('createIsolatedWorktree does not throw when called without a dependencies argument', async () => { + const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); + const created = await createIsolatedWorktree(sha, root); + assert.equal(created.ok, true); + await removeIsolatedWorktree(created.path, root); + }); + + it('removeIsolatedWorktree does not throw when called without a dependencies argument', async () => { + const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); + const created = await createIsolatedWorktree(sha, root, {}); + await assert.doesNotReject(() => removeIsolatedWorktree(created.path, root)); + }); + + it('installDependencies does not throw when called without a dependencies argument', async () => { + // QNBS-v3: an unreachable repoRoot fails fast, but the call itself must not throw on undefined deps. + await assert.doesNotReject(() => installDependencies('/tmp/worldscript-exact-tree-nonexistent')); + }); + + it('pruneStaleWorktrees (via createIsolatedWorktree) does not throw with dependencies omitted', async () => { + const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); + // QNBS-v3: createIsolatedWorktree forwards dependencies to pruneStaleWorktrees internally. + const created = await createIsolatedWorktree(sha, root); + assert.equal(created.ok, true); + await removeIsolatedWorktree(created.path, root); + }); +}); + describe('installDependencies (workspace-package soundness -- the core regression)', () => { it('resolves workspace packages to the isolated worktree\'s committed source, never the live checkout, including a transitive package-local link', async () => { const { root, sha } = makeWorkspaceFixture('committed'); From 41816ad373a68861d6e71cc52664055037335c34 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:24:31 +0200 Subject: [PATCH 5/9] fix: refuse tracked node_modules and disable hooks in exact-tree verification Three further trust-boundary gaps in the isolated exact-tree materialization path, found and validated empirically in this review epoch: - An arbitrary ref could force-track a node_modules path (root, nested, or a tracked node_modules symlink itself) even under .gitignore; the previously real-pnpm-install fix alone did not clear pre-existing tracked content, so a malicious node_modules/.bin/tsgo could execute or a symlink could redirect writes outside the worktree. verify-exact-tree.mjs now lists the target commit's own git tree (reusing dependency-state.mjs's listTreeFiles, now exported, instead of a second parser) and refuses -- UNKNOWN, never touching disk -- any commit tracking a node_modules path anywhere. - git worktree add can run a configured post-checkout hook (this repo ships a graphify:hooks post-checkout integration); createIsolatedWorktree now scopes core.hooksPath to a temporary empty directory for that one invocation only, never touching the user's real git config, cleaned up immediately after. - .pnpmfile.cjs executes pnpm's own hook mechanism, separate from --ignore-scripts; installDependencies now also passes --ignore-pnpmfile. - The isolated tsgo run's timeout is raised from 3 to 6 minutes -- the repo separately documents ~300s for the full multi-checker typecheck, so the single-checker isolated run now clears that figure with real margin, not a bare cutoff at it. - tests/unit/tooling/verify-exact-tree.test.mjs: adversarial regressions for all of the above (tracked node_modules in three forms, each proven via a call-count spy that materialization never starts, not just via the final UNKNOWN state; a real post-checkout hook proven not to fire; a real side-effecting .pnpmfile.cjs proven not to execute even at require time). test:node now runs with --test-concurrency=1 so these fixtures' git/pnpm/ worktree operations never overlap on this hardware. Real end-to-end smoke test against this repo's own HEAD (isolated worktree, hook-free, real frozen-lockfile offline install, real tsgo) still PASSes under the fully remediated path. --- CLAUDE.md | 2 +- package.json | 2 +- scripts/dependency-state.d.mts | 1 + scripts/dependency-state.mjs | 6 +- scripts/verify-exact-tree.d.mts | 5 + scripts/verify-exact-tree.mjs | 59 ++++++- tests/unit/tooling/verify-exact-tree.test.mjs | 167 +++++++++++++++++- 7 files changed, 231 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 15f9b9ec4..5f5418791 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,7 +45,7 @@ pnpm run token:audit # audit-tokens.mjs — design-token usage gate (CI b **Quality gate (local pre-push subset):** `pnpm run ci:prepush` runs dependency-state/docs/CSP/native-readiness checks unconditionally (never full-repository lint — see the pre-push gate note above for what runs lint locally), and the single-checker local typecheck and i18n/content-guard checks only for changes the classifier marks as potentially impacting them (fail-closed to "run everything conditional" when evidence is incomplete); CI additionally runs full-repository lint, the 4-checker typecheck, full-suite coverage, and heavy jobs regardless of what the local gate ran. Locally use only the targeted form `pnpm exec vitest run --coverage` when debugging coverage. Full pipeline graph: [`docs/CI.md`](docs/CI.md). Coverage thresholds: lines 74, branches 60, functions 67, statements 72 (see `vitest.config.ts`). -**Exact-tree typecheck verification (opt-in, not part of `ci:prepush`):** `pnpm run verify:exact-tree [ref...]` (default `HEAD`) creates an isolated `git worktree` at the exact commit, runs a real `pnpm install --frozen-lockfile --offline --ignore-scripts` there (`COREPACK_ENABLE_NETWORK=0` set too, since `pnpm` itself is a Corepack shim that could otherwise reach the network before pnpm's own `--offline` takes effect), and runs the same single-checker `tsgo --noEmit` inside that isolated tree. Reconstructs pnpm's own real dependency-resolution graph (root, package-local, and transitive workspace links included) rather than symlinking the live checkout's `node_modules` — an earlier symlink-based design was found, in review, to leak uncommitted/live workspace-package source back into the "isolated" result. `--ignore-scripts`: verifying an arbitrary ref must never execute that ref's lifecycle scripts with developer OS permissions; confirmed empirically that this repo's real typecheck still passes without them. A missing package in the local offline store fails the install and reports `UNKNOWN`, never a silent wrong answer — this tool never falls back to the network. Closes the gap where `ci-prepush-lowend.mjs`'s normal typecheck runs against whatever is currently on disk, not necessarily the exact tree about to be pushed. Deliberately **not** wired into the default `ci:prepush` path — the real install alone measures over a minute on this hardware, unacceptable as an always-on default; run it manually before a risky push or when investigating a CI/local typecheck mismatch. Diagnostic-only: reports `PASS | FAIL | NOT_APPLICABLE | UNKNOWN` and never blocks a push on its own; required CI remains the sole merge-safety authority regardless of its result. Its `node:test` suite (`scripts/verify-exact-tree.mjs`'s own tooling test, alongside `scripts/dependency-state.mjs`'s) runs via `pnpm run test:node`, wired into CI's quality job separately from Vitest (whose include glob is `.ts`/`.tsx`-only). +**Exact-tree typecheck verification (opt-in, not part of `ci:prepush`):** `pnpm run verify:exact-tree [ref...]` (default `HEAD`) proves the *exact committed tree* of a ref typechecks in full isolation from the live checkout. Before touching disk, it lists the target commit's own git tree (`dependency-state.mjs`'s `listTreeFiles`, reused rather than a second parser) and refuses (`UNKNOWN`) any commit force-tracking a `node_modules` path anywhere (root, nested, or a tracked `node_modules` symlink itself) — an arbitrary ref could otherwise smuggle in an attacker-controlled `node_modules/.bin/tsgo` for the tool to trust and execute. It then creates an isolated `git worktree` at the exact commit with Git hooks disabled for that one invocation (`git -c core.hooksPath= worktree add --detach`, cleaned up immediately after) — this repo's `graphify:hooks` `post-checkout` integration, or any other configured hook, must never fire during materialization. Inside that hook-free worktree it runs a real `pnpm install --frozen-lockfile --offline --ignore-scripts --ignore-pnpmfile` (`COREPACK_ENABLE_NETWORK=0` too, since `pnpm` here is a Corepack shim that could otherwise reach the network before pnpm's own `--offline` applies; `--ignore-pnpmfile` blocks pnpm's own hook-file mechanism, a separate arbitrary-code path from `--ignore-scripts`), reconstructing pnpm's own real dependency-resolution graph (root, package-local, and transitive workspace links) rather than symlinking the live checkout's `node_modules` — an earlier symlink-based design was found, in review, to leak uncommitted/live workspace-package source back into the "isolated" result. Finally it runs the same single-checker `tsgo --noEmit` inside that tree with an explicit 6-minute timeout (measured the single-checker run alone at ~56s here; the repo separately documents ~300s for the full multi-checker `pnpm run typecheck`, so 6 minutes clears both figures with real margin, not a bare 300s cutoff). A missing package in the local offline store, an unreadable tree, a hook-dir failure, or a signal/timeout always fails the install/typecheck step and reports `UNKNOWN`, never a silent wrong answer — this tool never falls back to the network and never treats an unprovable state as a pass. Closes the gap where `ci-prepush-lowend.mjs`'s normal typecheck runs against whatever is currently on disk, not necessarily the exact tree about to be pushed. Deliberately **not** wired into the default `ci:prepush` path — the real install alone measures over a minute on this hardware, unacceptable as an always-on default; run it manually before a risky push or when investigating a CI/local typecheck mismatch. Diagnostic-only: reports `PASS | FAIL | NOT_APPLICABLE | UNKNOWN` and never blocks a push on its own; required CI remains the sole merge-safety authority regardless of its result. Its `node:test` suite (`scripts/verify-exact-tree.mjs`'s own tooling test, alongside `scripts/dependency-state.mjs`'s) runs serially (`--test-concurrency=1`, since these fixtures interleave git/pnpm/worktree operations that must not overlap on this hardware) via `pnpm run test:node`, wired into CI's quality job separately from Vitest (whose include glob is `.ts`/`.tsx`-only). **CI pipeline order:** `security` → `quality` (Biome + tsgo + Vitest matrix) → `build` / `e2e` / `storybook` (parallel) → `lighthouse` (after build) → `deploy` on `main`. `ci-success` is a required-status aggregator (`needs: [security, quality, build]`) so branch protection can require one context instead of three/four individual ones — see `docs/CI.md`. Two additional jobs run in parallel with `quality`, both path-scoped via the `changes` job (legitimately `skipping` on PRs that don't touch their directory, which `ci-success` treats as a pass for that job only): `rust-tauri` (`src-tauri/**` — fmt/check/clippy/test, needs the GTK/WebKit apt-get steps) and `core-rust` (`crates/**` — same fmt/check/clippy/test for the renderer-neutral Rust Core, no GUI deps so no apt-get steps needed). diff --git a/package.json b/package.json index 49862381e..85ef383db 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "test:turbo": "turbo run test", "test:run": "vitest run", "test:coverage": "vitest run --coverage", - "test:node": "node --test tests/unit/tooling/dependency-state.test.mjs tests/unit/tooling/verify-exact-tree.test.mjs", + "test:node": "node --test --test-concurrency=1 tests/unit/tooling/dependency-state.test.mjs tests/unit/tooling/verify-exact-tree.test.mjs", "bench": "vitest bench --run tests/bench", "test:e2e": "node -e \"if (process.env.CI !== 'true') { console.error('E2E tests are CI-only. Set CI=true or run via GitHub Actions.'); process.exit(1); }\" && playwright test", "test:e2e:ui": "node -e \"if (process.env.CI !== 'true') { console.error('E2E tests are CI-only. Set CI=true or run via GitHub Actions.'); process.exit(1); }\" && playwright test --ui", diff --git a/scripts/dependency-state.d.mts b/scripts/dependency-state.d.mts index ae227695a..cc63e1c80 100644 --- a/scripts/dependency-state.d.mts +++ b/scripts/dependency-state.d.mts @@ -1,6 +1,7 @@ // QNBS-v3: diagnostic-only dimension, independent of resolvePushEvidence's canonical evidence validity. export type DependencyState = 'MATCHES' | 'DIVERGED' | 'NOT_APPLICABLE' | 'UNKNOWN'; +export function listTreeFiles(sha: string, cwd?: string): string[] | null; export function dependencyFiles(root?: string): string[]; export function calculateDependencyFingerprint(root?: string): string; export function fingerprintPath(root?: string): string; diff --git a/scripts/dependency-state.mjs b/scripts/dependency-state.mjs index 26df2c6e3..803812289 100644 --- a/scripts/dependency-state.mjs +++ b/scripts/dependency-state.mjs @@ -76,8 +76,8 @@ export function calculateDependencyFingerprint(root = projectRoot) { return hashManifests(entries); } -// QNBS-v3: --full-tree ignores cwd-subdirectory scoping; -z disables git's default path C-quoting. -function defaultListTreeFiles(sha, cwd) { +// QNBS-v3: -z avoids path C-quoting; exported so verify-exact-tree.mjs reuses this, not a second parser. +export function listTreeFiles(sha, cwd) { const result = spawnSync('git', ['ls-tree', '-r', '--full-tree', '--name-only', '-z', sha], { cwd, encoding: 'utf8', @@ -89,7 +89,7 @@ function defaultListTreeFiles(sha, cwd) { // QNBS-v3: diagnostic-only; mirrors dependencyFiles' inclusion rules against a commit, not disk. export function dependencyFilesFromRef(sha, root = projectRoot, dependencies = {}) { - const listTree = dependencies.listTree ?? ((ref) => defaultListTreeFiles(ref, root)); + const listTree = dependencies.listTree ?? ((ref) => listTreeFiles(ref, root)); const allPaths = listTree(sha); if (allPaths === null) return null; const rootFiles = new Set(['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']); diff --git a/scripts/verify-exact-tree.d.mts b/scripts/verify-exact-tree.d.mts index e98eb1d51..5a8785bf8 100644 --- a/scripts/verify-exact-tree.d.mts +++ b/scripts/verify-exact-tree.d.mts @@ -26,10 +26,15 @@ export interface VerifyExactTreeDependencies { // QNBS-v3: a distinct, pre-existing (#494) synchronous/output-capturing wrapper -- not BoundedResult. runGit?: (args: string[], options?: GitOptions) => GitResult; mkdtempFn?: () => Promise; + // QNBS-v3: a separate temp dir authority from mkdtempFn -- distinct lifecycle (hooks dir vs. worktree dir). + mkdtempHooksFn?: () => Promise; rmFn?: (path: string) => Promise; installTimeoutMs?: number; tsgoArgs?: string[]; + tsgoTimeoutMs?: number; repoRoot?: string; + // QNBS-v3: reuses dependency-state.mjs's git-tree enumeration authority -- not a second parser. + listTreeFiles?: (sha: string, cwd: string) => string[] | null; } export function createIsolatedWorktree( diff --git a/scripts/verify-exact-tree.mjs b/scripts/verify-exact-tree.mjs index cd1d9dbd6..0b1c3e67f 100644 --- a/scripts/verify-exact-tree.mjs +++ b/scripts/verify-exact-tree.mjs @@ -3,12 +3,15 @@ import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import process from 'node:process'; import { isMainModule } from './ci-prepush-range-resolver.mjs'; +import { listTreeFiles } from './dependency-state.mjs'; import { runBounded, runLocalBinaryDetailed } from './hooks/shared.mjs'; import { runGit as defaultRunGit } from './signing/signing-core.mjs'; const DEFAULT_TSGO_ARGS = ['--project', 'tsconfig.tsgo.json', '--noEmit', '--checkers', '1']; // QNBS-v3: measured ~2m20s for the full project on this hardware with a warm store; 5min gives margin. const DEFAULT_INSTALL_TIMEOUT_MS = 300_000; +// QNBS-v3: single-checker measured ~56s; docs cite ~300s for full typecheck -- 6min clears both with margin. +const DEFAULT_TSGO_TIMEOUT_MS = 360_000; // QNBS-v3: lifecycle commands (worktree/install) -- any non-zero or unreadable exit fails outright. function boundedCommandFailed(result) { @@ -30,10 +33,32 @@ async function pruneStaleWorktrees(repoRoot, dependencies = {}) { await runGit('git', ['worktree', 'prune'], { cwd: repoRoot }); } +// QNBS-v3: an arbitrary ref can force-track a node_modules path anywhere; could execute/redirect. Fail closed. +function hasTrackedNodeModules(paths) { + return paths.some((path) => path.split('/').includes('node_modules')); +} + +// QNBS-v3: checked against the exact commit's own git objects, before any worktree/pnpm step touches disk. +function verifyNoTrackedNodeModules(sha, repoRoot, dependencies = {}) { + const listTree = dependencies.listTreeFiles ?? listTreeFiles; + const paths = listTree(sha, repoRoot); + if (paths === null) return false; // an unreadable tree can never be proven clean. + return !hasTrackedNodeModules(paths); +} + +// QNBS-v3: hook-free dir so 'git worktree add' can't run repo/user hooks -- scoped via -c, not global config. +async function createEmptyHooksDir(dependencies = {}) { + const makeHooksDir = + dependencies.mkdtempHooksFn ?? + (() => mkdtempAsync(join(tmpdir(), 'worldscript-exact-tree-hooks-'))); + return makeHooksDir(); +} + export async function createIsolatedWorktree(sha, repoRoot, dependencies = {}) { const runGit = dependencies.runBounded ?? runBounded; const makeTempDir = dependencies.mkdtempFn ?? (() => mkdtempAsync(join(tmpdir(), 'worldscript-exact-tree-'))); + const removeDir = dependencies.rmFn ?? ((p) => rmAsync(p, { recursive: true, force: true })); await pruneStaleWorktrees(repoRoot, dependencies); let path; try { @@ -41,9 +66,28 @@ export async function createIsolatedWorktree(sha, repoRoot, dependencies = {}) { } catch { return { ok: false, path: undefined }; } - const result = await runGit('git', ['worktree', 'add', '--detach', path, sha], { cwd: repoRoot }); - if (boundedCommandFailed(result)) return { ok: false, path }; - return { ok: true, path }; + + let hooksDir; + try { + hooksDir = await createEmptyHooksDir(dependencies); + } catch { + return { ok: false, path }; + } + try { + const result = await runGit( + 'git', + ['-c', `core.hooksPath=${hooksDir}`, 'worktree', 'add', '--detach', path, sha], + { cwd: repoRoot }, + ); + if (boundedCommandFailed(result)) return { ok: false, path }; + return { ok: true, path }; + } finally { + try { + await removeDir(hooksDir); + } catch { + // Best-effort: nothing more can be done from here; the directory is under os.tmpdir(). + } + } } // QNBS-v3: fail-closed -- git's own removal failing falls back to a raw sweep plus a metadata prune. @@ -77,8 +121,8 @@ export async function installDependencies(worktreePath, dependencies = {}) { const timeoutMs = dependencies.installTimeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS; const result = await runPnpm( 'pnpm', - // QNBS-v3: verifying an arbitrary ref must never run that ref's lifecycle scripts as the developer. - ['install', '--frozen-lockfile', '--offline', '--ignore-scripts'], + // QNBS-v3: verifying an arbitrary ref must never run that ref's scripts or .pnpmfile.cjs hooks. + ['install', '--frozen-lockfile', '--offline', '--ignore-scripts', '--ignore-pnpmfile'], { cwd: worktreePath, timeoutMs, @@ -104,6 +148,9 @@ export async function verifyExactTreeTypecheck(sha, repoRoot = process.cwd(), de // QNBS-v3: a relative repoRoot would produce ambiguous git-cwd and install-cwd semantics. const absoluteRepoRoot = resolve(repoRoot); try { + // QNBS-v3: refuse before any materialization -- a tracked node_modules must never reach pnpm/tsgo. + if (!verifyNoTrackedNodeModules(sha, absoluteRepoRoot, dependencies)) return 'UNKNOWN'; + const created = await createIsolatedWorktree(sha, absoluteRepoRoot, dependencies); if (!created.ok) { await removeIsolatedWorktree(created.path, absoluteRepoRoot, dependencies); @@ -115,9 +162,11 @@ export async function verifyExactTreeTypecheck(sha, repoRoot = process.cwd(), de const runDetailed = dependencies.runLocalBinaryDetailed ?? runLocalBinaryDetailed; const tsgoArgs = dependencies.tsgoArgs ?? DEFAULT_TSGO_ARGS; + const tsgoTimeoutMs = dependencies.tsgoTimeoutMs ?? DEFAULT_TSGO_TIMEOUT_MS; const result = await runDetailed('tsgo', tsgoArgs, { root: created.path, cwd: created.path, + timeoutMs: tsgoTimeoutMs, }); // QNBS-v3: a signal (including an external OOM kill) must yield UNKNOWN, never a false FAIL. if (tsgoResultUnknown(result)) return 'UNKNOWN'; diff --git a/tests/unit/tooling/verify-exact-tree.test.mjs b/tests/unit/tooling/verify-exact-tree.test.mjs index 4aca9ebad..504a67969 100644 --- a/tests/unit/tooling/verify-exact-tree.test.mjs +++ b/tests/unit/tooling/verify-exact-tree.test.mjs @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import { execFileSync } from 'node:child_process'; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { afterEach, describe, it } from 'node:test'; import { @@ -185,6 +185,31 @@ describe('installDependencies (workspace-package soundness -- the core regressio } }); + it('never executes a side-effecting .pnpmfile.cjs, even its top-level (require-time) code', async () => { + const { root } = makeWorkspaceFixture('committed'); + const markerRoot = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-pnpmfilemark-')); + temporaryRoots.push(markerRoot); + const markerFile = join(markerRoot, 'pnpmfile-ran.txt'); + // QNBS-v3: top-level (require-time) side effect -- proves --ignore-pnpmfile stops it before any hook fires. + writeFileSync( + join(root, '.pnpmfile.cjs'), + `require('fs').writeFileSync(${JSON.stringify(markerFile)}, 'executed');\nmodule.exports = { hooks: {} };\n`, + ); + git(root, ['add', '-A']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'add pnpmfile']); + const pnpmfileSha = git(root, ['rev-parse', 'HEAD']).trim(); + + const created = await createIsolatedWorktree(pnpmfileSha, root, {}); + assert.equal(created.ok, true); + try { + const installed = await installDependencies(created.path, {}); + assert.equal(installed, true); + assert.equal(existsSync(markerFile), false, '.pnpmfile.cjs must not have executed'); + } finally { + await removeIsolatedWorktree(created.path, root, {}); + } + }); + it('returns false (mapped to UNKNOWN by callers) when the frozen-lockfile install fails', async () => { const installed = await installDependencies('/tmp/worldscript-exact-tree-fake', { runBounded: async () => ({ @@ -202,6 +227,7 @@ describe('installDependencies (workspace-package soundness -- the core regressio describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semantics -- DI only)', () => { it('reports UNKNOWN when git worktree add fails', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], runBounded: (_command, args) => { if (args.includes('add')) return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; @@ -213,6 +239,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('reports UNKNOWN, never a false PASS/FAIL, when the install fails', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: (_command, args) => { if (args.includes('worktree')) return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; @@ -229,6 +256,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('reports PASS/FAIL correctly from a genuine tsgo exit status once install succeeds (DI)', async () => { const runBounded = () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }); const pass = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded, runLocalBinaryDetailed: async () => ({ @@ -242,6 +270,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti assert.equal(pass, 'PASS'); const fail = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded, runLocalBinaryDetailed: async () => ({ @@ -257,6 +286,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('reports UNKNOWN, not FAIL, when tsgo is terminated by a signal (e.g. external OOM kill)', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), runLocalBinaryDetailed: async () => ({ @@ -272,6 +302,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('reports UNKNOWN, not FAIL, on a null status with no signal (defensive)', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), runLocalBinaryDetailed: async () => ({ @@ -285,9 +316,42 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti assert.equal(state, 'UNKNOWN'); }); + it('passes a generous default timeoutMs to the tsgo call, overridable via dependencies', async () => { + const runBounded = () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }); + let seenTimeoutMs; + await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded, + runLocalBinaryDetailed: async (_binary, _args, options) => { + seenTimeoutMs = options?.timeoutMs; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + // QNBS-v3: repo docs cite ~300s for the full multi-checker typecheck; the default must clear that too. + assert.ok(seenTimeoutMs >= 300_000, `expected margin above the documented ~300s figure, got ${seenTimeoutMs}`); + + let overriddenTimeoutMs; + await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded, + tsgoTimeoutMs: 42_000, + runLocalBinaryDetailed: async (_binary, _args, options) => { + overriddenTimeoutMs = options?.timeoutMs; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.equal(overriddenTimeoutMs, 42_000); + }); + it('canonicalizes a relative repoRoot to an absolute path before any git/install call', async () => { const seenCwds = []; await verifyExactTreeTypecheck('a'.repeat(40), '.', { + listTreeFiles: (_sha, cwd) => { + seenCwds.push(cwd); + return []; + }, mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: (_command, _args, options) => { seenCwds.push(options?.cwd); @@ -299,6 +363,102 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti }); }); +describe('verifyNoTrackedNodeModules (P1: refuse before any materialization touches disk)', () => { + function makeRepoWithTrackedPath(relativePath, { symlink } = {}) { + const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-nm-')); + temporaryRoots.push(root); + git(root, ['init', '--quiet', '--initial-branch=main']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + writeFileSync(join(root, 'README.md'), 'fixture\n'); + const fullPath = join(root, relativePath); + mkdirSync(join(fullPath, '..'), { recursive: true }); + if (symlink) { + execFileSync('ln', ['-s', '/nonexistent-target', fullPath]); + } else { + writeFileSync(fullPath, '#!/bin/sh\necho attacker-controlled\n'); + } + git(root, ['add', '-A']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'test']); + const sha = git(root, ['rev-parse', 'HEAD']).trim(); + return { root, sha }; + } + + // QNBS-v3: counts calls, not just final state -- a missing precondition check could also catch-to-UNKNOWN. + function refusingMaterializationSpy() { + const calls = { runBounded: 0, runLocalBinaryDetailed: 0 }; + return { + calls, + runBounded: async () => { + calls.runBounded += 1; + throw new Error('must not be called -- a tracked node_modules must be refused before materialization'); + }, + runLocalBinaryDetailed: async () => { + calls.runLocalBinaryDetailed += 1; + throw new Error('must not be called -- a tracked node_modules must be refused before materialization'); + }, + }; + } + + it('refuses a commit with a tracked root-level node_modules/.bin/tsgo (UNKNOWN, no materialization)', async () => { + const { root, sha } = makeRepoWithTrackedPath('node_modules/.bin/tsgo'); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for a tracked node_modules commit'); + }); + + it('refuses a commit with a tracked nested packages/foo/node_modules/x (UNKNOWN, no materialization)', async () => { + const { root, sha } = makeRepoWithTrackedPath('packages/foo/node_modules/x/index.js'); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for a tracked node_modules commit'); + }); + + it('refuses a commit with a tracked node_modules symlink itself (UNKNOWN, no materialization)', async () => { + const { root, sha } = makeRepoWithTrackedPath('node_modules', { symlink: true }); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for a tracked node_modules commit'); + }); + + it('leaves an ordinary commit with no tracked node_modules eligible for materialization', async () => { + const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); + let worktreeAddCalled = false; + const state = await verifyExactTreeTypecheck(sha, root, { + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('add')) worktreeAddCalled = true; + return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.equal(worktreeAddCalled, true); + assert.equal(state, 'UNKNOWN'); // fake path -- worktree add itself is mocked to fail, but it was reached. + }); +}); + +describe('createIsolatedWorktree (P2: post-checkout hooks must not execute)', () => { + it('does not execute a configured post-checkout hook while materializing the isolated worktree', async () => { + const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); + const markerRoot = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-hookmark-')); + temporaryRoots.push(markerRoot); + const markerFile = join(markerRoot, 'hook-ran.txt'); + const hooksDir = join(root, '.git', 'hooks'); + mkdirSync(hooksDir, { recursive: true }); + writeFileSync(join(hooksDir, 'post-checkout'), `#!/bin/sh\necho ran > "${markerFile}"\n`, { mode: 0o755 }); + + const created = await createIsolatedWorktree(sha, root, {}); + try { + assert.equal(created.ok, true); + assert.equal(existsSync(markerFile), false, 'post-checkout hook must not have run'); + } finally { + await removeIsolatedWorktree(created.path, root, {}); + } + }); +}); + describe('resolveRef (bounded, output-capturing ref resolution)', () => { it('resolves a valid ref to its full commit SHA via the injected runGit', () => { const sha = resolveRef('HEAD', '/repo', { @@ -327,6 +487,7 @@ describe('verifyExactTreeForShas (dedup, sequential, aggregation)', () => { it('deduplicates identical SHAs so the underlying check runs once', async () => { let calls = 0; const state = await verifyExactTreeForShas(['a'.repeat(40), 'a'.repeat(40)], '/repo', { + listTreeFiles: () => [], mkdtempFn: async () => { calls += 1; return '/tmp/worldscript-exact-tree-fake'; @@ -348,6 +509,7 @@ describe('verifyExactTreeForShas (dedup, sequential, aggregation)', () => { // QNBS-v3: sequential processing -- alternate by call order for one genuine FAIL, one UNKNOWN. let tsgoCallCount = 0; const state = await verifyExactTreeForShas(['a'.repeat(40), 'b'.repeat(40)], '/repo', { + listTreeFiles: () => [], mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), runLocalBinaryDetailed: async () => { @@ -371,6 +533,7 @@ describe('main (real CLI entry path, realistic DI)', () => { try { await main([], { repoRoot: '/repo', + listTreeFiles: () => [], runGit: () => ({ status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }), runBounded: (_command, args) => { if (args.includes('add')) return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; @@ -392,6 +555,7 @@ describe('main (real CLI entry path, realistic DI)', () => { try { await main(['main', 'feature-branch'], { repoRoot: '/repo', + listTreeFiles: () => [], runGit: (args) => { resolvedRefs.push(args[2]); return { status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }; @@ -435,6 +599,7 @@ describe('main (real CLI entry path, realistic DI)', () => { try { await main(['HEAD'], { repoRoot: '/repo', + listTreeFiles: () => [], runGit: () => ({ status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }), runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', From 7a173fd8a9778cb055bdfa4c844da339629c016f Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:33:29 +0200 Subject: [PATCH 6/9] fix: match node_modules case-insensitively in tracked-tree refusal CodeRabbit review on 41816ad3: hasTrackedNodeModules compared path segments case-sensitively, so a committed NODE_MODULES/.bin/tsgo would not be flagged even though it aliases node_modules/.bin/tsgo on the case-insensitive filesystems this tool already targets (it already special-cases process.platform === 'win32' for the pnpm invocation). On such a checkout the attacker-controlled binary would satisfy any lookup for node_modules/.bin/tsgo and be trusted as the compiler. Compare each path segment via toLowerCase() instead. Added a regression proving a tracked NODE_MODULES/.bin/tsgo is refused (UNKNOWN, no materialization attempted) the same way the existing lowercase cases are. --- scripts/verify-exact-tree.mjs | 5 ++++- tests/unit/tooling/verify-exact-tree.test.mjs | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/verify-exact-tree.mjs b/scripts/verify-exact-tree.mjs index 0b1c3e67f..f0ec66486 100644 --- a/scripts/verify-exact-tree.mjs +++ b/scripts/verify-exact-tree.mjs @@ -35,7 +35,10 @@ async function pruneStaleWorktrees(repoRoot, dependencies = {}) { // QNBS-v3: an arbitrary ref can force-track a node_modules path anywhere; could execute/redirect. Fail closed. function hasTrackedNodeModules(paths) { - return paths.some((path) => path.split('/').includes('node_modules')); + // QNBS-v3: case-insensitive -- NODE_MODULES aliases node_modules on Windows/macOS checkouts. + return paths.some((path) => + path.split('/').some((segment) => segment.toLowerCase() === 'node_modules'), + ); } // QNBS-v3: checked against the exact commit's own git objects, before any worktree/pnpm step touches disk. diff --git a/tests/unit/tooling/verify-exact-tree.test.mjs b/tests/unit/tooling/verify-exact-tree.test.mjs index 504a67969..2ceaf3a27 100644 --- a/tests/unit/tooling/verify-exact-tree.test.mjs +++ b/tests/unit/tooling/verify-exact-tree.test.mjs @@ -424,6 +424,14 @@ describe('verifyNoTrackedNodeModules (P1: refuse before any materialization touc assert.equal(spy.calls.runBounded, 0, 'materialization must never start for a tracked node_modules commit'); }); + it('refuses a case-variant NODE_MODULES/.bin/tsgo (aliases node_modules on case-insensitive filesystems)', async () => { + const { root, sha } = makeRepoWithTrackedPath('NODE_MODULES/.bin/tsgo'); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for a tracked node_modules commit'); + }); + it('leaves an ordinary commit with no tracked node_modules eligible for materialization', async () => { const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); let worktreeAddCalled = false; From c19e80135107a51944534809e5745d9b0115df17 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:24:12 +0200 Subject: [PATCH 7/9] fix: gate compiler trust and pnpm output paths, propagate interruption Three further trust-boundary gaps in the isolated exact-tree verification path, found and validated empirically in this review epoch (HEAD 7a173fd8): - Compiler provenance: an untrusted ref could point its tsgo-providing dependency at a workspace package with its own "tsgo" bin. The legitimate, scripts-disabled frozen install still creates that bin via normal workspace linking (not a lifecycle script), and the previous code trusted and executed whatever node_modules/.bin/tsgo the isolated worktree's own install produced. verifyExactTreeTypecheck now only invokes tsgo once #502's computeDependencyState proves this SHA's manifests are byte-exact to the trusted repoRoot's currently-installed ones (reusing that existing authority, not a second fingerprint system) -- and then resolves the compiler binary itself from the trusted repoRoot, never the isolated worktree, while cwd/--project still point exclusively at the isolated tree being analyzed. Any non-MATCHES state refuses before creating a worktree at all. - pnpm output-path containment: an untrusted ref's .npmrc can redirect modules-dir/virtual-store-dir/lockfile-dir/store-dir outside the isolated worktree (reproduced: modules-dir=../ESCAPE). installDependencies now pins all four via explicit CLI flags (highest precedence over any project .npmrc). store-dir is resolved from a fresh, empty, neutral temp directory rather than any project path -- discovered along the way that `pnpm store path` itself loads and executes .pnpmfile.cjs (no --ignore-pnpmfile support), so no project directory, trusted or not, is safe as its cwd either. - Interruption: runBounded already reports interrupted:true on SIGINT, but the previous code collapsed that to UNKNOWN like any other failure, so a multi-ref run could start another several-minute verification right after the user asked it to stop. Worktree creation, install, and tsgo now each surface interruption distinctly (a new ExactTreeInterrupted control-flow signal, never swallowed by the outer fail-closed catch); the multi-ref loop and the CLI both stop immediately and exit 130 instead of continuing or printing a normal PASS/FAIL/UNKNOWN result. Cleanup still always runs via the existing finally blocks. tests/unit/tooling/verify-exact-tree.test.mjs: adversarial regressions for all three -- a real workspace package supplying a malicious tsgo bin, never executed or yielding PASS; real escaping-.npmrc fixtures for both modules-dir and virtual-store-dir, proving no external directory is created; DI-level proofs that a compiler invocation always resolves from the trusted repoRoot; and interruption propagation through worktree creation, install, tsgo, the multi-ref loop, and main()'s exit code. Real end-to-end smoke test against this repo's own HEAD (isolated worktree, hook-free, dependencyState-gated trusted tsgo, pinned pnpm output paths) still PASSes under the fully remediated path; cleanup verified. --- scripts/verify-exact-tree.d.mts | 10 +- scripts/verify-exact-tree.mjs | 93 +++++- tests/unit/tooling/verify-exact-tree.test.mjs | 308 +++++++++++++++++- 3 files changed, 401 insertions(+), 10 deletions(-) diff --git a/scripts/verify-exact-tree.d.mts b/scripts/verify-exact-tree.d.mts index 5a8785bf8..b4e00c1e7 100644 --- a/scripts/verify-exact-tree.d.mts +++ b/scripts/verify-exact-tree.d.mts @@ -1,3 +1,4 @@ +import type { DependencyState } from './dependency-state.d.mts'; import type { BoundedResult } from './hooks/shared.d.mts'; import type { GitOptions, GitResult } from './signing/signing-core.d.mts'; @@ -35,13 +36,20 @@ export interface VerifyExactTreeDependencies { repoRoot?: string; // QNBS-v3: reuses dependency-state.mjs's git-tree enumeration authority -- not a second parser. listTreeFiles?: (sha: string, cwd: string) => string[] | null; + // QNBS-v3: reuses #502's manifest-compatibility authority to gate which tsgo binary may be trusted. + computeDependencyState?: (sha: string, root: string) => DependencyState; + // QNBS-v3: bypasses the real `pnpm store path` query in tests; undefined means "resolve it for real". + storeDir?: string | null; + resolveStoreDir?: (dependencies?: VerifyExactTreeDependencies) => Promise | string | null; + // QNBS-v3: a separate temp dir authority for the neutral store-dir query -- distinct from mkdtempFn/mkdtempHooksFn. + mkdtempStoreDirFn?: () => Promise; } export function createIsolatedWorktree( sha: string, repoRoot: string, dependencies?: VerifyExactTreeDependencies, -): Promise<{ ok: boolean; path: string | undefined }>; +): Promise<{ ok: boolean; path: string | undefined; interrupted?: boolean }>; export function removeIsolatedWorktree( worktreePath: string | undefined, diff --git a/scripts/verify-exact-tree.mjs b/scripts/verify-exact-tree.mjs index f0ec66486..1d28c0ab3 100644 --- a/scripts/verify-exact-tree.mjs +++ b/scripts/verify-exact-tree.mjs @@ -1,12 +1,21 @@ +import { spawnSync } from 'node:child_process'; import { mkdtemp as mkdtempAsync, rm as rmAsync } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import process from 'node:process'; import { isMainModule } from './ci-prepush-range-resolver.mjs'; -import { listTreeFiles } from './dependency-state.mjs'; +import { computeDependencyState, listTreeFiles } from './dependency-state.mjs'; import { runBounded, runLocalBinaryDetailed } from './hooks/shared.mjs'; import { runGit as defaultRunGit } from './signing/signing-core.mjs'; +// QNBS-v3: distinct from every other UNKNOWN cause -- explicit user intent must stop the whole run. +class ExactTreeInterrupted extends Error { + constructor() { + super('verify-exact-tree: interrupted'); + this.name = 'ExactTreeInterrupted'; + } +} + const DEFAULT_TSGO_ARGS = ['--project', 'tsconfig.tsgo.json', '--noEmit', '--checkers', '1']; // QNBS-v3: measured ~2m20s for the full project on this hardware with a warm store; 5min gives margin. const DEFAULT_INSTALL_TIMEOUT_MS = 300_000; @@ -57,6 +66,31 @@ async function createEmptyHooksDir(dependencies = {}) { return makeHooksDir(); } +// QNBS-v3: fresh empty temp dir -- 'pnpm store path' loads .pnpmfile.cjs too and has no --ignore-pnpmfile flag. +async function defaultResolveStoreDir(dependencies = {}) { + const makeTempDir = + dependencies.mkdtempStoreDirFn ?? + (() => mkdtempAsync(join(tmpdir(), 'worldscript-exact-tree-storequery-'))); + const removeDir = dependencies.rmFn ?? ((p) => rmAsync(p, { recursive: true, force: true })); + let neutralDir; + try { + neutralDir = await makeTempDir(); + } catch { + return null; + } + try { + const result = spawnSync('pnpm', ['store', 'path'], { cwd: neutralDir, encoding: 'utf8', timeout: 10_000 }); + if (result.error || result.status !== 0) return null; + return result.stdout.trim(); + } finally { + try { + await removeDir(neutralDir); + } catch { + // Best-effort: nothing more can be done from here; the directory is under os.tmpdir(). + } + } +} + export async function createIsolatedWorktree(sha, repoRoot, dependencies = {}) { const runGit = dependencies.runBounded ?? runBounded; const makeTempDir = @@ -82,6 +116,8 @@ export async function createIsolatedWorktree(sha, repoRoot, dependencies = {}) { ['-c', `core.hooksPath=${hooksDir}`, 'worktree', 'add', '--detach', path, sha], { cwd: repoRoot }, ); + // QNBS-v3: reported via the return shape, not a throw -- the caller must still clean up this path. + if (result.interrupted) return { ok: false, path, interrupted: true }; if (boundedCommandFailed(result)) return { ok: false, path }; return { ok: true, path }; } finally { @@ -122,10 +158,30 @@ export async function removeIsolatedWorktree(worktreePath, repoRoot, dependencie export async function installDependencies(worktreePath, dependencies = {}) { const runPnpm = dependencies.runBounded ?? runBounded; const timeoutMs = dependencies.installTimeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS; + const resolveStoreDir = dependencies.resolveStoreDir ?? defaultResolveStoreDir; + const storeDir = + dependencies.storeDir !== undefined ? dependencies.storeDir : await resolveStoreDir(dependencies); + if (storeDir === null) return false; // could not establish a trusted store-dir -- fail closed. + const result = await runPnpm( 'pnpm', - // QNBS-v3: verifying an arbitrary ref must never run that ref's scripts or .pnpmfile.cjs hooks. - ['install', '--frozen-lockfile', '--offline', '--ignore-scripts', '--ignore-pnpmfile'], + [ + 'install', + '--frozen-lockfile', + '--offline', + // QNBS-v3: verifying an arbitrary ref must never run that ref's scripts or .pnpmfile.cjs hooks. + '--ignore-scripts', + '--ignore-pnpmfile', + // QNBS-v3: pins every pnpm output path -- CLI flags outrank a target-controlled .npmrc, containing writes. + '--modules-dir', + 'node_modules', + '--virtual-store-dir', + 'node_modules/.pnpm', + '--lockfile-dir', + worktreePath, + '--store-dir', + storeDir, + ], { cwd: worktreePath, timeoutMs, @@ -134,6 +190,7 @@ export async function installDependencies(worktreePath, dependencies = {}) { env: { COREPACK_ENABLE_NETWORK: '0' }, }, ); + if (result.interrupted) throw new ExactTreeInterrupted(); return !boundedCommandFailed(result); } @@ -146,7 +203,7 @@ function aggregateExactTreeState(states) { return 'PASS'; } -// QNBS-v3: never throws -- an opt-in diagnostic tool must fail closed to UNKNOWN, not crash the caller. +// QNBS-v3: never throws UNKNOWN-worthy failures -- only ExactTreeInterrupted escapes, to stop the whole run. export async function verifyExactTreeTypecheck(sha, repoRoot = process.cwd(), dependencies = {}) { // QNBS-v3: a relative repoRoot would produce ambiguous git-cwd and install-cwd semantics. const absoluteRepoRoot = resolve(repoRoot); @@ -154,30 +211,39 @@ export async function verifyExactTreeTypecheck(sha, repoRoot = process.cwd(), de // QNBS-v3: refuse before any materialization -- a tracked node_modules must never reach pnpm/tsgo. if (!verifyNoTrackedNodeModules(sha, absoluteRepoRoot, dependencies)) return 'UNKNOWN'; + // QNBS-v3: the checked ref may supply source/types/deps but never the compiler that certifies it. + const computeState = dependencies.computeDependencyState ?? computeDependencyState; + if (computeState(sha, absoluteRepoRoot, dependencies) !== 'MATCHES') return 'UNKNOWN'; + const created = await createIsolatedWorktree(sha, absoluteRepoRoot, dependencies); if (!created.ok) { await removeIsolatedWorktree(created.path, absoluteRepoRoot, dependencies); + if (created.interrupted) throw new ExactTreeInterrupted(); return 'UNKNOWN'; } try { - if (!(await installDependencies(created.path, dependencies))) return 'UNKNOWN'; + const installed = await installDependencies(created.path, dependencies); + if (!installed) return 'UNKNOWN'; const runDetailed = dependencies.runLocalBinaryDetailed ?? runLocalBinaryDetailed; const tsgoArgs = dependencies.tsgoArgs ?? DEFAULT_TSGO_ARGS; const tsgoTimeoutMs = dependencies.tsgoTimeoutMs ?? DEFAULT_TSGO_TIMEOUT_MS; const result = await runDetailed('tsgo', tsgoArgs, { - root: created.path, + // QNBS-v3: root is the TRUSTED checkout's own tsgo -- cwd stays the isolated tree being analyzed. + root: absoluteRepoRoot, cwd: created.path, timeoutMs: tsgoTimeoutMs, }); + if (result.interrupted) throw new ExactTreeInterrupted(); // QNBS-v3: a signal (including an external OOM kill) must yield UNKNOWN, never a false FAIL. if (tsgoResultUnknown(result)) return 'UNKNOWN'; return result.status === 0 ? 'PASS' : 'FAIL'; } finally { await removeIsolatedWorktree(created.path, absoluteRepoRoot, dependencies); } - } catch { + } catch (error) { + if (error instanceof ExactTreeInterrupted) throw error; return 'UNKNOWN'; } } @@ -215,7 +281,18 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) { shas.push(sha); } console.log(`[verify-exact-tree] verifying ${shas.length} commit(s) in isolated worktree(s)...`); - const state = await verifyExactTreeForShas(shas, repoRoot, dependencies); + let state; + try { + state = await verifyExactTreeForShas(shas, repoRoot, dependencies); + } catch (error) { + // QNBS-v3: explicit user intent -- stop here, never continue to another multi-minute verification. + if (error instanceof ExactTreeInterrupted) { + console.log('[verify-exact-tree] interrupted; stopping without completing verification.'); + process.exitCode = 130; + return; + } + throw error; + } console.log(`[verify-exact-tree] result: ${state}`); if (state === 'UNKNOWN') { console.log( diff --git a/tests/unit/tooling/verify-exact-tree.test.mjs b/tests/unit/tooling/verify-exact-tree.test.mjs index 2ceaf3a27..50b105b15 100644 --- a/tests/unit/tooling/verify-exact-tree.test.mjs +++ b/tests/unit/tooling/verify-exact-tree.test.mjs @@ -77,6 +77,72 @@ function makeWorkspaceFixture(innerContent) { return { root, sha }; } +// QNBS-v3: .npmrc added AFTER the setup install, so fixture setup itself never escapes -- only the tested install can. +function makeEscapingNpmrcFixture(npmrcContent) { + const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-npmrc-')); + temporaryRoots.push(root); + mkdirSync(join(root, 'packages', 'demo-pkg'), { recursive: true }); + writeFileSync( + join(root, 'package.json'), + '{"name":"fixture-root","private":true,"dependencies":{"@fixture/demo-pkg":"workspace:*"}}\n', + ); + writeFileSync(join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n'); + writeFileSync(join(root, 'packages', 'demo-pkg', 'package.json'), '{"name":"@fixture/demo-pkg","version":"1.0.0"}\n'); + writeFileSync(join(root, 'packages', 'demo-pkg', 'index.js'), "export const DEMO = 'ok';\n"); + writeFileSync(join(root, '.gitignore'), 'node_modules\n'); + execFileSync('pnpm', ['install', '--offline'], { cwd: root, stdio: 'ignore' }); + writeFileSync(join(root, '.npmrc'), npmrcContent); + git(root, ['init', '--quiet', '--initial-branch=main']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + git(root, ['add', '-A']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'escaping npmrc']); + const sha = git(root, ['rev-parse', 'HEAD']).trim(); + return { root, sha }; +} + +// QNBS-v3: a workspace package legitimately providing a "tsgo" bin -- bin-linking isn't a lifecycle script. +function makeMaliciousTsgoFixture() { + const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-eviltsgo-')); + temporaryRoots.push(root); + const markerRoot = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-eviltsgomark-')); + temporaryRoots.push(markerRoot); + const markerFile = join(markerRoot, 'evil-tsgo-ran.txt'); + + mkdirSync(join(root, 'packages', 'evil-tsgo'), { recursive: true }); + writeFileSync( + join(root, 'package.json'), + '{"name":"fixture-root","private":true,"dependencies":{"@fixture/evil-tsgo":"workspace:*"}}\n', + ); + writeFileSync(join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n'); + writeFileSync( + join(root, 'packages', 'evil-tsgo', 'package.json'), + '{"name":"@fixture/evil-tsgo","version":"1.0.0","bin":{"tsgo":"./evil.js"}}\n', + ); + writeFileSync( + join(root, 'packages', 'evil-tsgo', 'evil.js'), + `require('fs').writeFileSync(${JSON.stringify(markerFile)}, 'executed');\n`, + ); + writeFileSync(join(root, '.gitignore'), 'node_modules\n'); + execFileSync('pnpm', ['install', '--offline'], { cwd: root, stdio: 'ignore' }); + git(root, ['init', '--quiet', '--initial-branch=main']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + git(root, ['add', '-A']); + git(root, [ + '-c', + 'commit.gpgsign=false', + 'commit', + '--quiet', + '-m', + 'malicious: workspace package provides a fake tsgo bin', + ]); + const maliciousSha = git(root, ['rev-parse', 'HEAD']).trim(); + // QNBS-v3: the setup install above also installs at the fixture root -- delete it so it stays untainted. + rmSync(join(root, 'node_modules'), { recursive: true, force: true }); + return { root, maliciousSha, markerFile }; +} + describe('createIsolatedWorktree / removeIsolatedWorktree (real git, no pnpm)', () => { it('creates a worktree at the exact commit and leaves nothing registered after cleanup', async () => { const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); @@ -212,6 +278,7 @@ describe('installDependencies (workspace-package soundness -- the core regressio it('returns false (mapped to UNKNOWN by callers) when the frozen-lockfile install fails', async () => { const installed = await installDependencies('/tmp/worldscript-exact-tree-fake', { + storeDir: '/fake-store', runBounded: async () => ({ status: 1, error: null, @@ -222,12 +289,59 @@ describe('installDependencies (workspace-package soundness -- the core regressio }); assert.equal(installed, false); }); + + it('returns false (mapped to UNKNOWN) when the trusted store-dir cannot be resolved', async () => { + const installed = await installDependencies('/tmp/worldscript-exact-tree-fake', { + resolveStoreDir: () => null, + runBounded: async () => { + throw new Error('must not be called -- an unresolved store-dir must fail closed first'); + }, + }); + assert.equal(installed, false); + }); +}); + +describe('installDependencies (P1: contain pnpm output paths against an escaping .npmrc)', () => { + it('does not let modules-dir escape the isolated worktree', async () => { + const { root, sha } = makeEscapingNpmrcFixture('modules-dir=../ESCAPE-modules\n'); + const created = await createIsolatedWorktree(sha, root, {}); + assert.equal(created.ok, true); + try { + const installed = await installDependencies(created.path, {}); + assert.equal(installed, true); + assert.equal( + existsSync(join(created.path, '..', 'ESCAPE-modules')), + false, + 'modules-dir must not escape the isolated worktree', + ); + } finally { + await removeIsolatedWorktree(created.path, root, {}); + } + }); + + it('does not let virtual-store-dir escape the isolated worktree', async () => { + const { root, sha } = makeEscapingNpmrcFixture('virtual-store-dir=../ESCAPE-virtualstore\n'); + const created = await createIsolatedWorktree(sha, root, {}); + assert.equal(created.ok, true); + try { + const installed = await installDependencies(created.path, {}); + assert.equal(installed, true); + assert.equal( + existsSync(join(created.path, '..', 'ESCAPE-virtualstore')), + false, + 'virtual-store-dir must not escape the isolated worktree', + ); + } finally { + await removeIsolatedWorktree(created.path, root, {}); + } + }); }); describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semantics -- DI only)', () => { it('reports UNKNOWN when git worktree add fails', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', runBounded: (_command, args) => { if (args.includes('add')) return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; @@ -240,6 +354,8 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('reports UNKNOWN, never a false PASS/FAIL, when the install fails', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: (_command, args) => { if (args.includes('worktree')) return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; @@ -257,6 +373,8 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti const runBounded = () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }); const pass = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded, runLocalBinaryDetailed: async () => ({ @@ -271,6 +389,8 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti const fail = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded, runLocalBinaryDetailed: async () => ({ @@ -287,6 +407,8 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('reports UNKNOWN, not FAIL, when tsgo is terminated by a signal (e.g. external OOM kill)', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), runLocalBinaryDetailed: async () => ({ @@ -303,6 +425,8 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('reports UNKNOWN, not FAIL, on a null status with no signal (defensive)', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), runLocalBinaryDetailed: async () => ({ @@ -321,6 +445,8 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti let seenTimeoutMs; await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded, runLocalBinaryDetailed: async (_binary, _args, options) => { @@ -334,6 +460,8 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti let overriddenTimeoutMs; await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded, tsgoTimeoutMs: 42_000, @@ -345,13 +473,17 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti assert.equal(overriddenTimeoutMs, 42_000); }); - it('canonicalizes a relative repoRoot to an absolute path before any git/install call', async () => { + it('canonicalizes a relative repoRoot to an absolute path before any git/install/dependencyState call', async () => { const seenCwds = []; await verifyExactTreeTypecheck('a'.repeat(40), '.', { listTreeFiles: (_sha, cwd) => { seenCwds.push(cwd); return []; }, + computeDependencyState: (_sha, root) => { + seenCwds.push(root); + return 'MATCHES'; + }, mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: (_command, _args, options) => { seenCwds.push(options?.cwd); @@ -363,6 +495,51 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti }); }); +describe('verifyExactTreeTypecheck (P1: the checked ref must never supply the compiler that certifies it)', () => { + it('invokes tsgo using the trusted repoRoot, never the isolated worktree, once dependencyState proves MATCHES', async () => { + let seenRoot; + let seenCwd; + const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), + runLocalBinaryDetailed: async (_binary, _args, options) => { + seenRoot = options?.root; + seenCwd = options?.cwd; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.equal(state, 'PASS'); + assert.equal(seenRoot, '/repo', 'the compiler binary must resolve from the trusted repoRoot'); + assert.equal(seenCwd, '/tmp/worldscript-exact-tree-fake', 'the compile must still analyze the isolated tree'); + }); + + it('refuses (UNKNOWN) without creating any worktree when dependencyState is not MATCHES', async () => { + for (const dependencyState of ['DIVERGED', 'UNKNOWN', 'NOT_APPLICABLE']) { + let worktreeCalls = 0; + const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], + computeDependencyState: () => dependencyState, + runBounded: () => { + worktreeCalls += 1; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.equal(state, 'UNKNOWN', `expected UNKNOWN for dependencyState=${dependencyState}`); + assert.equal(worktreeCalls, 0, `expected no worktree materialization for dependencyState=${dependencyState}`); + } + }); + + it('never executes a workspace-supplied tsgo binary or yields PASS, even via a legitimate frozen install', async () => { + const { root, maliciousSha, markerFile } = makeMaliciousTsgoFixture(); + const state = await verifyExactTreeTypecheck(maliciousSha, root, {}); + assert.notEqual(state, 'PASS', 'a ref-supplied compiler must never certify itself'); + assert.equal(existsSync(markerFile), false, 'the ref-supplied tsgo bin must never execute'); + }); +}); + describe('verifyNoTrackedNodeModules (P1: refuse before any materialization touches disk)', () => { function makeRepoWithTrackedPath(relativePath, { symlink } = {}) { const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-nm-')); @@ -436,6 +613,7 @@ describe('verifyNoTrackedNodeModules (P1: refuse before any materialization touc const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); let worktreeAddCalled = false; const state = await verifyExactTreeTypecheck(sha, root, { + computeDependencyState: () => 'MATCHES', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: (_command, args) => { if (args.includes('add')) worktreeAddCalled = true; @@ -496,6 +674,7 @@ describe('verifyExactTreeForShas (dedup, sequential, aggregation)', () => { let calls = 0; const state = await verifyExactTreeForShas(['a'.repeat(40), 'a'.repeat(40)], '/repo', { listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', mkdtempFn: async () => { calls += 1; return '/tmp/worldscript-exact-tree-fake'; @@ -518,6 +697,8 @@ describe('verifyExactTreeForShas (dedup, sequential, aggregation)', () => { let tsgoCallCount = 0; const state = await verifyExactTreeForShas(['a'.repeat(40), 'b'.repeat(40)], '/repo', { listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), runLocalBinaryDetailed: async () => { @@ -533,6 +714,127 @@ describe('verifyExactTreeForShas (dedup, sequential, aggregation)', () => { }); }); +describe('interruption handling (P2: explicit user intent must stop the whole run)', () => { + it('createIsolatedWorktree reports interrupted:true in its return shape, not a generic failure', async () => { + const created = await createIsolatedWorktree('a'.repeat(40), '/repo', { + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('add')) return { status: null, error: null, signal: null, timedOut: false, interrupted: true }; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.equal(created.ok, false); + assert.equal(created.interrupted, true); + }); + + it('verifyExactTreeTypecheck rejects (does not return UNKNOWN) when worktree creation is interrupted', async () => { + await assert.rejects( + verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('add')) return { status: null, error: null, signal: null, timedOut: false, interrupted: true }; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }), + { name: 'ExactTreeInterrupted' }, + ); + }); + + it('verifyExactTreeTypecheck rejects when the pnpm install is interrupted, and still cleans up the worktree', async () => { + let removeCalled = false; + await assert.rejects( + verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('remove')) removeCalled = true; + if (args.includes('worktree')) return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + // QNBS-v3: the pnpm install call. + return { status: null, error: null, signal: null, timedOut: false, interrupted: true }; + }, + }), + { name: 'ExactTreeInterrupted' }, + ); + assert.equal(removeCalled, true, 'cleanup must still run even when interrupted mid-install'); + }); + + it('verifyExactTreeTypecheck rejects when tsgo itself is interrupted, and still cleans up the worktree', async () => { + let removeCalled = false; + await assert.rejects( + verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('remove')) removeCalled = true; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + runLocalBinaryDetailed: async () => ({ + status: null, + error: null, + signal: null, + timedOut: false, + interrupted: true, + }), + }), + { name: 'ExactTreeInterrupted' }, + ); + assert.equal(removeCalled, true, 'cleanup must still run even when interrupted mid-typecheck'); + }); + + it('verifyExactTreeForShas stops processing remaining SHAs immediately when one is interrupted', async () => { + let worktreeAddCalls = 0; + await assert.rejects( + verifyExactTreeForShas(['a'.repeat(40), 'b'.repeat(40)], '/repo', { + listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('add')) { + worktreeAddCalls += 1; + return { status: null, error: null, signal: null, timedOut: false, interrupted: true }; + } + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }), + { name: 'ExactTreeInterrupted' }, + ); + assert.equal(worktreeAddCalls, 1, 'the second SHA must never start a worktree add after the first is interrupted'); + }); + + it('main() reports an interrupted outcome and exits 130 instead of continuing or printing a normal result', async () => { + const logs = []; + const originalLog = console.log; + console.log = (message) => logs.push(message); + const originalExitCode = process.exitCode; + process.exitCode = 0; + try { + await main(['HEAD'], { + repoRoot: '/repo', + listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + runGit: () => ({ status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }), + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('add')) return { status: null, error: null, signal: null, timedOut: false, interrupted: true }; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.equal(process.exitCode, 130); + } finally { + console.log = originalLog; + process.exitCode = originalExitCode; + } + assert.ok(logs.some((line) => line.includes('interrupted')), logs.join('\n')); + assert.ok(!logs.some((line) => line.includes('result:')), 'must not print a normal PASS/FAIL/UNKNOWN result line'); + }); +}); + describe('main (real CLI entry path, realistic DI)', () => { it('resolves HEAD by default, verifies it, and prints the result without crashing', async () => { const logs = []; @@ -542,6 +844,7 @@ describe('main (real CLI entry path, realistic DI)', () => { await main([], { repoRoot: '/repo', listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', runGit: () => ({ status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }), runBounded: (_command, args) => { if (args.includes('add')) return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; @@ -564,6 +867,7 @@ describe('main (real CLI entry path, realistic DI)', () => { await main(['main', 'feature-branch'], { repoRoot: '/repo', listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', runGit: (args) => { resolvedRefs.push(args[2]); return { status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }; @@ -608,6 +912,8 @@ describe('main (real CLI entry path, realistic DI)', () => { await main(['HEAD'], { repoRoot: '/repo', listTreeFiles: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', runGit: () => ({ status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }), runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', From f796bf45c81838ac7639367e12e2f523bc2cd06d Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:58:15 +0200 Subject: [PATCH 8/9] fix: neutralize checkout filters, escaping symlinks, and prune interrupts Root-clustered remediation for this review epoch (HEAD c19e8013), plus one finding found proactively before the bot surfaced it: - Store-dir resolution (P1/P2 x3, one authority): reverted the prior neutral-temp-dir design back to resolving from the trusted repoRoot -- a neutral dir picks Corepack's default pnpm major rather than this repo's own pinned packageManager version, which can select the wrong store/vN layout and turn a valid offline verification into a spurious UNKNOWN. Running in the trusted repoRoot is safe (a .pnpmfile.cjs there is the developer's own code); the query now also passes COREPACK_ENABLE_NETWORK=0 and the same Windows shell handling as the real install call. - Checkout filters (P1): core.hooksPath only disables hooks, not smudge/clean filters (e.g. Git LFS). A tracked .gitattributes referencing a filter the developer has registered globally would otherwise run that program, with network access, during `git worktree add`, before any pnpm safeguard runs. Empirically verified fix: pointing GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM at nonexistent paths (reusing the existing hooks temp dir, no new resource) makes any referenced filter driver undefined, so checkout leaves the blob content untransformed instead of invoking it. - Escaping symlinks (P2): a tracked symlink whose target resolves outside the isolated worktree (relative ../.. escapes or an absolute path) would let tsgo dereference live/external content while the tool still reports full isolation. The preflight now lists tracked entries with their git mode (dependency-state.mjs's listTreeEntries, extended from the existing listTreeFiles rather than a second parser) and reads each symlink's target via the existing git-object blob reader (now exported as readFileAtRef), refusing before any worktree is created if any target normalizes outside the tree root. - Interrupted stale-worktree pruning (P2): the initial `git worktree prune` ignored its result entirely, so a Ctrl-C during that step was silently swallowed and the run proceeded into several more minutes of work. It now raises the same ExactTreeInterrupted signal as every other lifecycle step. tests/unit/tooling/verify-exact-tree.test.mjs: real adversarial regressions for the filter (a fake globally-registered smudge filter, proven not to fire), escaping symlinks (relative and absolute targets refused; an ordinary in-tree symlink stays eligible), and the prune interrupt (worktree add never starts afterward); a DI proof that store-dir resolves from the trusted repoRoot, not the worktree. One existing fixture (the .pnpmfile.cjs regression) needed a corresponding realism fix: it had been committing the malicious file onto repoRoot's own checked-out branch, which the reverted trusted-root store-dir query would then legitimately load -- repoRoot's working copy no longer carries it, matching how a developer's own checkout would actually look while verifying a separate, untrusted ref. Real end-to-end smoke test against this repo's own HEAD still PASSes under the fully remediated path; cleanup verified. --- scripts/dependency-state.d.mts | 8 + scripts/dependency-state.mjs | 27 ++- scripts/verify-exact-tree.d.mts | 14 +- scripts/verify-exact-tree.mjs | 97 ++++++---- tests/unit/tooling/verify-exact-tree.test.mjs | 170 +++++++++++++++--- 5 files changed, 237 insertions(+), 79 deletions(-) diff --git a/scripts/dependency-state.d.mts b/scripts/dependency-state.d.mts index cc63e1c80..a8fab5f7e 100644 --- a/scripts/dependency-state.d.mts +++ b/scripts/dependency-state.d.mts @@ -1,7 +1,15 @@ // QNBS-v3: diagnostic-only dimension, independent of resolvePushEvidence's canonical evidence validity. export type DependencyState = 'MATCHES' | 'DIVERGED' | 'NOT_APPLICABLE' | 'UNKNOWN'; +export interface TreeEntry { + mode: string; + type: string; + hash: string; + path: string; +} +export function listTreeEntries(sha: string, cwd?: string): TreeEntry[] | null; export function listTreeFiles(sha: string, cwd?: string): string[] | null; +export function readFileAtRef(sha: string, relativePath: string, cwd?: string): Buffer | null; export function dependencyFiles(root?: string): string[]; export function calculateDependencyFingerprint(root?: string): string; export function fingerprintPath(root?: string): string; diff --git a/scripts/dependency-state.mjs b/scripts/dependency-state.mjs index 803812289..df7183a3d 100644 --- a/scripts/dependency-state.mjs +++ b/scripts/dependency-state.mjs @@ -76,15 +76,28 @@ export function calculateDependencyFingerprint(root = projectRoot) { return hashManifests(entries); } -// QNBS-v3: -z avoids path C-quoting; exported so verify-exact-tree.mjs reuses this, not a second parser. -export function listTreeFiles(sha, cwd) { - const result = spawnSync('git', ['ls-tree', '-r', '--full-tree', '--name-only', '-z', sha], { +// QNBS-v3: -z avoids path C-quoting; includes mode so callers (e.g. symlink detection) don't need a second parser. +export function listTreeEntries(sha, cwd) { + const result = spawnSync('git', ['ls-tree', '-r', '--full-tree', '-z', sha], { cwd, encoding: 'utf8', timeout: 5000, }); if (result.error || result.status !== 0) return null; - return result.stdout.split('\0').filter(Boolean); + return result.stdout + .split('\0') + .filter(Boolean) + .map((entry) => { + const tabIndex = entry.indexOf('\t'); + const [mode, type, hash] = entry.slice(0, tabIndex).split(' '); + return { mode, type, hash, path: entry.slice(tabIndex + 1) }; + }); +} + +// QNBS-v3: exported so verify-exact-tree.mjs reuses this authority instead of a second parser. +export function listTreeFiles(sha, cwd) { + const entries = listTreeEntries(sha, cwd); + return entries === null ? null : entries.map((entry) => entry.path); } // QNBS-v3: diagnostic-only; mirrors dependencyFiles' inclusion rules against a commit, not disk. @@ -101,8 +114,8 @@ export function dependencyFilesFromRef(sha, root = projectRoot, dependencies = { .sort(); } -// QNBS-v3: no encoding -- raw Buffer stdout, matching readFileSync's raw bytes for invalid UTF-8 safety. -function defaultReadFileAtRef(sha, relativePath, cwd) { +// QNBS-v3: raw Buffer stdout for UTF-8 safety; exported so verify-exact-tree.mjs reuses this, not a second reader. +export function readFileAtRef(sha, relativePath, cwd) { const result = spawnSync('git', ['show', `${sha}:${relativePath}`], { cwd, timeout: 5000, @@ -117,7 +130,7 @@ export function calculateDependencyFingerprintFromRef(sha, root = projectRoot, d const listFiles = dependencies.dependencyFilesFromRef ?? (() => dependencyFilesFromRef(sha, root, dependencies)); const files = listFiles(sha); if (files === null) return null; - const readContent = dependencies.readFileAtRef ?? ((path) => defaultReadFileAtRef(sha, path, root)); + const readContent = dependencies.readFileAtRef ?? ((path) => readFileAtRef(sha, path, root)); const entries = []; for (const relativePath of files) { const content = readContent(relativePath); diff --git a/scripts/verify-exact-tree.d.mts b/scripts/verify-exact-tree.d.mts index b4e00c1e7..3ce5d1d02 100644 --- a/scripts/verify-exact-tree.d.mts +++ b/scripts/verify-exact-tree.d.mts @@ -1,4 +1,4 @@ -import type { DependencyState } from './dependency-state.d.mts'; +import type { DependencyState, TreeEntry } from './dependency-state.d.mts'; import type { BoundedResult } from './hooks/shared.d.mts'; import type { GitOptions, GitResult } from './signing/signing-core.d.mts'; @@ -34,15 +34,17 @@ export interface VerifyExactTreeDependencies { tsgoArgs?: string[]; tsgoTimeoutMs?: number; repoRoot?: string; - // QNBS-v3: reuses dependency-state.mjs's git-tree enumeration authority -- not a second parser. - listTreeFiles?: (sha: string, cwd: string) => string[] | null; + // QNBS-v3: reuses dependency-state.mjs's mode-aware git-tree enumeration authority -- not a second parser. + listTreeEntries?: (sha: string, cwd: string) => TreeEntry[] | null; + // QNBS-v3: reuses dependency-state.mjs's git-object blob reader for symlink target content. + readBlobAtRef?: (sha: string, relativePath: string, cwd: string) => Buffer | null; // QNBS-v3: reuses #502's manifest-compatibility authority to gate which tsgo binary may be trusted. computeDependencyState?: (sha: string, root: string) => DependencyState; + // QNBS-v3: the trusted checkout installDependencies resolves the pinned store-dir from -- never the worktree. + trustedRepoRoot?: string; // QNBS-v3: bypasses the real `pnpm store path` query in tests; undefined means "resolve it for real". storeDir?: string | null; - resolveStoreDir?: (dependencies?: VerifyExactTreeDependencies) => Promise | string | null; - // QNBS-v3: a separate temp dir authority for the neutral store-dir query -- distinct from mkdtempFn/mkdtempHooksFn. - mkdtempStoreDirFn?: () => Promise; + resolveStoreDir?: (trustedRepoRoot: string | undefined) => Promise | string | null; } export function createIsolatedWorktree( diff --git a/scripts/verify-exact-tree.mjs b/scripts/verify-exact-tree.mjs index 1d28c0ab3..4146de0fa 100644 --- a/scripts/verify-exact-tree.mjs +++ b/scripts/verify-exact-tree.mjs @@ -1,10 +1,10 @@ import { spawnSync } from 'node:child_process'; import { mkdtemp as mkdtempAsync, rm as rmAsync } from 'node:fs/promises'; import { tmpdir } from 'node:os'; -import { join, resolve } from 'node:path'; +import { join, posix, resolve } from 'node:path'; import process from 'node:process'; import { isMainModule } from './ci-prepush-range-resolver.mjs'; -import { computeDependencyState, listTreeFiles } from './dependency-state.mjs'; +import { computeDependencyState, listTreeEntries, readFileAtRef } from './dependency-state.mjs'; import { runBounded, runLocalBinaryDetailed } from './hooks/shared.mjs'; import { runGit as defaultRunGit } from './signing/signing-core.mjs'; @@ -39,23 +39,41 @@ function tsgoResultUnknown(result) { // QNBS-v3: sweeps entries orphaned by a prior crashed/killed run before creating a new one. async function pruneStaleWorktrees(repoRoot, dependencies = {}) { const runGit = dependencies.runBounded ?? runBounded; - await runGit('git', ['worktree', 'prune'], { cwd: repoRoot }); + const result = await runGit('git', ['worktree', 'prune'], { cwd: repoRoot }); + if (result.interrupted) throw new ExactTreeInterrupted(); } // QNBS-v3: an arbitrary ref can force-track a node_modules path anywhere; could execute/redirect. Fail closed. -function hasTrackedNodeModules(paths) { +function hasTrackedNodeModules(entries) { // QNBS-v3: case-insensitive -- NODE_MODULES aliases node_modules on Windows/macOS checkouts. - return paths.some((path) => - path.split('/').some((segment) => segment.toLowerCase() === 'node_modules'), + return entries.some((entry) => + entry.path.split('/').some((segment) => segment.toLowerCase() === 'node_modules'), ); } +// QNBS-v3: a tracked symlink whose target escapes the tree root would let tsgo read live/external content. +function hasEscapingSymlink(entries, sha, repoRoot, dependencies) { + const readBlob = dependencies.readBlobAtRef ?? readFileAtRef; + for (const entry of entries) { + if (entry.mode !== '120000') continue; + const targetBuffer = readBlob(sha, entry.path, repoRoot); + if (targetBuffer === null) return true; // unreadable target -- fail closed, treat as escaping. + const target = targetBuffer.toString('utf8').trim(); + if (posix.isAbsolute(target)) return true; + const resolved = posix.normalize(posix.join(posix.dirname(entry.path), target)); + if (resolved.startsWith('..') || posix.isAbsolute(resolved)) return true; + } + return false; +} + // QNBS-v3: checked against the exact commit's own git objects, before any worktree/pnpm step touches disk. -function verifyNoTrackedNodeModules(sha, repoRoot, dependencies = {}) { - const listTree = dependencies.listTreeFiles ?? listTreeFiles; - const paths = listTree(sha, repoRoot); - if (paths === null) return false; // an unreadable tree can never be proven clean. - return !hasTrackedNodeModules(paths); +function verifyExactTreePreflight(sha, repoRoot, dependencies = {}) { + const listEntries = dependencies.listTreeEntries ?? listTreeEntries; + const entries = listEntries(sha, repoRoot); + if (entries === null) return false; // an unreadable tree can never be proven clean. + if (hasTrackedNodeModules(entries)) return false; + if (hasEscapingSymlink(entries, sha, repoRoot, dependencies)) return false; + return true; } // QNBS-v3: hook-free dir so 'git worktree add' can't run repo/user hooks -- scoped via -c, not global config. @@ -66,29 +84,18 @@ async function createEmptyHooksDir(dependencies = {}) { return makeHooksDir(); } -// QNBS-v3: fresh empty temp dir -- 'pnpm store path' loads .pnpmfile.cjs too and has no --ignore-pnpmfile flag. -async function defaultResolveStoreDir(dependencies = {}) { - const makeTempDir = - dependencies.mkdtempStoreDirFn ?? - (() => mkdtempAsync(join(tmpdir(), 'worldscript-exact-tree-storequery-'))); - const removeDir = dependencies.rmFn ?? ((p) => rmAsync(p, { recursive: true, force: true })); - let neutralDir; - try { - neutralDir = await makeTempDir(); - } catch { - return null; - } - try { - const result = spawnSync('pnpm', ['store', 'path'], { cwd: neutralDir, encoding: 'utf8', timeout: 10_000 }); - if (result.error || result.status !== 0) return null; - return result.stdout.trim(); - } finally { - try { - await removeDir(neutralDir); - } catch { - // Best-effort: nothing more can be done from here; the directory is under os.tmpdir(). - } - } +// QNBS-v3: resolved from the trusted repoRoot -- picks up its pinned packageManager, and a .pnpmfile.cjs there is the developer's own code. +function defaultResolveStoreDir(trustedRepoRoot) { + if (!trustedRepoRoot) return null; // no trusted root to resolve the pinned toolchain from -- fail closed. + const result = spawnSync('pnpm', ['store', 'path'], { + cwd: trustedRepoRoot, + encoding: 'utf8', + timeout: 10_000, + shell: process.platform === 'win32', + env: { ...process.env, COREPACK_ENABLE_NETWORK: '0' }, + }); + if (result.error || result.status !== 0) return null; + return result.stdout.trim(); } export async function createIsolatedWorktree(sha, repoRoot, dependencies = {}) { @@ -114,7 +121,14 @@ export async function createIsolatedWorktree(sha, repoRoot, dependencies = {}) { const result = await runGit( 'git', ['-c', `core.hooksPath=${hooksDir}`, 'worktree', 'add', '--detach', path, sha], - { cwd: repoRoot }, + { + cwd: repoRoot, + // QNBS-v3: core.hooksPath disables hooks only, not smudge/clean filters (e.g. LFS) -- nonexistent global/system config makes any referenced filter a no-op. + env: { + GIT_CONFIG_GLOBAL: join(hooksDir, 'no-global-config'), + GIT_CONFIG_SYSTEM: join(hooksDir, 'no-system-config'), + }, + }, ); // QNBS-v3: reported via the return shape, not a throw -- the caller must still clean up this path. if (result.interrupted) return { ok: false, path, interrupted: true }; @@ -160,7 +174,9 @@ export async function installDependencies(worktreePath, dependencies = {}) { const timeoutMs = dependencies.installTimeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS; const resolveStoreDir = dependencies.resolveStoreDir ?? defaultResolveStoreDir; const storeDir = - dependencies.storeDir !== undefined ? dependencies.storeDir : await resolveStoreDir(dependencies); + dependencies.storeDir !== undefined + ? dependencies.storeDir + : await resolveStoreDir(dependencies.trustedRepoRoot); if (storeDir === null) return false; // could not establish a trusted store-dir -- fail closed. const result = await runPnpm( @@ -208,8 +224,8 @@ export async function verifyExactTreeTypecheck(sha, repoRoot = process.cwd(), de // QNBS-v3: a relative repoRoot would produce ambiguous git-cwd and install-cwd semantics. const absoluteRepoRoot = resolve(repoRoot); try { - // QNBS-v3: refuse before any materialization -- a tracked node_modules must never reach pnpm/tsgo. - if (!verifyNoTrackedNodeModules(sha, absoluteRepoRoot, dependencies)) return 'UNKNOWN'; + // QNBS-v3: refuse before any materialization -- tracked node_modules or an escaping symlink must never reach pnpm/tsgo. + if (!verifyExactTreePreflight(sha, absoluteRepoRoot, dependencies)) return 'UNKNOWN'; // QNBS-v3: the checked ref may supply source/types/deps but never the compiler that certifies it. const computeState = dependencies.computeDependencyState ?? computeDependencyState; @@ -223,7 +239,10 @@ export async function verifyExactTreeTypecheck(sha, repoRoot = process.cwd(), de } try { - const installed = await installDependencies(created.path, dependencies); + const installed = await installDependencies(created.path, { + ...dependencies, + trustedRepoRoot: absoluteRepoRoot, + }); if (!installed) return 'UNKNOWN'; const runDetailed = dependencies.runLocalBinaryDetailed ?? runLocalBinaryDetailed; diff --git a/tests/unit/tooling/verify-exact-tree.test.mjs b/tests/unit/tooling/verify-exact-tree.test.mjs index 50b105b15..127222543 100644 --- a/tests/unit/tooling/verify-exact-tree.test.mjs +++ b/tests/unit/tooling/verify-exact-tree.test.mjs @@ -221,7 +221,7 @@ describe('installDependencies (workspace-package soundness -- the core regressio const created = await createIsolatedWorktree(sha, root, {}); assert.equal(created.ok, true); try { - const installed = await installDependencies(created.path, {}); + const installed = await installDependencies(created.path, { trustedRepoRoot: root }); assert.equal(installed, true); // QNBS-v3: root workspace link (node_modules/@fixture/demo-pkg) resolving into the isolated tree. @@ -264,11 +264,13 @@ describe('installDependencies (workspace-package soundness -- the core regressio git(root, ['add', '-A']); git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'add pnpmfile']); const pnpmfileSha = git(root, ['rev-parse', 'HEAD']).trim(); + // QNBS-v3: store-dir resolution runs in repoRoot's own dir; the commit object is unaffected either way. + rmSync(join(root, '.pnpmfile.cjs'), { force: true }); const created = await createIsolatedWorktree(pnpmfileSha, root, {}); assert.equal(created.ok, true); try { - const installed = await installDependencies(created.path, {}); + const installed = await installDependencies(created.path, { trustedRepoRoot: root }); assert.equal(installed, true); assert.equal(existsSync(markerFile), false, '.pnpmfile.cjs must not have executed'); } finally { @@ -299,6 +301,19 @@ describe('installDependencies (workspace-package soundness -- the core regressio }); assert.equal(installed, false); }); + + it('resolves store-dir from trustedRepoRoot, never the untrusted worktree, so the pinned pnpm version is used', async () => { + let seenTrustedRoot; + await installDependencies('/tmp/worldscript-exact-tree-fake-worktree', { + trustedRepoRoot: '/repo', + resolveStoreDir: (trustedRepoRoot) => { + seenTrustedRoot = trustedRepoRoot; + return '/fake-store'; + }, + runBounded: async () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), + }); + assert.equal(seenTrustedRoot, '/repo', 'must resolve store-dir from the trusted repoRoot, not the worktree'); + }); }); describe('installDependencies (P1: contain pnpm output paths against an escaping .npmrc)', () => { @@ -307,7 +322,7 @@ describe('installDependencies (P1: contain pnpm output paths against an escaping const created = await createIsolatedWorktree(sha, root, {}); assert.equal(created.ok, true); try { - const installed = await installDependencies(created.path, {}); + const installed = await installDependencies(created.path, { trustedRepoRoot: root }); assert.equal(installed, true); assert.equal( existsSync(join(created.path, '..', 'ESCAPE-modules')), @@ -324,7 +339,7 @@ describe('installDependencies (P1: contain pnpm output paths against an escaping const created = await createIsolatedWorktree(sha, root, {}); assert.equal(created.ok, true); try { - const installed = await installDependencies(created.path, {}); + const installed = await installDependencies(created.path, { trustedRepoRoot: root }); assert.equal(installed, true); assert.equal( existsSync(join(created.path, '..', 'ESCAPE-virtualstore')), @@ -340,7 +355,7 @@ describe('installDependencies (P1: contain pnpm output paths against an escaping describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semantics -- DI only)', () => { it('reports UNKNOWN when git worktree add fails', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', runBounded: (_command, args) => { if (args.includes('add')) return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; @@ -353,7 +368,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('reports UNKNOWN, never a false PASS/FAIL, when the install fails', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -372,7 +387,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('reports PASS/FAIL correctly from a genuine tsgo exit status once install succeeds (DI)', async () => { const runBounded = () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }); const pass = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -388,7 +403,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti assert.equal(pass, 'PASS'); const fail = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -406,7 +421,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('reports UNKNOWN, not FAIL, when tsgo is terminated by a signal (e.g. external OOM kill)', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -424,7 +439,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('reports UNKNOWN, not FAIL, on a null status with no signal (defensive)', async () => { const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -444,7 +459,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti const runBounded = () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }); let seenTimeoutMs; await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -459,7 +474,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti let overriddenTimeoutMs; await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -476,7 +491,7 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti it('canonicalizes a relative repoRoot to an absolute path before any git/install/dependencyState call', async () => { const seenCwds = []; await verifyExactTreeTypecheck('a'.repeat(40), '.', { - listTreeFiles: (_sha, cwd) => { + listTreeEntries: (_sha, cwd) => { seenCwds.push(cwd); return []; }, @@ -500,7 +515,7 @@ describe('verifyExactTreeTypecheck (P1: the checked ref must never supply the co let seenRoot; let seenCwd; const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -520,7 +535,7 @@ describe('verifyExactTreeTypecheck (P1: the checked ref must never supply the co for (const dependencyState of ['DIVERGED', 'UNKNOWN', 'NOT_APPLICABLE']) { let worktreeCalls = 0; const state = await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => dependencyState, runBounded: () => { worktreeCalls += 1; @@ -540,7 +555,7 @@ describe('verifyExactTreeTypecheck (P1: the checked ref must never supply the co }); }); -describe('verifyNoTrackedNodeModules (P1: refuse before any materialization touches disk)', () => { +describe('verifyExactTreePreflight (P1: refuse before any materialization touches disk)', () => { function makeRepoWithTrackedPath(relativePath, { symlink } = {}) { const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-nm-')); temporaryRoots.push(root); @@ -623,9 +638,56 @@ describe('verifyNoTrackedNodeModules (P1: refuse before any materialization touc assert.equal(worktreeAddCalled, true); assert.equal(state, 'UNKNOWN'); // fake path -- worktree add itself is mocked to fail, but it was reached. }); + + function makeRepoWithTrackedSymlink(linkPath, target) { + const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-symlink-')); + temporaryRoots.push(root); + git(root, ['init', '--quiet', '--initial-branch=main']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + writeFileSync(join(root, 'real.txt'), 'inside the tree\n'); + const fullPath = join(root, linkPath); + mkdirSync(join(fullPath, '..'), { recursive: true }); + execFileSync('ln', ['-s', target, fullPath]); + git(root, ['add', '-A']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'test']); + const sha = git(root, ['rev-parse', 'HEAD']).trim(); + return { root, sha }; + } + + it('refuses a tracked symlink whose relative target escapes the isolated tree (UNKNOWN, no materialization)', async () => { + const { root, sha } = makeRepoWithTrackedSymlink('sub/escaping-link', '../../../outside-target'); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for an escaping symlink commit'); + }); + + it('refuses a tracked symlink with an absolute target (UNKNOWN, no materialization)', async () => { + const { root, sha } = makeRepoWithTrackedSymlink('escaping-abs-link', '/etc/passwd'); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for an escaping symlink commit'); + }); + + it('leaves a commit with an ordinary, non-escaping tracked symlink eligible for materialization', async () => { + const { root, sha } = makeRepoWithTrackedSymlink('safe-link', 'real.txt'); + let worktreeAddCalled = false; + const state = await verifyExactTreeTypecheck(sha, root, { + computeDependencyState: () => 'MATCHES', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('add')) worktreeAddCalled = true; + return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.equal(worktreeAddCalled, true); + assert.equal(state, 'UNKNOWN'); // fake path -- worktree add itself is mocked to fail, but it was reached. + }); }); -describe('createIsolatedWorktree (P2: post-checkout hooks must not execute)', () => { +describe('createIsolatedWorktree (P1/P2: post-checkout hooks and checkout filters must not execute)', () => { it('does not execute a configured post-checkout hook while materializing the isolated worktree', async () => { const { root, sha } = makeTinyTsRepo('const x: number = 1;\n'); const markerRoot = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-hookmark-')); @@ -643,6 +705,42 @@ describe('createIsolatedWorktree (P2: post-checkout hooks must not execute)', () await removeIsolatedWorktree(created.path, root, {}); } }); + + it('does not execute a configured smudge/checkout filter (e.g. Git LFS-style) while materializing the isolated worktree', async () => { + const markerRoot = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-filtermark-')); + temporaryRoots.push(markerRoot); + const markerFile = join(markerRoot, 'filter-ran.txt'); + + const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-filter-')); + temporaryRoots.push(root); + git(root, ['init', '--quiet', '--initial-branch=main']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + writeFileSync(join(root, '.gitattributes'), 'tracked.bin filter=evilfilter\n'); + writeFileSync(join(root, 'tracked.bin'), 'secret content\n'); + git(root, ['add', '-A']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'test']); + const sha = git(root, ['rev-parse', 'HEAD']).trim(); + + // QNBS-v3: simulates a developer with an LFS-style filter registered globally (as `git lfs install` would). + const fakeHome = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-fakehome-')); + temporaryRoots.push(fakeHome); + const filterCommand = `sh -c "echo ran > '${markerFile}'; cat"`; + git(root, ['config', '--file', join(fakeHome, '.gitconfig'), 'filter.evilfilter.smudge', filterCommand]); + git(root, ['config', '--file', join(fakeHome, '.gitconfig'), 'filter.evilfilter.required', 'true']); + + const originalHome = process.env.HOME; + process.env.HOME = fakeHome; + let created; + try { + created = await createIsolatedWorktree(sha, root, {}); + assert.equal(created.ok, true); + assert.equal(existsSync(markerFile), false, 'the configured smudge filter must not have run'); + } finally { + process.env.HOME = originalHome; + if (created) await removeIsolatedWorktree(created.path, root, {}); + } + }); }); describe('resolveRef (bounded, output-capturing ref resolution)', () => { @@ -673,7 +771,7 @@ describe('verifyExactTreeForShas (dedup, sequential, aggregation)', () => { it('deduplicates identical SHAs so the underlying check runs once', async () => { let calls = 0; const state = await verifyExactTreeForShas(['a'.repeat(40), 'a'.repeat(40)], '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', mkdtempFn: async () => { calls += 1; @@ -696,7 +794,7 @@ describe('verifyExactTreeForShas (dedup, sequential, aggregation)', () => { // QNBS-v3: sequential processing -- alternate by call order for one genuine FAIL, one UNKNOWN. let tsgoCallCount = 0; const state = await verifyExactTreeForShas(['a'.repeat(40), 'b'.repeat(40)], '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -715,6 +813,24 @@ describe('verifyExactTreeForShas (dedup, sequential, aggregation)', () => { }); describe('interruption handling (P2: explicit user intent must stop the whole run)', () => { + it('rejects when the initial stale-worktree prune is interrupted, before any worktree is created', async () => { + let addCalled = false; + await assert.rejects( + verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeEntries: () => [], + computeDependencyState: () => 'MATCHES', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('prune')) return { status: null, error: null, signal: null, timedOut: false, interrupted: true }; + if (args.includes('add')) addCalled = true; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }), + { name: 'ExactTreeInterrupted' }, + ); + assert.equal(addCalled, false, 'worktree add must never start after the prune step is interrupted'); + }); + it('createIsolatedWorktree reports interrupted:true in its return shape, not a generic failure', async () => { const created = await createIsolatedWorktree('a'.repeat(40), '/repo', { mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -730,7 +846,7 @@ describe('interruption handling (P2: explicit user intent must stop the whole ru it('verifyExactTreeTypecheck rejects (does not return UNKNOWN) when worktree creation is interrupted', async () => { await assert.rejects( verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: (_command, args) => { @@ -746,7 +862,7 @@ describe('interruption handling (P2: explicit user intent must stop the whole ru let removeCalled = false; await assert.rejects( verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -766,7 +882,7 @@ describe('interruption handling (P2: explicit user intent must stop the whole ru let removeCalled = false; await assert.rejects( verifyExactTreeTypecheck('a'.repeat(40), '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -791,7 +907,7 @@ describe('interruption handling (P2: explicit user intent must stop the whole ru let worktreeAddCalls = 0; await assert.rejects( verifyExactTreeForShas(['a'.repeat(40), 'b'.repeat(40)], '/repo', { - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', runBounded: (_command, args) => { @@ -816,7 +932,7 @@ describe('interruption handling (P2: explicit user intent must stop the whole ru try { await main(['HEAD'], { repoRoot: '/repo', - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', runGit: () => ({ status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }), mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', @@ -843,7 +959,7 @@ describe('main (real CLI entry path, realistic DI)', () => { try { await main([], { repoRoot: '/repo', - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', runGit: () => ({ status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }), runBounded: (_command, args) => { @@ -866,7 +982,7 @@ describe('main (real CLI entry path, realistic DI)', () => { try { await main(['main', 'feature-branch'], { repoRoot: '/repo', - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', runGit: (args) => { resolvedRefs.push(args[2]); @@ -911,7 +1027,7 @@ describe('main (real CLI entry path, realistic DI)', () => { try { await main(['HEAD'], { repoRoot: '/repo', - listTreeFiles: () => [], + listTreeEntries: () => [], computeDependencyState: () => 'MATCHES', storeDir: '/fake-store', runGit: () => ({ status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '', error: undefined }), From b3d8ab5155c90c25faa3c81e45af1c9e1f6669ba Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 26 Aug 2026 02:33:55 +0200 Subject: [PATCH 9/9] fix: close compiler-configuration weaponization and replace-ref gaps Root-clustered remediation for this review epoch (HEAD f796bf45), covering two CodeRabbit findings plus seven codex findings: - Empty pnpm store path (CodeRabbit): defaultResolveStoreDir now treats a trimmed-empty stdout the same as a failure, closing a path where pnpm would have received --store-dir '' and resolved it relative to the untrusted worktree cwd instead of the pinned trusted store. - Malicious-tsgo test depth (CodeRabbit): the test previously only proved refusal via the dependencyState gate, never reaching a real install. Split into two tests -- the default-gate refusal, and a second test with dependencyState mocked to MATCHES, proving the deeper property: even with a real, legitimate frozen install of the evil workspace package, tsgo is resolved from the trusted repoRoot (which has no tsgo of its own) and the isolated tree's freshly-installed evil binary is still never touched. - Repository-local checkout filters (codex, P1): the prior fix only neutralized global/system git config; a filter registered via `git config --local` (e.g. `git lfs install --local`) was untouched. Git has no clean way to force an arbitrary filter driver (smudge vs. clean vs. process all differ) to a no-op via config override, so this closes it structurally instead: the preflight now parses every tracked .gitattributes (root and nested) for `filter=` references and refuses before any worktree exists if that name has smudge/clean/process configured in any scope. - Windows symlink escapes (codex): the escape check used node:path.posix exclusively, so a backslash-relative target, a drive-letter absolute path, or a UNC path wasn't recognized as escaping on this Linux development machine. A tracked symlink is now refused if its target looks like any of those forms, regardless of the host platform running the check. - Interrupts from worktree cleanup (codex): the fallback cleanup path in removeIsolatedWorktree checked only for an ordinary failure, silently absorbing an interrupted git worktree remove. Cleanup still always runs to completion (never aborted mid-way), but now re-raises ExactTreeInterrupted afterward so a multi-ref run stops instead of starting the next verification. - tsBuildInfoFile writes and noCheck bypass (codex, one P1 + one P1): a checked ref's own tsconfig.tsgo.json could set noCheck:true (tsgo exits 0 without full type checking) or point tsBuildInfoFile at an external, developer-writable path (written even under --noEmit once incremental is set). Empirically verified that explicit --noCheck false and --tsBuildInfoFile .tsbuildinfo on the CLI outrank both settings, the same "CLI wins" precedent already used for pnpm's own flags. - Unmaterialized gitlinks (codex): a tracked submodule entry is left uninitialized by `git worktree add`, so tsgo could silently omit its source while still exiting 0. The preflight now refuses any commit containing a gitlink (tree entry type 'commit') outright. - Git replace refs (codex, P1): refs/replace/ would transparently substitute a different tree while ls-tree/show/worktree add kept reporting the original SHA. GIT_NO_REPLACE_OBJECTS=1 is now set globally at module load, so every git invocation in this process -- including dependency-state.mjs's and signing-core.mjs's -- inherits it. Partial, explicitly-documented mitigation for a related but broader codex finding ("confine compiler reads to the isolated worktree"): an escaping absolute/relative path in the checked ref's own tsconfig.tsgo.json (include/exclude/files/extends) is now refused by the preflight. An escaping import written directly inside tracked source content is a documented residual this cannot close without OS-level process sandboxing -- a materially different mechanism than this diagnostic tool implements. tests/unit/tooling/verify-exact-tree.test.mjs: adversarial regressions for every fix above, including a real git-replace-refs fixture proving the preflight sees the original tree rather than the substituted one, and a DI proof that --noCheck false / --tsBuildInfoFile .tsbuildinfo are always present on the tsgo invocation. Real end-to-end smoke test against this repo's own HEAD, including the new CLI overrides against its real tsconfig.tsgo.json, still PASSes; cleanup verified. --- scripts/verify-exact-tree.d.mts | 4 +- scripts/verify-exact-tree.mjs | 102 +++++++- tests/unit/tooling/verify-exact-tree.test.mjs | 236 +++++++++++++++++- 3 files changed, 336 insertions(+), 6 deletions(-) diff --git a/scripts/verify-exact-tree.d.mts b/scripts/verify-exact-tree.d.mts index 3ce5d1d02..1e395a0ba 100644 --- a/scripts/verify-exact-tree.d.mts +++ b/scripts/verify-exact-tree.d.mts @@ -36,8 +36,10 @@ export interface VerifyExactTreeDependencies { repoRoot?: string; // QNBS-v3: reuses dependency-state.mjs's mode-aware git-tree enumeration authority -- not a second parser. listTreeEntries?: (sha: string, cwd: string) => TreeEntry[] | null; - // QNBS-v3: reuses dependency-state.mjs's git-object blob reader for symlink target content. + // QNBS-v3: reuses dependency-state.mjs's git-object blob reader for symlink/.gitattributes content. readBlobAtRef?: (sha: string, relativePath: string, cwd: string) => Buffer | null; + // QNBS-v3: checks any scope (local/global/system) for a tracked .gitattributes filter name. + isFilterConfigured?: (name: string, repoRoot: string) => boolean; // QNBS-v3: reuses #502's manifest-compatibility authority to gate which tsgo binary may be trusted. computeDependencyState?: (sha: string, root: string) => DependencyState; // QNBS-v3: the trusted checkout installDependencies resolves the pinned store-dir from -- never the worktree. diff --git a/scripts/verify-exact-tree.mjs b/scripts/verify-exact-tree.mjs index 4146de0fa..ffef5f20e 100644 --- a/scripts/verify-exact-tree.mjs +++ b/scripts/verify-exact-tree.mjs @@ -16,7 +16,21 @@ class ExactTreeInterrupted extends Error { } } -const DEFAULT_TSGO_ARGS = ['--project', 'tsconfig.tsgo.json', '--noEmit', '--checkers', '1']; +// QNBS-v3: refs/replace/ would substitute a different tree; set globally so every git call in this process inherits it. +process.env.GIT_NO_REPLACE_OBJECTS = '1'; + +const DEFAULT_TSGO_ARGS = [ + '--project', + 'tsconfig.tsgo.json', + '--noEmit', + '--checkers', + '1', + // QNBS-v3: CLI flags outrank the checked ref's own tsconfig -- must not disable checking or redirect build-info writes. + '--noCheck', + 'false', + '--tsBuildInfoFile', + '.tsbuildinfo', +]; // QNBS-v3: measured ~2m20s for the full project on this hardware with a warm store; 5min gives margin. const DEFAULT_INSTALL_TIMEOUT_MS = 300_000; // QNBS-v3: single-checker measured ~56s; docs cite ~300s for full typecheck -- 6min clears both with margin. @@ -51,6 +65,11 @@ function hasTrackedNodeModules(entries) { ); } +// QNBS-v3: a Windows checkout recreates backslash/drive-letter/UNC targets literally -- posix alone misses them. +function looksLikeWindowsEscapingTarget(target) { + return /^[a-zA-Z]:[\\/]/.test(target) || target.startsWith('\\\\') || target.includes('\\'); +} + // QNBS-v3: a tracked symlink whose target escapes the tree root would let tsgo read live/external content. function hasEscapingSymlink(entries, sha, repoRoot, dependencies) { const readBlob = dependencies.readBlobAtRef ?? readFileAtRef; @@ -59,6 +78,7 @@ function hasEscapingSymlink(entries, sha, repoRoot, dependencies) { const targetBuffer = readBlob(sha, entry.path, repoRoot); if (targetBuffer === null) return true; // unreadable target -- fail closed, treat as escaping. const target = targetBuffer.toString('utf8').trim(); + if (looksLikeWindowsEscapingTarget(target)) return true; if (posix.isAbsolute(target)) return true; const resolved = posix.normalize(posix.join(posix.dirname(entry.path), target)); if (resolved.startsWith('..') || posix.isAbsolute(resolved)) return true; @@ -66,13 +86,81 @@ function hasEscapingSymlink(entries, sha, repoRoot, dependencies) { return false; } +// QNBS-v3: an uninitialized submodule leaves its path silently empty -- tsgo could exit 0 while omitting it. +function hasUnmaterializedGitlink(entries) { + return entries.some((entry) => entry.type === 'commit'); +} + +// QNBS-v3: git has no clean way to force a filter driver to a no-op (smudge vs. process differ); refuse instead. +function defaultIsFilterConfigured(name, repoRoot) { + for (const key of ['smudge', 'clean', 'process']) { + const result = spawnSync('git', ['config', '--get', `filter.${name}.${key}`], { + cwd: repoRoot, + encoding: 'utf8', + timeout: 5000, + }); + if (!result.error && result.status === 0) return true; + } + return false; +} + +// QNBS-v3: a tracked .gitattributes selecting a filter configured in ANY scope (local/global/system) would run it. +function hasActiveTrackedFilter(entries, sha, repoRoot, dependencies) { + const readBlob = dependencies.readBlobAtRef ?? readFileAtRef; + const isFilterConfigured = dependencies.isFilterConfigured ?? defaultIsFilterConfigured; + const filterNames = new Set(); + for (const entry of entries) { + if (entry.path !== '.gitattributes' && !entry.path.endsWith('/.gitattributes')) continue; + const contentBuffer = readBlob(sha, entry.path, repoRoot); + if (contentBuffer === null) return true; // unreadable .gitattributes -- fail closed. + for (const line of contentBuffer.toString('utf8').split('\n')) { + const match = line.match(/(?:^|\s)filter=(\S+)/); + if (match) filterNames.add(match[1]); + } + } + for (const name of filterNames) { + if (isFilterConfigured(name, repoRoot)) return true; + } + return false; +} + +// QNBS-v3: partial mitigation for the tsconfig-level vector; an escaping source-level import is a documented residual (needs OS sandboxing). +function hasEscapingTsconfigScope(entries, sha, repoRoot, dependencies) { + const readBlob = dependencies.readBlobAtRef ?? readFileAtRef; + const tsconfigEntry = entries.find((entry) => entry.path === 'tsconfig.tsgo.json'); + if (!tsconfigEntry) return false; // a missing tsconfig fails the install/tsgo steps on their own terms. + const contentBuffer = readBlob(sha, 'tsconfig.tsgo.json', repoRoot); + if (contentBuffer === null) return true; // unreadable -- fail closed. + let parsed; + try { + parsed = JSON.parse(contentBuffer.toString('utf8')); + } catch { + return true; // unparseable -- refuse rather than let tsgo interpret it unexamined. + } + const values = [ + ...(Array.isArray(parsed.include) ? parsed.include : []), + ...(Array.isArray(parsed.exclude) ? parsed.exclude : []), + ...(Array.isArray(parsed.files) ? parsed.files : []), + ...(typeof parsed.extends === 'string' ? [parsed.extends] : []), + ].filter((value) => typeof value === 'string'); + return values.some( + (value) => + looksLikeWindowsEscapingTarget(value) || + posix.isAbsolute(value) || + posix.normalize(value).startsWith('..'), + ); +} + // QNBS-v3: checked against the exact commit's own git objects, before any worktree/pnpm step touches disk. function verifyExactTreePreflight(sha, repoRoot, dependencies = {}) { const listEntries = dependencies.listTreeEntries ?? listTreeEntries; const entries = listEntries(sha, repoRoot); if (entries === null) return false; // an unreadable tree can never be proven clean. if (hasTrackedNodeModules(entries)) return false; + if (hasUnmaterializedGitlink(entries)) return false; if (hasEscapingSymlink(entries, sha, repoRoot, dependencies)) return false; + if (hasActiveTrackedFilter(entries, sha, repoRoot, dependencies)) return false; + if (hasEscapingTsconfigScope(entries, sha, repoRoot, dependencies)) return false; return true; } @@ -95,7 +183,9 @@ function defaultResolveStoreDir(trustedRepoRoot) { env: { ...process.env, COREPACK_ENABLE_NETWORK: '0' }, }); if (result.error || result.status !== 0) return null; - return result.stdout.trim(); + const storePath = result.stdout.trim(); + // QNBS-v3: an empty store path would resolve --store-dir '' relative to the untrusted worktree cwd. + return storePath === '' ? null : storePath; } export async function createIsolatedWorktree(sha, repoRoot, dependencies = {}) { @@ -143,7 +233,7 @@ export async function createIsolatedWorktree(sha, repoRoot, dependencies = {}) { } } -// QNBS-v3: fail-closed -- git's own removal failing falls back to a raw sweep plus a metadata prune. +// QNBS-v3: fail-closed removal fallback; cleanup always finishes even when interrupted, then re-raises it. export async function removeIsolatedWorktree(worktreePath, repoRoot, dependencies = {}) { if (!worktreePath) return; const runGit = dependencies.runBounded ?? runBounded; @@ -151,13 +241,16 @@ export async function removeIsolatedWorktree(worktreePath, repoRoot, dependencie const result = await runGit('git', ['worktree', 'remove', '--force', worktreePath], { cwd: repoRoot, }); + let interrupted = Boolean(result.interrupted); if (boundedCommandFailed(result)) { try { await removeDir(worktreePath); } catch { // Best-effort: nothing more can be done from here; the directory is under os.tmpdir(). } - await runGit('git', ['worktree', 'prune'], { cwd: repoRoot }); + const pruneResult = await runGit('git', ['worktree', 'prune'], { cwd: repoRoot }); + interrupted ||= Boolean(pruneResult.interrupted); + if (interrupted) throw new ExactTreeInterrupted(); return; } // QNBS-v3: git leaves the now-empty mkdtemp-created dir in place after remove -- sweep it too. @@ -166,6 +259,7 @@ export async function removeIsolatedWorktree(worktreePath, repoRoot, dependencie } catch { // Best-effort: nothing more can be done from here; the directory is under os.tmpdir(). } + if (interrupted) throw new ExactTreeInterrupted(); } // QNBS-v3: real pnpm install, not a hand-reconstructed symlink graph -- offline, fails to UNKNOWN below. diff --git a/tests/unit/tooling/verify-exact-tree.test.mjs b/tests/unit/tooling/verify-exact-tree.test.mjs index 127222543..77aeb62e9 100644 --- a/tests/unit/tooling/verify-exact-tree.test.mjs +++ b/tests/unit/tooling/verify-exact-tree.test.mjs @@ -508,6 +508,22 @@ describe('verifyExactTreeTypecheck (fail-closed lifecycle, signal/status semanti assert.ok(seenCwds.length > 0); for (const cwd of seenCwds) assert.equal(cwd, resolve('.'), `expected absolute cwd, got ${cwd}`); }); + + it('forces --noCheck false and a worktree-local --tsBuildInfoFile on the tsgo invocation, outranking the checked ref\'s own tsconfig', async () => { + let seenArgs; + await verifyExactTreeTypecheck('a'.repeat(40), '/repo', { + listTreeEntries: () => [], + computeDependencyState: () => 'MATCHES', + storeDir: '/fake-store', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: () => ({ status: 0, error: null, signal: null, timedOut: false, interrupted: false }), + runLocalBinaryDetailed: async (_binary, args) => { + seenArgs = args; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.deepEqual(seenArgs.slice(seenArgs.indexOf('--noCheck')), ['--noCheck', 'false', '--tsBuildInfoFile', '.tsbuildinfo']); + }); }); describe('verifyExactTreeTypecheck (P1: the checked ref must never supply the compiler that certifies it)', () => { @@ -547,9 +563,17 @@ describe('verifyExactTreeTypecheck (P1: the checked ref must never supply the co } }); - it('never executes a workspace-supplied tsgo binary or yields PASS, even via a legitimate frozen install', async () => { + it('refuses (UNKNOWN) a workspace-supplied tsgo binary via the default dependencyState gate, before any install', async () => { const { root, maliciousSha, markerFile } = makeMaliciousTsgoFixture(); const state = await verifyExactTreeTypecheck(maliciousSha, root, {}); + assert.equal(state, 'UNKNOWN'); + assert.equal(existsSync(markerFile), false, 'the ref-supplied tsgo bin must never execute'); + }); + + it('never executes a workspace-supplied tsgo binary even via a real, legitimate frozen install (dependencyState mocked to MATCHES)', async () => { + const { root, maliciousSha, markerFile } = makeMaliciousTsgoFixture(); + // QNBS-v3: bypasses the gate above -- root's own tsgo lookup finds nothing, so the isolated tree's real, freshly-installed evil bin is still never touched. + const state = await verifyExactTreeTypecheck(maliciousSha, root, { computeDependencyState: () => 'MATCHES' }); assert.notEqual(state, 'PASS', 'a ref-supplied compiler must never certify itself'); assert.equal(existsSync(markerFile), false, 'the ref-supplied tsgo bin must never execute'); }); @@ -685,6 +709,153 @@ describe('verifyExactTreePreflight (P1: refuse before any materialization touche assert.equal(worktreeAddCalled, true); assert.equal(state, 'UNKNOWN'); // fake path -- worktree add itself is mocked to fail, but it was reached. }); + + function makeRepoWithGitattributesFilter(filterName, attributesPath = '.gitattributes') { + const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-filterattr-')); + temporaryRoots.push(root); + git(root, ['init', '--quiet', '--initial-branch=main']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + const fullAttrPath = join(root, attributesPath); + mkdirSync(join(fullAttrPath, '..'), { recursive: true }); + writeFileSync(fullAttrPath, `tracked.bin filter=${filterName}\n`); + writeFileSync(join(root, 'tracked.bin'), 'secret content\n'); + git(root, ['add', '-A']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'test']); + const sha = git(root, ['rev-parse', 'HEAD']).trim(); + return { root, sha }; + } + + it('refuses (UNKNOWN, no materialization) when a tracked .gitattributes selects a LOCALLY-configured filter', async () => { + const { root, sha } = makeRepoWithGitattributesFilter('evilfilter'); + // QNBS-v3: --local specifically, not --global -- proves this closes the local-scope gap too. + git(root, ['config', '--local', 'filter.evilfilter.smudge', 'cat']); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start when a configured filter is selected'); + }); + + it('refuses (UNKNOWN, no materialization) when a NESTED tracked .gitattributes selects a configured filter', async () => { + const { root, sha } = makeRepoWithGitattributesFilter('nestedfilter', 'sub/.gitattributes'); + git(root, ['config', '--local', 'filter.nestedfilter.process', 'cat']); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start when a configured filter is selected'); + }); + + it('leaves a commit eligible when its .gitattributes selects a filter name that is not configured anywhere', async () => { + const { root, sha } = makeRepoWithGitattributesFilter('totally-undefined-filter-name'); + let worktreeAddCalled = false; + const state = await verifyExactTreeTypecheck(sha, root, { + computeDependencyState: () => 'MATCHES', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('add')) worktreeAddCalled = true; + return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.equal(worktreeAddCalled, true); + assert.equal(state, 'UNKNOWN'); // fake path -- worktree add itself is mocked to fail, but it was reached. + }); + + it('refuses (UNKNOWN) a commit containing an unmaterialized gitlink (submodule)', async () => { + const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-gitlink-')); + temporaryRoots.push(root); + git(root, ['init', '--quiet', '--initial-branch=main']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + writeFileSync(join(root, 'real.ts'), 'const x: number = 1;\n'); + // QNBS-v3: a real submodule commit hash object need not exist locally -- ls-tree/mode is what matters. + git(root, ['update-index', '--add', '--cacheinfo', '160000', 'a'.repeat(40), 'sub-module']); + git(root, ['add', 'real.ts']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'test']); + const sha = git(root, ['rev-parse', 'HEAD']).trim(); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start when a gitlink is present'); + }); + + it('refuses (UNKNOWN) a tracked symlink with a Windows-style backslash-relative escaping target', async () => { + const { root, sha } = makeRepoWithTrackedSymlink('sub/escaping-link', '..\\..\\..\\outside-target'); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for an escaping symlink commit'); + }); + + it('refuses (UNKNOWN) a tracked symlink with a Windows drive-letter absolute target', async () => { + const { root, sha } = makeRepoWithTrackedSymlink('escaping-drive-link', 'C:\\Windows\\System32\\drivers\\etc\\hosts'); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for an escaping symlink commit'); + }); + + it('refuses (UNKNOWN) a tracked symlink with a UNC path target', async () => { + const { root, sha } = makeRepoWithTrackedSymlink('escaping-unc-link', '\\\\attacker-host\\share\\payload'); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for an escaping symlink commit'); + }); + + function makeRepoWithTsconfigScope(tsconfigContent) { + const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-tsconfigscope-')); + temporaryRoots.push(root); + git(root, ['init', '--quiet', '--initial-branch=main']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + writeFileSync(join(root, 'tsconfig.tsgo.json'), tsconfigContent); + writeFileSync(join(root, 'index.ts'), 'const x: number = 1;\n'); + git(root, ['add', '-A']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'test']); + const sha = git(root, ['rev-parse', 'HEAD']).trim(); + return { root, sha }; + } + + it('refuses (UNKNOWN) when tsconfig.tsgo.json include escapes the tree via ../..', async () => { + const { root, sha } = makeRepoWithTsconfigScope( + JSON.stringify({ include: ['../../../etc/**/*.ts'] }), + ); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for an escaping tsconfig scope'); + }); + + it('refuses (UNKNOWN) when tsconfig.tsgo.json extends an absolute path', async () => { + const { root, sha } = makeRepoWithTsconfigScope(JSON.stringify({ extends: '/etc/tsconfig-base.json' })); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for an escaping tsconfig scope'); + }); + + it('refuses (UNKNOWN) when tsconfig.tsgo.json is not valid JSON', async () => { + const { root, sha } = makeRepoWithTsconfigScope('{ not valid json'); + const spy = refusingMaterializationSpy(); + const state = await verifyExactTreeTypecheck(sha, root, spy); + assert.equal(state, 'UNKNOWN'); + assert.equal(spy.calls.runBounded, 0, 'materialization must never start for an unparseable tsconfig'); + }); + + it('leaves a commit eligible when tsconfig.tsgo.json only uses ordinary in-tree relative paths', async () => { + const { root, sha } = makeRepoWithTsconfigScope(JSON.stringify({ include: ['index.ts'] })); + let worktreeAddCalled = false; + const state = await verifyExactTreeTypecheck(sha, root, { + computeDependencyState: () => 'MATCHES', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('add')) worktreeAddCalled = true; + return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.equal(worktreeAddCalled, true); + assert.equal(state, 'UNKNOWN'); // fake path -- worktree add itself is mocked to fail, but it was reached. + }); }); describe('createIsolatedWorktree (P1/P2: post-checkout hooks and checkout filters must not execute)', () => { @@ -843,6 +1014,34 @@ describe('interruption handling (P2: explicit user intent must stop the whole ru assert.equal(created.interrupted, true); }); + it('removeIsolatedWorktree still completes cleanup, then rejects, when git worktree remove is interrupted', async () => { + let removeCalled = false; + let pruneCalled = false; + let rmCalled = false; + await assert.rejects( + removeIsolatedWorktree('/tmp/worldscript-exact-tree-fake', '/repo', { + runBounded: (_command, args) => { + if (args.includes('remove')) { + removeCalled = true; + return { status: null, error: null, signal: null, timedOut: false, interrupted: true }; + } + if (args.includes('prune')) { + pruneCalled = true; + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + } + return { status: 0, error: null, signal: null, timedOut: false, interrupted: false }; + }, + rmFn: async () => { + rmCalled = true; + }, + }), + { name: 'ExactTreeInterrupted' }, + ); + assert.equal(removeCalled, true); + assert.equal(rmCalled, true, 'the fallback sweep must still run even though remove was interrupted'); + assert.equal(pruneCalled, true, 'the fallback prune must still run even though remove was interrupted'); + }); + it('verifyExactTreeTypecheck rejects (does not return UNKNOWN) when worktree creation is interrupted', async () => { await assert.rejects( verifyExactTreeTypecheck('a'.repeat(40), '/repo', { @@ -951,6 +1150,41 @@ describe('interruption handling (P2: explicit user intent must stop the whole ru }); }); +describe('git replace refs (P1: exact-tree checks must not be fooled by refs/replace)', () => { + it('operates on the original tree, not a git-replace substitute, when the preflight lists tracked entries', async () => { + const root = mkdtempSync(join(process.cwd(), '.worldscript-exact-tree-replace-')); + temporaryRoots.push(root); + git(root, ['init', '--quiet', '--initial-branch=main']); + git(root, ['config', 'user.email', 'test@example.com']); + git(root, ['config', 'user.name', 'test']); + writeFileSync(join(root, 'README.md'), 'original\n'); + git(root, ['add', '-A']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'original']); + const originalSha = git(root, ['rev-parse', 'HEAD']).trim(); + + // QNBS-v3: if refs/replace were honored, the preflight would see the replacement's node_modules and refuse. + mkdirSync(join(root, 'node_modules', '.bin'), { recursive: true }); + writeFileSync(join(root, 'node_modules', '.bin', 'tsgo'), '#!/bin/sh\necho attacker\n'); + git(root, ['add', '-A']); + git(root, ['-c', 'commit.gpgsign=false', 'commit', '--quiet', '-m', 'replacement']); + const replacedSha = git(root, ['rev-parse', 'HEAD']).trim(); + git(root, ['reset', '--hard', '--quiet', originalSha]); + git(root, ['replace', originalSha, replacedSha]); + + let worktreeAddCalled = false; + const state = await verifyExactTreeTypecheck(originalSha, root, { + computeDependencyState: () => 'MATCHES', + mkdtempFn: async () => '/tmp/worldscript-exact-tree-fake', + runBounded: (_command, args) => { + if (args.includes('add')) worktreeAddCalled = true; + return { status: 1, error: null, signal: null, timedOut: false, interrupted: false }; + }, + }); + assert.equal(worktreeAddCalled, true, 'must operate on the original tree, not the git-replace substitute'); + assert.equal(state, 'UNKNOWN'); // fake path -- worktree add itself is mocked to fail, but it was reached. + }); +}); + describe('main (real CLI entry path, realistic DI)', () => { it('resolves HEAD by default, verifies it, and prints the result without crashing', async () => { const logs = [];