FN-8937: rescue project engine test quarantine

Rescue the project engine suite by making subprocess watchdog behavior deterministic.

- Capture real timer APIs for subprocess watchdogs and isolate failure ownership.
- Mock integration-branch resolution to prevent host git during lifecycle tests.
- Add watchdog regression coverage and remove the expired quarantine exclusion.

Files changed:
 docs/testing.md                                    |   3 +
 packages/core/src/__test-utils__/vitest-setup.ts   |  74 ++++++++++-
 .../__tests__/subprocess-guard-fake-timers.test.ts | 140 +++++++++++++++++++++
 .../engine/src/__tests__/project-engine.test.ts    |  63 +++++++---
 packages/engine/vitest.config.ts                   |  12 +-
 scripts/lib/test-quarantine.json                   |   8 +-
 6 files changed, 265 insertions(+), 35 deletions(-)

Fusion-Task-Id: FN-8937

Fusion-Task-Lineage: 9fe166b5-b101-4683-bb2b-4855ee73df10

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-10 03:50:58 -07:00
parent 08a3f2851b
commit 0fbeba50d1
6 changed files with 265 additions and 35 deletions

View File

@@ -395,6 +395,9 @@ Flags:
<!-- FNXC:DashboardTestQuarantine 2026-08-10-05:53: FN-8936 rescued PlanningModeModal's high-value planning-flow suite before its 2026-08-20 deadline. A resumed plan can replace the newly discovered Proceed action during hydration, so each direct-create test now settles that commit and re-queries the live action before dispatch; do not replace this structural fix with waits, retries, or weaker assertions. -->
**2026-08-10 Planning Mode disposition (FN-8936):** Rescued `PlanningModeModal.planning-flow.test.tsx` before its 2026-08-20 deadline. Investigation found no product state-machine race: Proceed snapshots stable session/summary refs and takes its single-flight guard before create. The loaded failure was a test harness detached-node race when session hydration replaced an action-bar button returned by `findByRole`. All unsafe direct Proceed handoffs now settle hydration and query a live button before clicking, while retaining strict create arguments, task-created/onTaskCreated, desktop/mobile handoff, claim-retry, retry, and multi-task assertions. Exact and loaded-file runs passed, and the matching ledger entry and dashboard Vitest exclude were removed together without changing timeouts or adding retries.
<!-- FNXC:TestSubprocessGuard 2026-08-10-09:35: FN-8937 rescues project-engine.test.ts by fixing real-git and virtual-watchdog seam defects, without treating a guard budget as a scheduling interval. -->
**2026-08-10 project-engine disposition (FN-8937):** Rescued `project-engine.test.ts` before its 2026-08-20 deadline. The suite's un-mocked `exec`-based integration-branch probe spawned real git, while the shared subprocess guard watchdog used fakeable timers; a duplicate-registration path could also orphan a watchdog handle. The resolver is now a deterministic suite seam, and watchdogs use captured real timers with owner-scoped failure draining, preserving sibling-test failure ownership. The ledger claim that runtime schedules 120s was a misread of `FUSION_TEST_SUBPROCESS_TIMEOUT_MS`: production retains its correct 60s ladder at `packages/engine/src/project-engine.ts:4551`, and the test correctly forbids its uncapped 120000ms rung. Thus the request to update the assertion to 120s is a documented deviation; no timeout was widened, retry added, or assertion weakened. The ledger/config exclusions were removed together, while the file remains outside `engine-core` pending separate gate-admission evidence.
### Validate before excluding and preserve timeout budgets
Capture **full runner output** before recording or filing a ledger entry—for example, tee it to a file. Never pipe a dot reporter through `tail`: the summary remains but the `FAIL` identity lines needed for evidence are truncated.

View File

@@ -88,6 +88,14 @@ const DEFAULT_TEST_SUBPROCESS_TIMEOUT_MS = Math.max(
1_000,
Number.parseInt(process.env.FUSION_TEST_SUBPROCESS_TIMEOUT_MS ?? "30000", 10) || 30_000,
);
let currentSubprocessTimeoutMs = DEFAULT_TEST_SUBPROCESS_TIMEOUT_MS;
/*
FNXC:TestSubprocessGuard 2026-08-10-09:35:
FN-8937 requires watchdogs to measure real elapsed time even when a test advances
Vitest's virtual clock. Capture timer APIs before tests can install fake timers.
*/
const realSetTimeout = globalThis.setTimeout.bind(globalThis);
const realClearTimeout = globalThis.clearTimeout.bind(globalThis);
const BLOCKED_TEST_CLI_PATTERN =
/(^|[\s"'\\/])(?:claude|droid|paperclipai|hermes|openclaw)(?:\.(?:cmd|bat|ps1|exe))?(?=$|[\s"'\\/])/i;
@@ -885,7 +893,7 @@ function withDefaultTimeout<T extends { timeout?: number | undefined }>(options:
}
return {
...(options ?? {}),
timeout: DEFAULT_TEST_SUBPROCESS_TIMEOUT_MS,
timeout: currentSubprocessTimeoutMs,
} as T;
}
@@ -893,13 +901,19 @@ function cleanupTrackedSubprocess(proc: ChildProcess): void {
const tracked = trackedSubprocesses.get(proc);
if (!tracked) return;
if (tracked.timeoutTimer) {
clearTimeout(tracked.timeoutTimer);
realClearTimeout(tracked.timeoutTimer);
tracked.timeoutTimer = null;
}
trackedSubprocesses.delete(proc);
}
function registerTrackedSubprocess(proc: ChildProcess, commandLine: string): void {
/*
FNXC:TestSubprocessGuard 2026-08-10-09:35:
FN-8937 requires duplicate registration to cancel its prior watchdog; map size
alone cannot expose the otherwise orphaned timer that later fabricates a timeout.
*/
cleanupTrackedSubprocess(proc);
const tracked: TrackedSubprocess = {
commandLine,
startedAt: Date.now(),
@@ -909,18 +923,19 @@ function registerTrackedSubprocess(proc: ChildProcess, commandLine: string): voi
};
trackedSubprocesses.set(proc, tracked);
tracked.timeoutTimer = setTimeout(() => {
tracked.timeoutTimer = realSetTimeout(() => {
tracked.timedOut = true;
completedSubprocessFailures.push({
ownerTestName: tracked.testName,
message: `Timed out after ${DEFAULT_TEST_SUBPROCESS_TIMEOUT_MS}ms: ${tracked.commandLine}${tracked.testName ? ` (${tracked.testName})` : ""}`,
message: `Timed out after ${currentSubprocessTimeoutMs}ms: ${tracked.commandLine}${tracked.testName ? ` (${tracked.testName})` : ""}`,
});
try {
proc.kill("SIGKILL");
} catch {
// Ignore — the process may have already exited.
}
}, DEFAULT_TEST_SUBPROCESS_TIMEOUT_MS);
}, currentSubprocessTimeoutMs);
tracked.timeoutTimer.unref?.();
const finish = () => cleanupTrackedSubprocess(proc);
proc.once("close", finish);
@@ -1195,6 +1210,55 @@ function removeSelfMintedWorkerRootWithRetry(
}
}
/*
FNXC:TestSubprocessGuard 2026-08-10-09:35:
FN-8937 exposes a deliberately narrow owner-scoped harness: fake timers must not
fabricate timeouts, real hangs must still report, and sibling failures stay queued
for their own afterEach. Never add a blanket drain or suppression path here.
*/
export const __fusionSubprocessGuardTestHooks = {
getSubprocessTimeoutMs: (): number => currentSubprocessTimeoutMs,
setSubprocessTimeoutMsForTests: (ms: number | null): void => {
currentSubprocessTimeoutMs = ms ?? DEFAULT_TEST_SUBPROCESS_TIMEOUT_MS;
},
takeOwnedSubprocessFailures: (): string[] => {
const ownerTestName = currentTestName();
const owned: string[] = [];
const remaining: CompletedSubprocessFailure[] = [];
for (const failure of completedSubprocessFailures) {
if (failure.ownerTestName === ownerTestName) owned.push(failure.message);
else remaining.push(failure);
}
completedSubprocessFailures.length = 0;
completedSubprocessFailures.push(...remaining);
return owned;
},
peekForeignSubprocessFailureCount: (): number =>
completedSubprocessFailures.filter((failure) => failure.ownerTestName !== currentTestName()).length,
listForeignSubprocessFailureMessages: (): readonly string[] =>
completedSubprocessFailures
.filter((failure) => failure.ownerTestName !== currentTestName())
.map((failure) => failure.message),
recordSubprocessFailureForTests: (ownerTestName: string | null, message: string): void => {
completedSubprocessFailures.push({ ownerTestName, message });
},
/*
FNXC:TestSubprocessGuard 2026-08-10-09:35:
This removes only synthetic entries staged by the calling test so cleanup never
discards a real recorded guard failure or leaks a foreign entry to another test.
*/
removeStagedFailureForTests: (message: string): boolean => {
const index = completedSubprocessFailures.findIndex((failure) => failure.message === message);
if (index < 0) return false;
completedSubprocessFailures.splice(index, 1);
return true;
},
registerTrackedSubprocessForTests: (proc: ChildProcess, commandLine: string): void => {
registerTrackedSubprocess(proc, commandLine);
},
getTrackedSubprocessCount: (): number => trackedSubprocesses.size,
};
export const __fusionWorkerRootCleanupTestHooks = {
removeSelfMintedWorkerRootWithRetry,
writeWorkerRootOwnerMarker,

View File

@@ -0,0 +1,140 @@
import { exec, execFile, fork, spawn, type ChildProcess } from "node:child_process";
import { once } from "node:events";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { __fusionSubprocessGuardTestHooks as guard } from "../__test-utils__/vitest-setup";
const temporaryPaths: string[] = [];
function waitForClose(proc: ChildProcess): Promise<void> {
return once(proc, "close").then(() => undefined);
}
function waitForRealTime(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function startShortChild(): ChildProcess {
return spawn(process.execPath, ["-e", "0"]);
}
function startHangingChild(): ChildProcess {
return spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"]);
}
afterEach(() => {
vi.useRealTimers();
guard.setSubprocessTimeoutMsForTests(null);
expect(guard.peekForeignSubprocessFailureCount()).toBe(0);
expect(guard.takeOwnedSubprocessFailures()).toEqual([]);
for (const path of temporaryPaths.splice(0)) rmSync(path, { recursive: true, force: true });
});
describe("subprocess guard watchdog timers", () => {
it.each([
["spawn", () => spawn(process.execPath, ["-e", "0"])],
["exec", () => exec(`${JSON.stringify(process.execPath)} -e "0"`)],
["execFile", () => execFile(process.execPath, ["-e", "0"])],
["fork", () => {
const directory = mkdtempSync(join(tmpdir(), "fn-subprocess-guard-"));
temporaryPaths.push(directory);
const modulePath = join(directory, "child.cjs");
writeFileSync(modulePath, "process.exit(0);\n");
return fork(modulePath);
}],
])("S1 ignores virtual elapsed time for %s", async (_name, start) => {
vi.useFakeTimers();
const before = vi.getTimerCount();
const proc = start();
const after = vi.getTimerCount();
await vi.advanceTimersByTimeAsync(guard.getSubprocessTimeoutMs() * 2);
vi.useRealTimers();
await waitForClose(proc);
expect(guard.takeOwnedSubprocessFailures()).toEqual([]);
expect(after).toBe(before);
});
it("S2 ignores virtual elapsed time installed after spawn", async () => {
const proc = startShortChild();
vi.useFakeTimers();
await vi.advanceTimersByTimeAsync(guard.getSubprocessTimeoutMs() * 2);
vi.useRealTimers();
await waitForClose(proc);
expect(guard.takeOwnedSubprocessFailures()).toEqual([]);
expect(guard.getTrackedSubprocessCount()).toBe(0);
});
it("S3 retains the real-timer baseline", async () => {
const proc = startShortChild();
await waitForClose(proc);
expect(guard.takeOwnedSubprocessFailures()).toEqual([]);
expect(guard.getTrackedSubprocessCount()).toBe(0);
expect(guard.getSubprocessTimeoutMs()).toSatisfy((ms: number) => Number.isFinite(ms) && ms >= 1_000);
});
it("S4 reports and kills a genuinely hung child", async () => {
guard.setSubprocessTimeoutMsForTests(150);
const proc = startHangingChild();
await waitForClose(proc);
expect(guard.takeOwnedSubprocessFailures()).toEqual([expect.stringMatching(/Timed out after 150ms/)]);
expect(proc.signalCode).toBe("SIGKILL");
expect(guard.getTrackedSubprocessCount()).toBe(0);
});
it("S5 cleanup cancels its real watchdog", async () => {
guard.setSubprocessTimeoutMsForTests(150);
const proc = startShortChild();
await waitForClose(proc);
await waitForRealTime(450);
expect(guard.takeOwnedSubprocessFailures()).toEqual([]);
expect(guard.getTrackedSubprocessCount()).toBe(0);
});
it("S6 duplicate registration does not orphan a watchdog", async () => {
guard.setSubprocessTimeoutMsForTests(150);
const proc = startShortChild();
guard.registerTrackedSubprocessForTests(proc, "duplicate-registration-probe");
await waitForClose(proc);
await waitForRealTime(450);
// The map is keyed by proc, so only absence of a fabricated failure proves cleanup.
expect(guard.takeOwnedSubprocessFailures()).toEqual([]);
expect(guard.getTrackedSubprocessCount()).toBe(0);
});
it("S7 leaves a foreign-only failure queued", () => {
expect(guard.takeOwnedSubprocessFailures()).toEqual([]);
expect(guard.peekForeignSubprocessFailureCount()).toBe(0);
const message = "Timed out after 1ms: synthetic-foreign";
guard.recordSubprocessFailureForTests("FN-8937 synthetic sibling test", message);
expect(guard.takeOwnedSubprocessFailures()).toEqual([]);
expect(guard.peekForeignSubprocessFailureCount()).toBe(1);
expect(guard.removeStagedFailureForTests(message)).toBe(true);
expect(guard.peekForeignSubprocessFailureCount()).toBe(0);
});
it("S8 drains owned failures while preserving foreign order", async () => {
guard.setSubprocessTimeoutMsForTests(150);
guard.recordSubprocessFailureForTests("FN-8937 synthetic sibling A", "synthetic-F1");
const proc = startHangingChild();
await waitForClose(proc);
expect(guard.peekForeignSubprocessFailureCount()).toBe(1);
guard.recordSubprocessFailureForTests("FN-8937 synthetic sibling B", "synthetic-F2");
expect(guard.takeOwnedSubprocessFailures()).toEqual([expect.stringMatching(/Timed out after 150ms/)]);
expect(guard.peekForeignSubprocessFailureCount()).toBe(2);
expect(guard.listForeignSubprocessFailureMessages()).toEqual(["synthetic-F1", "synthetic-F2"]);
expect(guard.removeStagedFailureForTests("synthetic-F1")).toBe(true);
expect(guard.removeStagedFailureForTests("synthetic-F2")).toBe(true);
expect(guard.peekForeignSubprocessFailureCount()).toBe(0);
});
});

View File

@@ -132,6 +132,19 @@ vi.mock("node:child_process", async (importOriginal) => {
};
});
/*
FNXC:EngineTests 2026-08-10-09:35:
FN-8937 seals the exec-based integration-branch probe so this ProjectEngine suite
never spawns host git while fake timers exercise workspace dispatch. Resolver data
states remain owned by integration-branch.test.ts; this seam supplies only a stable branch.
*/
vi.mock("../merge/integration-branch.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../merge/integration-branch.js")>()),
resolveIntegrationBranch: vi.fn().mockResolvedValue("main"),
resolveIntegrationBranchSync: vi.fn().mockReturnValue("main"),
__resetIntegrationBranchCacheForTests: vi.fn(),
}));
vi.mock("../merge/pr-monitor.js", () => ({
PrMonitor: vi.fn().mockImplementation(function () {
return {
@@ -2119,7 +2132,12 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
await engine.start();
const taskMovedHandler = mockStore.store.on.mock.calls.find((c: unknown[]) => c[0] === "task:moved")?.[1] as
/*
FNXC:EngineTests 2026-08-10-10:34:
FN-8937 must invoke the auto-merge listener, not spec-drift's earlier observer;
the latest registration owns the merge handoff assertions below.
*/
const taskMovedHandler = mockStore.store.on.mock.calls.findLast((c: unknown[]) => c[0] === "task:moved")?.[1] as
| ((payload: { task: { id: string; column: string; paused?: boolean }; to: string }) => Promise<void>)
| undefined;
@@ -2145,7 +2163,7 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
await engine.start();
enqueueSpy.mockClear();
const taskUpdatedHandler = mockStore.store.on.mock.calls.find((c: unknown[]) => c[0] === "task:updated")?.[1] as
const taskUpdatedHandler = mockStore.store.on.mock.calls.findLast((c: unknown[]) => c[0] === "task:updated")?.[1] as
| ((task: { id: string; column: string; paused?: boolean; status?: string | null }) => Promise<void>)
| undefined;
if (!taskUpdatedHandler) throw new Error("task:updated handler was not registered");
@@ -2703,7 +2721,7 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
expect(mocks.runAiMerge).toHaveBeenCalledTimes(1);
});
const taskUpdatedHandler = mockStore.store.on.mock.calls.find((c: unknown[]) => c[0] === "task:updated")?.[1] as
const taskUpdatedHandler = mockStore.store.on.mock.calls.findLast((c: unknown[]) => c[0] === "task:updated")?.[1] as
| ((task: { id: string; column: string; paused?: boolean }) => void)
| undefined;
if (!taskUpdatedHandler) throw new Error("task:updated handler was not registered");
@@ -2731,8 +2749,15 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
{ id: "FN-paused", column: "in-review", paused: true, mergeRetries: 0, status: null },
{ id: "FN-ready", column: "in-review", paused: false, mergeRetries: 0, status: null },
];
// Critical stale-status cleanup reads first; deferred startup then evaluates eligibility.
mockStore.store.listTasks.mockResolvedValueOnce([]).mockResolvedValueOnce(inReviewTasks);
/*
FNXC:EngineTests 2026-08-10-10:34:
FN-8937 keeps this rescue suite aligned with the startup ownership contract:
spec-drift seeds first, stale-status cleanup reads second, then deferred merge admission reads the candidates.
*/
mockStore.store.listTasks
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockResolvedValueOnce(inReviewTasks);
mocks.currentStore = mockStore.store;
const engine = createEngine();
const privateEngine = engine as unknown as { internalEnqueueMerge: (taskId: string) => void };
@@ -2768,8 +2793,14 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
{ id: "FN-opted-in", column: "in-review", paused: false, mergeRetries: 0, status: null, autoMerge: true, branchContext: { assignmentMode: "shared", groupId: "BG-5819", source: "planning" } },
{ id: "FN-plain", column: "in-review", paused: false, mergeRetries: 0, status: null },
];
// Critical stale-status cleanup reads first; deferred startup then evaluates eligibility.
mockStore.store.listTasks.mockResolvedValueOnce([]).mockResolvedValueOnce(inReviewTasks);
/*
FNXC:EngineTests 2026-08-10-10:34:
FN-8937 preserves the three startup readers: spec-drift seed, stale-status cleanup, then deferred merge admission.
*/
mockStore.store.listTasks
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockResolvedValueOnce(inReviewTasks);
mocks.currentStore = mockStore.store;
const engine = createEngine();
const privateEngine = engine as unknown as { internalEnqueueMerge: (taskId: string) => void };
@@ -2799,7 +2830,7 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
await engine.start();
enqueueSpy.mockClear();
const movedHandler = mockStore.store.on.mock.calls.find((c: unknown[]) => c[0] === "task:moved")?.[1] as
const movedHandler = mockStore.store.on.mock.calls.findLast((c: unknown[]) => c[0] === "task:moved")?.[1] as
| ((event: { task: Task; to: string }) => void)
| undefined;
if (!movedHandler) throw new Error("task:moved handler was not registered");
@@ -3247,11 +3278,12 @@ describe("ProjectEngine swallowed error hardening", () => {
const engine = createEngine();
await engine.start();
await vi.waitFor(() => expect(mockStore.store.listTasks).toHaveBeenCalledTimes(2));
// Spec-drift's initial scan precedes stale-status cleanup and deferred merge admission.
await vi.waitFor(() => expect(mockStore.store.listTasks).toHaveBeenCalledTimes(3));
mockStore.store.getSettings.mockRejectedValueOnce(new Error("db locked"));
const handler = mockStore.store.on.mock.calls.find((c: unknown[]) => c[0] === "task:moved")?.[1] as
const handler = mockStore.store.on.mock.calls.findLast((c: unknown[]) => c[0] === "task:moved")?.[1] as
| ((payload: { task: { id: string; column: string }; to: string }) => Promise<void>)
| undefined;
expect(handler).toBeTypeOf("function");
@@ -3279,8 +3311,9 @@ describe("ProjectEngine swallowed error hardening", () => {
it("warns when startup merge sweep fails", async () => {
const mockStore = createMockStore({ ...baseSettings, autoMerge: true });
mocks.currentStore = mockStore.store;
// The critical stale-status read precedes the deferred enqueue sweep.
// Spec-drift scans first, stale-status cleanup is second, and deferred admission must fail third.
mockStore.store.listTasks
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockRejectedValueOnce(new Error("connection lost"));
@@ -3301,7 +3334,7 @@ describe("ProjectEngine swallowed error hardening", () => {
mocks.currentStore = mockStore.store;
const engine = createEngine();
await engine.start();
await vi.waitFor(() => expect(mockStore.store.listTasks).toHaveBeenCalledTimes(2));
await vi.waitFor(() => expect(mockStore.store.listTasks).toHaveBeenCalledTimes(3));
warnSpy.mockClear();
mockStore.store.listTasks.mockRejectedValueOnce(new Error("sweep db error"));
@@ -3319,7 +3352,7 @@ describe("ProjectEngine swallowed error hardening", () => {
mocks.currentStore = mockStore.store;
const engine = createEngine();
await engine.start();
await vi.waitFor(() => expect(mockStore.store.listTasks).toHaveBeenCalledTimes(2));
await vi.waitFor(() => expect(mockStore.store.listTasks).toHaveBeenCalledTimes(3));
warnSpy.mockClear();
mockStore.store.getSettings
@@ -3546,7 +3579,7 @@ describe("ProjectEngine stale mergeActive rescue (FN-3900)", () => {
});
const warnSpy = vi.spyOn(runtimeLog, "warn").mockImplementation(() => {});
const taskMovedHandler = mockStore.store.on.mock.calls.find((c: unknown[]) => c[0] === "task:moved")?.[1] as
const taskMovedHandler = mockStore.store.on.mock.calls.findLast((c: unknown[]) => c[0] === "task:moved")?.[1] as
| ((payload: { task: { id: string; column: string; paused?: boolean }; to: string }) => Promise<void>)
| undefined;
if (!taskMovedHandler) throw new Error("task:moved handler was not registered");
@@ -3658,7 +3691,7 @@ describe("ProjectEngine stale mergeActive rescue (FN-3900)", () => {
const enqueueSpy = vi.spyOn(privateEngine, "internalEnqueueMerge");
const warnSpy = vi.spyOn(runtimeLog, "warn").mockImplementation(() => {});
const taskMovedHandler = mockStore.store.on.mock.calls.find((c: unknown[]) => c[0] === "task:moved")?.[1] as
const taskMovedHandler = mockStore.store.on.mock.calls.findLast((c: unknown[]) => c[0] === "task:moved")?.[1] as
| ((payload: { task: { id: string; column: string; paused?: boolean }; to: string }) => Promise<void>)
| undefined;
if (!taskMovedHandler) throw new Error("task:moved handler was not registered");

View File

@@ -250,11 +250,10 @@ export default defineConfig({
"src/__tests__/merger-landed-files-capture.test.ts",
"src/__tests__/branch-attribution.test.ts",
/*
FNXC:EngineTests 2026-08-06-00:04:
FN-8811 quarantines project-engine.test.ts after its workspace-busy case
contradicted its 60s cap with a real 120s retry and left a timed-out git
subprocess. The paired ledger entry owns its 14-day deletion ratchet; keep
this gate allow-list free of the file until a root-cause fix rescues it.
FNXC:EngineTests 2026-08-10-09:35:
FN-8937 rescued project-engine.test.ts into engine-default by sealing its
git resolver seam and using real guard watchdog timers. It remains outside
engine-core: merge-gate admission requires separate deterministic evidence.
*/
/*
FNXC:EngineTests 2026-07-28-21:05 (#2520 review — greptile P1):
@@ -388,9 +387,6 @@ export default defineConfig({
Quarantined on sight per AGENTS.md; mirrored in scripts/lib/test-quarantine.json.
*/
// SQLite-path gate test evicted + quarantined (see engine-core comment + ledger).
// FNXC:EngineTests 2026-08-06-00:04: paired with the ledger's FN-8811
// workspace-busy quarantine; do not appease its timing assertion.
"src/__tests__/project-engine.test.ts",
"node_modules/**",
"dist/**",
// FNXC:PgMigrationQuarantine 2026-07-18-04:30: FN-8270 rescued the final seven VAL-REMOVAL-005 holdouts by awaiting PG audit reads and modeling async collaborators. Their paired ledger entries and excludes were removed only after targeted green runs.

View File

@@ -1,10 +1,4 @@
{
"$comment": "Flaky-test quarantine ledger (deletion ratchet — see AGENTS.md 'Flaky tests: quarantine on sight' and docs/testing.md 'Quarantine ledger and the deletion ratchet'). A test observed failing without a corresponding real bug is quarantined ON SIGHT: add an entry here AND a matching one-line `exclude` entry in that package's vitest config, in the same commit. Every entry needs a non-empty `reason` (link the failing run) and a `quarantinedAt` date — the entry expires 14 days later, at which point the test file is DELETED unless someone rescues it with evidence it catches real regressions plus a root-cause fix (never appeasement). There is deliberately no loader module and no automation around this file: it is a dated record, the vitest config is the enforcement.",
"entries": [
{
"file": "packages/engine/src/__tests__/project-engine.test.ts",
"reason": "FN-8811 observed B4/B5 workspace busy contention asserting a stale 60s cap while runtime schedules 120s, then the subprocess guard timed out on git remote; reproduced in isolation on 2026-08-06. Do not widen timing or weaken the assertion; restore only with a root-cause fix before the deletion deadline.",
"quarantinedAt": "2026-08-06"
}
]
"entries": []
}