From 3c235ce275b626a73da4fe508ac76fd6f5fbd686 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 16 Aug 2026 13:47:12 -0700 Subject: [PATCH] FN-9129: fence revision attribution assertions by identity Make settings revision attribution tests select only immutable revisions owned by each test. - Resolve reset-ordering failures by querying revision IDs from unique targets and payloads. - Prove late background system writes cannot contaminate actor assertions. - Document the loaded-lane diagnosis, evidence, and remediation. Files changed: .../suite-only-flakes-observed-register.md | 26 ++++++ .../settings-revision-attribution.test.ts | 104 ++++++++++++++++----- 2 files changed, 107 insertions(+), 23 deletions(-) Fusion-Task-Id: FN-9129 Fusion-Task-Lineage: ae336ed2-0870-400c-9599-37adb588e283 Co-authored-by: Fusion (runfusion.ai) --- .../suite-only-flakes-observed-register.md | 26 +++++ .../settings-revision-attribution.test.ts | 108 ++++++++++++++---- 2 files changed, 109 insertions(+), 25 deletions(-) diff --git a/docs/solutions/test-failures/suite-only-flakes-observed-register.md b/docs/solutions/test-failures/suite-only-flakes-observed-register.md index f42f4102a9..95f34559fd 100644 --- a/docs/solutions/test-failures/suite-only-flakes-observed-register.md +++ b/docs/solutions/test-failures/suite-only-flakes-observed-register.md @@ -222,6 +222,32 @@ FN-9116 adds deterministic ordering coverage for desktop and mobile rows across The flake is structurally removed rather than stabilized: every hydration/recovery writer now has an ownership boundary before it can overwrite a newer turn. This is a published behavior fix, so FN-9116 includes a patch changeset. +## 11. Settings revision attribution reset-ordering assertion + +- **File:** `packages/core/src/__tests__/settings-revision-attribution.test.ts` +- **Exact test:** `settings revision attribution > round-trips every explicit provenance variant through committed JSONB revisions` +- **Owner:** FN-9129 +- **Observed tree/SHA:** retained FN-9128 logs; remediation started at `5e5422de6e57ead4f0c4a253b47b59063c1f9fe3`. +- **Observed frequency:** 5/5 retained loaded full-core runs (default, 4, 8, 12, and sampled 12 workers), not 4/5. + +| run | result | +|---|---| +| FN-9128 loaded campaign | **failed 5/5**; retained `/tmp/fn-9128-core-*.log` | +| FN-9129 isolated pre-fix | **failed**; `/tmp/fn-9129-solo-1.log` | +| FN-9129 full core, 4 workers ×3 | subject passed after repair; first run had unrelated satellite-store failure, remaining two runs clean | +| FN-9129 full core, 12 workers ×1 | passed after repair; co-observed command-center cases passed | + +Verbatim observed failure: + +``` +FAIL src/__tests__/settings-revision-attribution.test.ts > settings revision attribution > round-trips every explicit provenance variant through committed JSONB revisions +AssertionError: expected [ { id: 'fusion-system', …(1) }, …(4) ] to deeply equal [ { kind: 'human', …(1) }, …(4) ] +``` + +**Resolved 2026-08-16 (FN-9129):** This was not configuration-provenance loss. A direct table dump retained in the FN-9129 `evidence` task document and `/tmp/fn-9129-instrumented.log` showed all five explicit actors physically persisted among 19 rows. The shared harness intentionally restarts identities between tests; consequently `ORDER BY sequence ASC` has duplicate values across reset boundaries, and the test's `.slice(before.length)` count window selected previous system rows. The assertion now identifies each explicit write by its test-owned `taskPrefix`, retains its immutable revision UUID, and re-reads only those IDs; regression coverage adds a post-snapshot background system write to prove it cannot enter the provenance assertion. This preserves the provenance invariant without retries, waits, quarantines, timeout changes, or a broad harness mutation. + +The two command-center durable-agent activity cases observed once at 12 workers are classified as co-observed identity-reuse risk, not this subject's cause: the repaired 12-worker campaign passed them. Core PostgreSQL quarantine remains forbidden and `quarantinedCoreTests` remains empty. + ## 9. Create Room picker loaded-lane state ordering - **File:** `packages/dashboard/app/components/__tests__/CreateRoomModal.test.tsx` diff --git a/packages/core/src/__tests__/settings-revision-attribution.test.ts b/packages/core/src/__tests__/settings-revision-attribution.test.ts index 28725f498f..9fa8f55778 100644 --- a/packages/core/src/__tests__/settings-revision-attribution.test.ts +++ b/packages/core/src/__tests__/settings-revision-attribution.test.ts @@ -21,16 +21,55 @@ pgDescribe("settings revision attribution", () => { afterEach(h.afterEach); afterAll(h.afterAll); - async function revisions(): Promise> { - return await h.adminDb().execute(sql` - SELECT id, config_kind AS "configKind", changed_by AS "changedBy" + /* + FNXC:ConfigVersioning 2026-08-16-20:17: + A snapshot-difference window admits late background revisions after a harness + reset. Attribution assertions instead compose only immutable IDs whose target + or payload is uniquely owned by this test's writes. + */ + async function revisionIdsForTarget(configKind: string, configTarget: Record) { + const rows = await h.adminDb().execute(sql` + SELECT id FROM project.configuration_revisions - ORDER BY sequence ASC - `) as Array<{ id: string; configKind: string; changedBy: ConfigChangedBy }>; + WHERE config_kind = ${configKind} + AND config_target @> ${JSON.stringify(configTarget)}::jsonb + ORDER BY created_at ASC, sequence ASC, id ASC + `) as Array<{ id: string }>; + return rows.map((row) => row.id); } - async function revisionActors(): Promise { - return (await revisions()).map((row) => row.changedBy); + async function revisionIdForAfterValue(configKind: string, key: string, value: string) { + const rows = await h.adminDb().execute(sql` + SELECT id + FROM project.configuration_revisions + WHERE config_kind = ${configKind} + AND "after" ->> ${key} = ${value} + ORDER BY created_at DESC, sequence DESC, id DESC + `) as Array<{ id: string }>; + expect(rows).toHaveLength(1); + return rows[0]!.id; + } + + async function revisionForTaskPrefix(taskPrefix: string) { + const rows = await h.adminDb().execute(sql` + SELECT id, config_kind AS "configKind", changed_by AS "changedBy" + FROM project.configuration_revisions + WHERE config_kind = 'project-settings' + AND "after" ->> 'taskPrefix' = ${taskPrefix} + ORDER BY created_at DESC, sequence DESC, id DESC + `) as Array<{ id: string; configKind: string; changedBy: ConfigChangedBy }>; + expect(rows).toHaveLength(1); + return rows[0]!; + } + + async function revisionById(id: string) { + const rows = await h.adminDb().execute(sql` + SELECT id, config_kind AS "configKind", changed_by AS "changedBy" + FROM project.configuration_revisions + WHERE id = ${id} + `) as Array<{ id: string; configKind: string; changedBy: ConfigChangedBy }>; + expect(rows).toHaveLength(1); + return rows[0]!; } it("persists system for every omitted-actor configuration writer", async () => { @@ -38,12 +77,16 @@ pgDescribe("settings revision attribution", () => { const layer = { ...store.getAsyncLayer()!, projectId: store.getWorkflowSettingsProjectId() }; const automationStore = new AutomationStore(h.rootDir, { asyncLayer: layer }); const routineStore = new RoutineStore(h.rootDir, { asyncLayer: layer }); - const before = await revisions(); - await store.updateSettings({ taskPrefix: "ATR" }); + const projectRevisionId = (await revisionForTaskPrefix("ATR")).id; await store.updateGlobalSettings({ defaultModelId: "attribution-model" }); + const globalRevisionId = await revisionIdForAfterValue("global-settings", "defaultModelId", "attribution-model"); const directGlobal = await store.globalSettingsStore.updateSettings({ defaultModelId: "direct-attribution-model" }); - await store.updateWorkflowSettingValues("builtin:coding", store.getWorkflowSettingsProjectId(), { workflowStepTimeoutMs: 1_000 }); + const directGlobalRevisionId = await revisionIdForAfterValue("global-settings", "defaultModelId", "direct-attribution-model"); + const workflowProjectId = store.getWorkflowSettingsProjectId(); + await store.updateWorkflowSettingValues("builtin:coding", workflowProjectId, { workflowStepTimeoutMs: 1_000 }); + const [workflowRevisionId] = await revisionIdsForTarget("workflow-settings", { workflowId: "builtin:coding", projectId: workflowProjectId }); + expect(workflowRevisionId).toBeDefined(); const schedule = await automationStore.createSchedule({ name: "Attribution schedule", scheduleType: "daily", command: "", @@ -60,22 +103,31 @@ pgDescribe("settings revision attribution", () => { }); await routineStore.updateRoutine(routine.id, { name: "Updated attribution routine" }); - const created = (await revisions()).slice(before.length); - const globalRevision = created.find((revision) => revision.configKind === "global-settings"); - expect(globalRevision).toBeDefined(); - await store.globalSettingsStore.rollbackConfiguration(globalRevision!.id); - - const automationRevision = created.find((revision) => revision.configKind === "automation" && revision.id !== created.find((candidate) => candidate.configKind === "automation")?.id); - expect(automationRevision).toBeDefined(); - await automationStore.rollbackConfiguration(automationRevision!.id); + const automationRevisionIds = await revisionIdsForTarget("automation", { automationId: schedule.id }); + expect(automationRevisionIds).toHaveLength(3); + const globalRollback = await store.globalSettingsStore.rollbackConfiguration(globalRevisionId); + await automationStore.rollbackConfiguration(automationRevisionIds[1]!); await automationStore.deleteSchedule(schedule.id); - const routineRevision = created.find((revision) => revision.configKind === "routine" && revision.id !== created.find((candidate) => candidate.configKind === "routine")?.id); - expect(routineRevision).toBeDefined(); - await routineStore.rollbackConfiguration(routineRevision!.id); + const routineRevisionIds = await revisionIdsForTarget("routine", { routineId: routine.id }); + expect(routineRevisionIds).toHaveLength(2); + await routineStore.rollbackConfiguration(routineRevisionIds[1]!); await routineStore.deleteRoutine(routine.id); - const actors = (await revisions()).slice(before.length).map((revision) => revision.changedBy); + /* FNXC:ConfigVersioning 2026-08-16-20:17: A post-snapshot system revision must not join this test-owned actor set. */ + await store.updateSettings({ taskPrefix: "ATR-background" }); + + const ownedRevisionIds = [ + projectRevisionId, + globalRevisionId, + directGlobalRevisionId, + workflowRevisionId!, + ...(await revisionIdsForTarget("automation", { automationId: schedule.id })), + ...(await revisionIdsForTarget("routine", { routineId: routine.id })), + globalRollback.id, + ]; + expect(new Set(ownedRevisionIds).size).toBe(14); + const actors = (await Promise.all(ownedRevisionIds.map(revisionById))).map((revision) => revision.changedBy); expect(actors).toHaveLength(14); expect(actors).toEqual(Array.from({ length: 14 }, () => ({ kind: "system", id: "fusion-system" }))); expect(actors).not.toContainEqual(expect.objectContaining({ kind: "human" })); @@ -84,7 +136,6 @@ pgDescribe("settings revision attribution", () => { it("round-trips every explicit provenance variant through committed JSONB revisions", async () => { const store = h.store(); - const before = await revisionActors(); const actors: ConfigChangedBy[] = [ { kind: "human", id: "future-auth-user" }, { kind: "agent", id: "agent-1" }, @@ -92,11 +143,18 @@ pgDescribe("settings revision attribution", () => { { kind: "api", id: "http:test-verified" }, { kind: "rollback", id: "rollback-test" }, ]; + const revisionIds: string[] = []; for (const [index, actor] of actors.entries()) { - await store.updateSettings({ taskPrefix: `ATR${index}` }, actor); + const taskPrefix = `ATR${index}`; + await store.updateSettings({ taskPrefix }, actor); + revisionIds.push((await revisionForTaskPrefix(taskPrefix)).id); } - expect((await revisionActors()).slice(before.length)).toEqual(actors); + /* FNXC:ConfigVersioning 2026-08-16-20:00: A background system write after the explicit writes must not enter their ID-addressed provenance assertion. */ + const lateBackgroundWrite = Promise.resolve().then(() => store.updateSettings({ taskPrefix: "ATR-background" })); + await lateBackgroundWrite; + + expect((await Promise.all(revisionIds.map(revisionById))).map((revision) => revision.changedBy)).toEqual(actors); }); });