diff --git a/packages/engine/src/__tests__/self-healing-db-corruption.test.ts b/packages/engine/src/__tests__/self-healing-db-corruption.test.ts index 3c9ae1ea2b..ab2a276c3e 100644 --- a/packages/engine/src/__tests__/self-healing-db-corruption.test.ts +++ b/packages/engine/src/__tests__/self-healing-db-corruption.test.ts @@ -1,8 +1,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { EventEmitter } from "node:events"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import type { Settings, TaskStore } from "@fusion/core"; +const osState = vi.hoisted(() => ({ tempRoot: "" })); + +vi.mock("node:os", async () => { + const actual = await vi.importActual("node:os"); + return { ...actual, tmpdir: vi.fn(() => osState.tempRoot || actual.tmpdir()) }; +}); + import { SelfHealingManager } from "../self-healing.js"; import type { NotificationService } from "../notification/notification-service.js"; import * as notifierModule from "../notifier.js"; @@ -97,8 +107,13 @@ function stubMaintenance(manager: SelfHealingManager) { vi.spyOn(manager, "archiveStaleDoneTasks").mockResolvedValue(0); } +const RM = { recursive: true, force: true, maxRetries: 5, retryDelay: 50 } as const; +let sandboxRoot = ""; + describe("FN-5284: self-healing DB corruption surfacing", () => { beforeEach(() => { + sandboxRoot = mkdtempSync(join(tmpdir(), "fusion-db-corruption-sandbox-")); + osState.tempRoot = sandboxRoot; vi.useFakeTimers(); vi.setSystemTime(new Date("2026-05-20T00:00:00.000Z")); vi.spyOn(notifierModule, "getActiveNotificationService").mockReturnValue(undefined); @@ -108,6 +123,13 @@ describe("FN-5284: self-healing DB corruption surfacing", () => { afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); + osState.tempRoot = ""; + try { + rmSync(sandboxRoot, RM); + } catch { + // Best-effort cleanup only. + } + sandboxRoot = ""; }); it("does not dispatch or audit when the database is healthy", async () => { diff --git a/packages/engine/vitest.config.ts b/packages/engine/vitest.config.ts index 3478d16eda..149f0f52be 100644 --- a/packages/engine/vitest.config.ts +++ b/packages/engine/vitest.config.ts @@ -95,6 +95,7 @@ export default defineConfig({ include: ["src/**/*.test.ts"], exclude: [ "src/__tests__/reliability-interactions/**/*.test.ts", + "src/__tests__/self-healing-db-corruption.test.ts", // Real-git heavy files run in the engine-slow project so local // `pnpm test` stays snappy. CI picks them up via `test:slow` // / `test:all` invoked from the root `test:full` script. diff --git a/scripts/lib/test-quarantine.json b/scripts/lib/test-quarantine.json index 39eac9c428..06b4f106dc 100644 --- a/scripts/lib/test-quarantine.json +++ b/scripts/lib/test-quarantine.json @@ -1,4 +1,10 @@ { "$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": [] + "entries": [ + { + "file": "packages/engine/src/__tests__/self-healing-db-corruption.test.ts", + "reason": "FN-6195: observed flake when runMaintenance() scanned the real OS tmpdir and picked up stale fusion-ai-merge-* worktrees from unrelated tests, adding unexpected audit calls. Failing run: Fusion task FN-6195 agent verification log, 2026-06-10.", + "quarantinedAt": "2026-06-10" + } + ] }