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:
@@ -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([]),
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user