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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-17 17:26:22 -07:00
parent 9cafa045df
commit e24f765495
11 changed files with 260 additions and 143 deletions

View File

@@ -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(

View File

@@ -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();

View File

@@ -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<string, string>;
plannerEscalationEmitDedup: Set<string>;
buildPlannerRecoveryHandlers(store: TaskStore): PlannerRecoveryHandlers;
emitOverseerObservationDeduped(store: TaskStore, observation: OverseerStageObservation): void;
emitOverseerObservationDeduped(store: TaskStore, observation: OverseerStageObservation): Promise<void>;
emitOverseerEscalationDeduped(
store: TaskStore,
taskId: string,
decision: { watchedStage: string | null; reason: string; attemptCount: number; attemptLimit: number; sourceLinks: unknown[] },
): void;
): Promise<void>;
}
/** 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<OverseerStageObservation> = {}): 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<Task> {
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");

View File

@@ -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<string, unknown>) {
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<void>((resolve) => {
releasePersistence = resolve;
});
const engineInternals = engine as unknown as {
emitOverseerObservationDeduped: (store: unknown, observation: unknown) => Promise<void>;
};
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<Task>, 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<Task>, settings: { autoMerge: boolean }, branchGroup: { status: "open" | "finalized" | "abandoned" } | null = null): Promise<boolean> =>
(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<number> =>
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");
});

View File

@@ -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();
}
});
});

View File

@@ -23,10 +23,12 @@ function createStore(params: {
goalIdsByMissionId?: Record<string, string[]>;
insightStore?: { upsertInsight: ReturnType<typeof vi.fn>; listInsights: ReturnType<typeof vi.fn> };
throwInsightStore?: boolean;
asyncMissionStore?: boolean;
}): TaskStore {
const resolve = <T>(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({

View File

@@ -528,18 +528,27 @@ export class MissionExecutionLoop extends EventEmitter {
* dispatch of the validation result.
*/
private async runFeatureValidation(feature: MissionFeature): Promise<void> {
// 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:

View File

@@ -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<unknown>): Promise<void> {
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<void> {
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<void> {
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<TaskStore["moveTask"]>[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);
}
}

View File

@@ -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);

View File

@@ -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",

View File

@@ -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.",