From e24f765495120f5ee24da8d968d90d08a0590032 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 17 Jul 2026 17:26:22 -0700 Subject: [PATCH] FN-8252: rescue quarantined engine tests Restore non-mechanical engine coverage with PostgreSQL-safe test fixtures and awaited overseer audit writes. - migrate eligible engine tests to shared PostgreSQL harnesses and restore their Vitest coverage - harden mission and advisory reporting paths for async persistence and observable failures - await the production planner-overseer audit callback and verify the start() wiring preserves persistence Files changed: .../__tests__/mission-autopilot-end-to-end.test.ts | 27 ++-- .../engine/src/__tests__/mission-autopilot.test.ts | 4 +- .../planner-overseer-intervention-wiring.test.ts | 39 +++--- .../engine/src/__tests__/project-engine.test.ts | 138 ++++++++++++++++----- .../unlinked-missions-advisory-reporter.pg.test.ts | 51 ++++++++ .../unlinked-missions-advisory-reporter.test.ts | 20 ++- packages/engine/src/mission-execution-loop.ts | 27 ++-- packages/engine/src/project-engine.ts | 41 +++--- .../src/unlinked-missions-advisory-reporter.ts | 23 ++-- packages/engine/vitest.config.ts | 6 +- scripts/lib/test-quarantine.json | 27 +--- 11 files changed, 260 insertions(+), 143 deletions(-) Fusion-Task-Id: FN-8252 Fusion-Task-Lineage: 4f86ce7e-11a2-4704-a5d1-00e0a8c1448e Co-authored-by: Fusion (runfusion.ai) --- .../mission-autopilot-end-to-end.test.ts | 27 ++-- .../src/__tests__/mission-autopilot.test.ts | 4 +- ...anner-overseer-intervention-wiring.test.ts | 39 ++--- .../src/__tests__/project-engine.test.ts | 138 +++++++++++++----- ...nked-missions-advisory-reporter.pg.test.ts | 51 +++++++ ...nlinked-missions-advisory-reporter.test.ts | 20 ++- packages/engine/src/mission-execution-loop.ts | 27 ++-- packages/engine/src/project-engine.ts | 41 +++--- .../unlinked-missions-advisory-reporter.ts | 23 ++- packages/engine/vitest.config.ts | 6 +- scripts/lib/test-quarantine.json | 27 +--- 11 files changed, 260 insertions(+), 143 deletions(-) create mode 100644 packages/engine/src/__tests__/unlinked-missions-advisory-reporter.pg.test.ts diff --git a/packages/engine/src/__tests__/mission-autopilot-end-to-end.test.ts b/packages/engine/src/__tests__/mission-autopilot-end-to-end.test.ts index e1cb017921..c6ff95f403 100644 --- a/packages/engine/src/__tests__/mission-autopilot-end-to-end.test.ts +++ b/packages/engine/src/__tests__/mission-autopilot-end-to-end.test.ts @@ -72,6 +72,7 @@ function makeHarness({ handlers.set(event, [...(handlers.get(event) ?? []), cb]); }); + let missionStore: any; const taskStore = { getTask: vi.fn(async () => task), getRootDir: vi.fn(() => "/tmp"), @@ -83,11 +84,13 @@ function makeHarness({ moveTask: vi.fn(async () => undefined), logEntry: vi.fn(async () => undefined), recordRunAuditEvent: vi.fn(async () => undefined), + updateSettings: vi.fn(async () => ({})), + getMissionStore: vi.fn(() => missionStore), on, off: vi.fn(), } as unknown as TaskStore; - const missionStore: any = { + missionStore = { getMission: vi.fn(() => mission), listMissions: vi.fn(() => [mission]), updateMission: vi.fn((_id: string, updates: any) => ({ ...mission, ...updates })), @@ -121,8 +124,10 @@ function makeHarness({ } return next; }), - listAssertionsForFeature: vi.fn(() => (withAssertions ? [{ id: "CA-1", milestoneId: milestone.id, title: "assert", assertion: "works", status: "pending", orderIndex: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }] : [])), - startValidatorRun: vi.fn(() => ({ id: "VR-001", featureId: feature.id, milestoneId: milestone.id, sliceId: "SL-001", status: "running", triggerType: "task_completion", implementationAttempt: 1, validatorAttempt: 1, startedAt: new Date().toISOString(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() })), + listAssertionsForFeature: vi.fn(async () => (withAssertions ? [{ id: "CA-1", milestoneId: milestone.id, title: "assert", assertion: "works", status: "pending", orderIndex: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }] : [])), + ensureFeatureAssertionLinked: vi.fn(async () => (withAssertions ? [{ id: "CA-1", milestoneId: milestone.id, title: "generated assert", assertion: "works", status: "pending", orderIndex: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }] : [])), + listGoalIdsForMission: vi.fn(async () => []), + startValidatorRun: vi.fn(async () => ({ id: "VR-001", featureId: feature.id, milestoneId: milestone.id, sliceId: "SL-001", status: "running", triggerType: "task_completion", implementationAttempt: 1, validatorAttempt: 1, startedAt: new Date().toISOString(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() })), completeValidatorRun: vi.fn(), recordValidatorFailures: vi.fn(), createGeneratedFixFeature: vi.fn(), @@ -137,9 +142,10 @@ function makeHarness({ const f = missionStore.getFeature(featureId); if (f?.taskId) await autopilot.handleTaskCompletion(f.taskId); } } }); - vi.spyOn(loop as any, "runValidation").mockImplementation( - runValidationImpl ?? (async () => ({ status: "pass", assertions: [], summary: "ok" })), - ); + vi.spyOn(loop as any, "runValidation").mockImplementation(async () => ({ + result: await (runValidationImpl ?? (async () => ({ status: "pass" as const, assertions: [], summary: "ok" })))(), + inspection: { workspaceStale: false }, + })); loop.start(); const scheduler = new Scheduler(taskStore, { @@ -159,8 +165,6 @@ function makeHarness({ task.column = to; const callbacks = handlers.get("task:moved") ?? []; await Promise.all(callbacks.map((cb) => cb({ task, from: "in-progress", to }))); - await Promise.resolve(); - await Promise.resolve(); }; scheduler.start(); @@ -174,10 +178,11 @@ describe("mission autopilot end-to-end wiring", () => { const processSpy = vi.spyOn(h.loop, "processTaskOutcome"); await h.emitTaskMoved("done"); + await vi.waitFor(() => expect(h.missionStore.getFeatureByTaskId("FN-001")?.status).toBe("done")); expect(h.missionStore.getFeatureByTaskId("FN-001")?.status).toBe("done"); expect(processSpy).toHaveBeenCalledWith("FN-001"); - expect(h.missionStore.startValidatorRun).toHaveBeenCalledWith("F-001", "task_completion"); + expect(h.missionStore.startValidatorRun).toHaveBeenCalledWith("F-001", "task_completion", "FN-001"); expect(h.missionStore.completeValidatorRun).toHaveBeenCalledWith("VR-001", "passed", "ok"); expect(h.slices.get("SL-001").status).toBe("complete"); expect(h.activateSpy).toHaveBeenCalledWith("M-001"); @@ -189,6 +194,7 @@ describe("mission autopilot end-to-end wiring", () => { const h = makeHarness({ withAssertions: false }); await h.emitTaskMoved("done"); + await vi.waitFor(() => expect(h.slices.get("SL-001").status).toBe("complete")); expect(h.missionStore.startValidatorRun).not.toHaveBeenCalled(); expect(h.slices.get("SL-001").status).toBe("complete"); @@ -202,7 +208,7 @@ describe("mission autopilot end-to-end wiring", () => { await h.loop.processTaskOutcome("FN-001"); - expect(h.missionStore.startValidatorRun).toHaveBeenCalledWith("F-001", "task_completion"); + expect(h.missionStore.startValidatorRun).toHaveBeenCalledWith("F-001", "task_completion", "FN-001"); expect(h.missionStore.completeValidatorRun).toHaveBeenCalledWith("VR-001", "passed", "ok"); expect(h.missionStore.getFeature("F-001")?.status).toBe("done"); expect(h.slices.get("SL-001").status).toBe("complete"); @@ -239,6 +245,7 @@ describe("mission autopilot end-to-end wiring", () => { }); await h.emitTaskMoved("done"); + await vi.waitFor(() => expect(h.missionStore.completeValidatorRun).toHaveBeenCalledWith("VR-001", "error", "runtime unavailable")); expect(h.missionStore.completeValidatorRun).toHaveBeenCalledWith("VR-001", "error", "runtime unavailable"); expect(h.missionStore.logMissionEvent).toHaveBeenCalledWith( diff --git a/packages/engine/src/__tests__/mission-autopilot.test.ts b/packages/engine/src/__tests__/mission-autopilot.test.ts index d512a5c582..f51ff4c402 100644 --- a/packages/engine/src/__tests__/mission-autopilot.test.ts +++ b/packages/engine/src/__tests__/mission-autopilot.test.ts @@ -1223,7 +1223,7 @@ describe("MissionAutopilot", () => { expect(ap.isWatching(mission.id)).toBe(false); }); - it("does not clear watched state when normalization is accidentally called for an active mission", () => { + it("does not clear watched state when normalization is accidentally called for an active mission", async () => { const mission = createMockMission({ id: "M-ACTIVE", status: "active", @@ -1237,7 +1237,7 @@ describe("MissionAutopilot", () => { ap.watchMission("M-ACTIVE"); store.updateMission.mockClear(); store.logMissionEvent.mockClear(); - (ap as any).normalizeCompleteMissionAutopilotState("M-ACTIVE", "test"); + await (ap as any).normalizeCompleteMissionAutopilotState("M-ACTIVE", "test"); expect(ap.isWatching("M-ACTIVE")).toBe(true); expect(store.updateMission).not.toHaveBeenCalled(); diff --git a/packages/engine/src/__tests__/planner-overseer-intervention-wiring.test.ts b/packages/engine/src/__tests__/planner-overseer-intervention-wiring.test.ts index 600589825e..361910b4bf 100644 --- a/packages/engine/src/__tests__/planner-overseer-intervention-wiring.test.ts +++ b/packages/engine/src/__tests__/planner-overseer-intervention-wiring.test.ts @@ -23,11 +23,9 @@ * FN-7840 removes the only real-tick producer of advisory merger awaiting-confirmation interventions, so this live-wiring harness now proves suppression at the source rather than request/resolution emission for an unreachable advisory pending-confirmation path. */ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "vitest"; import { TaskStore, getPlannerInterventionTimeline, type Task } from "@fusion/core"; +import { createSharedPgTaskStoreTestHarness, pgDescribe } from "../../../core/src/__test-utils__/pg-test-harness.js"; import { ProjectEngine } from "../project-engine.js"; import { PlannerOverseerMonitor, type OverseerStageObservation } from "../planner-overseer.js"; import { PlannerRecoveryController, type PlannerRecoveryHandlers, type PlannerRecoverySnapshotProvider } from "../planner-recovery-controller.js"; @@ -36,12 +34,12 @@ interface EngineOverseerInternals { plannerObservationEmitDedup: Map; plannerEscalationEmitDedup: Set; buildPlannerRecoveryHandlers(store: TaskStore): PlannerRecoveryHandlers; - emitOverseerObservationDeduped(store: TaskStore, observation: OverseerStageObservation): void; + emitOverseerObservationDeduped(store: TaskStore, observation: OverseerStageObservation): Promise; emitOverseerEscalationDeduped( store: TaskStore, taskId: string, decision: { watchedStage: string | null; reason: string; attemptCount: number; attemptLimit: number; sourceLinks: unknown[] }, - ): void; + ): Promise; } /** Extracts the real `ProjectEngine` prototype methods FN-7551 wired without running its heavy constructor/`start()`. */ @@ -57,7 +55,7 @@ function wireRealEngineOverseer(store: TaskStore) { const internals = makeEngineInternals(); const monitor = new PlannerOverseerMonitor({ store, - onObservation: (observation) => internals.emitOverseerObservationDeduped(store, observation), + onObservation: async (observation) => internals.emitOverseerObservationDeduped(store, observation), }); const handlers = internals.buildPlannerRecoveryHandlers(store); const controllerFromMonitor = new PlannerRecoveryController({ snapshotProvider: monitor, handlers }); @@ -91,24 +89,17 @@ function observation(overrides: Partial = {}): Oversee }; } -describe("FN-7551 — overseer decision points populate the intervention timeline via the live wiring", () => { - let rootDir: string; - let globalDir: string; +pgDescribe("FN-7551 — overseer decision points populate the intervention timeline via the live wiring", () => { + const harness = createSharedPgTaskStoreTestHarness({ prefix: "fusion_overseer_wiring" }); let store: TaskStore; + beforeAll(harness.beforeAll); beforeEach(async () => { - rootDir = mkdtempSync(join(tmpdir(), "fn-7551-engine-root-")); - globalDir = mkdtempSync(join(tmpdir(), "fn-7551-engine-global-")); - store = new TaskStore(rootDir, globalDir, { inMemoryDb: true }); - await store.init(); - }); - - afterEach(() => { - store.stopWatching(); - store.close(); - rmSync(rootDir, { recursive: true, force: true }); - rmSync(globalDir, { recursive: true, force: true }); + await harness.beforeEach(); + store = harness.store(); }); + afterEach(harness.afterEach); + afterAll(harness.afterAll); async function seedTask(column: "in-progress" | "in-review" = "in-progress"): Promise { const task = await store.createTask({ title: "T", description: "d" }); @@ -245,9 +236,9 @@ describe("FN-7551 — overseer decision points populate the intervention timelin sourceLinks: [], }; - emitEscalation(task.id, exhaustedDecision); - emitEscalation(task.id, exhaustedDecision); - emitEscalation(task.id, exhaustedDecision); + await emitEscalation(task.id, exhaustedDecision); + await emitEscalation(task.id, exhaustedDecision); + await emitEscalation(task.id, exhaustedDecision); const timeline = await getPlannerInterventionTimeline(store, task.id); const escalations = timeline.filter((e) => e.action === "escalate"); diff --git a/packages/engine/src/__tests__/project-engine.test.ts b/packages/engine/src/__tests__/project-engine.test.ts index bd66777a4d..e9a8145a21 100644 --- a/packages/engine/src/__tests__/project-engine.test.ts +++ b/packages/engine/src/__tests__/project-engine.test.ts @@ -82,7 +82,7 @@ vi.mock("../cron-runner.js", () => { // dispatch onto runAiMerge (merger-ai.js). project-engine no longer imports // aiMergeTask; the merge seam these tests mock/assert is now runAiMerge. vi.mock("../merger.js", () => ({ - sweepStaleAutostashes: vi.fn(async () => undefined), + sweepStaleAutostashes: vi.fn(async () => ({ dropped: 0, retained: 0 })), VerificationError: class VerificationError extends Error {}, })); @@ -205,6 +205,7 @@ vi.mock("../runtimes/in-process-runtime.js", () => ({ stop: mocks.runtimeStop, resumeAfterUnpause: mocks.runtimeResumeAfterUnpause, getTaskStore: () => mocks.currentStore, + getPluginRunner: vi.fn(() => undefined), getAgentStore: vi.fn(), getMessageStore: vi.fn(), getRoutineStore: vi.fn(), @@ -246,6 +247,7 @@ function createMockStore(initialSettings: Record) { return structuredClone(settings); }), logEntry: vi.fn(async () => undefined), + getAsyncLayer: vi.fn(() => ({ kind: "test-async-layer" })), /* FNXC:OverlapSelfHealing 2026-06-26-12:00: ProjectEngine's broad TaskStore fake is shared by maintenance wiring tests, so it carries clearStaleBlockedBy's overlap-path seam even when individual cases only exercise merge orchestration. @@ -404,6 +406,7 @@ describe("ProjectEngine notification ownership wiring", () => { const engine = createEngine({ skipNotifier: false, projectId: "proj_for_notifier" }); await engine.start(); + await vi.waitFor(() => expect(mocks.notifierStart).toHaveBeenCalledTimes(1)); expect(NotificationService).toHaveBeenCalledTimes(1); expect(OAuthAlertStateStore).toHaveBeenCalledTimes(1); @@ -443,6 +446,7 @@ describe("ProjectEngine notification ownership wiring", () => { await engine.start(); await engine.start(); + await vi.waitFor(() => expect(mocks.notifierStart).toHaveBeenCalledTimes(1)); // Root cause guard: if ProjectEngine.start is called more than once, it should not // wire a second NotificationService/NtfyNotifier pair for the same store. @@ -498,6 +502,47 @@ describe("ProjectEngine notification ownership wiring", () => { }); }); +describe("ProjectEngine planner overseer observation wiring", () => { + it("awaits the audit persistence promise through the monitor registered by start()", async () => { + const mockStore = createMockStore(baseSettings); + mocks.currentStore = mockStore.store; + const engine = createEngine(); + let releasePersistence!: () => void; + const persistence = new Promise((resolve) => { + releasePersistence = resolve; + }); + const engineInternals = engine as unknown as { + emitOverseerObservationDeduped: (store: unknown, observation: unknown) => Promise; + }; + const emitSpy = vi.spyOn(engineInternals, "emitOverseerObservationDeduped").mockReturnValue(persistence); + + await engine.start(); + try { + const monitor = engine.getPlannerOverseer(); + expect(monitor).toBeDefined(); + + /* + FNXC:PlannerOversight 2026-07-17-16:35: + ProjectEngine.start() must return the audit-persistence Promise from its + monitor callback. Awaiting only a test-local callback can hide dropped + PostgreSQL audit writes in the production wiring. + */ + const observed = monitor!.observeTask({ id: "FN-observation", column: "in-progress" } as Task, "autonomous"); + await vi.waitFor(() => expect(emitSpy).toHaveBeenCalledOnce()); + + let settled = false; + void observed.then(() => { settled = true; }); + await Promise.resolve(); + expect(settled).toBe(false); + + releasePersistence(); + await expect(observed).resolves.toEqual(expect.objectContaining({ taskId: "FN-observation" })); + } finally { + await engine.stop(); + } + }); +}); + describe("ProjectEngine accessors", () => { it("returns configured project id", () => { const engine = new ProjectEngine( @@ -565,6 +610,7 @@ describe("ProjectEngine auto-summarize wiring", () => { const engine = createEngine(); await engine.start(); + await vi.waitFor(() => expect(mocks.syncScheduledEvalBatchAutomation).toHaveBeenCalledTimes(1)); expect(mocks.syncInsightExtractionAutomation).toHaveBeenCalledTimes(1); expect(mocks.syncAutoSummarizeAutomation).toHaveBeenCalledTimes(1); @@ -602,6 +648,7 @@ describe("ProjectEngine auto-summarize wiring", () => { const engine = createEngine(); await engine.start(); + await vi.waitFor(() => expect(mocks.syncAutoSummarizeAutomation).toHaveBeenCalledTimes(1)); mocks.syncAutoSummarizeAutomation.mockClear(); const previous = { ...baseSettings }; @@ -644,6 +691,7 @@ describe("ProjectEngine memory dreams wiring", () => { const engine = createEngine(); await engine.start(); + await vi.waitFor(() => expect(mocks.syncMemoryDreamsAutomation).toHaveBeenCalledTimes(1)); const cronRunnerStartOrder = mocks.cronRunnerStart.mock.invocationCallOrder[0]; expect(mocks.syncMemoryDreamsAutomation).toHaveBeenCalledTimes(1); @@ -660,6 +708,7 @@ describe("ProjectEngine memory dreams wiring", () => { const engine = createEngine(); await engine.start(); + await vi.waitFor(() => expect(mocks.syncMemoryDreamsAutomation).toHaveBeenCalledTimes(1)); mocks.syncMemoryDreamsAutomation.mockClear(); const previous = { ...baseSettings }; @@ -682,6 +731,7 @@ describe("ProjectEngine memory dreams wiring", () => { const engine = createEngine(); await engine.start(); + await vi.waitFor(() => expect(mocks.syncMemoryDreamsAutomation).toHaveBeenCalledTimes(1)); mocks.syncMemoryDreamsAutomation.mockClear(); const previous = { ...baseSettings }; @@ -703,6 +753,7 @@ describe("ProjectEngine memory dreams wiring", () => { const engine = createEngine(); await engine.start(); + await vi.waitFor(() => expect(mocks.syncMemoryDreamsAutomation).toHaveBeenCalledTimes(1)); mocks.syncMemoryDreamsAutomation.mockClear(); const previous = { ...baseSettings }; @@ -725,6 +776,9 @@ describe("ProjectEngine memory dreams wiring", () => { const engine = createEngine(); await expect(engine.start()).resolves.toBeUndefined(); + await vi.waitFor(() => expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("Memory dreams automation startup sync failed"), + )); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining("Memory dreams automation startup sync failed"), @@ -745,6 +799,7 @@ describe("ProjectEngine memory dreams wiring", () => { const engine = createEngine(); await engine.start(); + await vi.waitFor(() => expect(mocks.syncMemoryDreamsAutomation).toHaveBeenCalledTimes(1)); warnSpy.mockClear(); mocks.syncMemoryDreamsAutomation.mockRejectedValueOnce(new Error("dream settings sync failed")); @@ -2568,16 +2623,19 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => { it("startup merge sweep skips paused in-review tasks", async () => { const mockStore = createMockStore({ ...baseSettings, autoMerge: true }); - mockStore.store.listTasks.mockResolvedValueOnce([ + const inReviewTasks = [ { 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); mocks.currentStore = mockStore.store; const engine = createEngine(); const privateEngine = engine as unknown as { internalEnqueueMerge: (taskId: string) => void }; const enqueueSpy = vi.spyOn(privateEngine, "internalEnqueueMerge"); await engine.start(); + await vi.waitFor(() => expect(enqueueSpy).toHaveBeenCalledWith("FN-ready")); expect(enqueueSpy).toHaveBeenCalledWith("FN-ready"); expect(enqueueSpy).not.toHaveBeenCalledWith("FN-paused"); @@ -2588,7 +2646,7 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => { it("startup merge sweep enqueues shared-group members when autoMerge is false", async () => { const mockStore = createMockStore({ ...baseSettings, autoMerge: false }); mockStore.store.getBranchGroup.mockReturnValue({ id: "BG-5819", status: "open", branchName: "fusion/groups/bg-5819" }); - mockStore.store.listTasks.mockResolvedValueOnce([ + const inReviewTasks = [ { id: "FN-shared", column: "in-review", @@ -2598,13 +2656,16 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => { 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); mocks.currentStore = mockStore.store; const engine = createEngine(); const privateEngine = engine as unknown as { internalEnqueueMerge: (taskId: string) => void }; const enqueueSpy = vi.spyOn(privateEngine, "internalEnqueueMerge"); await engine.start(); + await vi.waitFor(() => expect(enqueueSpy).toHaveBeenCalledWith("FN-shared")); expect(enqueueSpy).toHaveBeenCalledWith("FN-shared"); expect(enqueueSpy).not.toHaveBeenCalledWith("FN-plain"); @@ -3074,6 +3135,7 @@ describe("ProjectEngine swallowed error hardening", () => { const engine = createEngine(); await engine.start(); + await vi.waitFor(() => expect(mockStore.store.listTasks).toHaveBeenCalledTimes(2)); mockStore.store.getSettings.mockRejectedValueOnce(new Error("db locked")); @@ -3105,12 +3167,18 @@ describe("ProjectEngine swallowed error hardening", () => { it("warns when startup merge sweep fails", async () => { const mockStore = createMockStore({ ...baseSettings, autoMerge: true }); mocks.currentStore = mockStore.store; - mockStore.store.listTasks.mockRejectedValueOnce(new Error("connection lost")); + // The critical stale-status read precedes the deferred enqueue sweep. + mockStore.store.listTasks + .mockResolvedValueOnce([]) + .mockRejectedValueOnce(new Error("connection lost")); const engine = createEngine(); await engine.start(); + await vi.waitFor(() => expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("Auto-merge startup enqueue failed: connection lost"), + )); - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Auto-merge startup sweep failed")); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Auto-merge startup enqueue failed: connection lost")); await engine.stop(); }); @@ -3121,6 +3189,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)); warnSpy.mockClear(); mockStore.store.listTasks.mockRejectedValueOnce(new Error("sweep db error")); @@ -3138,6 +3207,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)); warnSpy.mockClear(); mockStore.store.getSettings @@ -3498,49 +3568,49 @@ describe("ProjectEngine stale mergeActive rescue (FN-3900)", () => { }); describe("allowInReviewMergeProcessing per-task autoMerge override", () => { - const gate = (task: Partial, settings: { autoMerge: boolean }, branchGroup: { status: "open" | "finalized" | "abandoned" } | null = null) => - (createEngine() as any).allowInReviewMergeProcessing(task, settings, { getBranchGroup: vi.fn(() => branchGroup) }) as boolean; + const gate = (task: Partial, settings: { autoMerge: boolean }, branchGroup: { status: "open" | "finalized" | "abandoned" } | null = null): Promise => + (createEngine() as any).allowInReviewMergeProcessing(task, settings, { getBranchGroup: vi.fn(() => branchGroup) }); - it("lets an explicit per-task autoMerge:true through when the global setting is off", () => { - expect(gate({ autoMerge: true }, { autoMerge: false })).toBe(true); + it("lets an explicit per-task autoMerge:true through when the global setting is off", async () => { + await expect(gate({ autoMerge: true }, { autoMerge: false })).resolves.toBe(true); }); - it("blocks tasks without a per-task override when the global setting is off", () => { - expect(gate({}, { autoMerge: false })).toBe(false); - expect(gate({ autoMerge: false }, { autoMerge: false })).toBe(false); + it("blocks tasks without a per-task override when the global setting is off", async () => { + await expect(gate({}, { autoMerge: false })).resolves.toBe(false); + await expect(gate({ autoMerge: false }, { autoMerge: false })).resolves.toBe(false); }); - it("keeps everything flowing when the global setting is on — explicit autoMerge:false is parked manual-required downstream", () => { - expect(gate({}, { autoMerge: true })).toBe(true); - expect(gate({ autoMerge: false }, { autoMerge: true })).toBe(true); + it("keeps everything flowing when the global setting is on — explicit autoMerge:false is parked manual-required downstream", async () => { + await expect(gate({}, { autoMerge: true })).resolves.toBe(true); + await expect(gate({ autoMerge: false }, { autoMerge: true })).resolves.toBe(true); }); - it("still exempts live shared-branch-group member integration when the global setting is off", () => { - expect(gate( + it("still exempts live shared-branch-group member integration when the global setting is off", async () => { + await expect(gate( { branchContext: { assignmentMode: "shared", groupId: "grp-1" } as Task["branchContext"] }, { autoMerge: false }, { status: "open" }, - )).toBe(true); + )).resolves.toBe(true); }); it.each([ ["missing", null], ["finalized", { status: "finalized" as const }], ["abandoned", { status: "abandoned" as const }], - ])("blocks shared-branch-group member integration for %s groups when global autoMerge is off", (_label, branchGroup) => { - expect(gate( + ])("blocks shared-branch-group member integration for %s groups when global autoMerge is off", async (_label, branchGroup) => { + await expect(gate( { branchContext: { assignmentMode: "shared", groupId: "grp-1" } as Task["branchContext"] }, { autoMerge: false }, branchGroup, - )).toBe(false); + )).resolves.toBe(false); }); it.each([ ["api", { sourceType: "api" }], ["user-created", { sourceType: undefined }], ["engine-created", { sourceType: "unknown", sourceMetadata: { fusionBranchContext: { assignmentMode: "shared", groupId: "grp-1", source: "mission" } } }], - ])("applies the dissolved-group manual hold regardless of %s provenance", (_label, provenance) => { - expect(gate( + ])("applies the dissolved-group manual hold regardless of %s provenance", async (_label, provenance) => { + await expect(gate( { ...provenance, autoMerge: undefined, @@ -3548,7 +3618,7 @@ describe("allowInReviewMergeProcessing per-task autoMerge override", () => { }, { autoMerge: false }, null, - )).toBe(false); + )).resolves.toBe(false); }); }); @@ -3589,21 +3659,21 @@ describe("enqueueEligibleInReviewTasks honors per-task autoMerge override (share const enqueueSpy = vi .spyOn(engine, "internalEnqueueMerge") .mockImplementation(() => true); - const run = (tasks: Task[], settings: { autoMerge: boolean }): number => - engine.enqueueEligibleInReviewTasks(tasks, settings) as number; + const run = (tasks: Task[], settings: { autoMerge: boolean }): Promise => + engine.enqueueEligibleInReviewTasks(tasks, settings); return { engine, enqueueSpy, run }; }; - it("enqueues an in-review task with autoMerge:true even when the global setting is off", () => { + it("enqueues an in-review task with autoMerge:true even when the global setting is off", async () => { const { enqueueSpy, run } = setup(); - const count = run([inReview("FN-override", { autoMerge: true })], { autoMerge: false }); + const count = await run([inReview("FN-override", { autoMerge: true })], { autoMerge: false }); expect(count).toBe(1); expect(enqueueSpy).toHaveBeenCalledWith("FN-override"); }); - it("does not enqueue a sibling task without an override in the same sweep when the global setting is off", () => { + it("does not enqueue a sibling task without an override in the same sweep when the global setting is off", async () => { const { enqueueSpy, run } = setup(); - const count = run( + const count = await run( [inReview("FN-override", { autoMerge: true }), inReview("FN-plain")], { autoMerge: false }, ); @@ -3612,9 +3682,9 @@ describe("enqueueEligibleInReviewTasks honors per-task autoMerge override (share expect(enqueueSpy).not.toHaveBeenCalledWith("FN-plain"); }); - it("still enqueues a task with autoMerge:false when the global setting is on (parked manual-required downstream)", () => { + it("still enqueues a task with autoMerge:false when the global setting is on (parked manual-required downstream)", async () => { const { enqueueSpy, run } = setup(); - const count = run([inReview("FN-explicit-false", { autoMerge: false })], { autoMerge: true }); + const count = await run([inReview("FN-explicit-false", { autoMerge: false })], { autoMerge: true }); expect(count).toBe(1); expect(enqueueSpy).toHaveBeenCalledWith("FN-explicit-false"); }); diff --git a/packages/engine/src/__tests__/unlinked-missions-advisory-reporter.pg.test.ts b/packages/engine/src/__tests__/unlinked-missions-advisory-reporter.pg.test.ts new file mode 100644 index 0000000000..5384c095e5 --- /dev/null +++ b/packages/engine/src/__tests__/unlinked-missions-advisory-reporter.pg.test.ts @@ -0,0 +1,51 @@ +/* +FNXC:UnlinkedMissionsAdvisory 2026-07-17-16:25: +The scheduler must persist the same active-unlinked advisory through PostgreSQL +as it does through the synchronous MissionStore. This integration test uses the +canonical TaskStore harness so query ordering and insight deduplication remain +real backend behavior rather than a structural mock. +*/ +import { expect, it } from "vitest"; +import type { AsyncGoalStore, AsyncMissionStore } from "@fusion/core"; +import { createTaskStoreForTest, pgDescribe } from "../../../core/src/__test-utils__/pg-test-harness.js"; +import { + UNLINKED_MISSIONS_ADVISORY_TITLE, + UnlinkedMissionsAdvisoryReporter, +} from "../unlinked-missions-advisory-reporter.js"; + +pgDescribe("UnlinkedMissionsAdvisoryReporter PostgreSQL", () => { + it("persists one advisory for only active unlinked missions without duplicating it", async () => { + const harness = await createTaskStoreForTest({ prefix: "fusion_unlinked_advisory" }); + try { + const missions = harness.store.getMissionStore() as AsyncMissionStore; + const goals = harness.store.getGoalStore() as AsyncGoalStore; + const linked = await missions.createMission({ title: "Linked" }); + const unlinked = await missions.createMission({ title: "Unlinked" }); + const archived = await missions.createMission({ title: "Archived" }); + const goal = await goals.createGoal({ title: "Goal" }); + await missions.updateMission(linked.id, { status: "active" }); + await missions.updateMission(unlinked.id, { status: "active" }); + await missions.linkGoal(linked.id, goal.id); + await missions.updateMission(archived.id, { status: "archived" }); + + const projectId = "pg-reporter"; + const reporter = new UnlinkedMissionsAdvisoryReporter({ + store: harness.store, + projectId, + now: () => Date.parse("2026-07-17T16:25:00.000Z"), + }); + + await expect(reporter.report()).resolves.toEqual({ alerted: true }); + await expect(reporter.report()).resolves.toEqual({ alerted: false, reason: "already-reported" }); + + const insights = await harness.store.getInsightStore().listInsights({ + projectId, + category: "workflow", + }); + expect(insights.filter((insight) => insight.title === UNLINKED_MISSIONS_ADVISORY_TITLE)).toHaveLength(1); + expect(JSON.parse(insights[0]!.content ?? "{}").missionIds).toEqual([unlinked.id]); + } finally { + await harness.teardown(); + } + }); +}); diff --git a/packages/engine/src/__tests__/unlinked-missions-advisory-reporter.test.ts b/packages/engine/src/__tests__/unlinked-missions-advisory-reporter.test.ts index 844a78fdaa..27b42e3f60 100644 --- a/packages/engine/src/__tests__/unlinked-missions-advisory-reporter.test.ts +++ b/packages/engine/src/__tests__/unlinked-missions-advisory-reporter.test.ts @@ -23,10 +23,12 @@ function createStore(params: { goalIdsByMissionId?: Record; insightStore?: { upsertInsight: ReturnType; listInsights: ReturnType }; throwInsightStore?: boolean; + asyncMissionStore?: boolean; }): TaskStore { + const resolve = (value: T) => params.asyncMissionStore ? Promise.resolve(value) : value; const missionStore = { - listMissions: vi.fn().mockReturnValue(params.missions ?? []), - listGoalIdsForMission: vi.fn().mockImplementation((missionId: string) => params.goalIdsByMissionId?.[missionId] ?? []), + listMissions: vi.fn().mockImplementation(() => resolve(params.missions ?? [])), + listGoalIdsForMission: vi.fn().mockImplementation((missionId: string) => resolve(params.goalIdsByMissionId?.[missionId] ?? [])), }; return { @@ -86,6 +88,20 @@ describe("UnlinkedMissionsAdvisoryReporter", () => { }); }); + it("supports promise-returning PostgreSQL-shaped mission stores", async () => { + const insightStore = { upsertInsight: vi.fn().mockResolvedValue(undefined), listInsights: vi.fn().mockResolvedValue([]) }; + const store = createStore({ + missions: [createMission({ id: "M-ASYNC" })], + insightStore, + asyncMissionStore: true, + }); + const reporter = new UnlinkedMissionsAdvisoryReporter({ store, projectId: "/tmp/project", logger }); + + await expect(reporter.report()).resolves.toEqual({ alerted: true }); + expect(insightStore.upsertInsight).toHaveBeenCalledTimes(1); + expect(store.getMissionStore).toHaveBeenCalledTimes(1); + }); + it("excludes active missions that already have linked goals", async () => { const insightStore = { upsertInsight: vi.fn(), listInsights: vi.fn().mockReturnValue([]) }; const store = createStore({ diff --git a/packages/engine/src/mission-execution-loop.ts b/packages/engine/src/mission-execution-loop.ts index 5f87566c97..ec1ba8783d 100644 --- a/packages/engine/src/mission-execution-loop.ts +++ b/packages/engine/src/mission-execution-loop.ts @@ -528,18 +528,27 @@ export class MissionExecutionLoop extends EventEmitter { * dispatch of the validation result. */ private async runFeatureValidation(feature: MissionFeature): Promise { - // Lazily guarantee a linked assertion before validation so every feature - // is evaluated by the validator even when legacy data is missing links. - let assertions = await this.missionStore.listAssertionsForFeature(feature.id); - if (assertions.length === 0) { - loopLog.log(`Feature ${feature.id} has no linked assertions; lazily ensuring store-managed assertion linkage`); - assertions = await this.missionStore.ensureFeatureAssertionLinked(feature.id); - } - - // Mark feature as being validated + /* + FNXC:MissionValidation 2026-07-17-16:40: + Claim validation before any asynchronous assertion lookup. Concurrent task + completion events must share one validator run, including the lazy-link path. + */ this.activeValidations.add(feature.id); try { + // Lazily guarantee a linked assertion before validation so every feature + // is evaluated by the validator even when legacy data is missing links. + let assertions = await this.missionStore.listAssertionsForFeature(feature.id); + if (assertions.length === 0) { + loopLog.log(`Feature ${feature.id} has no linked assertions; lazily ensuring store-managed assertion linkage`); + assertions = await this.missionStore.ensureFeatureAssertionLinked(feature.id); + } + if (assertions.length === 0) { + // FNXC:MissionValidation 2026-07-17-16:45: no-assertion features remain a valid completion path when linkage cannot derive an assertion. + await this.handleValidationPass(feature.id, undefined, "No assertions linked to feature"); + return; + } + loopLog.log(`Running internal validation for feature ${feature.id} — no board task created (policy: docs/missions.md)`); // FNXC:MissionValidation 2026-07-16-12:00: diff --git a/packages/engine/src/project-engine.ts b/packages/engine/src/project-engine.ts index 7bf4140bd3..d60ca40c47 100644 --- a/packages/engine/src/project-engine.ts +++ b/packages/engine/src/project-engine.ts @@ -750,6 +750,14 @@ export class ProjectEngine { return; } + /* + FNXC:EngineShutdown 2026-07-17-16:35: + stop() is a hard lifecycle boundary: public merge requests must remain + rejected after it settles. Reset the shutdown guard only when a subsequent + start begins a new lifecycle, never at the end of stop(). + */ + this.shuttingDown = false; + // 1. Start the core runtime (TaskStore, Scheduler, Executor, Triage, etc.) await this.runtime.start(); @@ -849,9 +857,7 @@ export class ProjectEngine { // through the FN-7520 façade for each real observation the monitor // records, using the real TaskStore. Best-effort — never throws (the // monitor already swallows callback errors around `onObservation`). - onObservation: (observation) => { - this.emitOverseerObservationDeduped(store, observation); - }, + onObservation: (observation) => this.emitOverseerObservationDeduped(store, observation), }); // FN-7512: bounded autonomous-recovery dispatcher, wired to the existing // steering-comment API + store retry/re-enqueue path only — no new @@ -1362,7 +1368,6 @@ export class ProjectEngine { await this.runtime.stop(); this.started = false; - this.shuttingDown = false; runtimeLog.log(`ProjectEngine stopped for ${this.config.projectId}`); } @@ -1641,9 +1646,9 @@ export class ProjectEngine { * try/catch-degrade-to-no-op contract every FN-7512/FN-7513/FN-7514 * handler already follows). */ - private emitOverseerInterventionSafe(fn: () => void): void { + private async emitOverseerInterventionSafe(fn: () => unknown | Promise): Promise { try { - fn(); + await fn(); } catch (err) { runtimeLog.warn(`Failed to emit overseer intervention: ${err instanceof Error ? err.message : String(err)}`); } @@ -1658,7 +1663,7 @@ export class ProjectEngine { * `(stage, signal)` pair emits. Best-effort: any store/façade failure is * swallowed so it never breaks `PlannerOverseerMonitor#observeTask`/the poll. */ - private emitOverseerObservationDeduped(store: TaskStore, observation: import("./planner-overseer.js").OverseerStageObservation): void { + private async emitOverseerObservationDeduped(store: TaskStore, observation: import("./planner-overseer.js").OverseerStageObservation): Promise { try { const dedupKey = `${observation.stage}:${observation.signal}`; const last = this.plannerObservationEmitDedup.get(observation.taskId); @@ -1666,7 +1671,7 @@ export class ProjectEngine { return; } this.plannerObservationEmitDedup.set(observation.taskId, dedupKey); - emitOverseerObservation({ + await emitOverseerObservation({ store, taskId: observation.taskId, stage: observation.stage, @@ -1690,18 +1695,18 @@ export class ProjectEngine { * clears the dedup entry and may escalate again in a future exhaustion). * Best-effort; never throws out of the poll. */ - private emitOverseerEscalationDeduped( + private async emitOverseerEscalationDeduped( store: TaskStore, taskId: string, decision: { watchedStage: PlannerOversightStage | null; reason: string; attemptCount: number; attemptLimit: number; sourceLinks: ReadonlyArray<{ kind: string; ref: string; url?: string }> }, - ): void { + ): Promise { if (!decision.watchedStage) return; const dedupKey = `${taskId}::${decision.watchedStage}`; if (this.plannerEscalationEmitDedup.has(dedupKey)) { return; } this.plannerEscalationEmitDedup.add(dedupKey); - this.emitOverseerInterventionSafe(() => + await this.emitOverseerInterventionSafe(() => emitOverseerEscalation({ store, taskId, @@ -1744,7 +1749,7 @@ export class ProjectEngine { // comment succeeds, through the real store, so the timeline reflects // the same guidance the agent actually saw. // FNXC:PlannerOversight 2026-07-13-23:05: tag lifecycle source for timeline vs session-advisor. - this.emitOverseerInterventionSafe(() => + await this.emitOverseerInterventionSafe(() => emitOverseerSteering({ store, taskId: task.id, @@ -1759,7 +1764,7 @@ export class ProjectEngine { await store.moveTask(task.id, "todo", { preserveProgress: true, moveSource: "engine" } as Parameters[2]); // FN-7551: the attempt just dispatched — record it as attemptCount + 1 // (decision.attemptCount is the count BEFORE this dispatch). - this.emitOverseerInterventionSafe(() => + await this.emitOverseerInterventionSafe(() => emitOverseerRetry({ store, taskId: task.id, @@ -1777,7 +1782,7 @@ export class ProjectEngine { ? `[planner-oversight] targeted-fix requested: ${decision.reason} (source: ${sourceRef})` : `[planner-oversight] targeted-fix requested: ${decision.reason}`; await store.addSteeringComment(task.id, text, "agent"); - this.emitOverseerInterventionSafe(() => + await this.emitOverseerInterventionSafe(() => emitOverseerRecoveryAttempt({ store, taskId: task.id, @@ -1810,7 +1815,7 @@ export class ProjectEngine { requestConfirmation: async (task, request) => { const text = `[planner-oversight] merge checkpoint (${request.sideEffectClass}): ${request.reason}`; await store.addSteeringComment(task.id, text, "agent"); - this.emitOverseerInterventionSafe(() => + await this.emitOverseerInterventionSafe(() => emitOverseerConfirmation({ store, taskId: task.id, @@ -1827,7 +1832,7 @@ export class ProjectEngine { // the timeline shows both the request and its resolution. Never touches // the approve/deny execution path itself. onConfirmationResolved: async (taskId, request, resolution) => { - this.emitOverseerInterventionSafe(() => + await this.emitOverseerInterventionSafe(() => emitOverseerConfirmation({ store, taskId, @@ -2629,7 +2634,7 @@ export class ProjectEngine { } private internalEnqueueMerge(taskId: string): boolean { - if (this.shuttingDown) return false; + if (this.shuttingDown || !this.started) return false; if (this.mergeActive.has(taskId)) { // Distinguish "actually being processed" (queued or active) from a // leaked entry. Reconcile leaks immediately so recovery paths and fresh @@ -2854,7 +2859,7 @@ export class ProjectEngine { // event — emit exactly one `escalate` entry per (taskId, stage) // while the stage remains exhausted across subsequent polls. if (decision?.exhausted && decision.watchedStage) { - this.emitOverseerEscalationDeduped(store, task.id, decision); + await this.emitOverseerEscalationDeduped(store, task.id, decision); } } diff --git a/packages/engine/src/unlinked-missions-advisory-reporter.ts b/packages/engine/src/unlinked-missions-advisory-reporter.ts index 97bf89a7cb..8c6ec0b806 100644 --- a/packages/engine/src/unlinked-missions-advisory-reporter.ts +++ b/packages/engine/src/unlinked-missions-advisory-reporter.ts @@ -1,4 +1,4 @@ -import { computeInsightFingerprint, MissionStore, type Mission, type TaskStore } from "@fusion/core"; +import { computeInsightFingerprint, type AsyncMissionStore, type Mission, type MissionStore, type TaskStore } from "@fusion/core"; import { createLogger } from "./logger.js"; const reporterLog = createLogger("unlinked-missions-advisory"); @@ -32,24 +32,21 @@ export class UnlinkedMissionsAdvisoryReporter { async report(): Promise<{ alerted: boolean; reason?: string }> { try { - // FNXC:MissionStore 2026-06-27-15:40: - // This reporter reads the MissionStore synchronously. In PG backend mode - // getMissionStore() returns the AsyncMissionStore (CRUD-only, not an - // EventEmitter); guard with instanceof and degrade gracefully — the advisory - // is sync-mode only this unit. - const resolvedMissionStore = this.store.getMissionStore(); - if (!(resolvedMissionStore instanceof MissionStore)) { - return { alerted: false, reason: "mission-store-async-unsupported" }; - } - const missionStore = resolvedMissionStore; - const missions = missionStore.listMissions(); + /* + FNXC:UnlinkedMissionsAdvisory 2026-07-17-16:20: + Scheduled advisory behavior must be identical for the synchronous MissionStore + and PostgreSQL AsyncMissionStore. Both expose the same mission and goal-link + queries, so await their union rather than disabling a production scheduler path. + */ + const missionStore: MissionStore | AsyncMissionStore = this.store.getMissionStore(); + const missions = await missionStore.listMissions(); const unlinkedActiveMissions: Mission[] = []; for (const mission of missions) { if (mission.status !== "active") { continue; } - if (missionStore.listGoalIdsForMission(mission.id).length > 0) { + if ((await missionStore.listGoalIdsForMission(mission.id)).length > 0) { continue; } unlinkedActiveMissions.push(mission); diff --git a/packages/engine/vitest.config.ts b/packages/engine/vitest.config.ts index 94d3448d81..d0f12ffd1d 100644 --- a/packages/engine/vitest.config.ts +++ b/packages/engine/vitest.config.ts @@ -295,6 +295,7 @@ export default defineConfig({ // SQLite-path gate test evicted + quarantined (see engine-core comment + ledger). "node_modules/**", "dist/**", + // FNXC:PgMigrationQuarantine 2026-07-17-16:50: FN-8252 rescued the semantic async-store holdouts through production PostgreSQL seams and current async harnesses; retain only the remaining paired quarantines. // FNXC:PgMigrationQuarantine 2026-07-14-08:00: // FNXC:WorkflowStepInstancePersistence 2026-07-16-20:35: FN-8157 restores this PG-backed foreach suite through async store persistence, so it must execute in engine-default. // VAL-REMOVAL-005 deleted the SQLite Database class. These engine-default files fail @@ -302,11 +303,7 @@ export default defineConfig({ // getDatabase, walCheckpoint) that throw/return-empty in backend mode, or have mock // drift from the async-satellite cutover. Quarantined on sight per AGENTS.md. "src/__tests__/backlog-pressure-reporter.test.ts", - "src/__tests__/mission-autopilot.test.ts", "src/__tests__/mission-factory-parity.integration.test.ts", - "src/__tests__/planner-overseer-intervention-wiring.test.ts", - "src/__tests__/project-engine.test.ts", - "src/__tests__/unlinked-missions-advisory-reporter.test.ts", "src/__tests__/workflow-graph-task-runner.test.ts", "src/__tests__/agent-tools-intake-column.test.ts", "src/__tests__/agent-workflow-tools-exposure.test.ts", @@ -316,7 +313,6 @@ export default defineConfig({ "src/__tests__/group-merge-coordinator.test.ts", "src/__tests__/hybrid-executor-multi-node-routing.test.ts", "src/__tests__/merger-cwd-fallback-removed.test.ts", - "src/__tests__/mission-autopilot-end-to-end.test.ts", "src/__tests__/routine-runner.test.ts", "src/__tests__/self-healing-meta-archive-guards.test.ts", "src/__tests__/triage-token-usage.test.ts", diff --git a/scripts/lib/test-quarantine.json b/scripts/lib/test-quarantine.json index a067edec8a..acfc27dfde 100644 --- a/scripts/lib/test-quarantine.json +++ b/scripts/lib/test-quarantine.json @@ -1,36 +1,16 @@ { - "$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.", + "$comment": "Flaky-test quarantine ledger (deletion ratchet \u2014 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 \u2014 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__/backlog-pressure-reporter.test.ts", "reason": "VAL-REMOVAL-005 PG migration: fails due to SQLite removal or sync API incompatibility in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.", "quarantinedAt": "2026-07-14" }, - { - "file": "packages/engine/src/__tests__/mission-autopilot.test.ts", - "reason": "VAL-REMOVAL-005 PG migration: fails due to SQLite removal or sync API incompatibility in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.", - "quarantinedAt": "2026-07-14" - }, { "file": "packages/engine/src/__tests__/mission-factory-parity.integration.test.ts", "reason": "VAL-REMOVAL-005 PG migration: fails due to SQLite removal or sync API incompatibility in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.", "quarantinedAt": "2026-07-14" }, - { - "file": "packages/engine/src/__tests__/planner-overseer-intervention-wiring.test.ts", - "reason": "VAL-REMOVAL-005 PG migration: fails due to SQLite removal or sync API incompatibility in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.", - "quarantinedAt": "2026-07-14" - }, - { - "file": "packages/engine/src/__tests__/project-engine.test.ts", - "reason": "VAL-REMOVAL-005 PG migration: fails due to SQLite removal or sync API incompatibility in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.", - "quarantinedAt": "2026-07-14" - }, - { - "file": "packages/engine/src/__tests__/unlinked-missions-advisory-reporter.test.ts", - "reason": "VAL-REMOVAL-005 PG migration: fails due to SQLite removal or sync API incompatibility in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.", - "quarantinedAt": "2026-07-14" - }, { "file": "packages/engine/src/__tests__/workflow-graph-task-runner.test.ts", "reason": "VAL-REMOVAL-005 PG migration: fails due to SQLite removal or sync API incompatibility in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.", @@ -76,11 +56,6 @@ "reason": "VAL-REMOVAL-005 PG migration: fails due to SQLite removal or sync API incompatibility in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.", "quarantinedAt": "2026-07-14" }, - { - "file": "packages/engine/src/__tests__/mission-autopilot-end-to-end.test.ts", - "reason": "VAL-REMOVAL-005 PG migration: fails due to SQLite removal or sync API incompatibility in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.", - "quarantinedAt": "2026-07-14" - }, { "file": "packages/engine/src/__tests__/routine-runner.test.ts", "reason": "VAL-REMOVAL-005 PG migration: fails due to SQLite removal or sync API incompatibility in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.",