From 08c546dc758a3bea0c33b7dbc26fc19e80356dae Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 15 Jul 2026 23:06:20 -0700 Subject: [PATCH] test(core): bind the U14 postgres harness to a project, as production does The usage-events round-trip failed because the harness ran unbound. Production binds `fusion.project_id` per connection (connection.ts) and only falls back to `fusion.project_bypass=on` when no projectId is given, so an unbound harness wrote blank project_ids that the migration-0006 trigger rewrote to '__legacy_unscoped__' -- and helpers scoping on `layer.projectId ?? ""` then looked for a literal '' the database never stores. Unbound is a shape production forbids: AgentStore.backendProjectId throws on it ("Reject unbound backend heartbeat/run access instead of silently reading or writing the legacy empty-string partition"). The harness was wrong, not the product -- an earlier attempt to make the product accommodate the unbound harness was reverted in b51de02a5. Binds both the layer and the admin connection: the admin connection seeds fixtures the layer reads back, so it must sit in the same partition or the layer cannot see its own setup. Three reads that relied on the unbound default now pass the project id, matching how production callers thread `layer.projectId` -- getLiveTaskColumn resolves a missing id to the sentinel partition, so omitting it looked in the wrong place once rows were bound. No product code changes. 24/24. The same binding does NOT fit the satellite suites and they are left alone: satellite-fusiondir has a test asserting the unbound APIs fail closed (binding defeats its premise) and another that binds two projects itself, so that harness needs an opt-out parameter rather than a blanket bind. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../postgres/taskstore-remaining.test.ts | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/core/src/__tests__/postgres/taskstore-remaining.test.ts b/packages/core/src/__tests__/postgres/taskstore-remaining.test.ts index 13ae4cff89..fcfdda3e72 100644 --- a/packages/core/src/__tests__/postgres/taskstore-remaining.test.ts +++ b/packages/core/src/__tests__/postgres/taskstore-remaining.test.ts @@ -95,6 +95,9 @@ const PG_AVAILABLE = 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)}`; } @@ -141,13 +144,35 @@ async function setupCtx(): Promise { 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); + const layer = createAsyncDataLayer(connections, { projectId: TEST_PROJECT_ID }); - const adminSql = postgres(testUrl, { max: 2, prepare: false, onnotice: () => {} }); + /* + 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 }; } @@ -313,12 +338,12 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { expect(doc2.content).toBe("v2 content"); // Read back. - const read = await getTaskDocument(ctx.layer.db, "KB-DOC-RT", "design"); + const read = await getTaskDocument(ctx.layer.db, "KB-DOC-RT", "design", TEST_PROJECT_ID); expect(read?.revision).toBe(2); expect(read?.content).toBe("v2 content"); // List shows the document. - const docs = await listTaskDocuments(ctx.layer.db, "KB-DOC-RT"); + const docs = await listTaskDocuments(ctx.layer.db, "KB-DOC-RT", TEST_PROJECT_ID); expect(docs).toHaveLength(1); }); @@ -344,7 +369,7 @@ pgDescribe("U14 taskstore-remaining (PostgreSQL)", () => { expect(read?.title).toBe("round-trip artifact"); expect(read?.metadata).toEqual({ source: "test" }); - const list = await getArtifacts(ctx.layer.db, "KB-ART-RT"); + const list = await getArtifacts(ctx.layer.db, "KB-ART-RT", TEST_PROJECT_ID); expect(list).toHaveLength(1); });