From 50ed68937996865e634974e207efbe4aaf3e7efa Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 16 Jul 2026 07:14:01 -0700 Subject: [PATCH] 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) --- .../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(-) diff --git a/packages/engine/src/__tests__/reliability-interactions/_helpers.ts b/packages/engine/src/__tests__/reliability-interactions/_helpers.ts index a61362a18a..ff789fd093 100644 --- a/packages/engine/src/__tests__/reliability-interactions/_helpers.ts +++ b/packages/engine/src/__tests__/reliability-interactions/_helpers.ts @@ -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; checkout: (branch: string) => Promise; mergeTask: () => Promise; + seedRawTaskColumns: (taskId: string, patch: Partial>) => Promise; selfHeal: { recoverAlreadyMergedReviewTasks: () => Promise; recoverMisclassifiedFailures: () => Promise; @@ -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(), diff --git a/packages/engine/src/__tests__/reliability-interactions/dependency-cycle-reconcile.test.ts b/packages/engine/src/__tests__/reliability-interactions/dependency-cycle-reconcile.test.ts index 8397739a9b..11ac27b6d5 100644 --- a/packages/engine/src/__tests__/reliability-interactions/dependency-cycle-reconcile.test.ts +++ b/packages/engine/src/__tests__/reliability-interactions/dependency-cycle-reconcile.test.ts @@ -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) { diff --git a/packages/engine/src/__tests__/reliability-interactions/self-defeating-dep-reconcile.test.ts b/packages/engine/src/__tests__/reliability-interactions/self-defeating-dep-reconcile.test.ts index b9cf64d8a6..2840047060 100644 --- a/packages/engine/src/__tests__/reliability-interactions/self-defeating-dep-reconcile.test.ts +++ b/packages/engine/src/__tests__/reliability-interactions/self-defeating-dep-reconcile.test.ts @@ -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); diff --git a/packages/engine/vitest.config.ts b/packages/engine/vitest.config.ts index fe57384b08..6c478dd1d9 100644 --- a/packages/engine/vitest.config.ts +++ b/packages/engine/vitest.config.ts @@ -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", diff --git a/scripts/lib/test-quarantine.json b/scripts/lib/test-quarantine.json index a3852aa2db..e99fd05009 100644 --- a/scripts/lib/test-quarantine.json +++ b/scripts/lib/test-quarantine.json @@ -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.",