feat(confidence-engine): generate investigation report on demand
This commit is contained in:
@@ -274,3 +274,168 @@ describe("Restart confirmation flow (v0.57)", () => {
|
||||
expect(mockClearStorage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Report lifecycle — v0.58 first Report generation (no report → generate → persist)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Report page lifecycle — v0.58", () => {
|
||||
let ReportPage;
|
||||
|
||||
beforeEach(async () => {
|
||||
setMockSnapshot(makeSnapshot());
|
||||
global.fetch = vi.fn();
|
||||
const mod = await import("@/app/investigations/[id]/report/page.jsx");
|
||||
ReportPage = mod.default;
|
||||
});
|
||||
|
||||
afterEach(async () => cleanup());
|
||||
|
||||
// 1. persisted Report → 0 overview requests
|
||||
it("renders persisted report without calling /api/cases/overview", async () => {
|
||||
const persistSnap = makeSnapshot({
|
||||
investigationReport: { understanding: "Persisted summary.", hasPlausibleInterpretations: false },
|
||||
});
|
||||
setMockSnapshot(persistSnap);
|
||||
|
||||
render(React.createElement(ReportPage));
|
||||
await screen.findByText(/Persisted summary\./i);
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// 2. no Report → skeleton / loading state appears
|
||||
it("shows What we understand heading when no report persists", async () => {
|
||||
setMockSnapshot(makeSnapshot());
|
||||
render(React.createElement(ReportPage));
|
||||
expect(await screen.findByText(/What we understand/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// 3. no Report → exactly 1 overview request
|
||||
it("makes exactly one POST /api/cases/overview when report absent", async () => {
|
||||
global.fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true, understanding: "Summary.", plausibleInterpretations: "None." }),
|
||||
});
|
||||
|
||||
setMockSnapshot(makeSnapshot());
|
||||
render(React.createElement(ReportPage));
|
||||
await screen.findByText(/Summary\./i);
|
||||
const calls = global.fetch.mock.calls.filter((c) => c[0] === "/api/cases/overview");
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
// 4. success → understanding renders
|
||||
it("renders understanding content after successful generation", async () => {
|
||||
global.fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true, understanding: "First report summary.", plausibleInterpretations: "None." }),
|
||||
});
|
||||
|
||||
setMockSnapshot(makeSnapshot());
|
||||
render(React.createElement(ReportPage));
|
||||
expect(await screen.findByText(/First report summary\./i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// 5. success → investigationReport persists
|
||||
it("persists investigationReport via canonical storage after generation", async () => {
|
||||
setMockSnapshot(makeSnapshot());
|
||||
|
||||
global.fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true, understanding: "Persisted via save.", plausibleInterpretations: "None." }),
|
||||
});
|
||||
|
||||
render(React.createElement(ReportPage));
|
||||
await screen.findByText(/Persisted via save\./i);
|
||||
|
||||
// saveInvestigation mock does not write to localStorage.
|
||||
// We verify persistence indirectly: the report rendered on screen confirms
|
||||
// the component received data AND the useEffect callback invoked saveInvestigation.
|
||||
// The storage unit tests (investigation-storage.test.js) verify saveInvestigation
|
||||
// writes correctly to localStorage — here we verify the integration path works.
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
"/api/cases/overview",
|
||||
expect.objectContaining({ method: "POST" }),
|
||||
);
|
||||
});
|
||||
|
||||
// 6. existing canonical fields preserved after generation persist
|
||||
it("preserves canonical investigation fields after report persists", async () => {
|
||||
setMockSnapshot(makeSnapshot());
|
||||
const scenarioOriginal = makeSnapshot().scenario;
|
||||
global.fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true, understanding: "Field check.", plausibleInterpretations: "None." }),
|
||||
});
|
||||
|
||||
render(React.createElement(ReportPage));
|
||||
await screen.findByText(/Field check\./i);
|
||||
|
||||
// Verify the snapshot in localStorage still contains original fields
|
||||
const storedRaw = localStorage.getItem("confidence-engine-investigation");
|
||||
expect(storedRaw).toBeTruthy();
|
||||
const stored = JSON.parse(storedRaw);
|
||||
expect(stored.scenario).toBe(scenarioOriginal);
|
||||
});
|
||||
|
||||
// 7. absent plausible interpretations → section omitted
|
||||
it("omits What remains plausible when hasPlausibleInterpretations is false", async () => {
|
||||
setMockSnapshot(makeSnapshot({
|
||||
investigationReport: { understanding: "Summary.", hasPlausibleInterpretations: false },
|
||||
}));
|
||||
|
||||
render(React.createElement(ReportPage));
|
||||
await screen.findByText(/Summary\./i);
|
||||
expect(screen.queryByText(/What remains plausible/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// 8. present plausible interpretations → separate section renders
|
||||
it("renders What remains plausible when hasPlausibleInterpretations is true and content exists", async () => {
|
||||
setMockSnapshot(makeSnapshot({
|
||||
investigationReport: { understanding: "Summary.", hasPlausibleInterpretations: true, plausibleInterpretations: "One alternative explanation." },
|
||||
}));
|
||||
|
||||
render(React.createElement(ReportPage));
|
||||
await screen.findByText(/Summary\./i);
|
||||
expect(await screen.findByText(/What remains plausible/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/One alternative explanation\./i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// 9. failed request → no partial report persists
|
||||
it("does not persist investigationReport on generation failure", async () => {
|
||||
setMockSnapshot(makeSnapshot());
|
||||
global.fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: false, stage: "internal", error: "Internal server error" }),
|
||||
});
|
||||
|
||||
render(React.createElement(ReportPage));
|
||||
await screen.findByText(/Report generation failed/i);
|
||||
|
||||
// Verify no report was saved — investigationReport should remain null
|
||||
const storedRaw = localStorage.getItem("confidence-engine-investigation");
|
||||
expect(storedRaw).toBeTruthy();
|
||||
const stored = JSON.parse(storedRaw);
|
||||
expect(stored.investigationReport).toBeNull();
|
||||
});
|
||||
|
||||
// 10. failed request → no automatic retry
|
||||
it("does not re-generate on state update after failure", async () => {
|
||||
let fetchCallCount = 0;
|
||||
global.fetch.mockImplementation(async (...args) => {
|
||||
fetchCallCount++;
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
return {
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: false }),
|
||||
};
|
||||
});
|
||||
|
||||
setMockSnapshot(makeSnapshot());
|
||||
render(React.createElement(ReportPage));
|
||||
await screen.findByText(/Report generation failed/i);
|
||||
// Allow time for any potential re-trigger
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user