chore(test-isolation): detect live engine lock + prune stale tests

Three coupled fixes to make `pnpm test:full` exit cleanly when the local
`fn` dashboard is running:

1. scripts/check-test-isolation.mjs — replace timing-based "is the
   engine writing?" heuristic with a deterministic check: if
   `.fusion/engine.lock.lock/` exists (proper-lockfile's held-lock
   marker), the dir is engine-active and auto-skipped from violation
   reporting. The 2-second mutability probe is retained as a backstop
   for dirs with another external writer but no live lock. Also adds
   `engine.lock` / `engine.lock.lock/` to RUNTIME_IGNORE_PATTERNS so
   a mid-test engine start/stop doesn't trip the signature compare.

2. packages/dashboard/.../__tests__/GitManagerModal.test.tsx — prune
   the Status-panel Sync button + Recent-advances-events describe
   blocks. Their UI was removed in 5d35b64bd ("remove duplicate
   integration-advances UI") but the tests stayed and were timing
   out at 1s each. The Remotes-panel Sync describe is kept because
   the `remotes-sync-integration-tip-btn` still exists.

3. packages/engine/.../merge-reuse-task-worktree.slow.test.ts —
   update the happy-path assertion to reflect 4c31e885b
   ("merger auto-syncs project-root checkout after ref advance").
   Before that change, the merger's `update-ref` advance left the
   project root's working tree stale, so `git status --porcelain`
   would differ after the merge. With auto-sync, the new file is
   tracked + clean at HEAD, so status doesn't change. Verify the
   file actually landed via `git ls-files` instead.

After this, `pnpm test:full` exits 0 with the local dashboard running.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-23 20:18:08 -07:00
parent 0c0839eeb6
commit b8919b7bb4
4 changed files with 76 additions and 361 deletions

View File

@@ -99,12 +99,34 @@ const RUNTIME_IGNORE_PATTERNS = [
/^scripts\.json$/,
/^update-check\.json$/,
/^disabled-auto-extension-discovery$/,
// Engine singleton lock (proper-lockfile creates `engine.lock.lock/` while
// held; `engine.lock` is the sentinel file). Their entries appear/disappear
// as the local dashboard starts/stops, which is not test pollution.
/^engine\.lock$/,
/^engine\.lock\.lock(?:[/\\]|$)/,
];
function isRuntimePath(relPath) {
return RUNTIME_IGNORE_PATTERNS.some((re) => re.test(relPath));
}
// A .fusion dir is "engine-active" when an engine process currently holds the
// singleton lock. proper-lockfile materializes this as `engine.lock.lock/`
// being present alongside the `engine.lock` sentinel. This is a deterministic
// signal — no timing/probe required — so we use it to auto-skip live-engine
// dirs instead of relying on the post-run mutability burst landing inside
// our 2-second probe window.
function isFusionEngineActive(fusionDir) {
if (!existsSync(fusionDir)) return false;
try {
const lockHeldDir = join(fusionDir, "engine.lock.lock");
const stat = statSync(lockHeldDir);
return stat.isDirectory();
} catch {
return false;
}
}
function collectFusionSignature(rootDir, out = []) {
if (!existsSync(rootDir)) return out;
let stat;
@@ -160,6 +182,13 @@ function recordBaseline() {
const unstableProtectedDirs = [];
const firstProtected = samples[0];
for (const first of firstProtected) {
// Live engine lock present → auto-mark unstable. Same outcome as the
// mutability probe below would (eventually) reach, but deterministic and
// immune to write-cadence gaps.
if (isFusionEngineActive(first.dir)) {
unstableProtectedDirs.push(first.dir);
continue;
}
let unstable = false;
for (let i = 1; i < samples.length; i++) {
const current = samples[i].find((entry) => entry.dir === first.dir);
@@ -239,28 +268,39 @@ function checkAgainstBaseline() {
const protectedViolations = [];
if (candidateViolations.length > 0) {
// A live local app can write in bursts (e.g. heartbeat every few seconds),
// so do a short mutability probe before blaming tests.
const postSamples = [currentProtected];
for (let i = 0; i < 4; i++) {
sleepMs(500);
postSamples.push(snapshotProtectedFusion());
}
// First: skip any candidate dir where an engine is currently holding the
// singleton lock. This is the dominant local-dev case (`fn` dashboard
// running while tests are invoked) and the engine-lock signal is
// race-free, unlike the post-test mutability probe.
const remainingCandidates = candidateViolations.filter((dir) => !isFusionEngineActive(dir));
for (const dir of candidateViolations) {
let externallyActive = false;
for (let i = 1; i < postSamples.length; i++) {
const prev = postSamples[i - 1].find((entry) => entry.dir === dir);
const next = postSamples[i].find((entry) => entry.dir === dir);
if (!prev || !next) continue;
if (JSON.stringify(prev.entries) !== JSON.stringify(next.entries)) {
externallyActive = true;
break;
}
if (remainingCandidates.length > 0) {
// A live local app can write in bursts (e.g. heartbeat every few seconds),
// so do a short mutability probe before blaming tests. Retained as a
// backstop for dirs that don't have an engine lock but do have an
// external writer (e.g. an `fn` process that crashed mid-run and left
// the lock stale).
const postSamples = [currentProtected];
for (let i = 0; i < 4; i++) {
sleepMs(500);
postSamples.push(snapshotProtectedFusion());
}
if (!externallyActive) {
protectedViolations.push(dir);
for (const dir of remainingCandidates) {
let externallyActive = false;
for (let i = 1; i < postSamples.length; i++) {
const prev = postSamples[i - 1].find((entry) => entry.dir === dir);
const next = postSamples[i].find((entry) => entry.dir === dir);
if (!prev || !next) continue;
if (JSON.stringify(prev.entries) !== JSON.stringify(next.entries)) {
externallyActive = true;
break;
}
}
if (!externallyActive) {
protectedViolations.push(dir);
}
}
}
}