FN-8044: restore dependency reconcile reliability tests

Restore dependency reconciliation coverage using a PostgreSQL raw-seeding seam.

- Add a cache-invalidating raw task-column seeding helper for corrupt fixture states.
- Update dependency-cycle and self-defeating reconciliation tests to use the PG seam.
- Remove restored suites from the reliability quarantine configuration and ledger.

Files changed:
 .../__tests__/reliability-interactions/_helpers.ts | 29 ++++++++
 .../dependency-cycle-reconcile.test.ts             | 86 +++++++++++-----------
 .../self-defeating-dep-reconcile.test.ts           |  7 +-
 packages/engine/vitest.config.ts                   |  7 +-
 scripts/lib/test-quarantine.json                   | 10 ---
 5 files changed, 82 insertions(+), 57 deletions(-)

Fusion-Task-Id: FN-8044

Fusion-Task-Lineage: 213f606c-1951-4d25-93b4-2ceb97460ace

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-16 07:14:01 -07:00
parent 6a1b53f9b3
commit 50ed689379
5 changed files with 82 additions and 57 deletions

View File

@@ -7,6 +7,7 @@ import {
AgentStore, DEFAULT_SETTINGS, TaskStore, type Settings, type Task,
type AsyncDataLayer, type CentralClaimStore, type ResolvedBackend,
createConnectionSetFromUrl, applySchemaBaseline, createAsyncDataLayer,
drizzleEq, postgresSchema,
} from "@fusion/core";
import { aiMergeTask } from "../../merger.js";
import { SelfHealingManager } from "../../self-healing.js";
@@ -235,6 +236,7 @@ export type ReliabilityFixture = {
createBranch: (branch: string) => Promise<void>;
checkout: (branch: string) => Promise<void>;
mergeTask: () => Promise<unknown>;
seedRawTaskColumns: (taskId: string, patch: Partial<Pick<Task, "dependencies" | "title" | "column">>) => Promise<void>;
selfHeal: {
recoverAlreadyMergedReviewTasks: () => Promise<number>;
recoverMisclassifiedFailures: () => Promise<number>;
@@ -321,6 +323,33 @@ export async function makeReliabilityFixture(input: {
git(rootDir, `git checkout ${branch}`);
},
mergeTask: async () => aiMergeTask(store, rootDir, task.id),
/*
FNXC:PgReliabilitySeeding 2026-07-16-04:48:
Reconcile tests must seed intentionally corrupt dependency/title rows that write-time guards reject.
Raw PostgreSQL updates bypass those guards, then clear both read-through snapshots: otherwise
startupSlimListMemo or taskCache can hide the corruption and reconcilers return zero. Require
exactly one persisted row so a bad fixture ID cannot make a negative-path test pass vacuously.
*/
seedRawTaskColumns: async (taskId, patch) => {
const values = {
...(patch.dependencies !== undefined ? { dependencies: patch.dependencies } : {}),
...(patch.title !== undefined ? { title: patch.title } : {}),
...(patch.column !== undefined ? { column: patch.column } : {}),
};
if (Object.keys(values).length === 0) {
throw new Error("seedRawTaskColumns requires at least one column");
}
const updated = await layer.db
.update(postgresSchema.project.tasks)
.set(values)
.where(drizzleEq(postgresSchema.project.tasks.id, taskId))
.returning({ id: postgresSchema.project.tasks.id });
if (updated.length !== 1) {
throw new Error(`seedRawTaskColumns expected one task row for ${taskId}, updated ${updated.length}`);
}
store.clearStartupSlimListMemo();
store.taskCache.clear();
},
selfHeal: {
recoverAlreadyMergedReviewTasks: async () => manager.recoverAlreadyMergedReviewTasks(),
recoverMisclassifiedFailures: async () => manager.recoverMisclassifiedFailures(),

View File

@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it } from "vitest";
import { DependencyCycleError } from "@fusion/core";
import { DependencyCycleError, detectSelfDefeatingDependency } from "@fusion/core";
// FNXC:SqliteRemoval 2026-07-14: hasPg guard added — makeReliabilityFixture requires PG after SQLite removal (VAL-REMOVAL-005).
import { hasGit, hasPg, makeReliabilityFixture } from "./_helpers.js";
@@ -23,8 +23,8 @@ describeIfGit("reliability interactions: dependency-cycle reconciliation", () =>
} as any);
const child = await fx.store.createTask({ id: "FN-5256-C", title: "Foundation child", description: "child" } as any);
fx.store.getDatabase().prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([child.id]), umbrella.id);
fx.store.getDatabase().prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([umbrella.id]), child.id);
await fx.seedRawTaskColumns(umbrella.id, { dependencies: [child.id] });
await fx.seedRawTaskColumns(child.id, { dependencies: [umbrella.id] });
const recovered = await fx.manager.reconcileDependencyCycles();
expect(recovered).toBe(1);
@@ -35,7 +35,7 @@ describeIfGit("reliability interactions: dependency-cycle reconciliation", () =>
expect(updatedUmbrella?.dependencies).toEqual([child.id]);
expect(updatedChild?.log.some((entry) => JSON.stringify(entry).includes("Auto-cleared umbrella back-edge"))).toBe(true);
const repairedAudit = fx.store.getRunAuditEvents({
const repairedAudit = await fx.store.getRunAuditEventsAsync({
taskId: child.id,
domain: "database",
mutationType: "task:auto-reconciled-dependency-cycle",
@@ -55,9 +55,9 @@ describeIfGit("reliability interactions: dependency-cycle reconciliation", () =>
const b = await fx.store.createTask({ id: "FN-5241", title: "Task B", description: "B" } as any);
const c = await fx.store.createTask({ id: "FN-5242", title: "Task C", description: "C" } as any);
fx.store.getDatabase().prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([b.id]), a.id);
fx.store.getDatabase().prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([c.id]), b.id);
fx.store.getDatabase().prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([a.id]), c.id);
await fx.seedRawTaskColumns(a.id, { dependencies: [b.id] });
await fx.seedRawTaskColumns(b.id, { dependencies: [c.id] });
await fx.seedRawTaskColumns(c.id, { dependencies: [a.id] });
const recovered = await fx.manager.reconcileDependencyCycles();
expect(recovered).toBe(0);
@@ -66,12 +66,12 @@ describeIfGit("reliability interactions: dependency-cycle reconciliation", () =>
expect((await fx.store.getTask(b.id))?.dependencies).toEqual([c.id]);
expect((await fx.store.getTask(c.id))?.dependencies).toEqual([a.id]);
const detected = fx.store.getRunAuditEvents({
const detected = await fx.store.getRunAuditEventsAsync({
taskId: a.id,
domain: "database",
mutationType: "task:dependency-cycle-detected",
});
const unrepaired = fx.store.getRunAuditEvents({
const unrepaired = await fx.store.getRunAuditEventsAsync({
taskId: a.id,
domain: "database",
mutationType: "task:dependency-cycle-unrepaired",
@@ -92,17 +92,19 @@ describeIfGit("reliability interactions: dependency-cycle reconciliation", () =>
dependencies: [child.id],
} as any);
fx.store.getDatabase().prepare("UPDATE tasks SET title = ?, dependencies = ? WHERE id = ?").run(
`Finalize ${child.id}: close loop`,
JSON.stringify([child.id]),
child.id,
);
await fx.seedRawTaskColumns(child.id, { title: "Finalize FN-100: close loop", dependencies: ["FN-100"], column: "todo" });
expect(await fx.store.getTask(child.id)).toMatchObject({
title: "Finalize FN-100: close loop", dependencies: ["FN-100"], column: "todo",
});
const seededChild = (await fx.store.listTasks({ column: "todo", slim: true })).find((task) => task.id === child.id)!;
expect(seededChild).toMatchObject({ title: "Finalize FN-100: close loop", dependencies: ["FN-100"], column: "todo" });
expect(detectSelfDefeatingDependency(seededChild.title, seededChild.dependencies)).toEqual({ matchedVerb: "finalize", operandTaskId: "FN-100" });
const selfDefRecovered = await fx.manager.reconcileSelfDefeatingDependencies();
expect(selfDefRecovered).toBe(1);
expect((await fx.store.getTask(child.id))?.dependencies).toEqual([]);
fx.store.getDatabase().prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([umbrella.id]), child.id);
await fx.seedRawTaskColumns(child.id, { dependencies: [umbrella.id] });
const cycleRecovered = await fx.manager.reconcileDependencyCycles();
expect(cycleRecovered).toBe(1);
@@ -110,12 +112,12 @@ describeIfGit("reliability interactions: dependency-cycle reconciliation", () =>
const updatedChild = await fx.store.getTask(child.id);
expect(updatedChild?.dependencies).toEqual([]);
const selfDefAudit = fx.store.getRunAuditEvents({
const selfDefAudit = await fx.store.getRunAuditEventsAsync({
taskId: child.id,
domain: "database",
mutationType: "task:auto-reconciled-self-defeating-dep",
});
const cycleAudit = fx.store.getRunAuditEvents({
const cycleAudit = await fx.store.getRunAuditEventsAsync({
taskId: child.id,
domain: "database",
mutationType: "task:auto-reconciled-dependency-cycle",
@@ -135,11 +137,10 @@ describeIfGit("reliability interactions: dependency-cycle reconciliation", () =>
const c = await fx.store.createTask({ id: "FN-5432-C", title: "Task C", description: "C" } as any);
const d = await fx.store.createTask({ id: "FN-5432-D", title: "Task D", description: "D" } as any);
const db = fx.store.getDatabase();
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([b.id]), a.id);
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([c.id]), b.id);
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([d.id]), c.id);
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([a.id]), d.id);
await fx.seedRawTaskColumns(a.id, { dependencies: [b.id] });
await fx.seedRawTaskColumns(b.id, { dependencies: [c.id] });
await fx.seedRawTaskColumns(c.id, { dependencies: [d.id] });
await fx.seedRawTaskColumns(d.id, { dependencies: [a.id] });
const recovered = await fx.manager.reconcileDependencyCycles();
expect(recovered).toBe(0);
@@ -149,10 +150,10 @@ describeIfGit("reliability interactions: dependency-cycle reconciliation", () =>
expect((await fx.store.getTask(c.id))?.dependencies).toEqual([d.id]);
expect((await fx.store.getTask(d.id))?.dependencies).toEqual([a.id]);
const repaired = fx.store.getRunAuditEvents({ domain: "database", mutationType: "task:auto-reconciled-dependency-cycle" });
const repaired = await fx.store.getRunAuditEventsAsync({ domain: "database", mutationType: "task:auto-reconciled-dependency-cycle" });
expect(repaired).toHaveLength(0);
const unrepaired = fx.store.getRunAuditEvents({ domain: "database", mutationType: "task:dependency-cycle-unrepaired" });
const unrepaired = await fx.store.getRunAuditEventsAsync({ domain: "database", mutationType: "task:dependency-cycle-unrepaired" });
expect(unrepaired).toHaveLength(1);
const cyclePath = unrepaired[0]?.metadata?.cyclePath as string[];
expect(Array.isArray(cyclePath)).toBe(true);
@@ -170,11 +171,10 @@ describeIfGit("reliability interactions: dependency-cycle reconciliation", () =>
const c = await fx.store.createTask({ id: "FN-5432-RC", title: "Task C", description: "C" } as any);
const d = await fx.store.createTask({ id: "FN-5432-RD", title: "Task D", description: "D" } as any);
const db = fx.store.getDatabase();
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([b.id]), a.id);
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([c.id]), b.id);
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([d.id]), c.id);
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([a.id]), d.id);
await fx.seedRawTaskColumns(a.id, { dependencies: [b.id] });
await fx.seedRawTaskColumns(b.id, { dependencies: [c.id] });
await fx.seedRawTaskColumns(c.id, { dependencies: [d.id] });
await fx.seedRawTaskColumns(d.id, { dependencies: [a.id] });
const sweepPromise = fx.manager.reconcileDependencyCycles();
const x = await fx.store.createTask({ id: "FN-5432-RX", title: "Task X", description: "X", dependencies: [a.id] } as any);
@@ -192,25 +192,30 @@ describeIfGit("reliability interactions: dependency-cycle reconciliation", () =>
const p = await fx.store.createTask({ title: "Task P", description: "P" } as any);
const q = await fx.store.createTask({ title: "Task Q", description: "Q", dependencies: [p.id] } as any);
const db = fx.store.getDatabase();
db.prepare("UPDATE tasks SET title = ?, dependencies = ? WHERE id = ?").run(`Finalize ${p.id}: now`, JSON.stringify([p.id]), p.id);
await fx.seedRawTaskColumns(p.id, { title: "Finalize FN-101: now", dependencies: ["FN-101"], column: "todo" });
expect(await fx.store.getTask(p.id)).toMatchObject({
title: "Finalize FN-101: now", dependencies: ["FN-101"], column: "todo",
});
const seededP = (await fx.store.listTasks({ column: "todo", slim: true })).find((task) => task.id === p.id)!;
expect(seededP).toMatchObject({ title: "Finalize FN-101: now", dependencies: ["FN-101"], column: "todo" });
expect(detectSelfDefeatingDependency(seededP.title, seededP.dependencies)).toEqual({ matchedVerb: "finalize", operandTaskId: "FN-101" });
const selfDefRecovered = await fx.manager.reconcileSelfDefeatingDependencies();
expect(selfDefRecovered).toBe(1);
expect((await fx.store.getTask(p.id))?.dependencies).toEqual([]);
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([q.id]), p.id);
await fx.seedRawTaskColumns(p.id, { dependencies: [q.id] });
const cycleRecovered = await fx.manager.reconcileDependencyCycles();
expect(cycleRecovered).toBe(0);
const selfDefAudit = fx.store.getRunAuditEvents({ taskId: p.id, domain: "database", mutationType: "task:auto-reconciled-self-defeating-dep" });
const unrepairedP = fx.store.getRunAuditEvents({ taskId: p.id, domain: "database", mutationType: "task:dependency-cycle-unrepaired" });
const selfDefAudit = await fx.store.getRunAuditEventsAsync({ taskId: p.id, domain: "database", mutationType: "task:auto-reconciled-self-defeating-dep" });
const unrepairedP = await fx.store.getRunAuditEventsAsync({ taskId: p.id, domain: "database", mutationType: "task:dependency-cycle-unrepaired" });
expect(selfDefAudit).toHaveLength(1);
expect(unrepairedP).toHaveLength(1);
const unrepairedPath = unrepairedP[0]?.metadata?.cyclePath as string[];
expect(unrepairedPath).toEqual([p.id, q.id, p.id]);
const cycleDetected = fx.store.getRunAuditEvents({ taskId: p.id, domain: "database", mutationType: "task:dependency-cycle-detected" });
const cycleDetected = await fx.store.getRunAuditEventsAsync({ taskId: p.id, domain: "database", mutationType: "task:dependency-cycle-detected" });
expect(cycleDetected).toHaveLength(1);
});
@@ -223,15 +228,14 @@ describeIfGit("reliability interactions: dependency-cycle reconciliation", () =>
const c = await fx.store.createTask({ id: "FN-5432-SC", title: "Task C", description: "C" } as any);
const d = await fx.store.createTask({ id: "FN-5432-SD", title: "Task D", description: "D" } as any);
const db = fx.store.getDatabase();
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([b.id]), a.id);
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([c.id]), b.id);
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([d.id]), c.id);
db.prepare("UPDATE tasks SET dependencies = ? WHERE id = ?").run(JSON.stringify([a.id]), d.id);
await fx.seedRawTaskColumns(a.id, { dependencies: [b.id] });
await fx.seedRawTaskColumns(b.id, { dependencies: [c.id] });
await fx.seedRawTaskColumns(c.id, { dependencies: [d.id] });
await fx.seedRawTaskColumns(d.id, { dependencies: [a.id] });
await fx.manager.reconcileDependencyCycles();
const unrepaired = fx.store.getRunAuditEvents({ domain: "database", mutationType: "task:dependency-cycle-unrepaired" });
const unrepaired = await fx.store.getRunAuditEventsAsync({ domain: "database", mutationType: "task:dependency-cycle-unrepaired" });
expect(unrepaired.length).toBeGreaterThan(0);
for (const event of unrepaired) {

View File

@@ -22,7 +22,8 @@ describeIfGit("reliability interactions: self-defeating dep reconciliation", ()
});
fixtures.push(fx);
fx.store.getDatabase().prepare("UPDATE tasks SET title = ? WHERE id = ?").run("Finalize FN-100: close loop", fx.task.id);
await fx.store.listTasks({ column: "todo", slim: true });
await fx.seedRawTaskColumns(fx.task.id, { title: "Finalize FN-100: close loop" });
const recovered = await fx.manager.reconcileSelfDefeatingDependencies();
expect(recovered).toBe(1);
@@ -33,7 +34,7 @@ describeIfGit("reliability interactions: self-defeating dep reconciliation", ()
updated?.log.some((entry) => JSON.stringify(entry).includes("Auto-reconciled self-defeating dependency")),
).toBe(true);
const events = fx.store.getRunAuditEvents({
const events = await fx.store.getRunAuditEventsAsync({
taskId: fx.task.id,
domain: "database",
mutationType: "task:auto-reconciled-self-defeating-dep",
@@ -77,7 +78,7 @@ describeIfGit("reliability interactions: self-defeating dep reconciliation", ()
});
fixtures.push(fx);
fx.store.getDatabase().prepare("UPDATE tasks SET title = ? WHERE id = ?").run("Finalize FN-100", fx.task.id);
await fx.seedRawTaskColumns(fx.task.id, { title: "Finalize FN-100" });
const recovered = await fx.manager.reconcileSelfDefeatingDependencies();
expect(recovered).toBe(0);

View File

@@ -371,18 +371,19 @@ export default defineConfig({
Database class being deleted. Quarantined on sight per AGENTS.md; mirrored in
scripts/lib/test-quarantine.json.
*/
// FNXC:PgMigrationQuarantine 2026-07-14-08:00:
// FNXC:PgMigrationQuarantine 2026-07-14-08:00:
// VAL-REMOVAL-005 deleted the SQLite Database class. These files use makeReliabilityFixture
// (now PG-backed) but fail on sync SQLite APIs (getRunAuditEvents, getDatabase) that
// return [] / throw in backend mode, or on mock drift from the async-satellite cutover.
// Quarantined on sight per AGENTS.md; mirrored in scripts/lib/test-quarantine.json.
// FNXC:PgMigrationQuarantine 2026-07-16-04:59:
// FN-8044 migrated dependency-reconcile suites to the PG corrupt-row seeding seam, so
// they are deliberately absent from this quarantine list and ledger.
// FNXC:PgMigrationQuarantine 2026-07-16-10:45:
// FN-8047 restored multi-node claim and owning-node handoff coverage with shared PG-backed
// AgentStores and AsyncCentralClaimStore; their paired ledger entries are intentionally live.
"src/__tests__/reliability-interactions/integration-worktree-state.test.ts",
"src/__tests__/reliability-interactions/explicit-duplicate-marker-sweep.test.ts",
"src/__tests__/reliability-interactions/self-defeating-dep-reconcile.test.ts",
"src/__tests__/reliability-interactions/dependency-cycle-reconcile.test.ts",
"src/__tests__/reliability-interactions/merge-runner-spawn-enoent-prevention.test.ts",
"src/__tests__/reliability-interactions/meta-archive-guard-composition.test.ts",
"src/__tests__/reliability-interactions/meta-chain-auto-close.test.ts",

View File

@@ -116,16 +116,6 @@
"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.",
"quarantinedAt": "2026-07-14"
},
{
"file": "packages/engine/src/__tests__/reliability-interactions/self-defeating-dep-reconcile.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.",
"quarantinedAt": "2026-07-14"
},
{
"file": "packages/engine/src/__tests__/reliability-interactions/dependency-cycle-reconcile.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.",
"quarantinedAt": "2026-07-14"
},
{
"file": "packages/engine/src/__tests__/reliability-interactions/merge-runner-spawn-enoent-prevention.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.",