From ece7653ca0012ecde2ab2942f64159d2fb08c4b8 Mon Sep 17 00:00:00 2001 From: mosaic-coder Date: Fri, 31 Jul 2026 17:50:49 -0500 Subject: [PATCH] test: capture remaining checkout race regressions --- .../rm-01-reproducible-checkout.md | 19 +++++++++++++ scripts/install-hooks.test.mjs | 28 +++++++++++++++++++ scripts/preflight.test.mjs | 28 +++++++++++++------ 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/docs/scratchpads/rm-01-reproducible-checkout.md b/docs/scratchpads/rm-01-reproducible-checkout.md index a536c951..75196840 100644 --- a/docs/scratchpads/rm-01-reproducible-checkout.md +++ b/docs/scratchpads/rm-01-reproducible-checkout.md @@ -18,3 +18,22 @@ - Worktree created at `/home/hermes/agent-work/rm-01` from `origin/main` `06e0d403`. - `/tmp` baseline: 28G used, 889M available (97%); worktree and planned store are on `/home`. - Root causes confirmed from source: committed `.npmrc` pins `/root`; `prepare` invokes Husky directly; web typecheck includes generated `.next` types without validating ownership/freshness. + +## Checkpoint evidence (c45e5e19) + +- AC1 IN PROGRESS: non-root `pnpm install --frozen-lockfile --store-dir "$HOME/.local/share/pnpm/store"` exited 0; `pnpm exec turbo run typecheck --force` exited 0 (45/45 uncached). Clean CI-container run not performed. +- AC2 DONE: with `node_modules` absent, `pnpm preflight` exited 42 with `MOSAIC_PREFLIGHT_MISSING_DEPS` and `run pnpm install`; after install it exited 0. +- AC3 DONE: appending `export const x: number = "s"` to `packages/types/src/index.ts` made `pnpm -w typecheck` exit 2 with TS2322; reverting made it exit 0. +- AC4 IN PROGRESS: local `pnpm -w build` exited 0 and `git status --porcelain` showed no generated residue beyond the intended RM-01 source changes. Fresh-clone proof not performed. +- AC5 DONE: non-root install exited 0; `pnpm store path` resolved `/home/hermes/.local/share/pnpm/store/v10`; no `/root` write was attempted. +- AC6 IN PROGRESS: focused failure/rollback tests passed, but final review found a concurrent-install race. Two installers can both observe `.husky/_` absent; after one installs successfully, the losing install's catch path can quarantine the winner's active hooks and restore stale Git config (`scripts/install-hooks.mjs`, activation/catch transaction). A RED regression is committed after the checkpoint. +- AC7 DONE: install/store/worktree were on `/home`; full `pnpm -w build` exited 0; `/tmp` usage changed by 4096 bytes during the build (23,805,173,760 → 23,805,177,856 bytes), not materially. +- AC8 DONE for the implemented path: store resolves under `$HOME`; test/quarantine/build state resolves under the worktree; no implemented component requires a writable path outside `$HOME` or the worktree. + +## Handoff + +1. Keep the newly committed RED tests red until implementing: (a) source-fingerprint marker support for valid incremental `.next` output, and (b) ownership-safe concurrent hook activation. +2. The latest automated review rejected oldest-generated-file mtime as a false positive for valid incremental Next output. Use a source-content fingerprint marker written only after successful `next build`; do not continue tuning mtimes. +3. For Husky, generation in an isolated temporary Git repo avoids mutating real `core.hooksPath` during staging. Preserve that design. Fix the losing concurrent process so it never removes a peer's completed hook set or restores stale config. +4. Codex review runs in a read-only sandbox, so its attempts to run the fixture-writing Node tests report opaque test-file failures. The same tests run normally in the worktree. +5. Full `pnpm test` is not green on this host: it exits 97 at the pre-existing Bash `BASH_LINENO` convention guard (#1003), after the changed checkout tests and package tests pass. Do not weaken that gate. diff --git a/scripts/install-hooks.test.mjs b/scripts/install-hooks.test.mjs index 1b5087dc..54e2c517 100644 --- a/scripts/install-hooks.test.mjs +++ b/scripts/install-hooks.test.mjs @@ -110,6 +110,34 @@ test('a mismatched complete hook set fails loudly instead of reporting a stale i assert.equal(await readFile(activeShim, 'utf8'), 'old-complete'); }); +test('a competing successful installer is not removed by the losing process', async () => { + const root = await fixture('concurrent'); + const activeShim = path.join(root, '.husky', '_', 'h'); + let restored = false; + + await assert.rejects( + installHooks({ + root, + quarantineRoot: path.join(fixtureRoot, 'concurrent-quarantine'), + runHusky: async (stagingHooks) => { + await mkdir(path.join(stagingHooks, '_'), { recursive: true }); + await writeFile(path.join(stagingHooks, '_', 'h'), 'ours'); + await mkdir(path.dirname(activeShim), { recursive: true }); + await writeFile(activeShim, 'peer'); + }, + activateHooks: async () => {}, + readHooksPath: async () => null, + restoreHooksPath: async () => { + restored = true; + }, + }), + /Hook installation failed/, + ); + + assert.equal(await readFile(activeShim, 'utf8'), 'peer'); + assert.equal(restored, false); +}); + test('an explicit interactive HUSKY=0 opt-out preserves existing hooks without running installer', async () => { const root = await fixture('disabled'); const activeShim = path.join(root, '.husky', '_', 'h'); diff --git a/scripts/preflight.test.mjs b/scripts/preflight.test.mjs index 0d63fb3c..a3c60f54 100644 --- a/scripts/preflight.test.mjs +++ b/scripts/preflight.test.mjs @@ -3,7 +3,7 @@ import { chmod, mkdir, rm, symlink, utimes, writeFile } from 'node:fs/promises'; import path from 'node:path'; import test from 'node:test'; -import { runPreflight } from './preflight.mjs'; +import { runPreflight, sourceFingerprint } from './preflight.mjs'; const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `preflight-${process.pid}`); @@ -83,18 +83,13 @@ test('foreign-owned generated Next state is identified separately from source er assert.match(result.message, /foreign-owned/); }); -test('stale generated Next state is identified separately from source errors', async () => { +test('a generated marker mismatch is identified separately from source errors', async () => { const root = await fixture('stale-next'); await installRequiredBins(root); const generated = path.join(root, 'apps', 'web', '.next', 'types', 'validator.ts'); await mkdir(path.dirname(generated), { recursive: true }); await writeFile(generated, 'stale generated output'); - const old = new Date('2020-01-01T00:00:00Z'); - await utimes(generated, old, old); - const fresh = path.join(root, 'apps', 'web', '.next', 'types', 'routes.ts'); - await writeFile(fresh, 'fresh generated output'); - const future = new Date('2030-01-01T00:00:00Z'); - await utimes(fresh, future, future); + await writeFile(path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash'), 'old-source'); const result = await runPreflight({ root }); assert.equal(result.code, 43); @@ -102,3 +97,20 @@ test('stale generated Next state is identified separately from source errors', a assert.match(result.message, /apps\/web\/\.next/); assert.match(result.message, /pnpm clean:generated/); }); + +test('a matching generation marker accepts incremental output with mixed mtimes', async () => { + const root = await fixture('incremental-next'); + await installRequiredBins(root); + const generated = path.join(root, 'apps', 'web', '.next', 'types', 'validator.ts'); + await mkdir(path.dirname(generated), { recursive: true }); + await writeFile(generated, 'unchanged generated output'); + await utimes(generated, new Date('2020-01-01T00:00:00Z'), new Date('2020-01-01T00:00:00Z')); + const fresh = path.join(root, 'apps', 'web', '.next', 'types', 'routes.ts'); + await writeFile(fresh, 'fresh generated output'); + await writeFile( + path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash'), + await sourceFingerprint(root), + ); + + assert.deepEqual(await runPreflight({ root }), { code: 0, message: 'checkout preflight passed' }); +});