Files
confidence-engine/tests/v059c-portfolio-freshness.test.jsx
robbond f06138de32 feat(confidence-engine): v0.59b-c — report freshness on Report page + Portfolio
v0.59b — Report page freshness UI:
- Shows Current / Update available beside the generated report
- Manual Update report action with duplicate prevention guard
- Explanation copy about investigation changes since generation
- Persists generatedFromRevision during update flow

v0.59c — Portfolio Report freshness state:
- Surfaces Current / Update available alongside existing View report link
- Derives solely from revision provenance (zero model calls)
- No Update report action on Portfolio (manual update owned by Report page)
- Neither state shown when no Report exists
- Updated makeSnapshot with investigationRevision for realistic test data
2026-09-03 14:41:02 +01:00

173 lines
5.9 KiB
React

/**
* v0.59c — targeted deterministic tests for Portfolio Report freshness UI.
*
* Exercises four cases on the Portfolio:
* 1. Report + matching revisions → "Current"
* 2. Report + differing revisions → "Update available"
* 3. differing revisions still render View report action
* 4. no Report → neither Current nor Update available
* 5. zero model/fetch calls during render
* 6. Portfolio does not mutate Investigation storage
* 7. existing Continue / Restart actions unchanged
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: () => {} }),
}));
let mockClearStorage = vi.fn();
let mockLoadResult = null;
function setMockSnapshot(snap) {
if (snap) {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(snap),
);
mockLoadResult = snap;
} else {
localStorage.removeItem("confidence-engine-investigation");
mockLoadResult = null;
}
}
vi.mock("@/lib/storage/investigation-storage", () => ({
loadInvestigation: () => mockLoadResult,
saveInvestigation: vi.fn(),
clearInvestigation: () => {
localStorage.removeItem("confidence-engine-investigation");
mockClearStorage();
},
}));
/** Creates a snapshot with report. revisionMatch controls freshness state. */
function makeFreshSnapshot(investigationRevision, revisionMatch) {
return {
scenario: "Test investigation",
situationGraph: { centralStatement: "CS", nodes: [], edges: [] },
selectedQuestion: null,
summary: "Test summary.",
updatedAt: new Date().toISOString(),
schemaVersion: 1,
investigationReport: {
understanding: "We understand complaints rose.",
hasPlausibleInterpretations: false,
generatedFromRevision: revisionMatch ? investigationRevision : (investigationRevision - 2),
},
investigationRevision,
findings: [],
};
}
/** Creates a snapshot with NO report. */
function makeNoReportSnapshot(investigationRevision) {
return {
scenario: "Test investigation",
situationGraph: { centralStatement: "CS", nodes: [], edges: [] },
selectedQuestion: null,
summary: "Test summary.",
updatedAt: new Date().toISOString(),
schemaVersion: 1,
investigationReport: null,
investigationRevision,
findings: [],
};
}
async function cleanup() {
localStorage.removeItem("confidence-engine-investigation");
}
describe("Portfolio freshness — Report + matching revisions", () => {
it("renders Current alongside View report", async () => {
setMockSnapshot(makeFreshSnapshot(3, true));
const mod = await import("@/app/page.jsx");
render(React.createElement(mod.default));
expect(await screen.findByText(/View report/i)).toBeInTheDocument();
expect(screen.getByText("Current")).toBeInTheDocument();
await cleanup();
});
it("makes zero fetch calls during render", async () => {
global.fetch = vi.fn();
setMockSnapshot(makeFreshSnapshot(3, true));
const mod = await import("@/app/page.jsx");
render(React.createElement(mod.default));
try {
await waitFor(() => screen.findByText(/View report/i), { timeout: 5000 });
} catch {}
// Portfolio should not call fetch at all — derives state from localStorage only
expect(global.fetch).not.toHaveBeenCalled();
global.fetch = undefined;
await cleanup();
});
});
describe("Portfolio freshness — Report + differing revisions", () => {
it("renders Update available alongside View report", async () => {
setMockSnapshot(makeFreshSnapshot(5, false));
const mod = await import("@/app/page.jsx");
render(React.createElement(mod.default));
expect(await screen.findByText(/View report/i)).toBeInTheDocument();
expect(screen.getByText("Update available")).toBeInTheDocument();
await cleanup();
});
it("View report still present as link for differing revisions", async () => {
setMockSnapshot(makeFreshSnapshot(5, false));
const mod = await import("@/app/page.jsx");
render(React.createElement(mod.default));
const link = await screen.findByRole("link", { name: /View report/i });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", "/investigations/case-1/report");
await cleanup();
});
});
describe("Portfolio freshness — no Report", () => {
it("renders neither Current nor Update available when no report exists", async () => {
setMockSnapshot(makeNoReportSnapshot(0));
const mod = await import("@/app/page.jsx");
render(React.createElement(mod.default));
expect(screen.queryByText(/View report/i)).not.toBeInTheDocument();
expect(screen.queryByText("Current")).not.toBeInTheDocument();
expect(screen.queryByText("Update available")).not.toBeInTheDocument();
await cleanup();
});
});
describe("Portfolio existing actions preserved", () => {
it("Continue investigation and Restart remain when a report exists", async () => {
setMockSnapshot(makeFreshSnapshot(3, true));
const mod = await import("@/app/page.jsx");
render(React.createElement(mod.default));
expect(await screen.findByText(/Continue investigation/i)).toBeInTheDocument();
expect(await screen.findByText(/Restart investigation/i)).toBeInTheDocument();
await cleanup();
});
});
describe("Portfolio freshness — differing revisions no-additional-calls", () => {
it("refreshing with differing data does not trigger additional API calls", async () => {
global.fetch = vi.fn();
setMockSnapshot(makeFreshSnapshot(5, false));
const mod = await import("@/app/page.jsx");
render(React.createElement(mod.default));
try {
await waitFor(() => screen.findByText(/Update available/i), { timeout: 2000 });
} catch {}
expect(global.fetch).not.toHaveBeenCalled();
global.fetch = undefined;
await cleanup();
});
});