fix(FUX-042): use in-scope store resolver for reliability endpoints

The reliability GET/reset handlers referenced getScopedStore, which is
defined inside setupBadgeWebSocket and is not visible in the createServer
scope where these handlers live — so the scoping change did not typecheck.
Switch to the in-scope resolveProjectScopedStore helper (used by the other
realtime endpoints), which also routes through engineManager for correct
per-project resolution.

Guard store resolution with try/catch returning a targeted 500, mirroring
the project SSE handler, instead of falling through to the generic error
handler. Add project-scoping regression tests: GET reads the project store,
GET without projectId falls back to root, and reset writes the project store.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-10 16:16:45 -07:00
parent 47f889a0da
commit ed163cfc81
2 changed files with 100 additions and 2 deletions

View File

@@ -1222,6 +1222,82 @@ describe("createServer health and headless mode", () => {
vi.useRealTimers();
});
// FNXC:ReliabilityHealth 2026-07-10-11:15:
// FUX-042 regression: reliability GET/reset must read/write the per-project store, not the shared root store.
// Enumerated surfaces: (1) GET with projectId reads project store, (2) GET without projectId falls back to root, (3) POST reset with projectId writes project store.
it("FUX-042: GET /api/health/reliability reads the project-scoped store, not the root store", async () => {
const rootStore = createMockStore({
getSettings: vi.fn().mockResolvedValue({ reliabilityStatsResetAt: "2026-01-01T00:00:00.000Z" }),
getRunAuditEvents: vi.fn().mockReturnValue([]),
});
const projectStore = createMockStore({
getSettings: vi.fn().mockResolvedValue({ reliabilityStatsResetAt: "2026-05-10T00:00:00.000Z" }),
getRunAuditEvents: vi.fn().mockReturnValue([]),
});
const getEngine = vi.fn((projectId: string) =>
projectId === "proj_a" ? { getTaskStore: vi.fn(() => projectStore) } : undefined,
);
const app = createServer(rootStore, {
engineManager: { getEngine } as unknown as import("@fusion/engine").ProjectEngineManager,
});
const res = await GET(app, "/api/health/reliability?projectId=proj_a");
expect(res.status).toBe(200);
expect((res.body as { resetAt: string }).resetAt).toBe("2026-05-10T00:00:00.000Z");
expect(projectStore.getSettings).toHaveBeenCalled();
expect(projectStore.getRunAuditEvents).toHaveBeenCalled();
expect(rootStore.getSettings).not.toHaveBeenCalled();
expect(rootStore.getRunAuditEvents).not.toHaveBeenCalled();
});
it("FUX-042: GET /api/health/reliability without projectId falls back to the root store", async () => {
const rootStore = createMockStore({
getSettings: vi.fn().mockResolvedValue({ reliabilityStatsResetAt: "2026-01-01T00:00:00.000Z" }),
getRunAuditEvents: vi.fn().mockReturnValue([]),
});
const projectStore = createMockStore({
getSettings: vi.fn().mockResolvedValue({ reliabilityStatsResetAt: "2026-05-10T00:00:00.000Z" }),
getRunAuditEvents: vi.fn().mockReturnValue([]),
});
const getEngine = vi.fn((projectId: string) =>
projectId === "proj_a" ? { getTaskStore: vi.fn(() => projectStore) } : undefined,
);
const app = createServer(rootStore, {
engineManager: { getEngine } as unknown as import("@fusion/engine").ProjectEngineManager,
});
const res = await GET(app, "/api/health/reliability");
expect(res.status).toBe(200);
expect((res.body as { resetAt: string }).resetAt).toBe("2026-01-01T00:00:00.000Z");
expect(rootStore.getSettings).toHaveBeenCalled();
expect(projectStore.getSettings).not.toHaveBeenCalled();
});
it("FUX-042: POST /api/health/reliability/reset writes the project-scoped store, not the root store", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-13T12:00:00.000Z"));
const rootStore = createMockStore({ updateSettings: vi.fn().mockResolvedValue({}) });
const projectStore = createMockStore({ updateSettings: vi.fn().mockResolvedValue({}) });
const getEngine = vi.fn((projectId: string) =>
projectId === "proj_a" ? { getTaskStore: vi.fn(() => projectStore) } : undefined,
);
const app = createServer(rootStore, {
engineManager: { getEngine } as unknown as import("@fusion/engine").ProjectEngineManager,
});
const res = await REQUEST(app, "POST", "/api/health/reliability/reset?projectId=proj_a");
expect(res.status).toBe(200);
expect(res.body).toEqual({ resetAt: "2026-05-13T12:00:00.000Z" });
expect(projectStore.updateSettings).toHaveBeenCalledWith({ reliabilityStatsResetAt: "2026-05-13T12:00:00.000Z" });
expect(rootStore.updateSettings).not.toHaveBeenCalled();
vi.useRealTimers();
});
it("rejects invalid windowDays values", async () => {
const app = createServer(createMockStore({
getActivityLog: vi.fn().mockResolvedValue([]),

View File

@@ -1654,7 +1654,19 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
app.get("/api/health/reliability", async (req, res) => {
const projectId = getProjectIdFromRequest(req);
const scopedStore = projectId ? await getScopedStore(projectId) : store;
/*
FNXC:ReliabilityHealth 2026-07-10-11:15:
Reliability GET/reset must read/write the per-project store so multi-project servers report per-project stats.
Use the in-scope resolveProjectScopedStore helper (createServer scope) — NOT the badge-websocket getScopedStore, which lives in a different function and is not visible here.
Store creation can fail (getOrCreateProjectStore throwing on a DB error); mirror the project SSE handler and return a targeted 500 instead of letting the failure fall through to the generic Express error handler with a vague message.
*/
let scopedStore: TaskStore;
try {
scopedStore = await resolveProjectScopedStore(projectId);
} catch (err: unknown) {
sendErrorResponse(res, 500, err instanceof Error ? err.message : "Failed to resolve project store");
return;
}
const rawWindowDays = req.query.windowDays;
const parsedWindowDays = rawWindowDays === undefined ? 7 : Number.parseInt(String(rawWindowDays), 10);
@@ -1760,7 +1772,17 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
app.post("/api/health/reliability/reset", async (req, res) => {
const projectId = getProjectIdFromRequest(req);
const scopedStore = projectId ? await getScopedStore(projectId) : store;
/*
FNXC:ReliabilityHealth 2026-07-10-11:15:
Same in-scope resolveProjectScopedStore + guard as the GET handler so the reset writes reliabilityStatsResetAt to the per-project store and a store-creation failure returns a targeted 500 rather than a vague generic error.
*/
let scopedStore: TaskStore;
try {
scopedStore = await resolveProjectScopedStore(projectId);
} catch (err: unknown) {
sendErrorResponse(res, 500, err instanceof Error ? err.message : "Failed to resolve project store");
return;
}
const resetAt = new Date().toISOString();
await scopedStore.updateSettings({ reliabilityStatsResetAt: resetAt });
res.json({ resetAt });