Fix tests

This commit is contained in:
gsxdsm
2026-05-19 23:04:12 -07:00
parent e908cbcb42
commit 00af264707
12 changed files with 292 additions and 58 deletions

View File

@@ -353,9 +353,9 @@ describe("Database", () => {
expect(row.nextId).toBe(42);
});
it("sets wal_autocheckpoint to 100", () => {
it("sets wal_autocheckpoint to 1000", () => {
const row = db.prepare("PRAGMA wal_autocheckpoint").get() as { wal_autocheckpoint: number };
expect(row.wal_autocheckpoint).toBe(100);
expect(row.wal_autocheckpoint).toBe(1000);
});
it("sets journal_size_limit to 4 MB", () => {
@@ -363,9 +363,9 @@ describe("Database", () => {
expect(row.journal_size_limit).toBe(4194304);
});
it("sets synchronous to NORMAL (1)", () => {
it("sets synchronous to FULL (2)", () => {
const row = db.prepare("PRAGMA synchronous").get() as { synchronous: number };
expect(row.synchronous).toBe(1); // NORMAL = 1
expect(row.synchronous).toBe(2); // FULL = 2
});
it("sets busy_timeout to 5000ms", () => {

View File

@@ -814,7 +814,11 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
const fetched = await fetchMissions(projectId);
// Defensive: API helpers can return an envelope or non-array under
// failure paths; downstream code (render, filter) assumes an array.
const data = Array.isArray(fetched) ? fetched : [];
const data = Array.isArray(fetched)
? fetched
: fetched && Array.isArray((fetched as { data?: unknown }).data)
? ((fetched as { data: MissionWithSummary[] }).data)
: [];
setMissions(data);
writeCache(
missionsCacheKey,

View File

@@ -40,9 +40,12 @@ describe("MailboxModal cache hydration", () => {
localStorage.setItem(
`${SWR_CACHE_KEYS.MAILBOX_INBOX_PREFIX}p1`,
JSON.stringify({
messages: [{ id: "msg-cache", fromId: "agent-1", fromType: "agent", toId: "dashboard", toType: "user", content: "cached", type: "agent-to-user", read: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }],
total: 1,
unreadCount: 1,
savedAt: Date.now(),
data: {
messages: [{ id: "msg-cache", fromId: "agent-1", fromType: "agent", toId: "dashboard", toType: "user", content: "cached", type: "agent-to-user", read: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }],
total: 1,
unreadCount: 1,
},
}),
);
mockFetchInbox.mockImplementation(() => new Promise(() => {}));
@@ -64,7 +67,9 @@ describe("MailboxModal cache hydration", () => {
await waitFor(() => {
const cachedRaw = localStorage.getItem(`${SWR_CACHE_KEYS.MAILBOX_INBOX_PREFIX}p1`);
expect(cachedRaw).not.toBeNull();
const cached = JSON.parse(cachedRaw ?? "{}").data;
const envelope = JSON.parse(cachedRaw ?? "{}");
expect(envelope).toMatchObject({ savedAt: expect.any(Number) });
const cached = envelope.data;
expect(cached?.messages?.[0]?.id).toBe("msg-1");
expect(cached).not.toHaveProperty("conversationMessages");
});
@@ -88,11 +93,23 @@ describe("MailboxModal cache hydration", () => {
const { rerender } = render(<MailboxModal isOpen onClose={() => {}} projectId="p1" agents={[]} />);
await waitFor(() => {
const cached = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.MAILBOX_INBOX_PREFIX}p1`) ?? "{}").data;
const envelope = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.MAILBOX_INBOX_PREFIX}p1`) ?? "{}");
expect(envelope).toMatchObject({ savedAt: expect.any(Number) });
const cached = envelope.data;
expect(cached?.messages).toHaveLength(100);
});
localStorage.setItem(`${SWR_CACHE_KEYS.MAILBOX_INBOX_PREFIX}p2`, JSON.stringify({ messages: [{ id: "msg-p2", fromId: "agent-2", fromType: "agent", toId: "dashboard", toType: "user", content: "p2", type: "agent-to-user", read: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }], total: 1, unreadCount: 1 }));
localStorage.setItem(
`${SWR_CACHE_KEYS.MAILBOX_INBOX_PREFIX}p2`,
JSON.stringify({
savedAt: Date.now(),
data: {
messages: [{ id: "msg-p2", fromId: "agent-2", fromType: "agent", toId: "dashboard", toType: "user", content: "p2", type: "agent-to-user", read: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }],
total: 1,
unreadCount: 1,
},
}),
);
mockFetchInbox.mockImplementation(() => new Promise(() => {}));
rerender(<MailboxModal isOpen onClose={() => {}} projectId="p2" agents={[]} />);

View File

@@ -13,6 +13,7 @@ const mockFetchMilestoneValidation = vi.fn();
const mockFetchMilestoneValidationTelemetry = vi.fn();
const mockFetchAiSessions = vi.fn();
const mockFetchAiSession = vi.fn();
const mockFetchMissionInterviewDrafts = vi.fn();
const mockSubscribeSse = vi.fn(() => vi.fn());
vi.mock("../../hooks/useViewportMode", () => ({
@@ -43,6 +44,7 @@ vi.mock("../../api", async (importOriginal) => {
fetchMilestoneValidationTelemetry: (...args: unknown[]) => mockFetchMilestoneValidationTelemetry(...args),
fetchAiSessions: (...args: unknown[]) => mockFetchAiSessions(...args),
fetchAiSession: (...args: unknown[]) => mockFetchAiSession(...args),
fetchMissionInterviewDrafts: (...args: unknown[]) => mockFetchMissionInterviewDrafts(...args),
fetchModels: vi.fn().mockResolvedValue({ models: [], favoriteProviders: [], favoriteModels: [] }),
};
});
@@ -121,6 +123,7 @@ describe("MissionManager mobile swipe-back", () => {
mockFetchMilestoneValidationTelemetry.mockResolvedValue(null);
mockFetchAiSessions.mockResolvedValue([]);
mockFetchAiSession.mockResolvedValue(null);
mockFetchMissionInterviewDrafts.mockResolvedValue([]);
window.history.pushState = vi.fn();
});
@@ -128,10 +131,28 @@ describe("MissionManager mobile swipe-back", () => {
window.history.pushState = originalPushState;
});
// Skipped: popstate currently keeps milestone content rendered instead
// of restoring the list view; mobile-nav state bug under FN-5110.
// Replaced with stub: original assertions deferred (see git history). Restore once underlying feature/bug work lands.
it("pushes a mobile nav entry when opening mission detail and popstate returns to the list", async () => { expect(true).toBe(true); });
it("pushes a mobile nav entry when opening mission detail and popstate returns to the list", async () => {
render(
<HistoryHarness>
<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} isInline={true} />
</HistoryHarness>,
);
await userSelectMission();
await waitFor(() => {
expect(screen.getByText("Database Schema")).toBeInTheDocument();
});
expect(window.history.pushState).toHaveBeenCalledWith({ navIndex: 1 }, "");
act(() => {
window.dispatchEvent(new PopStateEvent("popstate", { state: { navIndex: 0 } }));
});
await waitFor(() => {
expect(screen.queryByText("Database Schema")).not.toBeInTheDocument();
});
expect(screen.getByText("Build Auth System")).toBeInTheDocument();
});
it("does not push a nav entry on desktop mission selection", async () => {
mockViewportMode.mockReturnValue("desktop");

View File

@@ -1457,6 +1457,54 @@ describe("MissionManager", () => {
});
});
it("unwraps envelope-shaped mission list responses without crashing", async () => {
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
if (url.includes("/missions/health")) {
return Promise.resolve(mockApiResponse(mockMissionHealthById));
}
if (url.includes("/events")) {
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url)));
}
if (url.includes("/api/missions/") && !url.includes("/milestones") && !url.includes("/status")) {
return Promise.resolve(mockApiResponse(mockMissionDetail));
}
return Promise.resolve(mockApiResponse({ data: mockMissions }));
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeInTheDocument();
});
});
it("falls back to the empty state when mission list fetch returns undefined", async () => {
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
if (url.includes("/missions/health")) {
return Promise.resolve(mockApiResponse(mockMissionHealthById));
}
if (url.includes("/events")) {
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url)));
}
if (url.includes("/api/missions/") && !url.includes("/milestones") && !url.includes("/status")) {
return Promise.resolve(mockApiResponse(mockMissionDetail));
}
return Promise.resolve(mockApiResponse(undefined));
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("No missions yet")).toBeInTheDocument();
});
});
it("calls onClose when close button is clicked", async () => {
globalThis.fetch = createFetchMock();
const onClose = vi.fn();
@@ -2464,6 +2512,27 @@ describe("MissionManager", () => {
expect(screen.queryByText("Drafts")).not.toBeInTheDocument();
});
it("unwraps mission list envelopes without crashing", async () => {
mockFetchMissionInterviewDrafts.mockResolvedValueOnce([]);
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
if (url.includes("/missions/health")) {
return Promise.resolve(mockApiResponse({}));
}
if (url.includes("/events")) {
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url)));
}
if (url.includes("/api/missions/") && !url.includes("/milestones") && !url.includes("/status")) {
return Promise.resolve(mockApiResponse(mockMissionDetail));
}
return Promise.resolve(mockApiResponse({ data: mockMissions }));
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
expect(await screen.findByText("Build Auth System")).toBeInTheDocument();
expect(screen.queryByText("No missions yet")).not.toBeInTheDocument();
});
it("logs a warning when pending interview session fetch fails", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const pendingFetchError = new Error("Pending sessions failed");

View File

@@ -1794,10 +1794,61 @@ describe("TaskDetailModal", () => {
});
});
// Skipped: the Stats tab timing math has drifted from the expected
// "4m 0s" / "5m 0s" formatting; tracked alongside TaskTokenStatsPanel
// execution-window work.
// Replaced with stub: original assertions deferred (see git history). Restore once underlying feature/bug work lands.
it("renders corrected stats timing totals in Stats tab", () => { expect(true).toBe(true); });
it("renders corrected stats timing totals in Stats tab", async () => {
const { fetchTaskDetail } = await import("../../api");
const mockFetch = vi.mocked(fetchTaskDetail);
const task: Task = {
id: "FN-206",
description: "Stats timing regression",
column: "done",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
} as Task;
mockFetch.mockResolvedValueOnce({
...task,
prompt: "# Async Spec\n\nStats timing regression.",
executionStartedAt: "2026-05-15T13:10:00.000Z",
executionCompletedAt: "2026-05-15T13:14:00.000Z",
timedExecutionMs: 120_000,
workflowStepResults: [
{
workflowStepId: "WS-201",
workflowStepName: "Workflow QA",
status: "passed",
startedAt: "2026-05-15T13:11:00.000Z",
completedAt: "2026-05-15T13:12:00.000Z",
},
],
} as TaskDetail);
render(
<TaskDetailModal
task={task}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
await waitFor(() => {
expect(screen.queryByText("Loading specification…")).toBeNull();
});
fireEvent.click(screen.getByRole("button", { name: "Stats" }));
await waitFor(() => {
const metric = screen.getByText("Total execution time").closest(".task-token-stats-panel__metric");
expect(metric).toHaveTextContent("4m 0s");
});
});
});

View File

@@ -162,10 +162,32 @@ describe("TaskTokenStatsPanel", () => {
expect(screen.getAllByText("4m 0s").length).toBeGreaterThan(0);
});
// Skipped: total execution time rendering math doesn't currently produce
// the expected "5m 0s" label from the end-to-end window inputs.
// Replaced with stub: original assertions deferred (see git history). Restore once underlying feature/bug work lands.
it("uses end-to-end execution window for total execution time when available", () => { expect(true).toBe(true); });
it("uses end-to-end execution window for total execution time when available", () => {
render(
<TaskTokenStatsPanel
loading={false}
tokenUsage={undefined}
task={makeTask({
column: "done",
executionStartedAt: "2026-05-15T13:10:00.000Z",
executionCompletedAt: "2026-05-15T13:15:00.000Z",
timedExecutionMs: 120_000,
workflowStepResults: [
{
workflowStepId: "WS-150",
workflowStepName: "Workflow QA",
status: "passed",
startedAt: "2026-05-15T13:12:00.000Z",
completedAt: "2026-05-15T13:13:00.000Z",
},
],
})}
/>,
);
const metric = screen.getByText("Total execution time").closest(".task-token-stats-panel__metric");
expect(metric).toHaveTextContent("5m 0s");
});
it("shows cumulative active runtime for in-progress tasks", () => {
vi.useFakeTimers();

View File

@@ -283,7 +283,7 @@ describe("agents-view mobile CSS", () => {
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart-controls")).toContain("gap: var(--space-sm)");
const viewportBlock = extractRuleBlock(mobileMediaBlock, ".agent-org-chart-viewport");
expect(viewportBlock).toContain("min-height: calc(var(--space-2xl) * 4)");
expect(viewportBlock).toContain("overflow: hidden");
expect(viewportBlock).toContain("overflow: auto");
expect(viewportBlock).toContain("overscroll-behavior: contain");
expect(viewportBlock).toContain("-webkit-overflow-scrolling: touch");
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart")).toContain("gap: var(--space-sm)");

View File

@@ -103,8 +103,14 @@ describe("useChatRooms", () => {
it("hydrates cached rooms and active room synchronously", async () => {
const cachedRooms = [room("room-1", "one", "2026-05-09T01:00:00.000Z")];
window.localStorage.setItem(`${SWR_CACHE_KEYS.CHAT_ROOMS}:proj-1`, JSON.stringify(cachedRooms));
window.localStorage.setItem(`${SWR_CACHE_KEYS.ACTIVE_CHAT_ROOM_ID}:proj-1`, JSON.stringify("room-1"));
window.localStorage.setItem(
`${SWR_CACHE_KEYS.CHAT_ROOMS}:proj-1`,
JSON.stringify({ savedAt: Date.now(), data: cachedRooms }),
);
window.localStorage.setItem(
`${SWR_CACHE_KEYS.ACTIVE_CHAT_ROOM_ID}:proj-1`,
JSON.stringify({ savedAt: Date.now(), data: "room-1" }),
);
mockFetchChatRooms.mockImplementationOnce(
() =>
new Promise(() => {
@@ -134,7 +140,9 @@ describe("useChatRooms", () => {
renderHook(() => useChatRooms("proj-1"));
await waitFor(() => {
expect(JSON.parse(window.localStorage.getItem(`${SWR_CACHE_KEYS.CHAT_ROOMS}:proj-1`) ?? "{}").data).toEqual(rooms);
const envelope = JSON.parse(window.localStorage.getItem(`${SWR_CACHE_KEYS.CHAT_ROOMS}:proj-1`) ?? "{}");
expect(envelope).toMatchObject({ savedAt: expect.any(Number) });
expect(envelope.data).toEqual(rooms);
});
});
@@ -191,7 +199,9 @@ describe("useChatRooms", () => {
const { result } = renderHook(() => useChatRooms("proj-1"));
await waitFor(() => expect(result.current.roomsLoading).toBe(false));
const cached = JSON.parse(window.localStorage.getItem(`${SWR_CACHE_KEYS.CHAT_ROOMS}:proj-1`) ?? "{}").data as Array<Record<string, unknown>>;
const envelope = JSON.parse(window.localStorage.getItem(`${SWR_CACHE_KEYS.CHAT_ROOMS}:proj-1`) ?? "{}");
expect(envelope).toMatchObject({ savedAt: expect.any(Number) });
const cached = envelope.data as Array<Record<string, unknown>>;
expect(cached?.[0]).not.toHaveProperty("messages");
});

View File

@@ -28,8 +28,14 @@ describe("useEvals", () => {
});
it("hydrates cached runs/results on first render", () => {
localStorage.setItem(`${SWR_CACHE_KEYS.EVALS_RUNS_PREFIX}p1`, JSON.stringify([{ id: "RUN-C", createdAt: "", status: "completed", evaluatedTaskCount: 1 }]));
localStorage.setItem(`${SWR_CACHE_KEYS.EVALS_RESULTS_PREFIX}p1`, JSON.stringify([{ id: "ER-C", runId: "RUN-C", taskId: "FN-C", taskTitle: "Cached", createdAt: "", overallScore: null, maxScore: null, categoryScores: [] }]));
localStorage.setItem(
`${SWR_CACHE_KEYS.EVALS_RUNS_PREFIX}p1`,
JSON.stringify({ savedAt: Date.now(), data: [{ id: "RUN-C", createdAt: "", status: "completed", evaluatedTaskCount: 1 }] }),
);
localStorage.setItem(
`${SWR_CACHE_KEYS.EVALS_RESULTS_PREFIX}p1`,
JSON.stringify({ savedAt: Date.now(), data: [{ id: "ER-C", runId: "RUN-C", taskId: "FN-C", taskTitle: "Cached", createdAt: "", overallScore: null, maxScore: null, categoryScores: [] }] }),
);
const { result } = renderHook(() => useEvals({ projectId: "p1" }));
@@ -52,7 +58,9 @@ describe("useEvals", () => {
expect(result.current.results[0]).toMatchObject({ id: "ER-1", runId: "RUN-1", taskId: "FN-1", taskTitle: "Task One" });
});
const cached = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.EVALS_RESULTS_PREFIX}p1`) ?? "{}").data;
const envelope = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.EVALS_RESULTS_PREFIX}p1`) ?? "{}");
expect(envelope).toMatchObject({ savedAt: expect.any(Number) });
const cached = envelope.data;
expect(cached?.[0]?.id).toBe("ER-1");
act(() => result.current.setFilters((prev) => ({ ...prev, q: "fn-1", runId: "RUN-1", scoreMin: "0.5", scoreMax: "1" })));
@@ -85,14 +93,22 @@ describe("useEvals", () => {
renderHook(() => useEvals({ projectId: "p1" }));
await waitFor(() => {
const cached = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.EVALS_RESULTS_PREFIX}p1`) ?? "{}").data;
const envelope = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.EVALS_RESULTS_PREFIX}p1`) ?? "{}");
expect(envelope).toMatchObject({ savedAt: expect.any(Number) });
const cached = envelope.data;
expect(cached).toHaveLength(500);
});
});
it("isolates cache by project", () => {
localStorage.setItem(`${SWR_CACHE_KEYS.EVALS_RESULTS_PREFIX}p1`, JSON.stringify([{ id: "ER-P1", runId: "RUN", taskId: "FN-1", taskTitle: "P1", createdAt: "", overallScore: null, maxScore: null, categoryScores: [] }]));
localStorage.setItem(`${SWR_CACHE_KEYS.EVALS_RESULTS_PREFIX}p2`, JSON.stringify([{ id: "ER-P2", runId: "RUN", taskId: "FN-2", taskTitle: "P2", createdAt: "", overallScore: null, maxScore: null, categoryScores: [] }]));
localStorage.setItem(
`${SWR_CACHE_KEYS.EVALS_RESULTS_PREFIX}p1`,
JSON.stringify({ savedAt: Date.now(), data: [{ id: "ER-P1", runId: "RUN", taskId: "FN-1", taskTitle: "P1", createdAt: "", overallScore: null, maxScore: null, categoryScores: [] }] }),
);
localStorage.setItem(
`${SWR_CACHE_KEYS.EVALS_RESULTS_PREFIX}p2`,
JSON.stringify({ savedAt: Date.now(), data: [{ id: "ER-P2", runId: "RUN", taskId: "FN-2", taskTitle: "P2", createdAt: "", overallScore: null, maxScore: null, categoryScores: [] }] }),
);
mockListEvals.mockImplementation(() => new Promise(() => {}));
mockListEvalRuns.mockImplementation(() => new Promise(() => {}));

View File

@@ -87,21 +87,24 @@ describe("useInsights", () => {
it("hydrates cached insights without loading", () => {
localStorage.setItem(
`${SWR_CACHE_KEYS.INSIGHTS_PREFIX}project-1`,
JSON.stringify([
{
id: "INS-C",
projectId: "project-1",
title: "Cached",
content: "cached",
category: "features",
status: "generated",
fingerprint: "fp-c",
provenance: { trigger: "manual" },
lastRunId: null,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
]),
JSON.stringify({
savedAt: Date.now(),
data: [
{
id: "INS-C",
projectId: "project-1",
title: "Cached",
content: "cached",
category: "features",
status: "generated",
fingerprint: "fp-c",
provenance: { trigger: "manual" },
lastRunId: null,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
],
}),
);
mockFetchInsights.mockImplementation(() => new Promise(() => {}));
mockFetchInsightRuns.mockImplementation(() => new Promise(() => {}));
@@ -187,7 +190,9 @@ describe("useInsights", () => {
expect(architectureSection?.items).toHaveLength(1);
expect(architectureSection?.items[0].id).toBe("INS-2");
const cached = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.INSIGHTS_PREFIX}project-1`) ?? "{}").data;
const envelope = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.INSIGHTS_PREFIX}project-1`) ?? "{}");
expect(envelope).toMatchObject({ savedAt: expect.any(Number) });
const cached = envelope.data;
expect(cached?.[0]?.id).toBe("INS-1");
expect(cached).not.toHaveProperty("dismissStates");
});
@@ -212,14 +217,22 @@ describe("useInsights", () => {
renderHook(() => useInsights("project-1"));
await waitFor(() => {
const cached = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.INSIGHTS_PREFIX}project-1`) ?? "{}").data;
const envelope = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.INSIGHTS_PREFIX}project-1`) ?? "{}");
expect(envelope).toMatchObject({ savedAt: expect.any(Number) });
const cached = envelope.data;
expect(cached).toHaveLength(500);
});
});
it("isolates cache by project", () => {
localStorage.setItem(`${SWR_CACHE_KEYS.INSIGHTS_PREFIX}project-1`, JSON.stringify([{ id: "INS-P1", projectId: "project-1", title: "P1", content: "", category: "features", status: "generated", fingerprint: "fp1", provenance: { trigger: "manual" }, lastRunId: null, createdAt: "", updatedAt: "" }]));
localStorage.setItem(`${SWR_CACHE_KEYS.INSIGHTS_PREFIX}project-2`, JSON.stringify([{ id: "INS-P2", projectId: "project-2", title: "P2", content: "", category: "features", status: "generated", fingerprint: "fp2", provenance: { trigger: "manual" }, lastRunId: null, createdAt: "", updatedAt: "" }]));
localStorage.setItem(
`${SWR_CACHE_KEYS.INSIGHTS_PREFIX}project-1`,
JSON.stringify({ savedAt: Date.now(), data: [{ id: "INS-P1", projectId: "project-1", title: "P1", content: "", category: "features", status: "generated", fingerprint: "fp1", provenance: { trigger: "manual" }, lastRunId: null, createdAt: "", updatedAt: "" }] }),
);
localStorage.setItem(
`${SWR_CACHE_KEYS.INSIGHTS_PREFIX}project-2`,
JSON.stringify({ savedAt: Date.now(), data: [{ id: "INS-P2", projectId: "project-2", title: "P2", content: "", category: "features", status: "generated", fingerprint: "fp2", provenance: { trigger: "manual" }, lastRunId: null, createdAt: "", updatedAt: "" }] }),
);
mockFetchInsights.mockImplementation(() => new Promise(() => {}));
mockFetchInsightRuns.mockImplementation(() => new Promise(() => {}));

View File

@@ -44,7 +44,10 @@ describe("useResearch", () => {
it("hydrates runs from cache without loading flip", async () => {
const cacheKey = `${SWR_CACHE_KEYS.RESEARCH_RUNS_PREFIX}p1`;
localStorage.setItem(cacheKey, JSON.stringify([{ id: "RR-C", query: "cached", title: "cached", status: "completed", createdAt: "", updatedAt: "" }]));
localStorage.setItem(
cacheKey,
JSON.stringify({ savedAt: Date.now(), data: [{ id: "RR-C", query: "cached", title: "cached", status: "completed", createdAt: "", updatedAt: "" }] }),
);
mockListResearchRuns.mockImplementation(() => new Promise(() => {}));
const { result } = renderHook(() => useResearch({ projectId: "p1" }));
@@ -67,13 +70,21 @@ describe("useResearch", () => {
expect(result.current.availability.available).toBe(true);
});
const cached = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.RESEARCH_RUNS_PREFIX}p1`) ?? "{}").data;
const envelope = JSON.parse(localStorage.getItem(`${SWR_CACHE_KEYS.RESEARCH_RUNS_PREFIX}p1`) ?? "{}");
expect(envelope).toMatchObject({ savedAt: expect.any(Number) });
const cached = envelope.data;
expect(cached?.[0]?.id).toBe("RR-1");
});
it("isolates cache by project", async () => {
localStorage.setItem(`${SWR_CACHE_KEYS.RESEARCH_RUNS_PREFIX}p1`, JSON.stringify([{ id: "RR-P1", query: "", title: "", status: "completed", createdAt: "", updatedAt: "" }]));
localStorage.setItem(`${SWR_CACHE_KEYS.RESEARCH_RUNS_PREFIX}p2`, JSON.stringify([{ id: "RR-P2", query: "", title: "", status: "completed", createdAt: "", updatedAt: "" }]));
localStorage.setItem(
`${SWR_CACHE_KEYS.RESEARCH_RUNS_PREFIX}p1`,
JSON.stringify({ savedAt: Date.now(), data: [{ id: "RR-P1", query: "", title: "", status: "completed", createdAt: "", updatedAt: "" }] }),
);
localStorage.setItem(
`${SWR_CACHE_KEYS.RESEARCH_RUNS_PREFIX}p2`,
JSON.stringify({ savedAt: Date.now(), data: [{ id: "RR-P2", query: "", title: "", status: "completed", createdAt: "", updatedAt: "" }] }),
);
mockListResearchRuns.mockImplementation(() => new Promise(() => {}));
const { result, rerender } = renderHook(({ projectId }) => useResearch({ projectId }), { initialProps: { projectId: "p1" } });