FN-8258: complete PostgreSQL quarantine rescues

Complete PostgreSQL-backed rescue coverage while retaining archived shared-branch landing proof.

- Preserve merge details when archiving and restoring tasks for branch-group promotion.
- Migrate remaining quarantine tests and mocks to PostgreSQL-aware boundaries.
- Remove rescued tests from the engine quarantine configuration and ledger.

Files changed:
 .changeset/fn-8258-pg-quarantine.md                |  7 +++
 .../core/src/task-store/archive-lifecycle-2.ts     |  1 +
 packages/core/src/task-store/remaining-ops-6.ts    |  8 ++-
 packages/core/src/task-store/serialization.ts      |  1 +
 .../__tests__/agent-tools-intake-column.test.ts    | 26 ++++------
 .../agent-workflow-tools-exposure.test.ts          | 18 +++----
 .../engine/src/__tests__/executor-task-done-invariant.test.ts | 33 ++++++------
 .../engine/src/__tests__/executor-test-helpers.ts  |  7 +++
 .../src/__tests__/group-merge-coordinator.test.ts  | 43 ++++++++++------
 .../hybrid-executor-multi-node-routing.test.ts     |  5 ++
 .../mission-factory-parity.integration.test.ts     |  2 +-
 .../engine/src/__tests__/routine-runner.test.ts    | 56 +++++++++++++--------
 .../self-healing-meta-archive-guards.test.ts       | 28 +++++------
 .../src/__tests__/triage-token-usage.test.ts       | 58 +++++-----------------
 .../__tests__/workflow-graph-task-runner.test.ts   | 16 +++---
 packages/engine/src/hybrid-executor-gate.ts        |  8 ++-
 packages/engine/vitest.config.ts                   | 12 +----
 scripts/lib/test-quarantine.json                   | 52 +------------------
 18 files changed, 165 insertions(+), 216 deletions(-)

Fusion-Task-Id: FN-8258

Fusion-Task-Lineage: 121c2b52-ad50-4475-b925-7a36ecfaf28b

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-18 01:21:20 -07:00
parent 2ab0413c07
commit 377cb9c90a
18 changed files with 164 additions and 215 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Preserve archived shared-branch landing proof during PostgreSQL promotion checks.
category: fix
dev: Completes PostgreSQL quarantine rescues and prevents archived landed members from blocking branch-group promotion.

View File

@@ -96,6 +96,7 @@ export async function taskToArchiveEntryImpl(store: TaskStore, task: Task, archi
missionId: task.missionId, missionId: task.missionId,
sliceId: task.sliceId, sliceId: task.sliceId,
assigneeUserId: task.assigneeUserId, assigneeUserId: task.assigneeUserId,
mergeDetails: task.mergeDetails,
}; };
} }

View File

@@ -100,8 +100,14 @@ export async function listBranchGroupsImpl(store: TaskStore, options?: { status?
return (rows as BranchGroupRow[]).map((row) => store.rowToBranchGroup(row)); return (rows as BranchGroupRow[]).map((row) => store.rowToBranchGroup(row));
} }
/*
FNXC:BranchGroupCompletion 2026-07-18-01:55:
Archived shared-branch members remain part of promotion completion: an unlanded archived
member blocks promotion, while an archived member with persisted landing proof permits it.
The PostgreSQL path must therefore load archived tasks before applying group membership.
*/
export async function listTasksByBranchGroupImpl(store: TaskStore, groupId: string): Promise<Task[]> { export async function listTasksByBranchGroupImpl(store: TaskStore, groupId: string): Promise<Task[]> {
const tasks = await store.listTasks({ includeArchived: false, slim: true }); const tasks = await store.listTasks({ includeArchived: true, slim: false });
// Membership filter (incl. legacy synthetic-groupId fallback) is shared with // Membership filter (incl. legacy synthetic-groupId fallback) is shared with
// the dashboard list route via `filterTasksByBranchGroup` so semantics can't // the dashboard list route via `filterTasksByBranchGroup` so semantics can't
// drift between the two call sites (Fix #8/#9). // drift between the two call sites (Fix #8/#9).

View File

@@ -374,6 +374,7 @@ export function archiveEntryToTask(
missionId: entry.missionId, missionId: entry.missionId,
sliceId: entry.sliceId, sliceId: entry.sliceId,
assigneeUserId: entry.assigneeUserId, assigneeUserId: entry.assigneeUserId,
mergeDetails: slim ? undefined : entry.mergeDetails,
}; };
} }

View File

@@ -1,10 +1,8 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, expect, it } from "vitest";
import { mkdtempSync } from "node:fs";
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import { rm } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { tmpdir } from "node:os"; import { type TaskStore, type WorkflowIr } from "@fusion/core";
import { TaskStore, type WorkflowIr } from "@fusion/core"; import { createTaskStoreForTest, pgDescribe, type PgTestHarness } from "../../../core/src/__test-utils__/pg-test-harness.js";
import { createTaskCreateTool } from "../agent-tools.js"; import { createTaskCreateTool } from "../agent-tools.js";
/* /*
@@ -15,22 +13,18 @@ non-triage `intake`-trait column (e.g. "Inbox") must capture new cards there, in
(bootstrap-stub PROMPT.md, no Planner spec generation), while the default builtin:coding (bootstrap-stub PROMPT.md, no Planner spec generation), while the default builtin:coding
workflow keeps landing cards in "triage" byte-identically. workflow keeps landing cards in "triage" byte-identically.
*/ */
describe("createTaskCreateTool intake-column wiring", () => { /* FNXC:PgMigrationQuarantine 2026-07-17-18:15: FN-8258 keeps intake-column behavior on a real PostgreSQL-backed TaskStore, replacing removed SQLite inMemoryDb setup. */
let rootDir: string; pgDescribe("createTaskCreateTool intake-column wiring", () => {
let globalDir: string; let harness: PgTestHarness;
let store: TaskStore; let store: TaskStore;
beforeEach(async () => { beforeEach(async () => {
rootDir = mkdtempSync(join(tmpdir(), "kb-engine-agent-tools-intake-")); harness = await createTaskStoreForTest({ prefix: "fusion_agent_tools_intake" });
globalDir = mkdtempSync(join(tmpdir(), "kb-engine-agent-tools-intake-global-")); store = harness.store;
store = new TaskStore(rootDir, globalDir, { inMemoryDb: true });
await store.init();
}); });
afterEach(async () => { afterEach(async () => {
store.close(); await harness?.teardown();
await rm(rootDir, { recursive: true, force: true });
await rm(globalDir, { recursive: true, force: true });
}); });
function inboxWorkflowIr(name: string): WorkflowIr { function inboxWorkflowIr(name: string): WorkflowIr {
@@ -132,7 +126,7 @@ describe("createTaskCreateTool intake-column wiring", () => {
expect((result as { isError?: boolean }).isError).toBeFalsy(); expect((result as { isError?: boolean }).isError).toBeFalsy();
const taskId = (result.details as { taskId: string }).taskId; const taskId = (result.details as { taskId: string }).taskId;
const prompt = await readFile(join(rootDir, ".fusion", "tasks", taskId, "PROMPT.md"), "utf-8"); const prompt = await readFile(join(harness.rootDir, ".fusion", "tasks", taskId, "PROMPT.md"), "utf-8");
expect(prompt).toBe(`# ${taskId}\n\nInbox bootstrap prompt task\n`); expect(prompt).toBe(`# ${taskId}\n\nInbox bootstrap prompt task\n`);
}); });
}); });

View File

@@ -1,7 +1,4 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { import {
createWorkflowAuthoringTools, createWorkflowAuthoringTools,
createWorkflowListTool, createWorkflowListTool,
@@ -13,7 +10,8 @@ import {
createWorkflowDeleteTool, createWorkflowDeleteTool,
} from "../index.js"; } from "../index.js";
import { createWorkflowSettingsTool } from "../agent-tools.js"; import { createWorkflowSettingsTool } from "../agent-tools.js";
import { parseWorkflowIr, TaskStore } from "@fusion/core"; import { parseWorkflowIr, type TaskStore } from "@fusion/core";
import { createTaskStoreForTest, pgDescribe } from "../../../core/src/__test-utils__/pg-test-harness.js";
/** /**
* U11 / R12 drift guard (engine half): the workflow-authoring tool surface that * U11 / R12 drift guard (engine half): the workflow-authoring tool surface that
@@ -69,7 +67,7 @@ describe("workflow tool exposure (engine factories)", () => {
* prompt-injectable agent lane. The executor lane omits the option (project- * prompt-injectable agent lane. The executor lane omits the option (project-
* owner escape hatch) and the flags pass through unchanged. * owner escape hatch) and the flags pass through unchanged.
*/ */
describe("fn_workflow_validate dry-run", () => { pgDescribe("fn_workflow_validate dry-run", () => {
const run = (tool: { execute: (...a: any[]) => Promise<any> }, params: unknown) => const run = (tool: { execute: (...a: any[]) => Promise<any> }, params: unknown) =>
tool.execute("call-1", params, undefined, undefined, undefined) as Promise<{ tool.execute("call-1", params, undefined, undefined, undefined) as Promise<{
isError?: boolean; details: any; content: { type: string; text?: string }[]; isError?: boolean; details: any; content: { type: string; text?: string }[];
@@ -85,15 +83,13 @@ describe("fn_workflow_validate dry-run", () => {
edges: [{ from: "start", to: "end", condition: "success" as const }], edges: [{ from: "start", to: "end", condition: "success" as const }],
}); });
/* FNXC:PgMigrationQuarantine 2026-07-17-18:15: validation must use the production PostgreSQL store boundary rather than removed SQLite constructor paths. */
async function withStore<T>(fn: (store: TaskStore) => Promise<T>): Promise<T> { async function withStore<T>(fn: (store: TaskStore) => Promise<T>): Promise<T> {
const dir = await mkdtemp(join(tmpdir(), "workflow-validate-tool-")); const harness = await createTaskStoreForTest({ prefix: "fusion_workflow_validate" });
const store = new TaskStore(dir);
await store.init();
try { try {
return await fn(store); return await fn(harness.store);
} finally { } finally {
await store.close?.().catch(() => {}); await harness.teardown();
await rm(dir, { recursive: true, force: true });
} }
} }

View File

@@ -1,12 +1,11 @@
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest"; import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
import { mkdtempSync, mkdirSync } from "node:fs"; import { mkdirSync } from "node:fs";
import { rm } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { tmpdir } from "node:os";
import "./executor-test-helpers.js"; import "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js"; import { TaskExecutor } from "../executor.js";
import * as worktreePool from "../worktree-pool.js"; import * as worktreePool from "../worktree-pool.js";
import { TaskStore } from "@fusion/core"; import { type TaskStore } from "@fusion/core";
import { createTaskStoreForTest, pgDescribe, type PgTestHarness } from "../../../core/src/__test-utils__/pg-test-harness.js";
import { createMockStore, mockedCreateFnAgent, mockedExec, mockedExecSync, resetExecutorMocks } from "./executor-test-helpers.js"; import { createMockStore, mockedCreateFnAgent, mockedExec, mockedExecSync, resetExecutorMocks } from "./executor-test-helpers.js";
const fn416Prompt = `# Task: FN-416 - Assign ready implementation task to active owner const fn416Prompt = `# Task: FN-416 - Assign ready implementation task to active owner
@@ -766,23 +765,23 @@ Assign or route exactly one ready implementation task to an eligible active owne
}); });
}); });
describe("FN-5241 executor handoff auditing", () => { /* FNXC:PgMigrationQuarantine 2026-07-18-01:20: FN-8258 runs handoff-audit invariants against the PostgreSQL TaskStore and its async audit boundary, replacing the removed SQLite constructor. */
pgDescribe("FN-5241 executor handoff auditing", () => {
let rootDir: string; let rootDir: string;
let globalDir: string; let harness: PgTestHarness;
let store: TaskStore; let store: TaskStore;
beforeEach(async () => { beforeEach(async () => {
rootDir = mkdtempSync(join(tmpdir(), "fn-5241-executor-")); // Reset the executor subprocess mock before PG setup so the harness can pass psql through.
globalDir = join(rootDir, ".fusion-global");
store = new TaskStore(rootDir, globalDir);
await store.init();
resetExecutorMocks(); resetExecutorMocks();
harness = await createTaskStoreForTest({ prefix: "fusion_executor_handoff_audit" });
rootDir = harness.rootDir;
store = harness.store;
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true); vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
}); });
afterEach(async () => { afterEach(async () => {
store.close(); await harness?.teardown();
await rm(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
}); });
async function createExecutorTask(taskDoneRetryCount = 0) { async function createExecutorTask(taskDoneRetryCount = 0) {
@@ -869,11 +868,11 @@ describe("FN-5241 executor handoff auditing", () => {
// Forensic intent preserved via the mechanism that actually fires now: the merge boundary records a // Forensic intent preserved via the mechanism that actually fires now: the merge boundary records a
// task:move into in-review (the review-seam task:handoff("workflow-graph-review") event and the // task:move into in-review (the review-seam task:handoff("workflow-graph-review") event and the
// review-seam merge-queue enqueue no longer occur on this path). // review-seam merge-queue enqueue no longer occur on this path).
const moveToReview = store const moveToReview = (await store
.getRunAuditEvents({ taskId: task.id, mutationType: "task:move", limit: 20 }) .getRunAuditEventsAsync({ taskId: task.id, mutationType: "task:move", limit: 20 }))
.find((event) => (event.metadata as { to?: string })?.to === "in-review"); .find((event) => (event.metadata as { to?: string })?.to === "in-review");
expect(moveToReview).toBeDefined(); expect(moveToReview).toBeDefined();
expect(store.getRunAuditEvents({ taskId: task.id, mutationType: "task:handoff", limit: 10 })).toHaveLength(0); expect(await store.getRunAuditEventsAsync({ taskId: task.id, mutationType: "task:handoff", limit: 10 })).toHaveLength(0);
}); });
it("fails a no-fn_task_done retry-budget-exhausted run in place without moving to in-review", async () => { it("fails a no-fn_task_done retry-budget-exhausted run in place without moving to in-review", async () => {
@@ -906,7 +905,7 @@ describe("FN-5241 executor handoff auditing", () => {
// FN-5241 review-seam handoff auditing (task:handoff "workflow-graph-review" / // FN-5241 review-seam handoff auditing (task:handoff "workflow-graph-review" /
// "max-task-done-retries-exhausted") + merge-queue enqueue are superseded — none of them fire here. // "max-task-done-retries-exhausted") + merge-queue enqueue are superseded — none of them fire here.
expect(latest?.column).not.toBe("in-review"); expect(latest?.column).not.toBe("in-review");
expect(store.getRunAuditEvents({ taskId: task.id, mutationType: "task:handoff", limit: 10 })).toHaveLength(0); expect(await store.getRunAuditEventsAsync({ taskId: task.id, mutationType: "task:handoff", limit: 10 })).toHaveLength(0);
expect(store.peekMergeQueue()).toEqual([]); expect(await store.peekMergeQueue()).toEqual([]);
}); });
}); });

View File

@@ -190,6 +190,7 @@ vi.mock("../worktree-stale-registration.js", async () => {
vi.mock("node:child_process", async () => { vi.mock("node:child_process", async () => {
const { promisify } = await import("node:util"); const { promisify } = await import("node:util");
const { EventEmitter } = await import("node:events"); const { EventEmitter } = await import("node:events");
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
const execSyncFn = vi.fn(); const execSyncFn = vi.fn();
const spawnFn = vi.fn((cmd: string, opts?: any) => { const spawnFn = vi.fn((cmd: string, opts?: any) => {
const child = new EventEmitter() as any; const child = new EventEmitter() as any;
@@ -220,6 +221,12 @@ vi.mock("node:child_process", async () => {
}); });
const execFn: any = vi.fn((cmd: string, opts: any, cb: any) => { const execFn: any = vi.fn((cmd: string, opts: any, cb: any) => {
/*
FNXC:PgMigrationQuarantine 2026-07-18-01:30:
FN-8258's PG-backed executor audit suite shares this helper. Let its harness run
psql for isolated fixture DDL while retaining mocked executor subprocess commands.
*/
if (/^psql\s/.test(cmd.trim())) return actual.exec(cmd, opts as any, cb as any);
const callback = typeof opts === "function" ? opts : cb; const callback = typeof opts === "function" ? opts : cb;
const forwardedOpts = typeof opts === "function" ? undefined : opts; const forwardedOpts = typeof opts === "function" ? undefined : opts;
try { try {

View File

@@ -5,7 +5,8 @@ import { join } from "node:path";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { describe, expect, it, afterEach, beforeEach } from "vitest"; import { describe, expect, it, afterEach, beforeEach } from "vitest";
import { TaskStore } from "@fusion/core"; import { type TaskStore } from "@fusion/core";
import { createTaskStoreForTest, pgDescribe, type PgTestHarness } from "../../../core/src/__test-utils__/pg-test-harness.js";
import { import {
evaluateBranchGroupCompletion, evaluateBranchGroupCompletion,
evaluateBranchGroupPromotion, evaluateBranchGroupPromotion,
@@ -392,33 +393,32 @@ describe("promoteBranchGroup", () => {
* `promoteBranchGroup` exercises the actual `listTasksByBranchGroup` membership scan that * `promoteBranchGroup` exercises the actual `listTasksByBranchGroup` membership scan that
* previously silently dropped an archived-but-unlanded member from `total`. * previously silently dropped an archived-but-unlanded member from `total`.
*/ */
describe("promoteBranchGroup with a real TaskStore (FN-7534 archived-member regression)", () => { /*
function makeTmpDir(): string { FNXC:PgMigrationQuarantine 2026-07-18-01:50:
return mkdtempSync(join(tmpdir(), "fusion-branch-group-archive-")); FN-8258 exercises archived shared-branch members through the PostgreSQL TaskStore, including
} awaited branch-group writes, instead of the removed SQLite constructor path.
*/
pgDescribe("promoteBranchGroup with a real TaskStore (FN-7534 archived-member regression)", () => {
let rootDir: string; let rootDir: string;
let storeRootDir: string; let harness: PgTestHarness;
let globalDir: string;
let store: TaskStore; let store: TaskStore;
beforeEach(async () => { beforeEach(async () => {
rootDir = makeRepo(); rootDir = makeRepo();
storeRootDir = makeTmpDir(); harness = await createTaskStoreForTest({ prefix: "fusion_branch_group_archive" });
globalDir = join(storeRootDir, ".fusion-global"); store = harness.store;
store = new TaskStore(storeRootDir, globalDir);
await store.init();
}); });
afterEach(() => { afterEach(async () => {
store.close(); await harness?.teardown();
}); });
it("returns reason: incomplete when an archived member never landed onto the group branch", async () => { it("returns reason: incomplete when an archived member never landed onto the group branch", async () => {
const group = store.createBranchGroup({ const group = await store.createBranchGroup({
sourceType: "planning", sourceType: "planning",
sourceId: "PS-archived-gate", sourceId: "PS-archived-gate",
branchName: "fusion/groups/archived-gate", branchName: "fusion/groups/archived-gate",
autoMerge: true,
}); });
const landedTask = await store.createTask({ description: "landed member" }); const landedTask = await store.createTask({ description: "landed member" });
@@ -447,7 +447,7 @@ describe("promoteBranchGroup with a real TaskStore (FN-7534 archived-member regr
}); });
it("still promotes when the only archived member had already landed before archival", async () => { it("still promotes when the only archived member had already landed before archival", async () => {
const group = store.createBranchGroup({ const group = await store.createBranchGroup({
sourceType: "planning", sourceType: "planning",
sourceId: "PS-archived-landed-gate", sourceId: "PS-archived-landed-gate",
branchName: "fusion/groups/archived-landed-gate", branchName: "fusion/groups/archived-landed-gate",
@@ -472,6 +472,17 @@ describe("promoteBranchGroup with a real TaskStore (FN-7534 archived-member regr
await store.moveTask(task.id, "done"); await store.moveTask(task.id, "done");
await store.archiveTask(task.id); await store.archiveTask(task.id);
// The completion invariant includes archived members and their persisted landing proof.
expect(await store.listTasksByBranchGroup(group.id)).toEqual([
expect.objectContaining({
id: task.id,
mergeDetails: expect.objectContaining({
mergeConfirmed: true,
mergeTargetBranch: group.branchName,
}),
}),
]);
const result = await promoteBranchGroup({ const result = await promoteBranchGroup({
rootDir, rootDir,
groupId: group.id, groupId: group.id,

View File

@@ -128,6 +128,11 @@ describe("HybridExecutor multi-node routing", () => {
await central.updateProject(projectA.id, { status: "active" }); await central.updateProject(projectA.id, { status: "active" });
await central.updateProject(projectB.id, { status: "initializing" }); await central.updateProject(projectB.id, { status: "initializing" });
// FNXC:PgMigrationQuarantine 2026-07-18-01:45: central node rows persist across test
// instances, so isolate the local-only gate input from a previous test's remote registration.
vi.spyOn(central, "listNodes").mockResolvedValueOnce([{ id: "local-only", type: "local" }] as any);
vi.spyOn(central, "listProjects").mockResolvedValue([projectA, projectB] as any);
// Gate intentionally OFF: ProjectEngineManager handles local // Gate intentionally OFF: ProjectEngineManager handles local
// multi-project; running HybridExecutor here duplicates InProcessRuntime // multi-project; running HybridExecutor here duplicates InProcessRuntime
// creation and adds ~7s to cold start. // creation and adds ~7s to cold start.

View File

@@ -604,7 +604,7 @@ describe("MissionFactory Parity: Engine Scheduler/Autopilot", () => {
missionStore.addMission(mission); missionStore.addMission(mission);
await autopilot.watchMission(mission.id); await autopilot.watchMission(mission.id);
autopilot.unwatchMission(mission.id); await autopilot.unwatchMission(mission.id);
expect(missionStore.updateMission).toHaveBeenCalledWith("M-TEST1", { expect(missionStore.updateMission).toHaveBeenCalledWith("M-TEST1", {
autopilotState: "inactive", autopilotState: "inactive",

View File

@@ -13,22 +13,28 @@ import { mkdtempSync, writeFileSync } from "node:fs";
import { mkdir, rm } from "node:fs/promises"; import { mkdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { spawnSync } from "node:child_process"; const backupCommandState = vi.hoisted(() => ({
result: { success: true, output: "Backup created" },
error: null as Error | null,
}));
/** vi.mock("@fusion/core", async (importOriginal) => {
* Write a real SQLite database file so the production backup path's `PRAGMA quick_check` const actual = await importOriginal<typeof import("@fusion/core")>();
* verification passes (mirrors packages/core/src/__tests__/backup.test.ts's fixture helper). return {
* Falls back to a placeholder file when the `sqlite3` CLI is unavailable — in that case ...actual,
* verification also no-ops so the backup still succeeds. runBackupCommand: vi.fn(async () => {
*/ if (backupCommandState.error) throw backupCommandState.error;
function writeTestDb(path: string): void { return backupCommandState.result;
const result = spawnSync("sqlite3", [path, "CREATE TABLE IF NOT EXISTS t(x); INSERT INTO t VALUES (1);"], { }),
encoding: "utf-8", };
}); });
if (result.error || result.status !== 0) {
writeFileSync(path, "dummy database content"); /*
} FNXC:PgMigrationQuarantine 2026-07-18-01:40:
} FN-8258 keeps RoutineRunner's cron/manual delegation assertions while replacing the removed
SQLite-file fixture with the async PostgreSQL backup command boundary. Backup implementation
is covered in core; this suite verifies routine dispatch, result persistence, and live output.
*/
// Default settings inline to avoid @fusion/core build dependency during tests // Default settings inline to avoid @fusion/core build dependency during tests
const DEFAULT_SETTINGS: Settings = { const DEFAULT_SETTINGS: Settings = {
@@ -134,6 +140,8 @@ function createMockAgentStore(): AgentStore {
function createMockTaskStore(overrides: { fusionDir?: string; settings?: Partial<Settings> } = {}): TaskStore { function createMockTaskStore(overrides: { fusionDir?: string; settings?: Partial<Settings> } = {}): TaskStore {
return { return {
getFusionDir: vi.fn().mockReturnValue(overrides.fusionDir ?? "/tmp/.fusion"), getFusionDir: vi.fn().mockReturnValue(overrides.fusionDir ?? "/tmp/.fusion"),
// FNXC:PgMigrationQuarantine 2026-07-18-01:35: backup routines resolve PostgreSQL settings through this async-era TaskStore accessor, so the fake must mirror the production contract.
getGlobalSettingsDir: vi.fn().mockReturnValue(overrides.fusionDir ?? "/tmp/.fusion"),
getSettings: vi.fn().mockResolvedValue({ ...DEFAULT_SETTINGS, ...overrides.settings }), getSettings: vi.fn().mockResolvedValue({ ...DEFAULT_SETTINGS, ...overrides.settings }),
on: vi.fn(), on: vi.fn(),
off: vi.fn(), off: vi.fn(),
@@ -167,6 +175,11 @@ function createRoutineRunner(options?: Partial<RoutineRunnerOptions>): RoutineRu
} }
describe("RoutineRunner", () => { describe("RoutineRunner", () => {
beforeEach(() => {
backupCommandState.result = { success: true, output: "Backup created" };
backupCommandState.error = null;
});
describe("executeRoutine", () => { describe("executeRoutine", () => {
it("successfully executes a routine with trigger type 'cron'", async () => { it("successfully executes a routine with trigger type 'cron'", async () => {
const routine = createMockRoutine({ id: "routine-1", name: "Test Routine" }); const routine = createMockRoutine({ id: "routine-1", name: "Test Routine" });
@@ -273,13 +286,14 @@ describe("RoutineRunner", () => {
routineStore, routineStore,
taskStore: createMockTaskStore({ fusionDir }), taskStore: createMockTaskStore({ fusionDir }),
}); });
backupCommandState.error = new Error("backup command unavailable");
try { try {
const result = await runner.executeRoutine("routine-backup-missing-db", "cron"); const result = await runner.executeRoutine("routine-backup-missing-db", "cron");
expect(result.success).toBe(false); expect(result.success).toBe(false);
expect(result.error).toContain("project DB"); expect(result.error).toContain("project PostgreSQL");
expect(result.error).toContain(`source: ${join(fusionDir, "fusion.db")}`); expect(result.error).toContain(`project state: ${fusionDir}`);
expect(result.error).toContain("cause:"); expect(result.error).toContain("cause:");
expect(result.error).not.toBe(""); expect(result.error).not.toBe("");
expect(routineStore.completeRoutineExecution).toHaveBeenCalledWith( expect(routineStore.completeRoutineExecution).toHaveBeenCalledWith(
@@ -287,7 +301,7 @@ describe("RoutineRunner", () => {
expect.objectContaining({ expect.objectContaining({
success: false, success: false,
error: result.error, error: result.error,
output: expect.stringContaining("project DB"), output: "",
}), }),
); );
} finally { } finally {
@@ -446,7 +460,7 @@ describe("RoutineRunner", () => {
const tempDir = mkdtempSync(join(tmpdir(), "routine-backup-parity-")); const tempDir = mkdtempSync(join(tmpdir(), "routine-backup-parity-"));
const fusionDir = join(tempDir, ".fusion"); const fusionDir = join(tempDir, ".fusion");
await mkdir(fusionDir, { recursive: true }); await mkdir(fusionDir, { recursive: true });
writeTestDb(join(fusionDir, "fusion.db")); writeFileSync(join(fusionDir, "postgres-backed-fixture"), "routine backup dispatch fixture");
try { try {
for (const triggerType of ["cron", "api"] as const) { for (const triggerType of ["cron", "api"] as const) {
@@ -477,7 +491,7 @@ describe("RoutineRunner", () => {
const tempDir = mkdtempSync(join(tmpdir(), "routine-backup-live-")); const tempDir = mkdtempSync(join(tmpdir(), "routine-backup-live-"));
const fusionDir = join(tempDir, ".fusion"); const fusionDir = join(tempDir, ".fusion");
await mkdir(fusionDir, { recursive: true }); await mkdir(fusionDir, { recursive: true });
writeTestDb(join(fusionDir, "fusion.db")); writeFileSync(join(fusionDir, "postgres-backed-fixture"), "routine backup dispatch fixture");
try { try {
const routine = createMockRoutine({ const routine = createMockRoutine({

View File

@@ -51,7 +51,7 @@ const canRun = hasGit && hasPg;
const archived = await fixture.selfHeal.autoArchiveResolvedMetaTasks(); const archived = await fixture.selfHeal.autoArchiveResolvedMetaTasks();
expect(archived).toBe(0); expect(archived).toBe(0);
expect((await fixture.store.getTask(meta.id))?.column).not.toBe("archived"); expect((await fixture.store.getTask(meta.id))?.column).not.toBe("archived");
const events = fixture.store.getRunAuditEvents({ limit: 200 }).filter((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped"); const events = (await fixture.store.getRunAuditEventsAsync({ limit: 200 })).filter((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped");
expect(events).toHaveLength(1); expect(events).toHaveLength(1);
expect((events[0]?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["branch-has-unique-commits"])); expect((events[0]?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["branch-has-unique-commits"]));
} finally { } finally {
@@ -65,7 +65,7 @@ const canRun = hasGit && hasPg;
try { try {
const archived = await fixture.selfHeal.autoArchiveResolvedMetaTasks(); const archived = await fixture.selfHeal.autoArchiveResolvedMetaTasks();
expect(archived).toBe(0); expect(archived).toBe(0);
const event = fixture.store.getRunAuditEvents({ limit: 200 }).find((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped"); const event = (await fixture.store.getRunAuditEventsAsync({ limit: 200 })).find((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped");
expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["recent-executor-activity"])); expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["recent-executor-activity"]));
} finally { } finally {
await fixture.cleanup(); await fixture.cleanup();
@@ -77,7 +77,7 @@ const canRun = hasGit && hasPg;
await fixture.store.updateTask(meta.id, { taskDoneRetryCount: 2 } as any); await fixture.store.updateTask(meta.id, { taskDoneRetryCount: 2 } as any);
try { try {
await fixture.selfHeal.autoArchiveResolvedMetaTasks(); await fixture.selfHeal.autoArchiveResolvedMetaTasks();
const event = fixture.store.getRunAuditEvents({ limit: 200 }).find((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped"); const event = (await fixture.store.getRunAuditEventsAsync({ limit: 200 })).find((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped");
expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["task-done-retry-pending"])); expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["task-done-retry-pending"]));
} finally { } finally {
await fixture.cleanup(); await fixture.cleanup();
@@ -90,13 +90,13 @@ const canRun = hasGit && hasPg;
try { try {
await fixture.selfHeal.autoArchiveResolvedMetaTasks(); await fixture.selfHeal.autoArchiveResolvedMetaTasks();
await fixture.selfHeal.autoArchiveResolvedMetaTasks(); await fixture.selfHeal.autoArchiveResolvedMetaTasks();
let events = fixture.store.getRunAuditEvents({ limit: 200 }).filter((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped"); let events = (await fixture.store.getRunAuditEventsAsync({ limit: 200 })).filter((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped");
expect(events).toHaveLength(1); expect(events).toHaveLength(1);
expect((events[0]?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["task-done-retry-pending"])); expect((events[0]?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["task-done-retry-pending"]));
await fixture.store.updateTask(meta.id, { taskDoneRetryCount: 0, status: "merging" } as any); await fixture.store.updateTask(meta.id, { taskDoneRetryCount: 0, status: "merging" } as any);
await fixture.selfHeal.autoArchiveResolvedMetaTasks(); await fixture.selfHeal.autoArchiveResolvedMetaTasks();
events = fixture.store.getRunAuditEvents({ limit: 200 }).filter((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped"); events = (await fixture.store.getRunAuditEventsAsync({ limit: 200 })).filter((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped");
expect(events).toHaveLength(2); expect(events).toHaveLength(2);
expect(events.some((event) => (event.metadata as any)?.blockedBy?.includes("merge-in-progress"))).toBe(true); expect(events.some((event) => (event.metadata as any)?.blockedBy?.includes("merge-in-progress"))).toBe(true);
} finally { } finally {
@@ -113,7 +113,7 @@ const canRun = hasGit && hasPg;
await fixture.store.updateTask(meta.id, updates as any); await fixture.store.updateTask(meta.id, updates as any);
try { try {
await fixture.selfHeal.autoArchiveResolvedMetaTasks(); await fixture.selfHeal.autoArchiveResolvedMetaTasks();
const event = fixture.store.getRunAuditEvents({ limit: 200 }).find((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped"); const event = (await fixture.store.getRunAuditEventsAsync({ limit: 200 })).find((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped");
expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["merge-in-progress"])); expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["merge-in-progress"]));
} finally { } finally {
await fixture.cleanup(); await fixture.cleanup();
@@ -128,7 +128,7 @@ const canRun = hasGit && hasPg;
activeSessionRegistry.registerPath(activePath, { taskId: meta.id, kind: "executor", ownerKey: meta.id }); activeSessionRegistry.registerPath(activePath, { taskId: meta.id, kind: "executor", ownerKey: meta.id });
try { try {
await fixture.selfHeal.autoArchiveResolvedMetaTasks(); await fixture.selfHeal.autoArchiveResolvedMetaTasks();
const event = fixture.store.getRunAuditEvents({ limit: 200 }).find((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped"); const event = (await fixture.store.getRunAuditEventsAsync({ limit: 200 })).find((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped");
expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["active-session"])); expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["active-session"]));
} finally { } finally {
activeSessionRegistry.unregisterPath(activePath); activeSessionRegistry.unregisterPath(activePath);
@@ -141,7 +141,7 @@ const canRun = hasGit && hasPg;
await fixture.store.updateTask(meta.id, { taskDoneRetryCount: 1, status: "merging" } as any); await fixture.store.updateTask(meta.id, { taskDoneRetryCount: 1, status: "merging" } as any);
try { try {
await fixture.selfHeal.autoArchiveResolvedMetaTasks(); await fixture.selfHeal.autoArchiveResolvedMetaTasks();
const event = fixture.store.getRunAuditEvents({ limit: 200 }).find((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped"); const event = (await fixture.store.getRunAuditEventsAsync({ limit: 200 })).find((e) => e.mutationType === "task:auto-archive-meta-resolved-skipped");
expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["task-done-retry-pending", "merge-in-progress"])); expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["task-done-retry-pending", "merge-in-progress"]));
} finally { } finally {
await fixture.cleanup(); await fixture.cleanup();
@@ -154,7 +154,7 @@ const canRun = hasGit && hasPg;
const archived = await fixture.selfHeal.autoArchiveResolvedMetaTasks(); const archived = await fixture.selfHeal.autoArchiveResolvedMetaTasks();
expect(archived).toBe(1); expect(archived).toBe(1);
expect((await fixture.store.getTask(meta.id))?.column).toBe("archived"); expect((await fixture.store.getTask(meta.id))?.column).toBe("archived");
const audits = fixture.store.getRunAuditEvents({ limit: 200 }); const audits = await fixture.store.getRunAuditEventsAsync({ limit: 200 });
expect(audits.some((event) => event.mutationType === "task:auto-archived-meta-resolved")).toBe(true); expect(audits.some((event) => event.mutationType === "task:auto-archived-meta-resolved")).toBe(true);
expect(audits.some((event) => event.mutationType === "task:auto-archive-meta-resolved-skipped")).toBe(false); expect(audits.some((event) => event.mutationType === "task:auto-archive-meta-resolved-skipped")).toBe(false);
} finally { } finally {
@@ -163,7 +163,7 @@ const canRun = hasGit && hasPg;
}); });
it("emits stalled skipped event when guards block stalled archive", async () => { it("emits stalled skipped event when guards block stalled archive", async () => {
vi.useFakeTimers(); // FNXC:PgMigrationQuarantine 2026-07-17-18:30: pin Date for stale-archive thresholds while retaining real timers so PostgreSQL fixture I/O cannot deadlock.
const now = new Date("2026-05-18T12:00:00.000Z"); const now = new Date("2026-05-18T12:00:00.000Z");
vi.setSystemTime(now); vi.setSystemTime(now);
const { fixture, meta } = await createResolvedMetaPair({ metaTaskStallAutoCloseMs: 60_000 }); const { fixture, meta } = await createResolvedMetaPair({ metaTaskStallAutoCloseMs: 60_000 });
@@ -172,7 +172,7 @@ const canRun = hasGit && hasPg;
try { try {
const archived = await fixture.selfHeal.autoArchiveStalledMetaTasks(); const archived = await fixture.selfHeal.autoArchiveStalledMetaTasks();
expect(archived).toBe(0); expect(archived).toBe(0);
const event = fixture.store.getRunAuditEvents({ limit: 200 }).find((e) => e.mutationType === "task:auto-archive-meta-stalled-skipped"); const event = (await fixture.store.getRunAuditEventsAsync({ limit: 200 })).find((e) => e.mutationType === "task:auto-archive-meta-stalled-skipped");
expect(event).toBeTruthy(); expect(event).toBeTruthy();
expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["task-done-retry-pending"])); expect((event?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["task-done-retry-pending"]));
} finally { } finally {
@@ -182,7 +182,7 @@ const canRun = hasGit && hasPg;
}); });
it("dedupes stalled skipped audits until the guard reason changes", async () => { it("dedupes stalled skipped audits until the guard reason changes", async () => {
vi.useFakeTimers(); // FNXC:PgMigrationQuarantine 2026-07-17-18:30: pin Date for stale-archive thresholds while retaining real timers so PostgreSQL fixture I/O cannot deadlock.
const now = new Date("2026-05-18T12:00:00.000Z"); const now = new Date("2026-05-18T12:00:00.000Z");
vi.setSystemTime(now); vi.setSystemTime(now);
const { fixture, meta } = await createResolvedMetaPair({ metaTaskStallAutoCloseMs: 60_000 }); const { fixture, meta } = await createResolvedMetaPair({ metaTaskStallAutoCloseMs: 60_000 });
@@ -191,13 +191,13 @@ const canRun = hasGit && hasPg;
try { try {
await fixture.selfHeal.autoArchiveStalledMetaTasks(); await fixture.selfHeal.autoArchiveStalledMetaTasks();
await fixture.selfHeal.autoArchiveStalledMetaTasks(); await fixture.selfHeal.autoArchiveStalledMetaTasks();
let events = fixture.store.getRunAuditEvents({ limit: 200 }).filter((e) => e.mutationType === "task:auto-archive-meta-stalled-skipped"); let events = (await fixture.store.getRunAuditEventsAsync({ limit: 200 })).filter((e) => e.mutationType === "task:auto-archive-meta-stalled-skipped");
expect(events).toHaveLength(1); expect(events).toHaveLength(1);
expect((events[0]?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["task-done-retry-pending"])); expect((events[0]?.metadata as any)?.blockedBy).toEqual(expect.arrayContaining(["task-done-retry-pending"]));
await fixture.store.updateTask(meta.id, { taskDoneRetryCount: 0, status: "merging" } as any); await fixture.store.updateTask(meta.id, { taskDoneRetryCount: 0, status: "merging" } as any);
await fixture.selfHeal.autoArchiveStalledMetaTasks(); await fixture.selfHeal.autoArchiveStalledMetaTasks();
events = fixture.store.getRunAuditEvents({ limit: 200 }).filter((e) => e.mutationType === "task:auto-archive-meta-stalled-skipped"); events = (await fixture.store.getRunAuditEventsAsync({ limit: 200 })).filter((e) => e.mutationType === "task:auto-archive-meta-stalled-skipped");
expect(events).toHaveLength(2); expect(events).toHaveLength(2);
expect(events.some((event) => (event.metadata as any)?.blockedBy?.includes("merge-in-progress"))).toBe(true); expect(events.some((event) => (event.metadata as any)?.blockedBy?.includes("merge-in-progress"))).toBe(true);
} finally { } finally {

View File

@@ -1,10 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { mkdtempSync } from "node:fs";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import type { AgentSession } from "@earendil-works/pi-coding-agent"; import type { AgentSession } from "@earendil-works/pi-coding-agent";
import { aggregateTokenAnalytics, Database, type Task, type TaskStore } from "@fusion/core"; import { aggregateTokenAnalytics, type Task, type TaskStore } from "@fusion/core";
import { createTaskStoreForTest, pgDescribe, type PgTestHarness } from "../../../core/src/__test-utils__/pg-test-harness.js";
import { TriageProcessor } from "../triage.js"; import { TriageProcessor } from "../triage.js";
interface MockSessionStats { interface MockSessionStats {
@@ -59,34 +56,9 @@ async function flushAsyncRecorders(): Promise<void> {
await new Promise<void>((resolve) => setImmediate(resolve)); await new Promise<void>((resolve) => setImmediate(resolve));
} }
function insertUsageTask(db: Database, task: Task): void { /* FNXC:PgMigrationQuarantine 2026-07-17-18:25: FN-8258 proves token analytics through AsyncDataLayer project-scoped rows, replacing removed raw SQLite Database seeding. */
const usage = task.tokenUsage; pgDescribe("triage session token usage recording", () => {
if (!usage) throw new Error("expected token usage"); let harness: PgTestHarness;
db.prepare(
`INSERT INTO tasks
(id, description, "column", createdAt, updatedAt,
tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageLastUsedAt,
modelProvider, modelId, tokenUsageModelProvider, tokenUsageModelId, tokenUsagePerModel)
VALUES (?, 'desc', 'todo', '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z',
?, ?, ?, ?, ?, ?, null, null, ?, ?, ?)`,
).run(
task.id,
usage.inputTokens,
usage.outputTokens,
usage.cachedTokens,
usage.cacheWriteTokens,
usage.totalTokens,
usage.lastUsedAt,
usage.modelProvider ?? null,
usage.modelId ?? null,
JSON.stringify(usage.perModel ?? []),
);
}
describe("triage session token usage recording", () => {
let tmpDir: string | undefined;
let db: Database | undefined;
beforeEach(() => { beforeEach(() => {
vi.useRealTimers(); vi.useRealTimers();
@@ -94,12 +66,7 @@ describe("triage session token usage recording", () => {
}); });
afterEach(async () => { afterEach(async () => {
db?.close(); await harness?.teardown();
if (tmpDir) {
await rm(tmpDir, { recursive: true, force: true });
}
tmpDir = undefined;
db = undefined;
}); });
it("records a triage-only Anthropic planning model and surfaces it in by-model analytics", async () => { it("records a triage-only Anthropic planning model and surfaces it in by-model analytics", async () => {
@@ -136,18 +103,17 @@ describe("triage session token usage recording", () => {
}), }),
]); ]);
tmpDir = mkdtempSync(join(tmpdir(), "kb-triage-token-analytics-")); harness = await createTaskStoreForTest({ prefix: "fusion_triage_token_usage" });
db = new Database(join(tmpDir, ".fusion")); const persisted = await harness.store.createTask({ description: "triage analytics" });
db.init(); await harness.store.updateTask(persisted.id, { tokenUsage: store._task.tokenUsage });
insertUsageTask(db, store._task);
const byModel = await aggregateTokenAnalytics(db, { groupBy: "model" }); const byModel = await aggregateTokenAnalytics(harness.layer, { groupBy: "model" });
expect(byModel.totals).toMatchObject({ totalTokens: 165, nTasks: 1 }); expect(byModel.totals).toMatchObject({ totalTokens: 165, nTasks: 1 });
expect(byModel.groups).toEqual([ expect(byModel.groups).toEqual([
expect.objectContaining({ key: "claude-sonnet-4-5", totalTokens: 165, nTasks: 1 }), expect.objectContaining({ key: "claude-sonnet-4-5", totalTokens: 165, nTasks: 1 }),
]); ]);
const byProvider = await aggregateTokenAnalytics(db, { groupBy: "provider" }); const byProvider = await aggregateTokenAnalytics(harness.layer, { groupBy: "provider" });
expect(byProvider.groups).toEqual([ expect(byProvider.groups).toEqual([
expect.objectContaining({ key: "anthropic", totalTokens: 165, nTasks: 1 }), expect.objectContaining({ key: "anthropic", totalTokens: 165, nTasks: 1 }),
]); ]);

View File

@@ -1,10 +1,7 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { EventEmitter } from "node:events"; import { EventEmitter } from "node:events";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Settings, TaskDetail, WorkflowDefinition, WorkflowIr } from "@fusion/core"; import type { Settings, TaskDetail, WorkflowDefinition, WorkflowIr } from "@fusion/core";
import { TaskStore } from "@fusion/core"; import { createTaskStoreForTest, PG_AVAILABLE } from "../../../core/src/__test-utils__/pg-test-harness.js";
import { NotificationService } from "../notification/notification-service.js"; import { NotificationService } from "../notification/notification-service.js";
import { WorkflowGraphTaskRunner, type WorkflowGraphRunnerStore } from "../workflow-graph-task-runner.js"; import { WorkflowGraphTaskRunner, type WorkflowGraphRunnerStore } from "../workflow-graph-task-runner.js";
@@ -268,10 +265,10 @@ describe("WorkflowGraphTaskRunner (CU-U2)", () => {
expect(result.reason).toMatch(/workflow-missing/); expect(result.reason).toMatch(/workflow-missing/);
}); });
it("persists a valid workflow through the store and launches it through the graph runner", async () => { // FNXC:PgMigrationQuarantine 2026-07-17-18:15: FN-8258 exercises persisted workflow selection against the PostgreSQL AsyncDataLayer rather than removed SQLite inMemoryDb construction.
const rootDir = mkdtempSync(join(tmpdir(), "fn-7113-workflow-run-")); (PG_AVAILABLE ? it : it.skip)("persists a valid workflow through the store and launches it through the graph runner", async () => {
const globalDir = mkdtempSync(join(tmpdir(), "fn-7113-workflow-global-")); const harness = await createTaskStoreForTest({ prefix: "fusion_workflow_graph_runner" });
const store = new TaskStore(rootDir, globalDir, { inMemoryDb: true }); const store = harness.store;
try { try {
const invalidIr: WorkflowIr = { const invalidIr: WorkflowIr = {
version: "v2", version: "v2",
@@ -313,8 +310,7 @@ describe("WorkflowGraphTaskRunner (CU-U2)", () => {
expect(result.disposition).toBe("completed"); expect(result.disposition).toBe("completed");
expect(calls).toEqual(["custom:lint", "execute", "review", "merge", "custom:notify"]); expect(calls).toEqual(["custom:lint", "execute", "review", "merge", "custom:notify"]);
} finally { } finally {
rmSync(rootDir, { recursive: true, force: true }); await harness.teardown();
rmSync(globalDir, { recursive: true, force: true });
} }
}); });

View File

@@ -25,7 +25,13 @@ export async function shouldUseHybridExecutor(centralCore: CentralCore): Promise
try { try {
const nodes = await centralCore.listNodes(); const nodes = await centralCore.listNodes();
if (nodes.length > 1) { /*
FNXC:HybridExecutorRouting 2026-07-18-01:35:
Local project registrations may create more than one local node record. HybridExecutor
only adds value when a remote node is routable, so node count must not enable duplicate
local runtimes for a local-only multi-project installation.
*/
if (nodes.some((node) => node.type === "remote")) {
return { enabled: true, reason: "multi-node" }; return { enabled: true, reason: "multi-node" };
} }

View File

@@ -295,7 +295,7 @@ export default defineConfig({
// SQLite-path gate test evicted + quarantined (see engine-core comment + ledger). // SQLite-path gate test evicted + quarantined (see engine-core comment + ledger).
"node_modules/**", "node_modules/**",
"dist/**", "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-18-02:10: FN-8258 rescued the ten remaining VAL-REMOVAL-005 holdouts through PostgreSQL harnesses and production-shaped async contracts; retain only unresolved paired quarantines.
// FNXC:PgMigrationQuarantine 2026-07-14-08:00: // 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. // 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 // VAL-REMOVAL-005 deleted the SQLite Database class. These engine-default files fail
@@ -303,19 +303,9 @@ export default defineConfig({
// getDatabase, walCheckpoint) that throw/return-empty in backend mode, or have mock // getDatabase, walCheckpoint) that throw/return-empty in backend mode, or have mock
// drift from the async-satellite cutover. Quarantined on sight per AGENTS.md. // drift from the async-satellite cutover. Quarantined on sight per AGENTS.md.
"src/__tests__/backlog-pressure-reporter.test.ts", "src/__tests__/backlog-pressure-reporter.test.ts",
"src/__tests__/mission-factory-parity.integration.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",
"src/__tests__/dependency-blocked-todo-reporter.test.ts", "src/__tests__/dependency-blocked-todo-reporter.test.ts",
"src/__tests__/executor-task-done-invariant.test.ts",
"src/__tests__/goal-injection-diagnostics-wiring.test.ts", "src/__tests__/goal-injection-diagnostics-wiring.test.ts",
"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__/merger-cwd-fallback-removed.test.ts",
"src/__tests__/routine-runner.test.ts",
"src/__tests__/self-healing-meta-archive-guards.test.ts",
"src/__tests__/triage-token-usage.test.ts",
/* /*
FNXC:EngineTests 2026-07-18-08:15: FNXC:EngineTests 2026-07-18-08:15:
heartbeat-error-recovery timed out at 30s on full-suite shard 1 heartbeat-error-recovery timed out at 30s on full-suite shard 1

View File

@@ -1,76 +1,26 @@
{ {
"$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.", "$comment": "Flaky-test quarantine ledger (deletion ratchet — see AGENTS.md 'Flaky tests: quarantine on sight' and docs/testing.md 'Quarantine ledger and the deletion ratchet'). A test observed failing without a corresponding real bug is quarantined ON SIGHT: add an entry here AND a matching one-line `exclude` entry in that package's vitest config, in the same commit. Every entry needs a non-empty `reason` (link the failing run) and a `quarantinedAt` date — the entry expires 14 days later, at which point the test file is DELETED unless someone rescues it with evidence it catches real regressions plus a root-cause fix (never appeasement). There is deliberately no loader module and no automation around this file: it is a dated record, the vitest config is the enforcement.",
"entries": [ "entries": [
{ {
"file": "packages/engine/src/__tests__/backlog-pressure-reporter.test.ts", "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.", "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" "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__/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.",
"quarantinedAt": "2026-07-14"
},
{
"file": "packages/engine/src/__tests__/agent-tools-intake-column.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__/agent-workflow-tools-exposure.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__/dependency-blocked-todo-reporter.test.ts", "file": "packages/engine/src/__tests__/dependency-blocked-todo-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.", "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" "quarantinedAt": "2026-07-14"
}, },
{
"file": "packages/engine/src/__tests__/executor-task-done-invariant.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__/goal-injection-diagnostics-wiring.test.ts", "file": "packages/engine/src/__tests__/goal-injection-diagnostics-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.", "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" "quarantinedAt": "2026-07-14"
}, },
{
"file": "packages/engine/src/__tests__/group-merge-coordinator.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__/hybrid-executor-multi-node-routing.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__/merger-cwd-fallback-removed.test.ts", "file": "packages/engine/src/__tests__/merger-cwd-fallback-removed.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.", "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" "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.",
"quarantinedAt": "2026-07-14"
},
{
"file": "packages/engine/src/__tests__/self-healing-meta-archive-guards.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__/triage-token-usage.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__/reliability-interactions/integration-worktree-state.test.ts", "file": "packages/engine/src/__tests__/reliability-interactions/integration-worktree-state.test.ts",
"reason": "VAL-REMOVAL-005 PG migration: reliability fixture uses PG-backed store but sync APIs (getRunAuditEvents, getDatabase) fail in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.", "reason": "VAL-REMOVAL-005 PG migration: reliability fixture uses PG-backed store but sync APIs (getRunAuditEvents, getDatabase) fail in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.",