From fc9fb21470f149bcef767ed7d177e4e2d0796305 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 16 Jul 2026 08:06:49 -0700 Subject: [PATCH] FN-8095: repair dashboard API quality backfill Stabilize the dashboard API backfill baseline and validate emitted plugin registry artifacts. - Align API integration-test mocks and assertions with current service contracts. - Move dist-dependent plugin registry coverage into the build test command. - Exclude the artifact-dependent test from API backfill shards. Files changed: packages/dashboard/package.json | 2 +- .../src/__tests__/chat-project-services.test.ts | 3 + .../gitlab-source-issue-reconciler.test.ts | 27 +++---- .../src/__tests__/mcp-helper-forwarding.test.ts | 44 ++++++++--- .../src/__tests__/plugin-registry-dist.test.ts | 6 ++ .../src/__tests__/process-lifecycle.test.ts | 8 +- .../register-git-github.gitlab-lifecycle.test.ts | 11 ++- .../src/__tests__/register-signal-routes.test.ts | 86 +++++++++++----------- .../routes-agent-prompt-sizes-integration.test.ts | 54 ++++++++------ .../src/__tests__/server-view-preload.test.ts | 45 +++++++---- .../task-effective-settings-route.test.ts | 12 ++- .../routes/__tests__/agent-avatar-routes.test.ts | 3 + .../mission-workflow-triage-route.test.ts | 73 +++++++++--------- .../register-settings-memory-worktrunk.test.ts | 14 +++- ...sk-workflow-routes.merge-advance-events.test.ts | 18 +++-- ...r-task-workflow-routes.runtime-fallback.test.ts | 4 +- .../__tests__/tasks-overseer-controls.test.ts | 29 ++++---- .../__tests__/tasks-planner-overseer-state.test.ts | 56 +++++++------- .../__tests__/workflow-validate-route.test.ts | 21 ++---- packages/dashboard/vitest.config.ts | 4 + 20 files changed, 307 insertions(+), 213 deletions(-) Fusion-Task-Id: FN-8095 Fusion-Task-Lineage: 8f775431-2149-433a-a8b6-e6106eab661b Co-authored-by: Fusion (runfusion.ai) --- packages/dashboard/package.json | 2 +- .../__tests__/chat-project-services.test.ts | 3 + .../gitlab-source-issue-reconciler.test.ts | 27 +++--- .../__tests__/mcp-helper-forwarding.test.ts | 44 +++++++--- .../__tests__/plugin-registry-dist.test.ts | 6 ++ .../src/__tests__/process-lifecycle.test.ts | 8 +- ...gister-git-github.gitlab-lifecycle.test.ts | 11 ++- .../__tests__/register-signal-routes.test.ts | 86 +++++++++---------- ...tes-agent-prompt-sizes-integration.test.ts | 54 +++++++----- .../src/__tests__/server-view-preload.test.ts | 45 +++++++--- .../task-effective-settings-route.test.ts | 12 ++- .../__tests__/agent-avatar-routes.test.ts | 3 + .../mission-workflow-triage-route.test.ts | 73 ++++++++-------- ...register-settings-memory-worktrunk.test.ts | 14 ++- ...rkflow-routes.merge-advance-events.test.ts | 18 ++-- ...k-workflow-routes.runtime-fallback.test.ts | 4 +- .../__tests__/tasks-overseer-controls.test.ts | 29 +++---- .../tasks-planner-overseer-state.test.ts | 56 ++++++------ .../__tests__/workflow-validate-route.test.ts | 21 ++--- packages/dashboard/vitest.config.ts | 4 + 20 files changed, 307 insertions(+), 213 deletions(-) diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 8980275f9e..d2706fdca5 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -92,7 +92,7 @@ "test:api": "FUSION_DASHBOARD_DEEP=1 vitest run --project dashboard-api --silent=passed-only --reporter=dot", "test:deep": "FUSION_DASHBOARD_DEEP=1 vitest run --project dashboard-app --project dashboard-api --silent=passed-only --reporter=dot --exclude '**/build-output.test.ts'", "test:browser-smoke": "node scripts/browser-layout-smoke.mjs", - "test:build": "FUSION_DASHBOARD_DEEP=1 vitest run --project dashboard-app --silent=passed-only --reporter=dot app/__tests__/build-output.test.ts", + "test:build": "pnpm build && FUSION_DASHBOARD_DEEP=1 vitest run --project dashboard-app --project dashboard-api --silent=passed-only --reporter=dot app/__tests__/build-output.test.ts src/__tests__/plugin-registry-dist.test.ts", "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.app.json", "test:quality:api:backfill-1": "node scripts/run-vitest-with-heap.mjs --heap=6144 run --project dashboard-api-quality-backfill --silent=passed-only --reporter=dot --shard=1/2", "test:quality:api:backfill-2": "node scripts/run-vitest-with-heap.mjs --heap=6144 run --project dashboard-api-quality-backfill --silent=passed-only --reporter=dot --shard=2/2" diff --git a/packages/dashboard/src/__tests__/chat-project-services.test.ts b/packages/dashboard/src/__tests__/chat-project-services.test.ts index 8a017efc07..1a747053b1 100644 --- a/packages/dashboard/src/__tests__/chat-project-services.test.ts +++ b/packages/dashboard/src/__tests__/chat-project-services.test.ts @@ -10,6 +10,9 @@ function createStore(fusionDir = "/tmp/fusion-project") { getRootDir: vi.fn(() => "/tmp/project"), getSettings: vi.fn(async () => ({})), getDatabase: vi.fn(() => ({})), + // FNXC:PostgresCutover 2026-07-16-06:30: dashboard service doubles must + // expose the AsyncDataLayer contract used by AgentStore after SQLite removal. + getAsyncLayer: vi.fn(() => ({})), } as any; } diff --git a/packages/dashboard/src/__tests__/gitlab-source-issue-reconciler.test.ts b/packages/dashboard/src/__tests__/gitlab-source-issue-reconciler.test.ts index 04ee0c164f..b7f6fc84c5 100644 --- a/packages/dashboard/src/__tests__/gitlab-source-issue-reconciler.test.ts +++ b/packages/dashboard/src/__tests__/gitlab-source-issue-reconciler.test.ts @@ -1,9 +1,6 @@ import { 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 { TaskStore, type Task } from "@fusion/core"; +import { createTaskStoreForTest, pgDescribe } from "../../../core/src/__test-utils__/pg-test-harness.js"; import { GitLabSourceIssueReconciler } from "../gitlab-source-issue-reconciler.js"; const { mockResolveGitLabClient, mockGetProjectIssue, mockGetMergeRequest } = vi.hoisted(() => ({ @@ -157,13 +154,14 @@ describe("GitLabSourceIssueReconciler.backfillSourceIssueClosedAt", () => { expect(store.logEntry as any).toHaveBeenCalledWith("FN-9", "Skipped GitLab source issue closed-at backfill", "GitLab token missing"); }); - it("excludes real archived TaskStore rows instead of mutating archiveDb entries", async () => { - const rootDir = mkdtempSync(join(tmpdir(), "kb-gitlab-backfill-archive-test-")); - const globalDir = mkdtempSync(join(tmpdir(), "kb-gitlab-backfill-archive-global-")); - const store = new TaskStore(rootDir, globalDir, { inMemoryDb: true }); + pgDescribe("archived TaskStore rows", () => { + it("excludes real archived TaskStore rows instead of mutating archiveDb entries", async () => { + // FNXC:PostgresCutover 2026-07-16-06:50: archived-task reconciliation must + // use the production async-store archive path after SQLite removal. + const harness = await createTaskStoreForTest(); + const store = harness.store; - try { - await store.init(); + try { const task = await store.createTask({ description: "Archived GitLab issue", sourceIssue: gitlabTask("template").sourceIssue }); await store.moveTask(task.id, "todo"); await store.moveTask(task.id, "in-progress"); @@ -178,10 +176,9 @@ describe("GitLabSourceIssueReconciler.backfillSourceIssueClosedAt", () => { expect(result).toEqual({ scanned: 0, filled: 0, skipped: 0, errors: 0, hasMore: false }); expect(mockGetProjectIssue).not.toHaveBeenCalled(); expect(restored.sourceIssue?.closedAt).toBeUndefined(); - } finally { - store.close(); - await rm(rootDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); - await rm(globalDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); - } + } finally { + await harness.teardown(); + } + }); }); }); diff --git a/packages/dashboard/src/__tests__/mcp-helper-forwarding.test.ts b/packages/dashboard/src/__tests__/mcp-helper-forwarding.test.ts index 56b0db63b9..62cf421124 100644 --- a/packages/dashboard/src/__tests__/mcp-helper-forwarding.test.ts +++ b/packages/dashboard/src/__tests__/mcp-helper-forwarding.test.ts @@ -1,10 +1,6 @@ import express from "express"; -import { mkdtempSync } from "node:fs"; -import { rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { InsightStore, TaskStore as TaskStoreClass, type Task, type TaskStore } from "@fusion/core"; +import { type Task, type TaskStore } from "@fusion/core"; import * as coreModule from "@fusion/core"; import { request } from "../test-request.js"; @@ -72,10 +68,36 @@ function createTask(): Task { } as Task; } -async function createInsightTaskStore(mode: boolean | "no-settings-scope"): Promise<{ root: string; store: TaskStore }> { - const root = mkdtempSync(join(tmpdir(), "kb-mcp-helper-forwarding-")); - const store = new TaskStoreClass(root, join(root, ".fusion-global-settings"), { inMemoryDb: true }) as TaskStore & { mcpEnabledForTest?: boolean; getSettingsByScope?: unknown }; - await store.init(); +function createInsightTaskStore(mode: boolean | "no-settings-scope"): { root: string; store: TaskStore } { + const root = "/tmp/mcp-helper-forwarding"; + let run = { + id: "INS-MCP", + projectId: "", + trigger: "manual", + status: "pending", + lifecycle: { attempt: 1, maxAttempts: 2 }, + }; + // FNXC:PostgresCutover 2026-07-16-07:20: MCP forwarding is a route-boundary + // assertion, not TaskStore persistence coverage. Model the async insight-run + // contract directly so this test no longer constructs the deleted SQLite mode. + const insightStore = { + findActiveRun: vi.fn(async () => undefined), + createRunOrThrowConflict: vi.fn(async (projectId: string, input: Record) => { + run = { ...run, projectId, ...input } as typeof run; + return run; + }), + updateRun: vi.fn(async (_id: string, patch: Record) => { + run = { ...run, ...patch, lifecycle: { ...run.lifecycle, ...(patch.lifecycle as object ?? {}) } } as typeof run; + return run; + }), + appendRunEvent: vi.fn(async () => undefined), + listStalePendingRuns: vi.fn(async () => []), + }; + const store = { + getRootDir: () => root, + getSettings: vi.fn(async () => ({})), + getInsightStore: () => insightStore, + } as TaskStore & { mcpEnabledForTest?: boolean; getSettingsByScope?: unknown }; if (mode === true) store.mcpEnabledForTest = true; if (mode === "no-settings-scope") { Object.defineProperty(store, "getSettingsByScope", { value: undefined, configurable: true }); @@ -242,7 +264,7 @@ describe("MCP forwarding for readonly dashboard helper seams", () => { extractedAt: "2026-06-26T00:00:00.000Z", }); vi.spyOn(coreModule, "mergeInsights").mockReturnValue("# merged insights"); - const { root, store } = await createInsightTaskStore(mode); + const { root, store } = createInsightTaskStore(mode); const app = express(); app.use(express.json()); const router = createInsightsRouter(store) as ReturnType & { __disposeSweeper?: () => void }; @@ -257,8 +279,6 @@ describe("MCP forwarding for readonly dashboard helper seams", () => { expect(mockCreateFnAgent).toHaveBeenCalledWith(expect.objectContaining({ tools: "readonly", mcpServers: expectedServers })); } finally { router.__disposeSweeper?.(); - await store.close(); - await rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 }); } }); }); diff --git a/packages/dashboard/src/__tests__/plugin-registry-dist.test.ts b/packages/dashboard/src/__tests__/plugin-registry-dist.test.ts index 477b2b4007..44bc50cf06 100644 --- a/packages/dashboard/src/__tests__/plugin-registry-dist.test.ts +++ b/packages/dashboard/src/__tests__/plugin-registry-dist.test.ts @@ -13,6 +13,12 @@ async function readDashboardFile(relativePath: string): Promise { return readFile(path.join(dashboardRoot, relativePath), "utf-8"); } +/* +FNXC:DashboardDistArtifacts 2026-07-16-08:20: +This is an emitted-server-output assertion, not an API test. The explicit test:build +command builds the dashboard before collecting it, keeping this test bounded and +preventing API backfill shards from synchronously running a full package build. +*/ describe("plugin registry production output", () => { it("does not emit a Node 22-invalid static JSON import for registry-manifest.json", async () => { const pluginRoutesDist = await readDashboardFile("dist/plugin-routes.js"); diff --git a/packages/dashboard/src/__tests__/process-lifecycle.test.ts b/packages/dashboard/src/__tests__/process-lifecycle.test.ts index 3df3414b6a..b06927f5f4 100644 --- a/packages/dashboard/src/__tests__/process-lifecycle.test.ts +++ b/packages/dashboard/src/__tests__/process-lifecycle.test.ts @@ -50,8 +50,14 @@ describe("dashboard process lifecycle cleanup", () => { } const addedListeners = process.listenerCount("beforeExit") - baselineListeners; + /* + FNXC:DashboardProcessLifecycle 2026-07-16-08:28: + This suite owns the dashboard's beforeExit registration invariant. The host + process may independently register exit cleanup listeners, so only a + beforeExit MaxListeners warning proves this module-evaluation regression. + */ const maxListenerWarnings = warnings.filter( - (warning) => warning.name === "MaxListenersExceededWarning" + (warning) => warning.name === "MaxListenersExceededWarning" && (warning as { type?: string }).type === "beforeExit", ); expect(addedListeners).toBeLessThanOrEqual(1); diff --git a/packages/dashboard/src/__tests__/register-git-github.gitlab-lifecycle.test.ts b/packages/dashboard/src/__tests__/register-git-github.gitlab-lifecycle.test.ts index b07967d5f2..f00c439d62 100644 --- a/packages/dashboard/src/__tests__/register-git-github.gitlab-lifecycle.test.ts +++ b/packages/dashboard/src/__tests__/register-git-github.gitlab-lifecycle.test.ts @@ -11,10 +11,16 @@ function createStore() { return Object.assign(emitter, { getRootDir: vi.fn().mockReturnValue(process.cwd()), getFusionDir: vi.fn().mockReturnValue(`${process.cwd()}/.fusion`), + // FNXC:PostgresCutover 2026-07-16-06:55: refresh listeners expect the + // backend accessor even when their test double does not persist index rows. + getAsyncLayer: vi.fn().mockReturnValue(undefined), getSettings: vi.fn().mockResolvedValue({ gitlabAuthToken: "token", gitlabInstanceUrl: "https://gitlab.example.com", gitlabCommentOnDone: true, gitlabCloseSourceIssueOnDone: true }), getGlobalSettingsStore: () => ({ getSettings: vi.fn().mockResolvedValue({}) }), logEntry: vi.fn().mockResolvedValue(undefined), listTasks: vi.fn().mockResolvedValue([]), + // FNXC:PostgresCutover 2026-07-16-06:55: lifecycle refresh reads the + // current task asynchronously before deriving GitLab follow-up work. + getTask: vi.fn().mockResolvedValue(undefined), listTasksForGithubTrackingReconcile: vi.fn().mockResolvedValue({ tasks: [], hasMore: false }), updateTask: vi.fn().mockResolvedValue(undefined), }); @@ -45,7 +51,10 @@ describe("registerGitGitHubRoutes GitLab lifecycle services", () => { registerGitGitHubRoutes(createContext(store, disposers)); store.emit("task:moved", { task, from: "todo", to: "done" }); await vi.waitFor(() => expect(fetchImpl).toHaveBeenCalledTimes(5)); - expect(fetchImpl.mock.calls.filter(([url]) => String(url).endsWith("/notes"))).toHaveLength(2); + // FNXC:GitLabIssueComments 2026-07-16-07:25: an imported GitLab task can + // carry both source and tracking links to one issue; its done transition + // must produce exactly one comment, with tracking owning the richer payload. + expect(fetchImpl.mock.calls.filter(([url]) => String(url).endsWith("/notes"))).toHaveLength(1); for (const dispose of disposers) dispose(); store.emit("task:moved", { task, from: "done", to: "todo" }); await Promise.resolve(); diff --git a/packages/dashboard/src/__tests__/register-signal-routes.test.ts b/packages/dashboard/src/__tests__/register-signal-routes.test.ts index 13d3d3d332..665e87a22d 100644 --- a/packages/dashboard/src/__tests__/register-signal-routes.test.ts +++ b/packages/dashboard/src/__tests__/register-signal-routes.test.ts @@ -1,11 +1,9 @@ // @vitest-environment node import { createHmac } from "node:crypto"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { aggregateSignalsAnalytics, Database, type Task, type TaskStore } from "@fusion/core"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { aggregateSignalsAnalytics, drizzleSql as sql, type AsyncDataLayer, type Task, type TaskStore } from "@fusion/core"; +import { createTaskStoreForTest, pgDescribe, type PgTestHarness } from "../../../core/src/__test-utils__/pg-test-harness.js"; import { DeliveryNonceCache, type SignalSource } from "../signal-source.js"; import { ingestSignal, @@ -25,7 +23,7 @@ function sign(body: string, secret: string): string { } /** Minimal fake task store implementing only what the ingestion path uses. */ -function makeStore(db?: Database) { +function makeStore(layer?: AsyncDataLayer) { const tasks: Task[] = []; let counter = 0; const store = { @@ -45,26 +43,28 @@ function makeStore(db?: Database) { tasks.push(task); return task; }, - getDatabase() { - if (!db) throw new Error("test database not configured"); - return db; + getAsyncLayer() { + if (!layer) throw new Error("test AsyncDataLayer not configured"); + return layer; }, _tasks: tasks, }; return store as unknown as TaskStore & { _tasks: Task[] }; } -function makeDbStore() { - const dir = mkdtempSync(join(tmpdir(), "kb-signal-routes-")); - tempDirs.push(dir); - const db = new Database(join(dir, ".fusion")); - db.init(); - openDbs.push(db); - return { db, store: makeStore(db) }; +async function makeDbStore() { + const harness = await createTaskStoreForTest(); + // FNXC:PostgresCutover 2026-07-16-06:30: monitor writes are tenant-bound; + // bind the isolated layer so incident ingestion exercises that invariant. + (harness.layer as { projectId?: string }).projectId = "signal-routes-project"; + harnesses.push(harness); + return { layer: harness.layer, store: makeStore(harness.layer) }; } -function incidents(db: Database) { - return db.prepare("SELECT groupingKey, source, severity, status, meta FROM incidents ORDER BY id ASC").all() as Array<{ +async function incidents(layer: AsyncDataLayer) { + // FNXC:PostgresCutover 2026-07-16-06:30: inspect seeded connector rows via + // the project schema instead of the removed synchronous SQLite Database API. + return await layer.db.execute(sql`SELECT grouping_key AS "groupingKey", source, severity, status, meta::text AS meta FROM project.incidents WHERE project_id = ${layer.projectId} ORDER BY id ASC`) as Array<{ groupingKey: string; source: string | null; severity: string | null; @@ -82,8 +82,7 @@ const SECRETS: Record = { }; const savedEnv: Record = {}; -const tempDirs: string[] = []; -const openDbs: Database[] = []; +const harnesses: PgTestHarness[] = []; beforeEach(() => { for (const [k, v] of Object.entries(SECRETS)) { @@ -92,13 +91,12 @@ beforeEach(() => { } }); -afterEach(() => { +afterEach(async () => { for (const k of Object.keys(SECRETS)) { if (savedEnv[k] === undefined) delete process.env[k]; else process.env[k] = savedEnv[k]; } - while (openDbs.length > 0) openDbs.pop()?.close(); - while (tempDirs.length > 0) rmSync(tempDirs.pop()!, { recursive: true, force: true }); + while (harnesses.length > 0) await harnesses.pop()?.teardown(); }); function ctxFor(source: SignalSource, payload: object, headers: Record) { @@ -127,7 +125,7 @@ function signedSignalContext(source: SignalSource, payload: object) { } } -describe("getSignalSource registry", () => { +pgDescribe("getSignalSource registry", () => { it("resolves all five providers and rejects unknown", () => { expect(getSignalSource("webhook")).toBe(webhookSource); expect(getSignalSource("sentry")).toBe(sentrySource); @@ -138,7 +136,7 @@ describe("getSignalSource registry", () => { }); }); -describe("ingestSignal — generic webhook (must-work path)", () => { +pgDescribe("ingestSignal — generic webhook (must-work path)", () => { it("creates one triage task for a valid signed payload", async () => { const store = makeStore(); const ts = Date.now(); @@ -282,7 +280,7 @@ describe("ingestSignal — generic webhook (must-work path)", () => { }); }); -describe("ingestSignal — Sentry adapter", () => { +pgDescribe("ingestSignal — Sentry adapter", () => { it("creates one triage task with normalized title/severity/link + groupingKey from issue.id", async () => { const store = makeStore(); const payload = { @@ -331,7 +329,7 @@ describe("ingestSignal — Sentry adapter", () => { }); }); -describe("ingestSignal — Datadog & PagerDuty adapters (groupingKey from native primitive)", () => { +pgDescribe("ingestSignal — Datadog & PagerDuty adapters (groupingKey from native primitive)", () => { it("Datadog uses aggreg_key as groupingKey", async () => { const store = makeStore(); const payload = { aggreg_key: "agg-7", event_id: "ev-7", title: "High CPU", alert_type: "error" }; @@ -436,7 +434,7 @@ function asRecord(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; } -describe("ingestSignal — GitLab adapter", () => { +pgDescribe("ingestSignal — GitLab adapter", () => { it("creates a triage task for a valid project issue webhook from a self-managed URL", async () => { const store = makeStore(); const payload = gitlabIssuePayload(); @@ -488,7 +486,7 @@ describe("ingestSignal — GitLab adapter", () => { }); it("maps issue and merge-request lifecycle actions without suppressing recovery events", async () => { - const { db, store } = makeDbStore(); + const { layer, store } = await makeDbStore(); const open = gitlabIssuePayload({ object_attributes: { action: "open", state: "opened", updated_at: "2026-03-04T00:00:00.000Z" } }); const close = gitlabIssuePayload({ object_attributes: { action: "close", state: "closed", updated_at: "2026-03-04T00:05:00.000Z" } }); @@ -506,7 +504,7 @@ describe("ingestSignal — GitLab adapter", () => { })).status).toBe(201); expect(store._tasks).toHaveLength(2); - expect(incidents(db)).toMatchObject([{ groupingKey: "gitlab:gitlabhq/gitlab-test:issue:23", source: "gitlab", status: "resolved" }]); + expect(await incidents(layer)).toMatchObject([{ groupingKey: "gitlab:gitlabhq/gitlab-test:issue:23", source: "gitlab", status: "resolved" }]); const updateSignal = gitlabSource.normalize(gitlabIssuePayload({ object_attributes: { action: "update", state: "opened" } }), ctxFor(gitlabSource, open, { "x-gitlab-token": SECRETS.FUSION_SIGNAL_GITLAB_SECRET })); const reopenSignal = gitlabSource.normalize(gitlabIssuePayload({ object_attributes: { action: "reopen", state: "opened" } }), ctxFor(gitlabSource, open, { "x-gitlab-token": SECRETS.FUSION_SIGNAL_GITLAB_SECRET })); const mergedSignal = gitlabSource.normalize(gitlabMergeRequestPayload({ object_attributes: { action: "merge", state: "merged" } }), ctxFor(gitlabSource, open, { "x-gitlab-token": SECRETS.FUSION_SIGNAL_GITLAB_SECRET })); @@ -578,7 +576,7 @@ describe("ingestSignal — GitLab adapter", () => { }); }); -describe("ingestSignal — incident capture", () => { +pgDescribe("ingestSignal — incident capture", () => { it("writes source and normalized severity for all configured providers", async () => { const cases = [ { @@ -634,7 +632,7 @@ describe("ingestSignal — incident capture", () => { ] as const; for (const c of cases) { - const { db, store } = makeDbStore(); + const { layer, store } = await makeDbStore(); const raw = JSON.stringify(c.payload); const res = await ingestSignal({ source: c.source, @@ -645,7 +643,7 @@ describe("ingestSignal — incident capture", () => { nonceCache: new DeliveryNonceCache(), }); expect(res.status).toBe(201); - expect(incidents(db)).toMatchObject([{ + expect(await incidents(layer)).toMatchObject([{ groupingKey: c.expected.groupingKey, source: c.expected.source, severity: c.expected.severity, @@ -655,7 +653,7 @@ describe("ingestSignal — incident capture", () => { }); it("absorbs re-fires by grouping key without inserting duplicate incident rows", async () => { - const { db, store } = makeDbStore(); + const { layer, store } = await makeDbStore(); const mk = (id: string) => { const payload = { id, title: "Same outage", severity: "error", groupingKey: "same-outage" }; const raw = JSON.stringify(payload); @@ -675,7 +673,7 @@ describe("ingestSignal — incident capture", () => { expect((await ingestSignal(mk("refire-1"))).status).toBe(201); expect((await ingestSignal(mk("refire-2"))).status).toBe(201); - const rows = incidents(db); + const rows = await incidents(layer); expect(rows).toHaveLength(1); expect(JSON.parse(rows[0].meta ?? "{}")).toMatchObject({ occurrences: 2 }); }); @@ -745,7 +743,7 @@ describe("ingestSignal — incident capture", () => { ] as const; for (const c of cases) { - const { db, store } = makeDbStore(); + const { layer, store } = await makeDbStore(); expect((await ingestSignal({ source: c.source, store, @@ -759,12 +757,12 @@ describe("ingestSignal — incident capture", () => { nonceCache: new DeliveryNonceCache(), })).status).toBe(201); - expect(incidents(db)).toMatchObject([{ groupingKey: c.groupingKey, source: c.expectedSource, status: "resolved" }]); + expect(await incidents(layer)).toMatchObject([{ groupingKey: c.groupingKey, source: c.expectedSource, status: "resolved" }]); } }); it("also resolves PagerDuty incidents when only data.status is resolved", async () => { - const { db, store } = makeDbStore(); + const { layer, store } = await makeDbStore(); const openedAt = new Date().toISOString(); const openPayload = { event: { @@ -796,7 +794,7 @@ describe("ingestSignal — incident capture", () => { nonceCache: new DeliveryNonceCache(), })).status).toBe(201); - expect(incidents(db)).toMatchObject([{ groupingKey: "pd-status-resolve", source: "pagerduty", status: "resolved" }]); + expect(await incidents(layer)).toMatchObject([{ groupingKey: "pd-status-resolve", source: "pagerduty", status: "resolved" }]); }); it("keeps connector acceptance successful when the best-effort incident write fails", async () => { @@ -822,7 +820,7 @@ describe("ingestSignal — incident capture", () => { }); it("feeds connector-recorded incidents into aggregateSignalsAnalytics breakdowns", async () => { - const { db, store } = makeDbStore(); + const { layer, store } = await makeDbStore(); const sentryPayload = { id: "sentry-analytics-open", data: { issue: { id: "sentry-analytics", title: "Sentry analytics", level: "fatal" } }, @@ -849,7 +847,7 @@ describe("ingestSignal — incident capture", () => { nonceCache: new DeliveryNonceCache(), })).status).toBe(201); - const analytics = await aggregateSignalsAnalytics(db, { + const analytics = await aggregateSignalsAnalytics(layer, { from: "2026-03-01T00:00:00.000Z", to: "2026-03-31T00:00:00.000Z", }); @@ -866,7 +864,7 @@ describe("ingestSignal — incident capture", () => { }); it("does not write incidents for malformed or duplicate payloads", async () => { - const { db, store } = makeDbStore(); + const { layer, store } = await makeDbStore(); const malformed = { nope: true }; const malformedRaw = JSON.stringify(malformed); expect((await ingestSignal({ @@ -897,11 +895,11 @@ describe("ingestSignal — incident capture", () => { expect((await ingestSignal(mk())).status).toBe(201); expect((await ingestSignal(mk())).deduped).toBe(true); - expect(incidents(db)).toHaveLength(1); + expect(await incidents(layer)).toHaveLength(1); }); }); -describe("helpers", () => { +pgDescribe("helpers", () => { it("resolveSignalSecret reads the provider env var", () => { expect(resolveSignalSecret(webhookSource)).toBe("wh-secret"); expect(resolveSignalSecret(webhookSource, {})).toBeUndefined(); diff --git a/packages/dashboard/src/__tests__/routes-agent-prompt-sizes-integration.test.ts b/packages/dashboard/src/__tests__/routes-agent-prompt-sizes-integration.test.ts index f3876bfd2b..1a85a1c864 100644 --- a/packages/dashboard/src/__tests__/routes-agent-prompt-sizes-integration.test.ts +++ b/packages/dashboard/src/__tests__/routes-agent-prompt-sizes-integration.test.ts @@ -1,44 +1,56 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { mkdtempSync, rmSync } from "node:fs"; -import { join } from "node:path"; -import { tmpdir } from "node:os"; -import { AgentStore, TaskStore } from "@fusion/core"; +import { postgresSchema, TaskStore } from "@fusion/core"; +import { createTaskStoreForTest, pgDescribe, type PgTestHarness } from "../../../core/src/__test-utils__/pg-test-harness.js"; import { createServer } from "../server.js"; import { get } from "../test-request.js"; -describe("GET /api/agents/:id/prompt-sizes integration", () => { - let rootDir: string; +pgDescribe("GET /api/agents/:id/prompt-sizes integration", () => { + let harness: PgTestHarness; let store: TaskStore; let agentId: string; beforeEach(async () => { - rootDir = mkdtempSync(join(tmpdir(), "fn-4928-prompt-sizes-")); - store = new TaskStore(rootDir, join(rootDir, ".fusion-global-settings")); - await store.init(); + // FNXC:PostgresCutover 2026-07-16-06:30: route integration fixtures use + // the canonical PG harness because TaskStore no longer supports SQLite. + harness = await createTaskStoreForTest(); + // FNXC:PostgresCutover 2026-07-16-06:30: AgentStore persists agent runs + // by project, so bind the isolated harness layer before seeding telemetry. + (harness.layer as { projectId?: string }).projectId = "prompt-sizes-project"; + store = harness.store; - const agentStore = new AgentStore({ rootDir: store.getFusionDir() }); - await agentStore.init(); - - const agent = await agentStore.createAgent({ + agentId = "agent-prompt-sizes"; + const projectId = harness.layer.projectId!; + await harness.layer.db.insert(postgresSchema.project.agents).values({ + projectId, + id: agentId, name: "Prompt Sizes Agent", role: "executor", - metadata: {}, + state: "active", + createdAt: "2026-05-17T12:00:00.000Z", + updatedAt: "2026-05-17T12:00:00.000Z", + data: {}, }); - agentId = agent.id; - - await agentStore.saveRun({ + await harness.layer.db.insert(postgresSchema.project.agentRuns).values({ + projectId, id: "run-prompt-size-1", agentId, startedAt: "2026-05-17T12:00:00.000Z", endedAt: "2026-05-17T12:00:01.000Z", status: "completed", - systemPrompt: "sys prompt", - executionPrompt: "execute now", + data: { + id: "run-prompt-size-1", + agentId, + startedAt: "2026-05-17T12:00:00.000Z", + endedAt: "2026-05-17T12:00:01.000Z", + status: "completed", + systemPrompt: "sys prompt", + executionPrompt: "execute now", + }, }); }); - afterEach(() => { - rmSync(rootDir, { recursive: true, force: true }); + afterEach(async () => { + await harness.teardown(); }); it("returns prompt-size rows derived from startedAt and run JSON", async () => { diff --git a/packages/dashboard/src/__tests__/server-view-preload.test.ts b/packages/dashboard/src/__tests__/server-view-preload.test.ts index 3aabc4a432..1a8cb0e82e 100644 --- a/packages/dashboard/src/__tests__/server-view-preload.test.ts +++ b/packages/dashboard/src/__tests__/server-view-preload.test.ts @@ -5,8 +5,9 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import vm from "node:vm"; import { JSDOM } from "jsdom"; -import { afterEach, describe, expect, it } from "vitest"; -import { TaskStore } from "@fusion/core"; +import { afterEach, expect, it } from "vitest"; +import type { TaskStore } from "@fusion/core"; +import { createTaskStoreForTest, pgDescribe } from "../../../core/src/__test-utils__/pg-test-harness.js"; import { buildViewPreloadInjection, createServer } from "../server.js"; import { createLoopbackIntegrationTest } from "./loopback-integration-test.js"; @@ -54,24 +55,35 @@ function runPreloadBootstrap(injection: string, taskView: string, projectId?: st } async function startServerWithFixture(clientDir: string) { - const rootDir = makeTempDir("fn-4782-root-"); - const globalDir = makeTempDir("fn-4782-global-"); - const store = new TaskStore(rootDir, globalDir); - await store.init(); + // FNXC:PostgresCutover 2026-07-16-06:50: preload integration starts the + // real server against an isolated async TaskStore, never a retired SQLite store. + const harness = await createTaskStoreForTest(); + const store = harness.store; const previousClientDir = process.env.FUSION_CLIENT_DIR; process.env.FUSION_CLIENT_DIR = clientDir; - const app = createServer(store); + // FNXC:PostgresCutover 2026-07-16-07:30: preload tests exercise static HTML + // injection, not AI-session recovery. Supply the async satellite contract so + // its fire-and-forget recovery cannot outlive this fixture's isolated database. + const aiSessionStore = { + on: () => undefined, + off: () => undefined, + recoverStaleSessions: async () => 0, + listRecoverable: async () => [], + cleanupStaleSessions: async () => ({ terminalDeleted: 0, orphanedDeleted: 0 }), + stopScheduledCleanup: () => undefined, + }; + const app = createServer(store, { aiSessionStore: aiSessionStore as never }); const server = await new Promise((resolve) => { const s = app.listen(0, "127.0.0.1", () => resolve(s)); }); - return { server, restoreEnv: () => { process.env.FUSION_CLIENT_DIR = previousClientDir; }, + teardown: () => harness.teardown(), }; } @@ -82,7 +94,7 @@ afterEach(() => { tempRoots = []; }); -describe("server index preload injection", () => { +pgDescribe("server index preload injection", () => { serverViewPreloadIntegrationTest("injects view chunk map and modulepreload bootstrap", async () => { const clientDir = makeTempDir("fn-4782-client-"); mkdirSync(join(clientDir, ".vite"), { recursive: true }); @@ -98,7 +110,7 @@ describe("server index preload injection", () => { }), ); - const { server, restoreEnv } = await startServerWithFixture(clientDir); + const { server, restoreEnv, teardown } = await startServerWithFixture(clientDir); try { const address = server.address(); if (!address || typeof address === "string") throw new Error("Missing server address"); @@ -117,6 +129,7 @@ describe("server index preload injection", () => { } finally { restoreEnv(); await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))); + await teardown(); } }); @@ -164,7 +177,7 @@ describe("server index preload injection", () => { }), ); - const { server, restoreEnv } = await startServerWithFixture(clientDir); + const { server, restoreEnv, teardown } = await startServerWithFixture(clientDir); try { const address = server.address(); if (!address || typeof address === "string") throw new Error("Missing server address"); @@ -180,6 +193,7 @@ describe("server index preload injection", () => { } finally { restoreEnv(); await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))); + await teardown(); } }); @@ -229,7 +243,7 @@ describe("server index preload injection", () => { JSON.stringify({ "components/AgentsView.tsx": { file: "assets/AgentsView-marker.js" } }), ); - const { server, restoreEnv } = await startServerWithFixture(clientDir); + const { server, restoreEnv, teardown } = await startServerWithFixture(clientDir); try { const address = server.address(); if (!address || typeof address === "string") throw new Error("Missing server address"); @@ -242,6 +256,7 @@ describe("server index preload injection", () => { } finally { restoreEnv(); await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))); + await teardown(); } }); @@ -249,7 +264,7 @@ describe("server index preload injection", () => { const clientDir = makeTempDir("fn-4782-client-no-manifest-"); writeFileSync(join(clientDir, "index.html"), "
"); - const { server, restoreEnv } = await startServerWithFixture(clientDir); + const { server, restoreEnv, teardown } = await startServerWithFixture(clientDir); try { const address = server.address(); if (!address || typeof address === "string") throw new Error("Missing server address"); @@ -262,6 +277,7 @@ describe("server index preload injection", () => { } finally { restoreEnv(); await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))); + await teardown(); } }); @@ -274,7 +290,7 @@ describe("server index preload injection", () => { JSON.stringify({ "components/AgentsView.tsx": { file: "assets/AgentsView--abc.js" } }), ); - const { server, restoreEnv } = await startServerWithFixture(clientDir); + const { server, restoreEnv, teardown } = await startServerWithFixture(clientDir); try { const address = server.address(); if (!address || typeof address === "string") throw new Error("Missing server address"); @@ -288,6 +304,7 @@ describe("server index preload injection", () => { } finally { restoreEnv(); await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))); + await teardown(); } }); }); diff --git a/packages/dashboard/src/__tests__/task-effective-settings-route.test.ts b/packages/dashboard/src/__tests__/task-effective-settings-route.test.ts index 3516eb080a..ddf9cf421d 100644 --- a/packages/dashboard/src/__tests__/task-effective-settings-route.test.ts +++ b/packages/dashboard/src/__tests__/task-effective-settings-route.test.ts @@ -12,7 +12,17 @@ class MockStore extends EventEmitter { getRootDir(): string { return "/repo"; } getFusionDir(): string { return "/repo/.fusion"; } - getDatabase() { return { exec: vi.fn(), prepare: vi.fn().mockReturnValue({ run: vi.fn().mockReturnValue({ changes: 0 }), all: vi.fn().mockReturnValue([]), get: vi.fn() }) }; } + // FNXC:PostgresCutover 2026-07-16-06:55: server setup probes the async + // layer, so this route double exposes the production-shaped backend seam. + getAsyncLayer = vi.fn(() => ({ + db: { + update: vi.fn(() => ({ + set: vi.fn(() => ({ + where: vi.fn(() => ({ returning: vi.fn(async () => []) })), + })), + })), + }, + })); getSettings = vi.fn(async () => this.getSettingsFast()); getSettingsFast = vi.fn(async (): Promise => ({ defaultProvider: "base-default-provider", diff --git a/packages/dashboard/src/routes/__tests__/agent-avatar-routes.test.ts b/packages/dashboard/src/routes/__tests__/agent-avatar-routes.test.ts index 27dda293bc..99477ebe4b 100644 --- a/packages/dashboard/src/routes/__tests__/agent-avatar-routes.test.ts +++ b/packages/dashboard/src/routes/__tests__/agent-avatar-routes.test.ts @@ -80,6 +80,9 @@ function createMockStore(fusionDir: string) { return { getRootDir: vi.fn().mockReturnValue(path.dirname(fusionDir)), getFusionDir: vi.fn().mockReturnValue(fusionDir), + // FNXC:PostgresCutover 2026-07-16-06:30: avatar routes construct their + // AgentStore through the backend accessor, so doubles retain that shape. + getAsyncLayer: vi.fn().mockReturnValue(undefined), listTasks: vi.fn().mockResolvedValue([]), searchTasks: vi.fn().mockResolvedValue([]), getSettings: vi.fn().mockResolvedValue({}), diff --git a/packages/dashboard/src/routes/__tests__/mission-workflow-triage-route.test.ts b/packages/dashboard/src/routes/__tests__/mission-workflow-triage-route.test.ts index 9de46776ea..1d3a67da18 100644 --- a/packages/dashboard/src/routes/__tests__/mission-workflow-triage-route.test.ts +++ b/packages/dashboard/src/routes/__tests__/mission-workflow-triage-route.test.ts @@ -5,12 +5,10 @@ FNXC:MissionWorkflows 2026-06-25-00:00: Mission route tests encode the invariant that both single-feature and slice bulk triage honor a supplied workflowId, preserve default inheritance when it is omitted, and reject invalid workflow selections before linking features. */ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, expect, it } from "vitest"; import express from "express"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; import { TaskStore } from "@fusion/core"; +import { createTaskStoreForTest, pgDescribe, type PgTestHarness } from "../../../../core/src/__test-utils__/pg-test-harness.js"; import type { WorkflowIr } from "@fusion/core"; import { createApiRoutes } from "../../routes.js"; import { request as REQUEST } from "../../test-request.js"; @@ -31,119 +29,120 @@ function linearIr(name: string): WorkflowIr { }; } -describe("mission triage routes workflowId", () => { +pgDescribe("mission triage routes workflowId", () => { + let harness: PgTestHarness; let store: TaskStore; - let rootDir: string; - let globalDir: string; let app: express.Express; beforeEach(async () => { - rootDir = mkdtempSync(join(tmpdir(), "mission-wf-route-root-")); - globalDir = mkdtempSync(join(tmpdir(), "mission-wf-route-global-")); - store = new TaskStore(rootDir, globalDir, { inMemoryDb: true }); - await store.init(); + // FNXC:PostgresCutover 2026-07-16-06:50: mission triage route assertions + // need a real async store because the in-memory SQLite fixture was removed. + harness = await createTaskStoreForTest(); + store = harness.store; app = express(); app.use(express.json()); app.use("/api", createApiRoutes(store)); }); - afterEach(() => { - store.close(); - rmSync(rootDir, { recursive: true, force: true }); - rmSync(globalDir, { recursive: true, force: true }); + afterEach(async () => { + await harness.teardown(); }); const post = (path: string, body: unknown) => REQUEST(app, "POST", path, JSON.stringify(body), { "content-type": "application/json" }); - function createMissionFeature(title = "Feature") { + async function createMissionFeature(title = "Feature") { const missionStore = store.getMissionStore(); - const mission = missionStore.createMission({ title: "Mission" }); - const milestone = missionStore.addMilestone(mission.id, { title: "Milestone" }); - const slice = missionStore.addSlice(milestone.id, { title: "Slice" }); - const feature = missionStore.addFeature(slice.id, { title }); + // FNXC:PostgresCutover 2026-07-16-06:55: mission store mutations and + // reads are asynchronous against PostgreSQL; await each linked entity. + const mission = await missionStore.createMission({ title: "Mission" }); + const milestone = await missionStore.addMilestone(mission.id, { title: "Milestone" }); + const slice = await missionStore.addSlice(milestone.id, { title: "Slice" }); + const feature = await missionStore.addFeature(slice.id, { title }); return { missionStore, mission, milestone, slice, feature }; } it("POST /missions/features/:featureId/triage assigns the supplied workflowId", async () => { const workflow = await store.createWorkflowDefinition({ name: "Mission Feature Route", ir: linearIr("mission-feature-route") }); - const { feature } = createMissionFeature("Route Feature"); + const { feature } = await createMissionFeature("Route Feature"); const res = await post(`/api/missions/features/${feature.id}/triage`, { workflowId: workflow.id }); expect(res.status).toBe(200); const taskId = (res.body as { taskId: string }).taskId; - expect(store.getTaskWorkflowSelection(taskId)?.workflowId).toBe(workflow.id); + expect((await store.getTaskWorkflowSelectionAsync(taskId))?.workflowId).toBe(workflow.id); }); it("POST /missions/slices/:sliceId/triage-all assigns the supplied workflowId to all created tasks", async () => { const workflow = await store.createWorkflowDefinition({ name: "Mission Slice Route", ir: linearIr("mission-slice-route") }); - const { missionStore, slice } = createMissionFeature("Route Feature 1"); - missionStore.addFeature(slice.id, { title: "Route Feature 2" }); + const { missionStore, slice } = await createMissionFeature("Route Feature 1"); + await missionStore.addFeature(slice.id, { title: "Route Feature 2" }); const res = await post(`/api/missions/slices/${slice.id}/triage-all`, { workflowId: workflow.id }); expect(res.status).toBe(200); const triaged = (res.body as { triaged: Array<{ taskId: string }> }).triaged; expect(triaged).toHaveLength(2); - expect(triaged.map((feature) => store.getTaskWorkflowSelection(feature.taskId)?.workflowId)).toEqual([workflow.id, workflow.id]); + expect(await Promise.all(triaged.map(async (feature) => ( + await store.getTaskWorkflowSelectionAsync(feature.taskId) + )?.workflowId))).toEqual([workflow.id, workflow.id]); }); it("omitting workflowId preserves default workflow inheritance for feature triage", async () => { const workflow = await store.createWorkflowDefinition({ name: "Mission Route Default", ir: linearIr("mission-route-default") }); await store.setDefaultWorkflowId(workflow.id); - const { feature } = createMissionFeature("Default Route Feature"); + const { feature } = await createMissionFeature("Default Route Feature"); const res = await post(`/api/missions/features/${feature.id}/triage`, {}); expect(res.status).toBe(200); - expect(store.getTaskWorkflowSelection((res.body as { taskId: string }).taskId)?.workflowId).toBe(workflow.id); + expect((await store.getTaskWorkflowSelectionAsync((res.body as { taskId: string }).taskId))?.workflowId).toBe(workflow.id); }); it("omitting workflowId preserves default workflow inheritance for slice bulk triage", async () => { const workflow = await store.createWorkflowDefinition({ name: "Mission Bulk Route Default", ir: linearIr("mission-bulk-route-default") }); await store.setDefaultWorkflowId(workflow.id); - const { slice } = createMissionFeature("Default Bulk Route Feature"); + const { slice } = await createMissionFeature("Default Bulk Route Feature"); const res = await post(`/api/missions/slices/${slice.id}/triage-all`, {}); expect(res.status).toBe(200); const triaged = (res.body as { triaged: Array<{ taskId: string }> }).triaged; expect(triaged).toHaveLength(1); - expect(store.getTaskWorkflowSelection(triaged[0].taskId)?.workflowId).toBe(workflow.id); + expect((await store.getTaskWorkflowSelectionAsync(triaged[0].taskId))?.workflowId).toBe(workflow.id); }); it("invalid workflowId type returns 400 without linking the feature", async () => { - const { missionStore, feature } = createMissionFeature("Invalid Workflow Feature"); + const { missionStore, feature } = await createMissionFeature("Invalid Workflow Feature"); const res = await post(`/api/missions/features/${feature.id}/triage`, { workflowId: 42 }); expect(res.status).toBe(400); - expect(missionStore.getFeature(feature.id)?.taskId).toBeUndefined(); + expect((await missionStore.getFeature(feature.id))?.taskId).toBeUndefined(); }); it("invalid workflowId type for slice bulk triage returns 400 without linking features", async () => { - const { missionStore, slice } = createMissionFeature("Invalid Bulk Workflow Feature"); + const { missionStore, slice } = await createMissionFeature("Invalid Bulk Workflow Feature"); const res = await post(`/api/missions/slices/${slice.id}/triage-all`, { workflowId: 42 }); expect(res.status).toBe(400); - expect(missionStore.listFeatures(slice.id).every((candidate) => !candidate.taskId)).toBe(true); + expect((await missionStore.listFeatures(slice.id)).every((candidate) => !candidate.taskId)).toBe(true); }); it("unknown workflowId returns a 4xx without linking feature or slice tasks", async () => { - const { missionStore, slice, feature } = createMissionFeature("Unknown Workflow Feature"); - missionStore.addFeature(slice.id, { title: "Unknown Workflow Slice Feature" }); + const { missionStore, slice, feature } = await createMissionFeature("Unknown Workflow Feature"); + await missionStore.addFeature(slice.id, { title: "Unknown Workflow Slice Feature" }); const single = await post(`/api/missions/features/${feature.id}/triage`, { workflowId: "WF-MISSING" }); expect(single.status).toBeGreaterThanOrEqual(400); expect(single.status).toBeLessThan(500); - expect(missionStore.getFeature(feature.id)?.taskId).toBeUndefined(); + expect((await missionStore.getFeature(feature.id))?.taskId).toBeUndefined(); const bulk = await post(`/api/missions/slices/${slice.id}/triage-all`, { workflowId: "WF-MISSING" }); expect(bulk.status).toBeGreaterThanOrEqual(400); expect(bulk.status).toBeLessThan(500); - expect(missionStore.listFeatures(slice.id).every((candidate) => !candidate.taskId)).toBe(true); + expect((await missionStore.listFeatures(slice.id)).every((candidate) => !candidate.taskId)).toBe(true); }); }); diff --git a/packages/dashboard/src/routes/__tests__/register-settings-memory-worktrunk.test.ts b/packages/dashboard/src/routes/__tests__/register-settings-memory-worktrunk.test.ts index c8a6910266..60065ff506 100644 --- a/packages/dashboard/src/routes/__tests__/register-settings-memory-worktrunk.test.ts +++ b/packages/dashboard/src/routes/__tests__/register-settings-memory-worktrunk.test.ts @@ -64,6 +64,9 @@ function createApp(pluginRunner?: Record) { getSettings: vi.fn(async () => ({ worktrunk: { enabled: false }, memoryDreamsEnabled: true })), getRootDir: vi.fn(() => "/tmp/project"), getFusionDir: vi.fn(() => "/tmp/project/.fusion"), + // FNXC:PostgresCutover 2026-07-16-06:30: all scoped-store doubles expose + // the backend accessor even when the mocked AgentStore does not consume it. + getAsyncLayer: vi.fn(() => undefined), updateSettings: vi.fn(async (patch: Record) => patch), }; @@ -73,7 +76,16 @@ function createApp(pluginRunner?: Record) { options: { pluginRunner }, store: {} as any, runtimeLogger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() } as any, - getProjectContext: vi.fn(async () => ({ store: scopedStore, projectId: "p1" })), + getProjectContext: vi.fn(async () => ({ + store: scopedStore, + // FNXC:PostgresCutover 2026-07-16-06:30: memory routes resolve the + // project identity through the engine before constructing async agents. + engine: { + getProjectId: () => "p1", + getRoutineStore: () => undefined, + }, + projectId: "p1", + })), rethrowAsApiError: (err: unknown) => { throw err; }, diff --git a/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.merge-advance-events.test.ts b/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.merge-advance-events.test.ts index 5b26e6d795..038f6c4c7a 100644 --- a/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.merge-advance-events.test.ts +++ b/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.merge-advance-events.test.ts @@ -25,7 +25,9 @@ describe("merge advance events route", () => { it("returns empty events when audit store is empty", async () => { const store: TaskStore = { getRootDir: vi.fn(() => process.cwd()), - getRunAuditEvents: vi.fn(() => []), + // FNXC:PostgresCutover 2026-07-16-06:30: API routes use the async + // run-audit accessor when backed by PostgreSQL, including test doubles. + getRunAuditEventsAsync: vi.fn(async () => []), } as unknown as TaskStore; const app = express(); @@ -75,7 +77,7 @@ describe("merge advance events route", () => { const store: TaskStore = { getRootDir: vi.fn(() => process.cwd()), - getRunAuditEvents, + getRunAuditEventsAsync: vi.fn(async (filters?: { mutationType?: string }) => getRunAuditEvents(filters)), } as unknown as TaskStore; const app = express(); @@ -154,7 +156,7 @@ describe("merge advance events route", () => { const store: TaskStore = { getRootDir: vi.fn(() => process.cwd()), - getRunAuditEvents: vi.fn((filters?: { mutationType?: string }) => { + getRunAuditEventsAsync: vi.fn(async (filters?: { mutationType?: string }) => { if (filters?.mutationType === "merge:integration-ref-advance") return [advance]; if (filters?.mutationType === "merge:auto-sync") return [clean, conflict, stale]; return []; @@ -193,7 +195,7 @@ describe("merge advance events route", () => { const store: TaskStore = { getRootDir: vi.fn(() => process.cwd()), - getRunAuditEvents: vi.fn((filters?: { mutationType?: string }) => + getRunAuditEventsAsync: vi.fn(async (filters?: { mutationType?: string }) => filters?.mutationType === "merge:integration-ref-advance" ? [advance] : []), } as unknown as TaskStore; @@ -208,7 +210,7 @@ describe("merge advance events route", () => { it("defaults limit to 20, clamps max to 100, rejects invalid limit", async () => { const store: TaskStore = { getRootDir: vi.fn(() => process.cwd()), - getRunAuditEvents: vi.fn(() => []), + getRunAuditEventsAsync: vi.fn(async () => []), } as unknown as TaskStore; const app = express(); @@ -223,9 +225,9 @@ describe("merge advance events route", () => { const badRes = await REQUEST(app, "GET", "/api/tasks/merge-advance-events?limit=abc"); expect(badRes.status).toBe(400); - const getRunAuditEvents = store.getRunAuditEvents as unknown as ReturnType; - const firstAdvanceCall = getRunAuditEvents.mock.calls.find((call) => call[0]?.mutationType === "merge:integration-ref-advance"); - const secondAdvanceCall = [...getRunAuditEvents.mock.calls].reverse().find((call) => call[0]?.mutationType === "merge:integration-ref-advance"); + const getRunAuditEventsAsync = store.getRunAuditEventsAsync as unknown as ReturnType; + const firstAdvanceCall = getRunAuditEventsAsync.mock.calls.find((call) => call[0]?.mutationType === "merge:integration-ref-advance"); + const secondAdvanceCall = [...getRunAuditEventsAsync.mock.calls].reverse().find((call) => call[0]?.mutationType === "merge:integration-ref-advance"); expect(firstAdvanceCall?.[0]?.limit).toBe(20); expect(secondAdvanceCall?.[0]?.limit).toBe(100); }); diff --git a/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.runtime-fallback.test.ts b/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.runtime-fallback.test.ts index b1f07d57f3..9d5ff062ec 100644 --- a/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.runtime-fallback.test.ts +++ b/packages/dashboard/src/routes/__tests__/register-task-workflow-routes.runtime-fallback.test.ts @@ -41,7 +41,9 @@ const createHarness = (taskState: any, events: RunAuditEvent[]) => { } return taskState; }), - getRunAuditEvents: vi.fn((options: Record = {}) => { + // FNXC:PostgresCutover 2026-07-16-06:30: runtime fallback status reads + // audit events asynchronously from the PostgreSQL-backed task store. + getRunAuditEventsAsync: vi.fn(async (options: Record = {}) => { let filtered = events; if (options.taskId) { filtered = filtered.filter((e) => e.taskId === options.taskId); diff --git a/packages/dashboard/src/routes/__tests__/tasks-overseer-controls.test.ts b/packages/dashboard/src/routes/__tests__/tasks-overseer-controls.test.ts index 2f2fed3646..bc48b3d3fb 100644 --- a/packages/dashboard/src/routes/__tests__/tasks-overseer-controls.test.ts +++ b/packages/dashboard/src/routes/__tests__/tasks-overseer-controls.test.ts @@ -10,36 +10,35 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import express from "express"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; import { TaskStore } from "@fusion/core"; +import { createTaskStoreForTest, pgDescribe, type PgTestHarness } from "../../../../core/src/__test-utils__/pg-test-harness.js"; import type { ProjectEngine } from "@fusion/engine"; import { createApiRoutes } from "../../routes.js"; import { request as REQUEST } from "../../test-request.js"; -describe("task-detail planner-overseer control routes", () => { +pgDescribe("task-detail planner-overseer control routes", () => { + let harness: PgTestHarness; let store: TaskStore; - let rootDir: string; - let globalDir: string; beforeEach(async () => { - rootDir = mkdtempSync(join(tmpdir(), "overseer-controls-root-")); - globalDir = mkdtempSync(join(tmpdir(), "overseer-controls-global-")); - store = new TaskStore(rootDir, globalDir, { inMemoryDb: true }); - await store.init(); + // FNXC:PostgresCutover 2026-07-16-06:30: planner-overseer route coverage + // uses an isolated real backend rather than the retired in-memory SQLite mode. + harness = await createTaskStoreForTest(); + store = harness.store; }); - afterEach(() => { - store.close(); - rmSync(rootDir, { recursive: true, force: true }); - rmSync(globalDir, { recursive: true, force: true }); + afterEach(async () => { + await harness.teardown(); }); function buildApp(engine: Partial | undefined): express.Express { const app = express(); app.use(express.json()); - app.use("/api", createApiRoutes(store, engine ? { engine: engine as unknown as ProjectEngine } : undefined)); + app.use("/api", createApiRoutes(store, engine ? { + // FNXC:PostgresCutover 2026-07-16-06:30: project-aware routes resolve + // engine services only when the test double advertises its project id. + engine: { getProjectId: () => "test-project", ...engine } as unknown as ProjectEngine, + } : undefined)); return app; } diff --git a/packages/dashboard/src/routes/__tests__/tasks-planner-overseer-state.test.ts b/packages/dashboard/src/routes/__tests__/tasks-planner-overseer-state.test.ts index 468a590fca..c3efd5f38b 100644 --- a/packages/dashboard/src/routes/__tests__/tasks-planner-overseer-state.test.ts +++ b/packages/dashboard/src/routes/__tests__/tasks-planner-overseer-state.test.ts @@ -6,38 +6,37 @@ // snapshot, omit entirely (byte-identical payload) otherwise, and never // fail the board load even when the accessor throws. -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { it, expect, beforeEach, afterEach } from "vitest"; import express from "express"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; import { TaskStore } from "@fusion/core"; +import { createTaskStoreForTest, pgDescribe, type PgTestHarness } from "../../../../core/src/__test-utils__/pg-test-harness.js"; import type { ProjectEngine } from "@fusion/engine"; import { createApiRoutes } from "../../routes.js"; import { request as REQUEST } from "../../test-request.js"; -describe("GET /tasks — plannerOverseerState enrichment", () => { +pgDescribe("GET /tasks — plannerOverseerState enrichment", () => { + let harness: PgTestHarness; let store: TaskStore; - let rootDir: string; - let globalDir: string; beforeEach(async () => { - rootDir = mkdtempSync(join(tmpdir(), "planner-overseer-state-root-")); - globalDir = mkdtempSync(join(tmpdir(), "planner-overseer-state-global-")); - store = new TaskStore(rootDir, globalDir, { inMemoryDb: true }); - await store.init(); + // FNXC:PostgresCutover 2026-07-16-06:50: planner state API coverage must + // exercise the PostgreSQL TaskStore rather than the removed SQLite mode. + harness = await createTaskStoreForTest(); + store = harness.store; }); - afterEach(() => { - store.close(); - rmSync(rootDir, { recursive: true, force: true }); - rmSync(globalDir, { recursive: true, force: true }); + afterEach(async () => { + await harness.teardown(); }); function buildApp(engine: Partial | undefined): express.Express { const app = express(); app.use(express.json()); - app.use("/api", createApiRoutes(store, engine ? { engine: engine as unknown as ProjectEngine } : undefined)); + app.use("/api", createApiRoutes(store, engine ? { + // FNXC:PostgresCutover 2026-07-16-06:55: project-aware route enrichment + // resolves engine state only when the test double supplies its project id. + engine: { getProjectId: () => "test-project", ...engine } as unknown as ProjectEngine, + } : undefined)); return app; } @@ -118,28 +117,29 @@ describe("GET /tasks — plannerOverseerState enrichment", () => { // modal's Overseer/Nudge controls read the snapshot from the full-detail // payload, not the list payload, so the detail route previously never // carried it and Nudge always showed the periodic-observation disabled copy. -describe("GET /tasks/:id — plannerOverseerState enrichment", () => { +pgDescribe("GET /tasks/:id — plannerOverseerState enrichment", () => { + let harness: PgTestHarness; let store: TaskStore; - let rootDir: string; - let globalDir: string; beforeEach(async () => { - rootDir = mkdtempSync(join(tmpdir(), "planner-overseer-state-detail-root-")); - globalDir = mkdtempSync(join(tmpdir(), "planner-overseer-state-detail-global-")); - store = new TaskStore(rootDir, globalDir, { inMemoryDb: true }); - await store.init(); + // FNXC:PostgresCutover 2026-07-16-06:50: detail-route state enrichment + // shares the isolated async-store fixture used by the board-list surface. + harness = await createTaskStoreForTest(); + store = harness.store; }); - afterEach(() => { - store.close(); - rmSync(rootDir, { recursive: true, force: true }); - rmSync(globalDir, { recursive: true, force: true }); + afterEach(async () => { + await harness.teardown(); }); function buildApp(engine: Partial | undefined): express.Express { const app = express(); app.use(express.json()); - app.use("/api", createApiRoutes(store, engine ? { engine: engine as unknown as ProjectEngine } : undefined)); + app.use("/api", createApiRoutes(store, engine ? { + // FNXC:PostgresCutover 2026-07-16-06:55: project-aware route enrichment + // resolves engine state only when the test double supplies its project id. + engine: { getProjectId: () => "test-project", ...engine } as unknown as ProjectEngine, + } : undefined)); return app; } diff --git a/packages/dashboard/src/routes/__tests__/workflow-validate-route.test.ts b/packages/dashboard/src/routes/__tests__/workflow-validate-route.test.ts index 47d073933e..4e49c2749e 100644 --- a/packages/dashboard/src/routes/__tests__/workflow-validate-route.test.ts +++ b/packages/dashboard/src/routes/__tests__/workflow-validate-route.test.ts @@ -2,10 +2,8 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import express from "express"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; import { TaskStore } from "@fusion/core"; +import { createTaskStoreForTest, pgDescribe, type PgTestHarness } from "../../../../core/src/__test-utils__/pg-test-harness.js"; import type { WorkflowIr } from "@fusion/core"; import { registerWorkflowRoutes } from "../register-workflow-routes.js"; import { ApiError, sendErrorResponse } from "../../api-error.js"; @@ -23,17 +21,16 @@ function linearIr(): WorkflowIr { } as WorkflowIr; } -describe("POST /api/workflows/validate", () => { +pgDescribe("POST /api/workflows/validate", () => { + let harness: PgTestHarness; let store: TaskStore; - let rootDir: string; - let globalDir: string; let app: express.Express; beforeEach(async () => { - rootDir = mkdtempSync(join(tmpdir(), "wf-validate-root-")); - globalDir = mkdtempSync(join(tmpdir(), "wf-validate-global-")); - store = new TaskStore(rootDir, globalDir, { inMemoryDb: true }); - await store.init(); + // FNXC:PostgresCutover 2026-07-16-06:30: validation routes exercise the + // production async persistence contract rather than removed SQLite fixtures. + harness = await createTaskStoreForTest(); + store = harness.store; app = express(); app.use(express.json()); @@ -54,9 +51,7 @@ describe("POST /api/workflows/validate", () => { }); afterEach(async () => { - await store.close?.(); - rmSync(rootDir, { recursive: true, force: true }); - rmSync(globalDir, { recursive: true, force: true }); + await harness.teardown(); }); async function userDefCount(): Promise { diff --git a/packages/dashboard/vitest.config.ts b/packages/dashboard/vitest.config.ts index 744dd72128..fb5cb34094 100644 --- a/packages/dashboard/vitest.config.ts +++ b/packages/dashboard/vitest.config.ts @@ -377,6 +377,10 @@ const qualityAppBackfillTests = ["app/**/*.test.{ts,tsx}"]; const backfillApiExclude = [ ...qualityApiTests, + // FNXC:DashboardDistArtifacts 2026-07-16-08:20: plugin-registry-dist asserts + // emitted server files and runs through the explicit test:build command after + // its dist bootstrap, rather than adding a full build to API backfill shards. + "src/__tests__/plugin-registry-dist.test.ts", ...skipListDashboardGlobs.filter((file) => file.startsWith("src/")), ]; const qualityApiBackfillTests = ["src/**/*.test.{ts,tsx}"];