diff --git a/docs/testing.md b/docs/testing.md index de2da2e645..b78ecae2c5 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -201,7 +201,7 @@ Legitimate legacy exceptions must be recorded in `scripts/lib/test-timeout-appea -**2026-06-19 quarantine audit (FN-6740):** the 2026-06-19 ledger batch expires on **2026-07-03**. FN-6740 found no ledger/config half-state and chose no inline rescue/delete. The five CLI files (`extension-goal-tools`, `extension-mission-goal-tools`, `extension-task-tools`, `extension`, `research-extension-tools`) are explicitly deferred to FN-6734's outcome and must not get a duplicate rescue task. Five core files (`activity-analytics`, `db`, `store-create-summarize-deferred-hook`, `vitest-teardown-worker-root-cleanup`, `settings-export`) were rescued by FN-6741 after the loaded `@fusion/core` lane passed with only `store-concurrent-writes` re-quarantined; `settings-export` now closes its `TaskStore` before fixture cleanup. The dashboard files are split by likely root cause: FN-6742 owns `session-cross-tab` cleanup `ENOTEMPTY`, FN-6743 owns the third-repeat QuickEntryBox focus-restoration race after FN-6514/FN-6642, and FN-6744 owns WorkflowNodeEditor duplicate-merge-seam concurrency. Until those dashboard and remaining core follow-ups rescue with root-cause evidence or delete under the ratchet, leave all corresponding ledger entries and package excludes in lockstep. +**2026-06-19 quarantine audit (FN-6740):** the 2026-06-19 ledger batch expires on **2026-07-03**. FN-6740 found no ledger/config half-state and chose no inline rescue/delete. The five CLI files (`extension-goal-tools`, `extension-mission-goal-tools`, `extension-task-tools`, `extension`, `research-extension-tools`) are explicitly deferred to FN-6734's outcome and must not get a duplicate rescue task. Five core files (`activity-analytics`, `db`, `store-create-summarize-deferred-hook`, `vitest-teardown-worker-root-cleanup`, `settings-export`) were rescued by FN-6741 after the loaded `@fusion/core` lane passed with only `store-concurrent-writes` re-quarantined; `settings-export` now closes its `TaskStore` before fixture cleanup. The dashboard files were split by likely root cause: FN-6742 rescued `session-cross-tab` cleanup `ENOTEMPTY` by closing the route/task-store seam before fixture removal; FN-6743 owns the third-repeat QuickEntryBox focus-restoration race after FN-6514/FN-6642; and FN-6744 owns WorkflowNodeEditor duplicate-merge-seam concurrency. Until the remaining dashboard and core follow-ups rescue with root-cause evidence or delete under the ratchet, leave all corresponding ledger entries and package excludes in lockstep. + +**2026-06-19 dashboard session-cross-tab rescue (FN-6742):** `packages/dashboard/src/__tests__/session-cross-tab.test.ts` was rescued before its 2026-07-03 deletion deadline. The loaded `dashboard-api-quality-backfill` shard reproduced the original `fusion-test-workers-*` `ENOTEMPTY` cleanup failure with the quarantine exclude temporarily removed, while the test's assertions retained value by failing when the expected lock holder was mutated from `tab-a` to `tab-z`. The fix keeps the test unquarantined by disposing the created API router, stopping `AiSessionStore` scheduled cleanup, closing the real `TaskStore`/SQLite handles, hiding route EventEmitter hooks not used by this harness, and draining four bounded check-phase turns before deleting the temp root. The ledger and `packages/dashboard/vitest.config.ts` exclude were updated in lockstep; later loaded runs no longer failed this file, and unrelated dashboard loaded-suite failures are tracked separately rather than weakening this test. + **2026-06-16 rescue (FN-6514):** `packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx` was rescued before its 2026-06-30 deletion deadline. The file still caught real quick-entry behavior regressions, but it leaked jsdom descriptors for `window.innerWidth`, `window.matchMedia`, `document.visibilityState`, `URL.createObjectURL`, and `URL.revokeObjectURL`; a mobile viewport helper could leave later tests in the same dashboard backfill shard observing `innerWidth=375` and mismatched responsive assertions. The rescue removed the ledger/config quarantine entries in lockstep, captured each original `PropertyDescriptor` at module load, restored those descriptors (or deleted own properties that were originally absent) in `afterEach`, and added a guard test that mutates all rescued globals before asserting they return to their original descriptors. Reusable pattern: any test file that changes jsdom globals with `Object.defineProperty` or spies on replaceable globals must snapshot the original descriptor at the top of the file, restore it in every `afterEach`, and prove the invariant with a guard test; do not use timeout bumps, retries, worker changes, or blanket `vi.restoreAllMocks()` when module mocks depend on stable implementations. **Gate eviction:** a flake inside the merge gate cannot block all merges while red — it is evicted by removing its line from the `engine-core` allow-list (no quarantine entry needed unless it should also leave the non-blocking tier). diff --git a/packages/dashboard/src/__tests__/session-cross-tab.test.ts b/packages/dashboard/src/__tests__/session-cross-tab.test.ts index 8506b3c68d..e3c3f69445 100644 --- a/packages/dashboard/src/__tests__/session-cross-tab.test.ts +++ b/packages/dashboard/src/__tests__/session-cross-tab.test.ts @@ -45,6 +45,7 @@ describe("cross-tab session locking", () => { let db: Database; let aiSessionStore: AiSessionStore; let app: express.Express; + let apiRouter: express.Router & { dispose?: () => void }; beforeEach(async () => { tmpRoot = mkdtempSync(join(tmpdir(), "kb-session-cross-tab-")); @@ -60,10 +61,25 @@ describe("cross-tab session locking", () => { app = express(); app.use(express.json()); - app.use("/api", createApiRoutes(taskStore, { aiSessionStore })); + /* + FNXC:DashboardSessionTests 2026-06-19-15:55: + This harness exercises AI session lock routes only. Hide TaskStore's EventEmitter hooks before mounting createApiRoutes so unrelated route services do not subscribe background workers that can reopen or scan the temp .fusion tree after the test-owned store closes. + */ + Object.defineProperties(taskStore, { + on: { value: undefined, configurable: true }, + off: { value: undefined, configurable: true }, + }); + apiRouter = createApiRoutes(taskStore, { aiSessionStore }) as express.Router & { dispose?: () => void }; + app.use("/api", apiRouter); }); afterEach(async () => { + try { + apiRouter.dispose?.(); + } catch { + // no-op + } + aiSessionStore.stopScheduledCleanup(); try { taskStore.close(); } catch { @@ -74,8 +90,16 @@ describe("cross-tab session locking", () => { } catch { // no-op } - // FNXC:DashboardSessionTests 2026-06-14-09:20: TaskStore.close() closes watcher/database handles synchronously but their filesystem close callbacks settle on the next event-loop turn; drain that turn before deleting .fusion. - await new Promise((resolve) => setImmediate(resolve)); + /* + FNXC:DashboardSessionTests 2026-06-14-09:20: + TaskStore.close() closes watcher/database handles synchronously but their filesystem close callbacks settle on the next event-loop turn; drain that turn before deleting .fusion. + + FNXC:DashboardSessionTests 2026-06-19-15:39: + FN-6742 reproduced ENOTEMPTY under the loaded dashboard API backfill shard because route-owned disposables and nested .fusion close callbacks can outlive a single check-phase drain. Dispose the API router first, then drain several check phases before tmpRoot removal so cleanup proves closed handles instead of masking live writers with retry-rm loops. + */ + for (let i = 0; i < 4; i += 1) { + await new Promise((resolve) => setImmediate(resolve)); + } await rm(tmpRoot, { recursive: true, force: true }); }); diff --git a/packages/dashboard/vitest.config.ts b/packages/dashboard/vitest.config.ts index 4be18fb907..943d95d40e 100644 --- a/packages/dashboard/vitest.config.ts +++ b/packages/dashboard/vitest.config.ts @@ -278,9 +278,12 @@ Quarantine the focus-timing flake under the deletion ratchet instead of changing FNXC:DashboardTestQuarantine 2026-06-19-08:17: FN-6726 workspace verification observed the WorkflowNodeEditor duplicate-merge-seam template conflict test fail only in the broad components-b shard, then pass on targeted rerun. Quarantine the concurrency-sensitive workflow editor file under the deletion ratchet instead of changing unrelated template insertion behavior or appeasing the test. + +FNXC:DashboardTestQuarantine 2026-06-19-15:40: +FN-6742 rescued session-cross-tab before the 2026-07-03 deletion ratchet by reproducing ENOTEMPTY in dashboard-api-quality-backfill and fixing the test-owned route/close-callback teardown seam. +Keep it out of this exclude list so loaded API shards keep exercising cross-tab locking, beacon release, stale-lock expiry, SSE summaries, and stale-session cleanup. */ const quarantinedDashboardTests: string[] = [ - "src/__tests__/session-cross-tab.test.ts", "app/components/__tests__/QuickEntryBox.test.tsx", "app/components/__tests__/WorkflowNodeEditor.test.tsx", ]; diff --git a/scripts/lib/test-quarantine.json b/scripts/lib/test-quarantine.json index 4187ed8d48..584128b442 100644 --- a/scripts/lib/test-quarantine.json +++ b/scripts/lib/test-quarantine.json @@ -1,11 +1,6 @@ { "$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 exclude is the mechanism, and the sweep is policy executed by whoever touches the suite.", "entries": [ - { - "file": "packages/dashboard/src/__tests__/session-cross-tab.test.ts", - "reason": "FN-6690 local workspace `pnpm test` observed ENOTEMPTY while removing the test's temp .fusion directory in dashboard-api-quality-backfill shard; isolated rerun passed, indicating cleanup flake rather than a lazy-view CSS regression.", - "quarantinedAt": "2026-06-19" - }, { "file": "packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx", "reason": "FN-6697 local workspace `pnpm test` observed the post-submission focus restoration test fail in the broad dashboard app backfill shard, while a targeted rerun of QuickEntryBox with MailboxModal passed the QuickEntryBox assertions; quarantine the focus-timing flake instead of appeasing it while the terminal shortcut fix remains scoped.",