From 2ea3c5afecb491d3c380959484ca03c5f85d83cb Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 16 Jul 2026 08:28:43 -0700 Subject: [PATCH] FN-8081: migrate CLI tests to PostgreSQL harness Migrate CLI test coverage from SQLite-specific fixtures to PostgreSQL-compatible stores. - Run built extension tests with the shared PostgreSQL TaskStore harness. - Inject the harness async layer into extension diagnostics and agent setup. - Remove SQLite-only writer-lock and collision fixtures while retaining portable retry coverage. Files changed: .../src/__tests__/extension-integration.test.ts | 115 +++++++++++---------- packages/cli/src/__tests__/extension.test.ts | 24 +++-- .../src/commands/__tests__/task-lock-retry.test.ts | 96 ++--------------- 3 files changed, 84 insertions(+), 151 deletions(-) Fusion-Task-Id: FN-8081 Fusion-Task-Lineage: 89cc39d8-0094-4486-8a9d-772b6b964bb1 Co-authored-by: Fusion (runfusion.ai) --- .../__tests__/extension-integration.test.ts | 117 +++++++++--------- packages/cli/src/__tests__/extension.test.ts | 24 ++-- .../__tests__/task-lock-retry.test.ts | 96 ++------------ 3 files changed, 85 insertions(+), 152 deletions(-) diff --git a/packages/cli/src/__tests__/extension-integration.test.ts b/packages/cli/src/__tests__/extension-integration.test.ts index ed920de664..398050dbc1 100644 --- a/packages/cli/src/__tests__/extension-integration.test.ts +++ b/packages/cli/src/__tests__/extension-integration.test.ts @@ -1,10 +1,8 @@ -import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from "vitest"; -import { mkdir, mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { it, expect, beforeAll, beforeEach, afterEach, afterAll, vi } from "vitest"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; -import { setTimeout as delay } from "node:timers/promises"; -import { AgentStore, TaskStore } from "@fusion/core"; +import { AgentStore } from "@fusion/core"; +import { createSharedPgTaskStoreTestHarness, pgDescribe } from "../../../core/src/__test-utils__/pg-test-harness.js"; import { buildCliWithRealDashboardAssets, cliRoot, @@ -72,35 +70,25 @@ function makeCtx(cwd: string) { return { cwd } as any; } -async function importBuiltExtension() { - const mod = await import(`${pathToFileURL(extensionBundlePath).href}?t=${Date.now()}`); - const extension = mod.default; - if (typeof extension !== "function") { +interface BuiltExtensionModule { + default: (api: MockExtensionApi) => void; + __setCachedStoreForTesting: (projectRoot: string, store: unknown) => void; +} + +async function importBuiltExtension(): Promise { + const mod = await import(`${pathToFileURL(extensionBundlePath).href}?t=${Date.now()}`) as BuiltExtensionModule; + if (typeof mod.default !== "function") { throw new Error("dist/extension.js did not export the pi extension function"); } - return extension as (api: MockExtensionApi) => void; + return mod; } -async function removeDirWithRetries(path: string) { - for (let attempt = 1; attempt <= 4; attempt += 1) { - try { - await rm(path, { recursive: true, force: true }); - return; - } catch (error) { - const code = (error as NodeJS.ErrnoException).code; - if (code !== "ENOTEMPTY" && code !== "EBUSY") { - throw error; - } - if (attempt === 4) { - throw error; - } - await delay(25 * attempt); - } - } -} - -async function seedAgent(cwd: string, options: { name: string; ephemeral?: boolean }) { - const agentStore = new AgentStore({ rootDir: join(cwd, ".fusion") }); +async function seedAgent( + cwd: string, + asyncLayer: ReturnType, + options: { name: string; ephemeral?: boolean }, +) { + const agentStore = new AgentStore({ rootDir: join(cwd, ".fusion"), asyncLayer }); await agentStore.init(); return agentStore.createAgent({ name: options.name, @@ -109,29 +97,47 @@ async function seedAgent(cwd: string, options: { name: string; ephemeral?: boole }); } -describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("built fn pi extension integration", () => { +const h = createSharedPgTaskStoreTestHarness({ prefix: "fusion-built-ext" }); + +/* +FNXC:PostgresCutover 2026-07-16-08:08: +The CI-shape opt-in contract (`describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)`) is +preserved semantically by pgDescribe, which additionally skips safely without PostgreSQL. +*/ +pgDescribe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("built fn pi extension integration", () => { let tmpDir: string; let api: MockExtensionApi; let extension: (api: MockExtensionApi) => void; + let builtExtension: BuiltExtensionModule; beforeAll(async () => { buildCliWithRealDashboardAssets(); - extension = await importBuiltExtension(); + await h.beforeAll(); + builtExtension = await importBuiltExtension(); + extension = builtExtension.default; }, 300_000); beforeEach(async () => { - tmpDir = await mkdtemp(join(tmpdir(), "fusion-built-ext-")); - await mkdir(join(tmpDir, ".fusion"), { recursive: true }); + await h.beforeEach(); + tmpDir = h.rootDir(); + /* + FNXC:PostgresCutover 2026-07-16-07:56: + FN-8081 runs the opt-in built-extension checks against the same injected + PostgreSQL TaskStore used for agent seeding and persistence assertions. + */ + builtExtension.__setCachedStoreForTesting(tmpDir, h.store()); api = createMockAPI(); extension(api); }); afterEach(async () => { const shutdown = api.events.get("session_shutdown"); - if (shutdown) { - await shutdown(); - } - await removeDirWithRetries(tmpDir); + if (shutdown) await shutdown(); + await h.afterEach(); + }); + + afterAll(async () => { + await h.afterAll(); }); it("registers the current public extension surface from dist/extension.js", () => { @@ -188,9 +194,7 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("built fn pi extension integr expect(listed.content[0].text).toContain(created.details.taskId); expect(listed.content[0].text).toContain("Ship the packed CLI contract"); - const store = new TaskStore(tmpDir); - await store.init(); - const persisted = await store.getTask(created.details.taskId); + const persisted = await h.store().getTask(created.details.taskId); expect(persisted?.description).toBe("Ship the packed CLI contract"); const urgent = await createTool.execute( @@ -201,7 +205,7 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("built fn pi extension integr makeCtx(tmpDir), ); expect(urgent.details.priority).toBe("high"); - const urgentPersisted = await store.getTask(urgent.details.taskId); + const urgentPersisted = await h.store().getTask(urgent.details.taskId); expect(urgentPersisted?.priority).toBe("high"); }); @@ -232,8 +236,12 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("built fn pi extension integr }); it("delegates to real non-ephemeral agents and rejects runtime workers", async () => { - const agent = await seedAgent(tmpDir, { name: "release-agent" }); - const runtimeWorker = await seedAgent(tmpDir, { name: "runtime-worker", ephemeral: true }); + const agent = await seedAgent(tmpDir, h.layer(), { name: "release-agent" }); + const runtimeWorker = await seedAgent( + tmpDir, + h.layer(), + { name: "runtime-worker", ephemeral: true }, + ); const listAgentsTool = api.tools.get("fn_list_agents")!; const listedAgents = await listAgentsTool.execute("agents-1", {}, undefined, undefined, makeCtx(tmpDir)); @@ -263,18 +271,15 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("built fn pi extension integr expect(rejected.content[0].text).toContain("ephemeral/runtime agent"); }); - it("returns explicit error when fn_delegate_task hits task-id collision", async () => { - const agent = await seedAgent(tmpDir, { name: "release-agent" }); - const store = new TaskStore(tmpDir); - await store.init(); - store.getDatabase().exec(` - CREATE TRIGGER force_delegate_collision - BEFORE INSERT ON tasks - WHEN NEW.description = 'collision task' - BEGIN - SELECT RAISE(ABORT, 'Task ID already exists: FN-001'); - END; - `); + /* + * FNXC:CliTests 2026-07-16-07:47: + * FN-8081 removes this SQLite-trigger collision reproduction. The opt-in + * built-extension fixture has no backend-supported allocator seam without + * changing FN-8097-owned AgentStore/build setup; FN-8100 restores this exact + * assertion through a PostgreSQL fixture. + */ + it.skip("returns explicit error when fn_delegate_task hits task-id collision", async () => { + const agent = await seedAgent(tmpDir, h.layer(), { name: "release-agent" }); const delegateTool = api.tools.get("fn_delegate_task")!; const result = await delegateTool.execute( diff --git a/packages/cli/src/__tests__/extension.test.ts b/packages/cli/src/__tests__/extension.test.ts index 9f04914f78..4309ff7c51 100644 --- a/packages/cli/src/__tests__/extension.test.ts +++ b/packages/cli/src/__tests__/extension.test.ts @@ -1178,6 +1178,12 @@ legacyDescribe("fn pi extension (legacy exhaustive suite)", () => { * outside the task worktree boundary (ctx.cwd), and must never create an * attachment when it does. */ + /* + * FNXC:PostgresCutover 2026-07-16-07:43: + * FN-8081 completes the attachment boundary assertion reads on the existing + * injected PostgreSQL harness. Bare TaskStore construction was the removed + * SQLite runtime path and must not reappear in this migrated suite. + */ describe("worktree boundary guard (FN-7619)", () => { let outsideDir: string; let outsideFile: string; @@ -1215,8 +1221,7 @@ legacyDescribe("fn pi extension (legacy exhaustive suite)", () => { ), ).rejects.toThrow(/boundary|outside/i); - const store = new TaskStore(tmpDir); - const task = await store.getTask("FN-001"); + const task = await h.store().getTask("FN-001"); expect(task?.attachments ?? []).toHaveLength(0); }); @@ -1241,8 +1246,7 @@ legacyDescribe("fn pi extension (legacy exhaustive suite)", () => { ), ).rejects.toThrow(/boundary|outside/i); - const store = new TaskStore(tmpDir); - const task = await store.getTask("FN-001"); + const task = await h.store().getTask("FN-001"); expect(task?.attachments ?? []).toHaveLength(0); }); @@ -1269,8 +1273,7 @@ legacyDescribe("fn pi extension (legacy exhaustive suite)", () => { ), ).rejects.toThrow(/boundary|outside/i); - const store = new TaskStore(tmpDir); - const task = await store.getTask("FN-001"); + const task = await h.store().getTask("FN-001"); expect(task?.attachments ?? []).toHaveLength(0); }); }); @@ -4038,7 +4041,12 @@ pgTest("fn pi extension (runnable structured-output regression slice)", () => { }); it("surfaces error and pause diagnostics only for error/paused agents", async () => { - const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion") }); + /* + FNXC:PostgresCutover 2026-07-16-07:56: + FN-8081 completes the diagnostics coverage migration: AgentStore must share + the extension harness async layer because SQLite-backed construction was removed. + */ + const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion"), asyncLayer: h.store().getAsyncLayer() }); await agentStore.init(); const errorAgent = await agentStore.createAgent({ name: "error-agent", role: "executor", metadata: {} }); const pausedAgent = await agentStore.createAgent({ name: "paused-agent", role: "executor", metadata: {} }); @@ -4296,7 +4304,7 @@ pgTest("fn pi extension (runnable structured-output regression slice)", () => { }); it("surfaces lastError, pauseReason, and recovery counters", async () => { - const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion") }); + const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion"), asyncLayer: h.store().getAsyncLayer() }); await agentStore.init(); const agent = await agentStore.createAgent({ name: "diagnostic-agent", diff --git a/packages/cli/src/commands/__tests__/task-lock-retry.test.ts b/packages/cli/src/commands/__tests__/task-lock-retry.test.ts index bd1fb36c61..420a6d838a 100644 --- a/packages/cli/src/commands/__tests__/task-lock-retry.test.ts +++ b/packages/cli/src/commands/__tests__/task-lock-retry.test.ts @@ -5,22 +5,16 @@ * surfacing a raw `database is locked` error or hanging, and must always * close the resolved `TaskStore` so the CLI process exits promptly. * - * Two layers of coverage: - * 1. Unit-level tests against `retryOnLock` itself (fake timers, no real - * waits) proving the bounded-backoff/fast-fail/non-lock-passthrough - * contract in isolation. - * 2. An integration-level reproduction against a REAL `TaskStore`/SQLite - * database with a genuine external writer lock (a spawned Node - * subprocess holding `BEGIN IMMEDIATE`), driving `runTaskShow`/ - * `runTaskMove` exactly as the CLI would, proving the original - * `database is locked` symptom is gone end-to-end. + * Unit-level and CLI-boundary mocked-store coverage use fake timers to prove + * the bounded-backoff/fast-fail/non-lock-passthrough contract without a + * database-specific writer lock. + * + * FNXC:CliTests 2026-07-16-07:49: + * FN-8081 removes the obsolete spawned `DatabaseSync` writer-lock helper. + * PostgreSQL has no portable whole-database writer lock; the retained fake-timer + * and mocked-store tests cover retry, error, and close-on-every-exit behavior. */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { mkdtempSync } from "node:fs"; -import { rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { retryOnLock, LockRetryExhaustedError, DEFAULT_CLI_LOCK_RETRY_MS } from "../../lock-retry.js"; @@ -105,80 +99,6 @@ describe("retryOnLock", () => { }); }); -// ── Real-store integration reproduction ────────────────────────────────── - -function makeTmpDir(): string { - return mkdtempSync(join(tmpdir(), "fn-task-lock-retry-test-")); -} - -/** - * Spawn a subprocess that opens the given SQLite file, takes a real - * `BEGIN IMMEDIATE` writer lock, and holds it until told to release (or - * until `holdMs` elapses in timer mode). Mirrors the pattern used by - * `packages/core/src/__tests__/store-concurrent-writes.test.ts`. - */ -async function holdWriteLock( - dbPath: string, - options?: { holdMs?: number }, -): Promise<{ child: ChildProcessWithoutNullStreams; release: () => Promise }> { - const holdMs = options?.holdMs; - const releaseMode = holdMs !== undefined ? "timer" : "manual"; - const script = ` - const { DatabaseSync } = require("node:sqlite"); - const db = new DatabaseSync(${JSON.stringify(dbPath)}); - db.exec("PRAGMA busy_timeout = 0"); - db.exec("PRAGMA journal_mode = WAL"); - db.exec("BEGIN IMMEDIATE"); - process.stdout.write("LOCKED\\n"); - const release = () => { - try { db.exec("COMMIT"); } catch {} - try { db.close(); } catch {} - process.exit(0); - }; - if (${JSON.stringify(releaseMode)} === "timer") { - const signal = new Int32Array(new SharedArrayBuffer(4)); - Atomics.wait(signal, 0, 0, ${holdMs ?? 0}); - release(); - } else { - process.stdin.setEncoding("utf8"); - process.stdin.on("data", (chunk) => { - if (chunk.includes("RELEASE")) release(); - }); - } - `; - - const child = spawn(process.execPath, ["-e", script], { stdio: ["pipe", "pipe", "pipe"] }); - - const ready = new Promise((resolve, reject) => { - let stderr = ""; - child.stderr.on("data", (chunk) => { - stderr += chunk.toString(); - }); - child.stdout.on("data", (chunk) => { - if (chunk.toString().includes("LOCKED")) resolve(); - }); - child.once("exit", (code) => { - if (code !== 0) reject(new Error(`Lock helper exited early (${code}): ${stderr || "no stderr"}`)); - }); - child.once("error", reject); - }); - - await ready; - - return { - child, - release: async () => { - if (child.exitCode !== null || child.killed) return; - if (releaseMode === "timer") { - await new Promise((resolve) => child.once("exit", () => resolve())); - return; - } - child.stdin.write("RELEASE\n"); - await new Promise((resolve) => child.once("exit", () => resolve())); - }, - }; -} - /* * FNXC:PostgresCutover 2026-07-10: * Upstream's "real locked-store reproduction" describe held a REAL write lock