feat(confidence-engine): v0.60h migrate report route to use route [id] identity
This commit is contained in:
@@ -28,6 +28,8 @@ Object.defineProperty(global, "crypto", {
|
||||
|
||||
let mockClearStorage = vi.fn();
|
||||
let mockLoadResult = null;
|
||||
let mockLastLoadedId = undefined;
|
||||
let mockLastSavedSnapshot = null;
|
||||
|
||||
function setMockSnapshot(snap) {
|
||||
if (snap) {
|
||||
@@ -43,8 +45,14 @@ function setMockSnapshot(snap) {
|
||||
}
|
||||
|
||||
vi.mock("@/lib/storage/investigation-storage", () => ({
|
||||
loadInvestigation: () => mockLoadResult,
|
||||
saveInvestigation: () => {},
|
||||
listInvestigations: () => [],
|
||||
loadInvestigation: (id) => {
|
||||
mockLastLoadedId = id;
|
||||
return mockLoadResult;
|
||||
},
|
||||
saveInvestigation: (snapshot) => {
|
||||
mockLastSavedSnapshot = snapshot;
|
||||
},
|
||||
clearInvestigation: () => {
|
||||
localStorage.removeItem("confidence-engine-investigation");
|
||||
mockClearStorage();
|
||||
@@ -483,3 +491,139 @@ describe("Report page lifecycle — v0.58", () => {
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── v0.60h — Report route identity assertions ─────────────────────────── */
|
||||
|
||||
describe("Report route identity — v0.60h", () => {
|
||||
let ReportPage;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockLastLoadedId = undefined;
|
||||
mockLastSavedSnapshot = null;
|
||||
setMockSnapshot(makeSnapshot());
|
||||
global.fetch = vi.fn();
|
||||
const mod = await import("@/app/investigations/[id]/report/page.jsx");
|
||||
ReportPage = mod.default;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("inv-a identified load + first generation", async () => {
|
||||
setMockSnapshot(makeSnapshot({
|
||||
id: "inv-a",
|
||||
investigationReport: null,
|
||||
investigationRevision: 3,
|
||||
findings: [],
|
||||
}));
|
||||
|
||||
global.fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true, understanding: "Summary.", plausibleInterpretations: "None." }),
|
||||
});
|
||||
|
||||
render(React.createElement(ReportPage, { params: { id: "inv-a" } }));
|
||||
await screen.findByText(/Summary\./i);
|
||||
|
||||
// A — identified load
|
||||
expect(mockLastLoadedId).toBe("inv-a");
|
||||
|
||||
// C — first generation reaches overview exactly once
|
||||
const calls = global.fetch.mock.calls.filter((c) => c[0] === "/api/cases/overview");
|
||||
expect(calls).toHaveLength(1);
|
||||
|
||||
// C — saved snapshot retains durable id
|
||||
expect(mockLastSavedSnapshot.id).toBe("inv-a");
|
||||
|
||||
// C — generatedFromRevision correct
|
||||
expect(mockLastSavedSnapshot.investigationReport.generatedFromRevision).toBe(3);
|
||||
});
|
||||
|
||||
it("existing report for inv-b renders without generation", async () => {
|
||||
setMockSnapshot(makeSnapshot({
|
||||
id: "inv-b",
|
||||
investigationReport: { understanding: "Already generated.", hasPlausibleInterpretations: false, generatedFromRevision: 5 },
|
||||
investigationRevision: 5,
|
||||
}));
|
||||
|
||||
render(React.createElement(ReportPage, { params: { id: "inv-b" } }));
|
||||
await screen.findByText(/Already generated\./i);
|
||||
|
||||
// A — identified load even for existing report
|
||||
expect(mockLastLoadedId).toBe("inv-b");
|
||||
|
||||
// E — no overview call
|
||||
const calls = global.fetch.mock.calls.filter((c) => c[0] === "/api/cases/overview");
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("manual update reloads by same id and saves back", async () => {
|
||||
setMockSnapshot(makeSnapshot({
|
||||
id: "inv-c",
|
||||
investigationReport: { understanding: "R1.", hasPlausibleInterpretations: false, generatedFromRevision: 2 },
|
||||
investigationRevision: 4, // revision mismatch → "Update available"
|
||||
findings: [{ id: "f-1", proposition: "P1" }],
|
||||
}));
|
||||
|
||||
global.fetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true, understanding: "R2.", plausibleInterpretations: "None." }),
|
||||
});
|
||||
|
||||
render(React.createElement(ReportPage, { params: { id: "inv-c" } }));
|
||||
await screen.findByText(/Update available/i);
|
||||
|
||||
// Find and click Update report button
|
||||
const updateBtn = screen.getByRole("button", { name: /Update report/i });
|
||||
fireEvent.click(updateBtn);
|
||||
await screen.findByText(/R2\./i);
|
||||
|
||||
// D — manual update uses same durable id
|
||||
expect(mockLastLoadedId).toBe("inv-c");
|
||||
|
||||
// D — save back to same identified Investigation
|
||||
expect(mockLastSavedSnapshot.id).toBe("inv-c");
|
||||
expect(mockLastSavedSnapshot.investigationReport.generatedFromRevision).toBe(4);
|
||||
|
||||
// D — exactly one overview POST for the update
|
||||
const calls = global.fetch.mock.calls.filter((c) => c[0] === "/api/cases/overview");
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("existing report hydration retains zero-call behaviour", async () => {
|
||||
setMockSnapshot(makeSnapshot({
|
||||
id: "inv-d",
|
||||
investigationReport: { understanding: "R.", hasPlausibleInterpretations: false, generatedFromRevision: 3 },
|
||||
investigationRevision: 3, // matching → Current
|
||||
}));
|
||||
|
||||
render(React.createElement(ReportPage, { params: { id: "inv-d" } }));
|
||||
await screen.findByText(/Current/i);
|
||||
|
||||
// E — no singleton/unscoped load happens during Report lifecycle
|
||||
expect(mockLastLoadedId).toBe("inv-d");
|
||||
|
||||
// E — no overview call when report exists and revisions match
|
||||
const calls = global.fetch.mock.calls.filter((c) => c[0] === "/api/cases/overview");
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("generation failure preserves existing Report", async () => {
|
||||
setMockSnapshot(makeSnapshot({
|
||||
id: "inv-e",
|
||||
investigationReport: null,
|
||||
situationGraph: { evidence: [], reconstruction: {} },
|
||||
investigationRevision: 1,
|
||||
findings: [],
|
||||
}));
|
||||
|
||||
global.fetch.mockResolvedValue({ ok: false });
|
||||
|
||||
render(React.createElement(ReportPage, { params: { id: "inv-e" } }));
|
||||
await screen.findByText(/Report generation failed/i);
|
||||
|
||||
// F — failure semantics: no partial Report persisted
|
||||
expect(mockLastSavedSnapshot).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user