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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-16 08:28:43 -07:00
parent 0e7b86ecc4
commit 2ea3c5afec
3 changed files with 85 additions and 152 deletions

View File

@@ -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<BuiltExtensionModule> {
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<typeof h.layer>,
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(

View File

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

View File

@@ -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<void> }> {
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<void>((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<void>((resolve) => child.once("exit", () => resolve()));
return;
}
child.stdin.write("RELEASE\n");
await new Promise<void>((resolve) => child.once("exit", () => resolve()));
},
};
}
/*
* FNXC:PostgresCutover 2026-07-10:
* Upstream's "real locked-store reproduction" describe held a REAL write lock