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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-16 08:06:49 -07:00
parent 748cf9370d
commit fc9fb21470
20 changed files with 307 additions and 213 deletions

View File

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

View File

@@ -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;
}

View File

@@ -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();
}
});
});
});

View File

@@ -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<string, unknown>) => {
run = { ...run, projectId, ...input } as typeof run;
return run;
}),
updateRun: vi.fn(async (_id: string, patch: Record<string, unknown>) => {
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<typeof createInsightsRouter> & { __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 });
}
});
});

View File

@@ -13,6 +13,12 @@ async function readDashboardFile(relativePath: string): Promise<string> {
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");

View File

@@ -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);

View File

@@ -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();

View File

@@ -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<string, string> = {
};
const savedEnv: Record<string, string | undefined> = {};
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<string, string>) {
@@ -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<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
}
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();

View File

@@ -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 () => {

View File

@@ -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<import("node:http").Server>((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<void>((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<void>((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<void>((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"), "<!doctype html><html><head></head><body><div id=\"root\"></div></body></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<void>((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-</script>-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<void>((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
await teardown();
}
});
});

View File

@@ -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<Settings> => ({
defaultProvider: "base-default-provider",

View File

@@ -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({}),

View File

@@ -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);
});
});

View File

@@ -64,6 +64,9 @@ function createApp(pluginRunner?: Record<string, unknown>) {
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<string, unknown>) => patch),
};
@@ -73,7 +76,16 @@ function createApp(pluginRunner?: Record<string, unknown>) {
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;
},

View File

@@ -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<typeof vi.fn>;
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<typeof vi.fn>;
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);
});

View File

@@ -41,7 +41,9 @@ const createHarness = (taskState: any, events: RunAuditEvent[]) => {
}
return taskState;
}),
getRunAuditEvents: vi.fn((options: Record<string, unknown> = {}) => {
// 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<string, unknown> = {}) => {
let filtered = events;
if (options.taskId) {
filtered = filtered.filter((e) => e.taskId === options.taskId);

View File

@@ -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<ProjectEngine> | 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;
}

View File

@@ -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<ProjectEngine> | 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<ProjectEngine> | 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;
}

View File

@@ -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<number> {

View File

@@ -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}"];