diff --git a/packages/core/src/__test-utils__/pg-test-harness.ts b/packages/core/src/__test-utils__/pg-test-harness.ts index d4a6d48802..68ca50f409 100644 --- a/packages/core/src/__test-utils__/pg-test-harness.ts +++ b/packages/core/src/__test-utils__/pg-test-harness.ts @@ -671,6 +671,104 @@ function ensureSchemaTemplate(): Promise { return ready; } +/* + * FNXC:PgTestTemplateDb 2026-07-17-22:34: + * PostgreSQL can retain a just-closed baseline connection briefly. Terminate + * stale template sessions immediately before copying; the module-local copy + * mutex ensures this never interrupts a sibling copy using the same source. + * + * FNXC:PgTestHarness 2026-07-18-17:40: + * Keep terminate + DROP + CREATE TEMPLATE on one maintenance session and + * retry the short "source database is being accessed by other users" window + * (seen after switching admin DDL off shell psql). Split sessions left a race + * where a late-closing baseline/pool client reattached between terminate and + * CREATE DATABASE ... TEMPLATE. + * + * FNXC:PgTestHarnessBaselinedDb 2026-08-15-03:52: + * Extracted from createTaskStoreForTest so createBaselinedPgTestDatabase can + * share the identical serialized, retried clone path. + */ +async function cloneDatabaseFromTemplate(dbName: string, template: string): Promise { + await serializeTemplateCopy(async () => { + const maxAttempts = 5; + let lastError: unknown; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + await withMaintenanceSql(async (client) => { + await client` + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = ${template} AND pid <> pg_backend_pid() + `; + await client.unsafe(`DROP DATABASE IF EXISTS "${dbName}" WITH (FORCE)`).catch(() => {}); + await client.unsafe(`CREATE DATABASE "${dbName}" TEMPLATE "${template}"`); + }); + lastError = undefined; + break; + } catch (error) { + lastError = error; + const message = error instanceof Error ? error.message : String(error); + const contended = /being accessed by other users/i.test(message); + if (!contended || attempt === maxAttempts) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, 25 * attempt)); + } + } + if (lastError) throw lastError; + }); +} + +/* +FNXC:PgTestHarnessBaselinedDb 2026-08-15-03:52: +Slow-test fix for tests that need a baselined DATABASE but no TaskStore (e.g. +sqlite-migrator.test.ts, whose 43 integration tests each paid a fresh CREATE +DATABASE plus a full in-migrator applySchemaBaseline DDL run — ~3.5s/test). +Cloning from the run-shared golden template yields a database with the exact +applySchemaBaseline end-state (schema + markers), so the migrator's own +idempotent baseline call becomes a marker-check no-op. Callers own connections +to the returned URL; drop() force-drops the database. +*/ +export async function createBaselinedPgTestDatabase(prefix = "fusion_test"): Promise<{ + readonly dbName: string; + readonly testUrl: string; + drop(): Promise; +}> { + const dbName = uniqueDbName(prefix); + const template = await ensureGoldenTemplate(); + await cloneDatabaseFromTemplate(dbName, template); + return { + dbName, + testUrl: `${PG_TEST_URL_BASE}/${dbName}`, + drop: async () => { + await adminExecAsync(`DROP DATABASE IF EXISTS "${dbName}" WITH (FORCE)`); + }, + }; +} + +/* +FNXC:PgTestHarnessBaselinedDb 2026-08-15-03:52: +Companion to createBaselinedPgTestDatabase for tests whose CONTRACT is a +pristine target (e.g. VAL-MIGRATE-005: a dry-run against an external database +must leave no schemas/tables/markers behind — pre-applied baseline would make +that assertion vacuous). Plain CREATE DATABASE, no template, no baseline. +*/ +export async function createEmptyPgTestDatabase(prefix = "fusion_test"): Promise<{ + readonly dbName: string; + readonly testUrl: string; + drop(): Promise; +}> { + const dbName = uniqueDbName(prefix); + await adminExecAsync(`CREATE DATABASE "${dbName}"`); + return { + dbName, + testUrl: `${PG_TEST_URL_BASE}/${dbName}`, + drop: async () => { + await adminExecAsync(`DROP DATABASE IF EXISTS "${dbName}" WITH (FORCE)`); + }, + }; +} + /** * FNXC:TestMigrationTail 2026-06-24-16:00: * Create a fresh, isolated PostgreSQL database with the Fusion schema applied, @@ -721,47 +819,7 @@ export async function createTaskStoreForTest(options?: { const template = options?.copyFromGolden ? await ensureGoldenTemplate() : await ensureSchemaTemplate(); - await serializeTemplateCopy(async () => { - /* - * FNXC:PgTestTemplateDb 2026-07-17-22:34: - * PostgreSQL can retain a just-closed baseline connection briefly. Terminate - * stale template sessions immediately before copying; the module-local copy - * mutex ensures this never interrupts a sibling copy using the same source. - * - * FNXC:PgTestHarness 2026-07-18-17:40: - * Keep terminate + DROP + CREATE TEMPLATE on one maintenance session and - * retry the short "source database is being accessed by other users" window - * (seen after switching admin DDL off shell psql). Split sessions left a race - * where a late-closing baseline/pool client reattached between terminate and - * CREATE DATABASE ... TEMPLATE. - */ - const maxAttempts = 5; - let lastError: unknown; - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - await withMaintenanceSql(async (client) => { - await client` - SELECT pg_terminate_backend(pid) - FROM pg_stat_activity - WHERE datname = ${template} AND pid <> pg_backend_pid() - `; - await client.unsafe(`DROP DATABASE IF EXISTS "${dbName}" WITH (FORCE)`).catch(() => {}); - await client.unsafe(`CREATE DATABASE "${dbName}" TEMPLATE "${template}"`); - }); - lastError = undefined; - break; - } catch (error) { - lastError = error; - const message = error instanceof Error ? error.message : String(error); - const contended = /being accessed by other users/i.test(message); - if (!contended || attempt === maxAttempts) { - throw error; - } - await new Promise((resolve) => setTimeout(resolve, 25 * attempt)); - } - } - if (lastError) throw lastError; - }); + await cloneDatabaseFromTemplate(dbName, template); const testUrl = `${PG_TEST_URL_BASE}/${dbName}`; // The database already carries the full schema (copied from the template), @@ -945,6 +1003,48 @@ const ALL_APPLICATION_TABLES = [ ]; const TRUNCATE_ALL_SQL = `TRUNCATE TABLE ${ALL_APPLICATION_TABLES.join(", ")} RESTART IDENTITY CASCADE`; +/* +FNXC:PgTestHarnessResetSpeed 2026-08-15-03:52: +Slow-test fix (harness-wide): the per-test reset was a single TRUNCATE ... RESTART +IDENTITY CASCADE over all ~110 application tables. Profiled at 163ms of the 169ms +shared-harness beforeEach (mission-store.pg: 62 tests -> ~10s of pure TRUNCATE), +because TRUNCATE pays a per-table constant (new relfilenode + catalog churn + +fsync) regardless of row count — and in a typical test only a handful of tables +hold rows. Replace it with one DO block that: + 1. switches session_replication_role to 'replica' (transaction-local) so FK + triggers are inert and deletion order is irrelevant; + 2. DELETEs only tables that actually contain rows (EXISTS probe per table is + ~0.05ms; empty tables are skipped entirely); + 3. resets EVERY sequence in the three application schemas to its declared + start value, reproducing RESTART IDENTITY exactly (including sequences + advanced by insert-then-delete tests whose tables ended empty — the full + TRUNCATE reset those too, so the sweep must be unconditional). +Observable semantics are identical: every application table is empty and every +identity restarts, so ID-reuse assertions (KB-001) keep holding. The caller +falls back to the legacy full TRUNCATE if this fast path errors (e.g. a +non-superuser test role that may not set session_replication_role). +*/ +const FAST_RESET_SQL = `DO $fusion_reset$ +DECLARE + tbl text; + has_rows boolean; +BEGIN + PERFORM set_config('session_replication_role', 'replica', true); + FOREACH tbl IN ARRAY ARRAY[${ALL_APPLICATION_TABLES.map((t) => `'${t}'`).join(", ")}] LOOP + EXECUTE 'SELECT EXISTS(SELECT 1 FROM ' || tbl || ')' INTO has_rows; + IF has_rows THEN + EXECUTE 'DELETE FROM ' || tbl; + END IF; + END LOOP; + PERFORM setval(c.oid, s.seqstart, false) + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_sequence s ON s.seqrelid = c.oid + WHERE c.relkind = 'S' + AND n.nspname IN ('${PROJECT_SCHEMA}', '${CENTRAL_SCHEMA}', '${ARCHIVE_SCHEMA}'); +END +$fusion_reset$`; + export function createSharedPgTaskStoreTestHarness(options?: { readonly poolMax?: number; readonly prefix?: string; @@ -1036,7 +1136,13 @@ export function createSharedPgTaskStoreTestHarness(options?: { beforeEach: async () => { if (!harness || !store) throw new Error("SharedPgTaskStoreHarness: beforeAll not called yet"); // Wipe all application data and reset sequences in one statement. - await harness.adminDb.execute(sql.raw(TRUNCATE_ALL_SQL)); + // FNXC:PgTestHarnessResetSpeed 2026-08-15-03:52: fast DELETE-based reset + // (see FAST_RESET_SQL) with the legacy full TRUNCATE as an error fallback. + try { + await harness.adminDb.execute(sql.raw(FAST_RESET_SQL)); + } catch { + await harness.adminDb.execute(sql.raw(TRUNCATE_ALL_SQL)); + } // Re-seed the singleton config row with default project settings so the // store sees a clean project on every test. const defaults = await ensureDefaults(); diff --git a/packages/core/src/__tests__/postgres/data-layer.test.ts b/packages/core/src/__tests__/postgres/data-layer.test.ts index 0b2edd6fe9..9fae3d658c 100644 --- a/packages/core/src/__tests__/postgres/data-layer.test.ts +++ b/packages/core/src/__tests__/postgres/data-layer.test.ts @@ -28,11 +28,8 @@ * gate stays green without a running server. */ -import { describe, it, expect, afterEach, beforeAll } from "vitest"; -import postgres from "postgres"; -import { drizzle } from "drizzle-orm/postgres-js"; +import { it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest"; import { sql } from "drizzle-orm"; -import { execSync } from "node:child_process"; import { createAsyncDataLayer, recordRunAuditEvent, @@ -42,74 +39,55 @@ import { } from "../../postgres/data-layer.js"; import { createConnectionSetFromUrl } from "../../postgres/connection.js"; import type { ResolvedBackend } from "../../postgres/backend-resolver.js"; -import { applySchemaBaseline } from "../../postgres/schema-applier.js"; import * as schema from "../../postgres/schema/index.js"; - -const PG_ADMIN_URL = - process.env.FUSION_PG_TEST_ADMIN_URL ?? "postgresql://localhost:5432/postgres"; -const PG_TEST_URL_BASE = - process.env.FUSION_PG_TEST_URL_BASE ?? "postgresql://localhost:5432"; -const PG_AVAILABLE = - process.env.FUSION_PG_TEST_SKIP !== "1" && Boolean(PG_TEST_URL_BASE); - -const pgDescribe = PG_AVAILABLE ? describe : describe.skip; - -/** - * FNXC:AsyncDataLayer 2026-06-24-10:00: - * Create a uniquely-named fresh database for each test so tests are hermetic - * and never touch existing data. Mirrors the schema-applier test harness. - */ -function uniqueDbName(): string { - return `fusion_data_test_${process.pid}_${Math.random().toString(36).slice(2, 8)}`; -} +import { + pgDescribe, + createSharedPgTaskStoreTestHarness, + type SharedPgTaskStoreHarness, +} from "../../__test-utils__/pg-test-harness.js"; /* -FNXC:PgTestAuthFix 2026-07-14-00:00: -The inline adminExec used process.env.USER for the psql -U flag, which is 'runner' on GitHub Actions (not 'postgres'). Use the PG_TEST_URL_BASE connection string instead so credentials are always correct. +FNXC:AsyncDataLayer 2026-08-15-03:52: +Slow-test fix: this file hand-rolled CREATE DATABASE + full applySchemaBaseline +PER TEST (~4.6s/test, 65s for the file). The transaction primitives under test +write only data, so each describe block now shares one golden-template database +with the harness's per-test reset. Transaction-visibility semantics are +unchanged: the layer pool and the harness adminDb connection remain SEPARATE +sessions, which is what the VAL-DATA-004 concurrent-reader assertions rely on. +The close() lifecycle test builds a PRIVATE layer against the shared database +so closing it cannot break the harness's pooled layer for later tests. +`ctx` keeps its original shape so test bodies stay byte-identical. */ -function adminExec(statement: string): void { - // psql via execSync for DDL that the postgres.js connection pool can't run - // (CREATE/DROP DATABASE cannot run inside a transaction). Short deterministic - // DDL — the acceptable execSync use per AGENTS.md. - execSync( - `psql "${PG_TEST_URL_BASE}/postgres" -v ON_ERROR_STOP=1 -c "${statement.replace(/"/g, '\\"')}"`, - { stdio: "pipe", env: process.env }, - ); -} - interface TestLayer { - dbName: string; - testUrl: string; - layer: AsyncDataLayer; - adminSql: ReturnType; - adminDb: ReturnType; + readonly layer: AsyncDataLayer; + readonly adminDb: ReturnType; } -async function setupFreshLayer(): Promise { - const dbName = uniqueDbName(); - try { - adminExec(`DROP DATABASE IF EXISTS "${dbName}"`); - } catch { - // ignore — may not exist - } - adminExec(`CREATE DATABASE "${dbName}"`); - const testUrl = `${PG_TEST_URL_BASE}/${dbName}`; +const h = createSharedPgTaskStoreTestHarness({ prefix: "fusion_data" }); - // Apply the baseline schema so run_audit_events + tasks exist. - const schemaBackend: ResolvedBackend = { - mode: "external", - runtimeUrl: testUrl, - migrationUrl: testUrl, - migrationUrlOverridden: false, +/** + * Register the shared-harness lifecycle inside a pgDescribe block and return a + * `ctx` whose `layer`/`adminDb` getters resolve live from the harness, so the + * original `ctx.layer` / `ctx.adminDb` test bodies read unchanged. + */ +function useSharedLayer(): TestLayer { + beforeAll(h.beforeAll); + beforeEach(h.beforeEach); + afterEach(h.afterEach); + afterAll(h.afterAll); + return { + get layer() { + return h.layer(); + }, + get adminDb() { + return h.adminDb(); + }, }; - const schemaConnections = await createConnectionSetFromUrl(schemaBackend, { - poolMax: 1, - connectTimeoutSeconds: 5, - }); - await applySchemaBaseline(schemaConnections.migration); - await schemaConnections.close(); +} - // Now build the data layer against the migrated database. +/** Build a private AsyncDataLayer against the shared harness database. */ +async function createPrivateLayer(): Promise { + const testUrl = h.testUrl(); const dataBackend: ResolvedBackend = { mode: "external", runtimeUrl: testUrl, @@ -120,32 +98,7 @@ async function setupFreshLayer(): Promise { poolMax: 5, connectTimeoutSeconds: 5, }); - const layer = createAsyncDataLayer(connections); - - // Admin connection for direct row inspection (outside the data layer). - const adminSql = postgres(testUrl, { max: 2, prepare: false, onnotice: () => {} }); - const adminDb = drizzle(adminSql); - - return { dbName, testUrl, layer, adminSql, adminDb }; -} - -async function teardownLayer(ctx: TestLayer | null): Promise { - if (!ctx) return; - try { - await ctx.layer.close(); - } catch { - // best-effort - } - try { - await ctx.adminSql.end({ timeout: 5 }); - } catch { - // best-effort - } - try { - adminExec(`DROP DATABASE IF EXISTS "${ctx.dbName}"`); - } catch { - // best-effort - } + return createAsyncDataLayer(connections); } /** Count rows in project.run_audit_events via the admin connection. */ @@ -168,15 +121,9 @@ async function readAuditRows( } pgDescribe("AsyncDataLayer: VAL-DATA-002 — transaction atomicity (commit)", () => { - let ctx: TestLayer | null = null; - - afterEach(async () => { - await teardownLayer(ctx); - ctx = null; - }); + const ctx = useSharedLayer(); it("commits a multi-statement mutation with all writes visible after commit", async () => { - ctx = await setupFreshLayer(); const runId = "run-commit-multi"; const auditA: RunAuditEventInput = { runId, @@ -207,7 +154,6 @@ pgDescribe("AsyncDataLayer: VAL-DATA-002 — transaction atomicity (commit)", () }); it("transactionImmediate with a single write commits it", async () => { - ctx = await setupFreshLayer(); const runId = "run-commit-single"; await ctx.layer.transactionImmediate(async (tx) => { await recordRunAuditEventWithinTransaction(tx, { @@ -225,15 +171,9 @@ pgDescribe("AsyncDataLayer: VAL-DATA-002 — transaction atomicity (commit)", () }); pgDescribe("AsyncDataLayer: VAL-DATA-003 — transaction atomicity (rollback)", () => { - let ctx: TestLayer | null = null; - - afterEach(async () => { - await teardownLayer(ctx); - ctx = null; - }); + const ctx = useSharedLayer(); it("rolls back all writes when the callback throws, including the audit row", async () => { - ctx = await setupFreshLayer(); const runId = "run-rollback-throw"; const before = await countAuditRows(ctx.adminDb); expect(before).toBe(0); @@ -259,7 +199,6 @@ pgDescribe("AsyncDataLayer: VAL-DATA-003 — transaction atomicity (rollback)", }); it("rolls back when a constraint is violated mid-transaction (primary-key collision)", async () => { - ctx = await setupFreshLayer(); const runId = "run-rollback-pk"; const before = await countAuditRows(ctx.adminDb); expect(before).toBe(0); @@ -313,15 +252,9 @@ pgDescribe("AsyncDataLayer: VAL-DATA-003 — transaction atomicity (rollback)", }); pgDescribe("AsyncDataLayer: VAL-DATA-004 — concurrent transactions do not observe partial writes", () => { - let ctx: TestLayer | null = null; - - afterEach(async () => { - await teardownLayer(ctx); - ctx = null; - }); + const ctx = useSharedLayer(); it("a concurrent reader outside the writer's transaction does not see uncommitted writes", async () => { - ctx = await setupFreshLayer(); const runId = "run-concurrent-iso"; // Hold a transaction open with an uncommitted write, then verify a @@ -349,7 +282,6 @@ pgDescribe("AsyncDataLayer: VAL-DATA-004 — concurrent transactions do not obse }); it("a concurrent read via a separate pool transaction does not see uncommitted writes", async () => { - ctx = await setupFreshLayer(); const runId = "run-concurrent-iso-2"; // Use a barrier to coordinate: the writer holds its transaction open until @@ -378,7 +310,6 @@ pgDescribe("AsyncDataLayer: VAL-DATA-004 — concurrent transactions do not obse }); it("two concurrent writers both commit their own rows without cross-contamination", async () => { - ctx = await setupFreshLayer(); const runA = "run-concurrent-A"; const runB = "run-concurrent-B"; @@ -413,15 +344,9 @@ pgDescribe("AsyncDataLayer: VAL-DATA-004 — concurrent transactions do not obse }); pgDescribe("AsyncDataLayer: run-audit-event-within-transaction behavior", () => { - let ctx: TestLayer | null = null; - - afterEach(async () => { - await teardownLayer(ctx); - ctx = null; - }); + const ctx = useSharedLayer(); it("the standalone recordRunAuditEvent wraps the insert in its own transaction", async () => { - ctx = await setupFreshLayer(); const event = await recordRunAuditEvent(ctx.layer, { runId: "run-standalone", agentId: "agent-standalone", @@ -440,7 +365,6 @@ pgDescribe("AsyncDataLayer: run-audit-event-within-transaction behavior", () => }); it("records metadata as jsonb and round-trips it", async () => { - ctx = await setupFreshLayer(); const metadata = { filesChanged: 5, nested: { deep: [1, 2, 3] }, flag: true }; await recordRunAuditEvent(ctx.layer, { runId: "run-metadata", @@ -459,7 +383,6 @@ pgDescribe("AsyncDataLayer: run-audit-event-within-transaction behavior", () => }); it("an audit row paired with a task-like mutation rolls back together", async () => { - ctx = await setupFreshLayer(); const runId = "run-paired-rollback"; // Simulate the atomicWriteTaskJsonWithAudit pattern: a "task mutation" @@ -495,20 +418,13 @@ pgDescribe("AsyncDataLayer: run-audit-event-within-transaction behavior", () => }); pgDescribe("AsyncDataLayer: interface stability and connectivity", () => { - let ctx: TestLayer | null = null; - - afterEach(async () => { - await teardownLayer(ctx); - ctx = null; - }); + const ctx = useSharedLayer(); it("ping() succeeds against a healthy backend", async () => { - ctx = await setupFreshLayer(); await expect(ctx.layer.ping()).resolves.toBeUndefined(); }); it("the db member executes a raw query", async () => { - ctx = await setupFreshLayer(); const result = (await ctx.layer.db.execute( sql`SELECT 1 AS val`, )) as unknown as Array<{ val: number }>; @@ -516,26 +432,14 @@ pgDescribe("AsyncDataLayer: interface stability and connectivity", () => { }); it("close() releases the pool without error", async () => { - ctx = await setupFreshLayer(); - await expect(ctx.layer.close()).resolves.toBeUndefined(); - // Prevent teardownLayer from double-closing. - const captured = ctx; - ctx = null; - // The admin connection is still ours to close. - try { - await captured!.adminSql.end({ timeout: 5 }); - } catch { - // best-effort - } - try { - adminExec(`DROP DATABASE IF EXISTS "${captured!.dbName}"`); - } catch { - // best-effort - } + // FNXC:AsyncDataLayer 2026-08-15-03:52: close a PRIVATE layer built + // against the shared database — closing the harness's pooled layer would + // break every later test in the file. + const layer = await createPrivateLayer(); + await expect(layer.close()).resolves.toBeUndefined(); }); it("exposes the stable AsyncDataLayer contract (db, transaction, transactionImmediate, ping, close)", async () => { - ctx = await setupFreshLayer(); expect(typeof ctx.layer.db).toBe("object"); expect(typeof ctx.layer.transaction).toBe("function"); expect(typeof ctx.layer.transactionImmediate).toBe("function"); diff --git a/packages/core/src/__tests__/postgres/sqlite-migrator.test.ts b/packages/core/src/__tests__/postgres/sqlite-migrator.test.ts index bc464e0af1..d726ed68b6 100644 --- a/packages/core/src/__tests__/postgres/sqlite-migrator.test.ts +++ b/packages/core/src/__tests__/postgres/sqlite-migrator.test.ts @@ -28,7 +28,6 @@ import { describe, it, expect, afterEach, beforeEach } from "vitest"; import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; import { sql } from "drizzle-orm"; -import { execSync } from "node:child_process"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -43,6 +42,10 @@ import { type MigrationProgressEvent, } from "../../postgres/sqlite-migrator.js"; import { applySchemaBaseline } from "../../postgres/schema-applier.js"; +import { + createBaselinedPgTestDatabase, + createEmptyPgTestDatabase, +} from "../../__test-utils__/pg-test-harness.js"; const PG_TEST_URL_BASE = process.env.FUSION_PG_TEST_URL_BASE ?? "postgresql://localhost:5432"; @@ -80,25 +83,13 @@ describe("SQLite migration CLI progress", () => { }); }); -/** - * FNXC:PostgresMigration 2026-06-24-09:05: - * Create a uniquely-named fresh PostgreSQL database. Mirrors the - * schema-applier test harness. - */ -function uniqueDbName(): string { - return `fusion_migrate_test_${process.pid}_${Math.random().toString(36).slice(2, 8)}`; -} - /* -FNXC:PgTestAuthFix 2026-07-14-00:00: -The inline adminExec used process.env.USER for the psql -U flag, which is 'runner' on GitHub Actions (not 'postgres'). Use the PG_TEST_URL_BASE connection string instead so credentials are always correct. +FNXC:PostgresMigration 2026-08-15-03:52: +Database naming/creation/drop now lives in the shared pg-test-harness +(createBaselinedPgTestDatabase); the former inline psql adminExec (see +FNXC:PgTestAuthFix 2026-07-14-00:00) is gone with it, which also removes this +file's only execSync shellout. */ -function adminExec(statement: string): void { - execSync( - `psql "${PG_TEST_URL_BASE}/postgres" -v ON_ERROR_STOP=1 -c "${statement.replace(/"/g, '\\"')}"`, - { stdio: "pipe", env: process.env }, - ); -} /** A subset of the tasks table schema (the columns the migration tests touch). */ const TASKS_SQLITE_DDL = ` @@ -440,24 +431,28 @@ interface TestCtx { sqlConn: ReturnType; db: ReturnType; fusionDir: string; + dropDb: () => Promise; } +/* +FNXC:PostgresMigration 2026-08-15-03:52: +Slow-test fix: the per-test target used to be an EMPTY database, so every test +paid the migrator's full internal applySchemaBaseline DDL run (~3s). Clone the +target from the harness's run-shared golden template instead — that template IS +the applySchemaBaseline end-state (schema + version markers), so the migrator's +idempotent baseline call degrades to a marker-check no-op while every assertion, +including the explicit applySchemaBaseline() calls below, sees the identical +target state. +*/ async function setupCtx(): Promise { const fusionDir = mkdtempSync(join(tmpdir(), "fusion-migrate-")); buildPopulatedSqliteProject(fusionDir); buildPopulatedSqliteArchive(fusionDir); - const dbName = uniqueDbName(); - try { - adminExec(`DROP DATABASE IF EXISTS "${dbName}"`); - } catch { - // ignore - } - adminExec(`CREATE DATABASE "${dbName}"`); - const testUrl = `${PG_TEST_URL_BASE}/${dbName}`; - const sqlConn = postgres(testUrl, { max: 3, prepare: false, onnotice: () => {} }); + const baselined = await createBaselinedPgTestDatabase("fusion_migrate_test"); + const sqlConn = postgres(baselined.testUrl, { max: 3, prepare: false, onnotice: () => {} }); const db = drizzle(sqlConn); - return { dbName, sqlConn, db, fusionDir }; + return { dbName: baselined.dbName, sqlConn, db, fusionDir, dropDb: baselined.drop }; } async function teardownCtx(ctx: TestCtx | null): Promise { @@ -468,7 +463,7 @@ async function teardownCtx(ctx: TestCtx | null): Promise { // best-effort } try { - adminExec(`DROP DATABASE IF EXISTS "${ctx.dbName}"`); + await ctx.dropDb(); } catch { // best-effort } @@ -1876,8 +1871,18 @@ pgDescribe("SQLite-to-PostgreSQL migrator", () => { // VAL-MIGRATE-005 — dry-run reports without writing it("dry-run reports the plan without modifying PostgreSQL", async () => { + /* + FNXC:PostgresMigration 2026-08-15-03:52: + This test's contract is a PRISTINE external target (no schemas, no marker + table), so it cannot use the shared golden-template ctx.db whose baseline is + pre-applied — it provisions its own empty database. + */ + const empty = await createEmptyPgTestDatabase("fusion_migrate_pristine"); + const emptyConn = postgres(empty.testUrl, { max: 3, prepare: false, onnotice: () => {} }); + const emptyDb = drizzle(emptyConn); + try { const report = await migrateTest( - ctx!.db, + emptyDb, [{ sqlitePath: join(ctx!.fusionDir, "fusion.db"), pgSchema: "project" as const }], { dryRun: true }, ); @@ -1892,7 +1897,7 @@ pgDescribe("SQLite-to-PostgreSQL migrator", () => { FNXC:PostgresMigration 2026-07-14-23:47: VAL-MIGRATE-005 applies to catalog state as well as copied rows. A preview against a pristine external target must leave no schemas, tables, or migration marker behind after it reports the plan. */ - const catalog = (await ctx!.db.execute(sql` + const catalog = (await emptyDb.execute(sql` SELECT to_regnamespace('project')::text AS project_schema, to_regclass('project.tasks')::text AS tasks_table, @@ -1913,6 +1918,10 @@ pgDescribe("SQLite-to-PostgreSQL migrator", () => { // No sequences should have been bumped in dry-run. expect(report.sequenceBumps).toHaveLength(0); + } finally { + await emptyConn.end({ timeout: 5 }).catch(() => {}); + await empty.drop().catch(() => {}); + } }); // VAL-SEARCH-002 (search_vector population) — generated column auto-populates diff --git a/packages/core/src/__tests__/postgres/taskstore-lifecycle.test.ts b/packages/core/src/__tests__/postgres/taskstore-lifecycle.test.ts index 1b09288f45..3e38e96929 100644 --- a/packages/core/src/__tests__/postgres/taskstore-lifecycle.test.ts +++ b/packages/core/src/__tests__/postgres/taskstore-lifecycle.test.ts @@ -20,15 +20,13 @@ * gate stays green without a running server. */ -import { describe, it, expect, afterEach } from "vitest"; -import postgres from "postgres"; -import { drizzle } from "drizzle-orm/postgres-js"; -import { sql, eq } from "drizzle-orm"; -import { execSync } from "node:child_process"; -import { createAsyncDataLayer, type AsyncDataLayer } from "../../postgres/data-layer.js"; -import { createConnectionSetFromUrl } from "../../postgres/connection.js"; -import type { ResolvedBackend } from "../../postgres/backend-resolver.js"; -import { applySchemaBaseline } from "../../postgres/schema-applier.js"; +import { it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest"; +import { eq } from "drizzle-orm"; +import type { AsyncDataLayer } from "../../postgres/data-layer.js"; +import { + pgDescribe, + createSharedPgTaskStoreTestHarness, +} from "../../__test-utils__/pg-test-harness.js"; import * as schema from "../../postgres/schema/index.js"; import { insertTaskRow, softDeleteTaskRow } from "../../task-store/async/async-persistence.js"; import { @@ -49,87 +47,16 @@ import { import { recordRunAuditEventWithinTransaction } from "../../postgres/data-layer.js"; import type { MergeQueueRow } from "../../task-store/row-types.js"; -const PG_TEST_URL_BASE = - process.env.FUSION_PG_TEST_URL_BASE ?? "postgresql://localhost:5432"; -const PG_AVAILABLE = - process.env.FUSION_PG_TEST_SKIP !== "1" && Boolean(PG_TEST_URL_BASE); - -const pgDescribe = PG_AVAILABLE ? describe : describe.skip; - -function uniqueDbName(): string { - return `fusion_u13_test_${process.pid}_${Math.random().toString(36).slice(2, 8)}`; -} - /* -FNXC:PgTestAuthFix 2026-07-14-00:00: -The inline adminExec used process.env.USER for the psql -U flag, which is 'runner' on GitHub Actions (not 'postgres'). Use the PG_TEST_URL_BASE connection string instead so credentials are always correct. +FNXC:TaskStoreLifecycle 2026-08-15-03:52: +Slow-test fix: this file hand-rolled CREATE DATABASE + full applySchemaBaseline +PER TEST (~4.4s/test, 70s for the file). The helpers under test write only data +(no DDL), so the shared per-file harness (one golden-template DB + per-test +reset) preserves isolation with the schema built once. `ctx` keeps its original +shape so every test body is byte-identical. */ -function adminExec(statement: string): void { - execSync( - `psql "${PG_TEST_URL_BASE}/postgres" -v ON_ERROR_STOP=1 -c "${statement.replace(/"/g, '\\"')}"`, - { stdio: "pipe", env: process.env }, - ); -} - interface TestCtx { - dbName: string; - testUrl: string; layer: AsyncDataLayer; - adminSql: ReturnType; - adminDb: ReturnType; -} - -async function setupCtx(): Promise { - const dbName = uniqueDbName(); - try { - adminExec(`DROP DATABASE IF EXISTS "${dbName}"`); - } catch { - // may not exist - } - adminExec(`CREATE DATABASE "${dbName}"`); - const testUrl = `${PG_TEST_URL_BASE}/${dbName}`; - - const schemaBackend: ResolvedBackend = { - mode: "external", - runtimeUrl: testUrl, - migrationUrl: testUrl, - migrationUrlOverridden: false, - }; - const schemaConnections = await createConnectionSetFromUrl(schemaBackend, { - poolMax: 1, - connectTimeoutSeconds: 5, - }); - await applySchemaBaseline(schemaConnections.migration); - await schemaConnections.close(); - - const connections = await createConnectionSetFromUrl(schemaBackend, { - poolMax: 5, - connectTimeoutSeconds: 5, - }); - const layer = createAsyncDataLayer(connections); - - const adminSql = postgres(testUrl, { max: 2, prepare: false, onnotice: () => {} }); - const adminDb = drizzle(adminSql); - return { dbName, testUrl, layer, adminSql, adminDb }; -} - -async function teardownCtx(ctx: TestCtx | null): Promise { - if (!ctx) return; - try { - await ctx.layer.close(); - } catch { - // best-effort - } - try { - await ctx.adminSql.end({ timeout: 5 }); - } catch { - // best-effort - } - try { - adminExec(`DROP DATABASE IF EXISTS "${ctx.dbName}"`); - } catch { - // best-effort - } } /** A minimal task record with the NOT NULL columns filled. */ @@ -162,17 +89,20 @@ async function seedTaskWithParent( } pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { - let ctx: TestCtx | null = null; + const h = createSharedPgTaskStoreTestHarness({ prefix: "fusion_u13" }); + let ctx!: TestCtx; - afterEach(async () => { - await teardownCtx(ctx); - ctx = null; + beforeAll(h.beforeAll); + beforeEach(async () => { + await h.beforeEach(); + ctx = { layer: h.layer() }; }); + afterEach(h.afterEach); + afterAll(h.afterAll); // ── VAL-DATA-010: Lineage-integrity gate blocks parent delete with live children ── it("findLiveLineageChildren returns live children of a parent (VAL-DATA-010)", async () => { - ctx = await setupCtx(); // Parent + two live children + one archived child. await insertTaskRow(ctx.layer, makeMinimalTask("KB-PARENT"), { lineageId: null }); await seedTaskWithParent(ctx.layer, "KB-CHILD-1", "KB-PARENT", "todo"); @@ -186,7 +116,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { }); it("lineage gate blocks parent delete when live children exist (VAL-DATA-010)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-PARENT"), { lineageId: null }); await seedTaskWithParent(ctx.layer, "KB-LIVE", "KB-PARENT", "todo"); @@ -206,7 +135,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { // ── VAL-DATA-011: removeLineageReferences clears children ── it("removeLineageReferences clears lineage edges so parent can be deleted (VAL-DATA-011)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-PARENT"), { lineageId: null }); await seedTaskWithParent(ctx.layer, "KB-CHILD", "KB-PARENT", "todo"); @@ -247,7 +175,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { // ── VAL-DATA-012: Archived/soft-deleted children do not block parent delete ── it("archived children do not block parent delete (VAL-DATA-012)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-PARENT"), { lineageId: null }); // An archived child (column = 'archived' but not soft-deleted). await seedTaskWithParent(ctx.layer, "KB-ARCHIVED", "KB-PARENT", "archived"); @@ -267,7 +194,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { }); it("soft-deleted children do not block parent delete (VAL-DATA-012)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-PARENT"), { lineageId: null }); // A live child that we then soft-delete. await seedTaskWithParent(ctx.layer, "KB-SOFTDEL", "KB-PARENT", "todo"); @@ -290,7 +216,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { // ── VAL-DATA-013: Handoff-to-review mergeQueue transactional invariant ── it("handoff-to-review: column move + mergeQueue insert + audit are atomic (VAL-DATA-013)", async () => { - ctx = await setupCtx(); // Seed a task in a non-review column. await insertTaskRow(ctx.layer, makeMinimalTask("KB-HANDOFF", "in-progress"), { lineageId: null, @@ -343,7 +268,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { }); it("handoff-to-review: a failing audit rolls back the column move and queue insert (VAL-DATA-013)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-ROLLBACK", "in-progress"), { lineageId: null, }); @@ -400,7 +324,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { // ── VAL-DATA-014: Merge-queue lease semantics ── it("merge-queue lease is acquired priority-first (urgent before normal)", async () => { - ctx = await setupCtx(); // Seed three tasks in-review, enqueued at slightly different times so the // priority ordering is deterministic regardless of FIFO tiebreak. const t0 = "2026-01-01T00:00:00Z"; @@ -443,7 +366,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { }); it("merge-queue lease is FIFO within the same priority", async () => { - ctx = await setupCtx(); const t0 = "2026-01-01T00:00:00Z"; const t1 = "2026-01-01T00:00:01Z"; const t2 = "2026-01-01T00:00:02Z"; @@ -476,7 +398,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { }); it("expired leases recover without incrementing attemptCount (VAL-DATA-014)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-EXPIRE", "in-review"), { lineageId: null }); await enqueueMergeQueue(ctx.layer, "KB-EXPIRE"); @@ -509,7 +430,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { }); it("failure release increments attemptCount, success removes the row (VAL-DATA-014)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-OK", "in-review"), { lineageId: null }); await insertTaskRow(ctx.layer, makeMinimalTask("KB-FAIL", "in-review"), { lineageId: null }); // Enqueue OK first so it is the queue head (FIFO within same priority). @@ -547,7 +467,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { }); it("release by a non-holder is rejected (ownership check)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-OWN", "in-review"), { lineageId: null }); await enqueueMergeQueue(ctx.layer, "KB-OWN"); await acquireMergeQueueLease(ctx.layer, "worker-1", { leaseDurationMs: 60_000 }); @@ -558,7 +477,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { }); it("cleanupStaleMergeQueueRows removes entries whose task left in-review", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-STALE", "in-review"), { lineageId: null }); await enqueueMergeQueue(ctx.layer, "KB-STALE"); @@ -580,7 +498,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { }); it("enqueue rejects a task not in in-review column", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-REJECT", "todo"), { lineageId: null }); await expect(enqueueMergeQueue(ctx.layer, "KB-REJECT")).rejects.toThrow(); @@ -592,7 +509,6 @@ pgDescribe("U13 taskstore-lifecycle (PostgreSQL)", () => { }); it("peekMergeQueue orders priority-first then FIFO", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-A", "in-review"), { lineageId: null }); await insertTaskRow(ctx.layer, makeMinimalTask("KB-B", "in-review"), { lineageId: null }); await insertTaskRow(ctx.layer, makeMinimalTask("KB-C", "in-review"), { lineageId: null }); diff --git a/packages/core/src/__tests__/postgres/taskstore-remaining.test.ts b/packages/core/src/__tests__/postgres/taskstore-remaining.test.ts index e1ae1b695f..86c69ad6a6 100644 --- a/packages/core/src/__tests__/postgres/taskstore-remaining.test.ts +++ b/packages/core/src/__tests__/postgres/taskstore-remaining.test.ts @@ -20,15 +20,13 @@ * gate stays green without a running server. */ -import { describe, it, expect, afterEach } from "vitest"; -import postgres from "postgres"; -import { drizzle } from "drizzle-orm/postgres-js"; -import { eq, sql } from "drizzle-orm"; -import { execSync } from "node:child_process"; -import { createAsyncDataLayer, type AsyncDataLayer } from "../../postgres/data-layer.js"; -import { createConnectionSetFromUrl } from "../../postgres/connection.js"; -import type { ResolvedBackend } from "../../postgres/backend-resolver.js"; -import { applySchemaBaseline } from "../../postgres/schema-applier.js"; +import { it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest"; +import { eq } from "drizzle-orm"; +import type { AsyncDataLayer } from "../../postgres/data-layer.js"; +import { + pgDescribe, + createSharedPgTaskStoreTestHarness, +} from "../../__test-utils__/pg-test-harness.js"; import * as schema from "../../postgres/schema/index.js"; import { insertTaskRow, softDeleteTaskRow } from "../../task-store/async/async-persistence.js"; import { @@ -95,112 +93,22 @@ import { countSearchTasksLike, } from "../../task-store/async/async-search.js"; -const PG_TEST_URL_BASE = - process.env.FUSION_PG_TEST_URL_BASE ?? "postgresql://localhost:5432"; -const PG_AVAILABLE = - process.env.FUSION_PG_TEST_SKIP !== "1" && Boolean(PG_TEST_URL_BASE); - -const pgDescribe = PG_AVAILABLE ? describe : describe.skip; - /** FNXC:MultiProjectIsolation 2026-07-16-00:05: the project every harness row is owned by. */ const TEST_PROJECT_ID = "proj_test_u14"; -function uniqueDbName(): string { - return `fusion_u14_test_${process.pid}_${Math.random().toString(36).slice(2, 8)}`; -} - /* -FNXC:PgTestAuthFix 2026-07-14-00:00: -The inline adminExec used process.env.USER for the psql -U flag, which is 'runner' on GitHub Actions (not 'postgres'). Use the PG_TEST_URL_BASE connection string instead so credentials are always correct. +FNXC:TaskStoreRemaining 2026-08-15-03:52: +Slow-test fix: this file hand-rolled CREATE DATABASE + full applySchemaBaseline +PER TEST (~1.9s/test, 52s for the file). The helpers under test write only data, +so the shared per-file harness (one golden-template DB + per-test reset) keeps +isolation with the schema built once. The FNXC:MultiProjectIsolation 2026-07-16 +production-shape binding is preserved by passing `projectId: TEST_PROJECT_ID` +to the harness, which threads the `fusion.project_id` GUC into the runtime +connections and the layer exactly as the old inline setup did. `ctx` keeps its +original `{ layer }` shape so test bodies stay byte-identical. */ -function adminExec(statement: string): void { - execSync( - `psql "${PG_TEST_URL_BASE}/postgres" -v ON_ERROR_STOP=1 -c "${statement.replace(/"/g, '\\"')}"`, - { stdio: "pipe", env: process.env }, - ); -} - interface TestCtx { - dbName: string; - testUrl: string; layer: AsyncDataLayer; - adminSql: ReturnType; - adminDb: ReturnType; -} - -async function setupCtx(): Promise { - const dbName = uniqueDbName(); - try { - adminExec(`DROP DATABASE IF EXISTS "${dbName}"`); - } catch { - // may not exist - } - adminExec(`CREATE DATABASE "${dbName}"`); - const testUrl = `${PG_TEST_URL_BASE}/${dbName}`; - - const schemaBackend: ResolvedBackend = { - mode: "external", - runtimeUrl: testUrl, - migrationUrl: testUrl, - migrationUrlOverridden: false, - }; - const schemaConnections = await createConnectionSetFromUrl(schemaBackend, { - poolMax: 1, - connectTimeoutSeconds: 5, - }); - await applySchemaBaseline(schemaConnections.migration); - await schemaConnections.close(); - - /* - FNXC:MultiProjectIsolation 2026-07-16-00:05: - Bind the layer to a project, as production does. createConnectionSetFromUrl sets the - `fusion.project_id` GUC per connection when given a projectId, and falls back to - `fusion.project_bypass=on` when not; an unbound harness therefore ran with RLS bypassed and - wrote blank project_ids that the migration-0006 trigger rewrote to '__legacy_unscoped__', so - helpers scoping on `layer.projectId ?? ""` never found the rows they had just written. That is - a shape production forbids -- AgentStore.backendProjectId throws on an unbound id -- so the - tests, not the product, were wrong. - */ - const connections = await createConnectionSetFromUrl(schemaBackend, { - poolMax: 5, - connectTimeoutSeconds: 5, - projectId: TEST_PROJECT_ID, - }); - const layer = createAsyncDataLayer(connections, { projectId: TEST_PROJECT_ID }); - - /* - FNXC:MultiProjectIsolation 2026-07-16-00:05: - The admin connection seeds and inspects rows the bound layer then reads, so it must sit in the - SAME partition. Without the GUC its writes are blank, the migration-0006 trigger stamps them - __legacy_unscoped__, and the bound layer scoping on TEST_PROJECT_ID cannot see its own fixtures. - */ - const adminSql = postgres(testUrl, { - max: 2, - prepare: false, - onnotice: () => {}, - connection: { "fusion.project_id": TEST_PROJECT_ID }, - }); - const adminDb = drizzle(adminSql); - return { dbName, testUrl, layer, adminSql, adminDb }; -} - -async function teardownCtx(ctx: TestCtx | null): Promise { - if (!ctx) return; - try { - await ctx.layer.close(); - } catch { - // best-effort - } - try { - await ctx.adminSql.end({ timeout: 5 }); - } catch { - // best-effort - } - try { - adminExec(`DROP DATABASE IF EXISTS "${ctx.dbName}"`); - } catch { - // best-effort - } } /** A minimal task record with the NOT NULL columns filled. */ @@ -217,17 +125,23 @@ function makeMinimalTask(id: string, column = "todo"): Record { } pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { - let ctx: TestCtx | null = null; - - afterEach(async () => { - await teardownCtx(ctx); - ctx = null; + const h = createSharedPgTaskStoreTestHarness({ + prefix: "fusion_u14", + projectId: TEST_PROJECT_ID, }); + let ctx!: TestCtx; + + beforeAll(h.beforeAll); + beforeEach(async () => { + await h.beforeEach(); + ctx = { layer: h.layer() }; + }); + afterEach(h.afterEach); + afterAll(h.afterAll); // ── VAL-CROSS-014: Soft-deleting a child task allows parent deletion ── it("soft-deleting a child allows parent deletion (VAL-CROSS-014)", async () => { - ctx = await setupCtx(); // Seed a parent + a live child. await insertTaskRow(ctx.layer, makeMinimalTask("KB-PARENT"), { lineageId: null }); await insertTaskRow( @@ -259,7 +173,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { // ── VAL-CROSS-015: Archive scopes docs/artifacts out of live views ── it("archiving a parent scopes documents out of live views but preserves them (VAL-CROSS-015)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-DOC-PARENT"), { lineageId: null }); // Create a document on the live task. @@ -288,7 +201,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("archiving a parent scopes artifacts out of live views but preserves them (VAL-CROSS-015)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-ART-PARENT"), { lineageId: null }); // Register an artifact on the live task. @@ -323,7 +235,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { // ── Comments/attachments round-trip on active tasks ── it("task documents round-trip on active tasks (upsert + read + update)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-DOC-RT"), { lineageId: null }); // Initial create. @@ -355,7 +266,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("enforces task-document CAS atomically for creates and updates", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-DOC-CAS"), { lineageId: null }); expect(taskDocumentContentHash("line 1\r\nline 2")).toMatch(/^sha256:[0-9a-f]{64}$/); @@ -452,7 +362,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("artifacts round-trip on active tasks (register + read)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-ART-RT"), { lineageId: null }); const artifact = await insertArtifactRow(ctx.layer, { @@ -478,7 +387,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("document upsert is rejected against archived tasks", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-ARCH-DOC"), { lineageId: null }); await softDeleteTaskRow(ctx.layer, "KB-ARCH-DOC", new Date().toISOString()); @@ -491,7 +399,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("reads retained archived documents and serializes exactly one additive CAS publisher", async () => { - ctx = await setupCtx(); const taskId = "KB-ARCH-PUBLISH"; const task = makeMinimalTask(taskId); await insertTaskRow(ctx.layer, task, { lineageId: null }); @@ -578,7 +485,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("rejects malformed, live, missing, and inconsistent archived publication parents", async () => { - ctx = await setupCtx(); const taskId = "KB-ARCH-REJECT"; const task = makeMinimalTask(taskId); await insertTaskRow(ctx.layer, task, { lineageId: null }); @@ -611,7 +517,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { // ── Audit mutations and run-audit events commit/roll back together ── it("activity log entries round-trip (record + query)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-ACT"), { lineageId: null }); await recordActivityLogEntry(ctx.layer.db, ctx.layer.projectId ?? "", { @@ -629,7 +534,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("run-audit events query by taskId", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-AUDIT"), { lineageId: null }); // Record a run-audit event directly. @@ -656,7 +560,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { // ── Branch groups ── it("branch groups round-trip (create + read + update + list)", async () => { - ctx = await setupCtx(); const created = await createBranchGroup(ctx.layer.db, { sourceType: "mission", sourceId: "miss-1", @@ -686,7 +589,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("ensureBranchGroupForSource reuses existing group for same branch", async () => { - ctx = await setupCtx(); const g1 = await ensureBranchGroupForSource( ctx.layer.db, "mission", @@ -704,7 +606,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("PR entities round-trip (ensure + update + list active)", async () => { - ctx = await setupCtx(); const created = await ensurePrEntityForSource(ctx.layer.db, { sourceType: "task", sourceId: "task-1", @@ -743,7 +644,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("PR thread outcomes round-trip (record + read)", async () => { - ctx = await setupCtx(); const pr = await ensurePrEntityForSource(ctx.layer.db, { sourceType: "task", sourceId: "task-thread", @@ -761,7 +661,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { // ── Workflow work-items ── it("workflow work items round-trip (upsert + transition + terminal guard)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-WF"), { lineageId: null }); const item = await upsertWorkflowWorkItem(ctx.layer, { @@ -790,7 +689,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("workflow work item upsert is idempotent on composite key", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-WF-IDEM"), { lineageId: null }); const item1 = await upsertWorkflowWorkItem(ctx.layer, { @@ -812,7 +710,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("completion handoff markers round-trip (record + read)", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-HANDOFF"), { lineageId: null }); await recordCompletionHandoff(ctx.layer.db, "KB-HANDOFF", "engine"); @@ -821,7 +718,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("listDueWorkflowWorkItems returns items with expired/null leases", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-DUE"), { lineageId: null }); await upsertWorkflowWorkItem(ctx.layer, { @@ -839,7 +735,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { // ── Goal citations / usage events / plugin activations ── it("goal citations dedup on (goalId, surface, sourceRef)", async () => { - ctx = await setupCtx(); const inserted1 = await recordGoalCitations(ctx.layer.db, [ { goalId: "g1", agentId: "a1", surface: "task_document", sourceRef: "doc:1", snippet: "cite 1" }, ]); @@ -862,7 +757,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("usage events round-trip (emit + query)", async () => { - ctx = await setupCtx(); const inserted = await emitUsageEvent(ctx.layer.db, ctx.layer.projectId ?? "", { kind: "tool_call", taskId: "KB-USAGE", @@ -880,7 +774,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("usage events fail-soft on unknown kind", async () => { - ctx = await setupCtx(); const inserted = await emitUsageEvent(ctx.layer.db, ctx.layer.projectId ?? "", { // @ts-expect-error — intentionally invalid kind kind: "bogus_kind", @@ -889,7 +782,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("plugin activations round-trip (record)", async () => { - ctx = await setupCtx(); const activation = await recordPluginActivation(ctx.layer.db, { pluginId: "roadmap", source: "npm", @@ -902,7 +794,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { // ── Archive snapshots ── it("archived task snapshots round-trip (upsert + find + list + filter)", async () => { - ctx = await setupCtx(); const entry = { id: "KB-ARCH-SNAP", lineageId: "lineage-1", @@ -937,7 +828,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("searchTasksLike finds tasks by token and respects soft-delete", async () => { - ctx = await setupCtx(); await insertTaskRow( ctx.layer, { ...makeMinimalTask("KB-SEARCH-1"), title: "implement auth" }, @@ -963,7 +853,6 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { }); it("searchTasksLike returns empty for empty queries", async () => { - ctx = await setupCtx(); await insertTaskRow(ctx.layer, makeMinimalTask("KB-EMPTY"), { lineageId: null }); const results = await searchTasksLike(ctx.layer.db, "");