import { render, screen, within } from "@testing-library/react"; import { http, HttpResponse } from "msw"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { server } from "../test/msw"; import { SessionDocumentsPanel } from "./SessionDocumentsPanel"; import type { SessionSummary } from "../api/types"; function makeClient() { return new QueryClient({ defaultOptions: { queries: { retry: false } } }); } function wrap(ui: React.ReactElement, client = makeClient()) { const Wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); return render(ui, { wrapper: Wrapper }); } const base: SessionSummary = { id: "s1", status: "open", question: "How many ablations?", summary: null, created_at: "2026-01-01T00:00:00Z", updated_at: null, author: null, name: null, group: null, archived: false, }; beforeEach(() => { server.use(http.get("http://localhost:8787/sessions/s1/documents", () => HttpResponse.json([ { phase: "—", key: "question", title: "Original question", format: "text", content: "How many ablations?" }, { phase: "F7", key: "sql", title: "Final SQL", format: "sql", content: "SELECT 1" }, ]), )); }); test("renders the document titles from the API", async () => { wrap(); expect(await screen.findByRole("heading", { name: "Original question" })).toBeInTheDocument(); // "Final SQL" is both the section heading and the SqlViewer block label — target the heading. expect(screen.getByRole("heading", { name: "Final SQL" })).toBeInTheDocument(); }); test("lets the Final SQL document use the full resized panel width", async () => { wrap(); const sqlSection = (await screen.findByRole("heading", { name: "Final SQL" })).closest("section"); expect(sqlSection).toHaveClass("w-full"); expect(sqlSection?.parentElement).toHaveClass("w-full"); expect(sqlSection?.parentElement).not.toHaveClass("max-w-[76ch]"); }); test("lets text documents adapt to the full resized panel width", async () => { wrap(); const questionSection = ( await screen.findByRole("heading", { name: "Original question" }) ).closest("section"); expect(questionSection).toHaveClass("w-full"); expect(questionSection).not.toHaveClass("max-w-[76ch]"); }); test("shows Resume for resumable, hides it for finalized", async () => { const { rerender } = wrap(); expect(await screen.findByRole("button", { name: /resume/i })).toBeInTheDocument(); rerender(); expect(screen.queryByRole("button", { name: /resume/i })).not.toBeInTheDocument(); }); test("hides Resume for an archived session", async () => { wrap(); await screen.findByText("Original question"); expect(screen.queryByRole("button", { name: /resume/i })).not.toBeInTheDocument(); }); test("a malformed document renders a fallback without taking down its siblings", async () => { const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); // Valid JSON but missing `candidates` — SchemaLinkingViewer crashes on this shape. // Without a per-document boundary this crash blanks the whole panel (React 18 // unmounts the tree); the good sibling document must still render. // resetHandlers(...) replaces the beforeEach handler so this response is used. server.resetHandlers( http.get("http://localhost:8787/sessions/s1/documents", () => HttpResponse.json([ { phase: "F4", key: "schema", title: "Schema linking", format: "schema-linking", content: '{"joins":[]}' }, { phase: "—", key: "question", title: "Original question", format: "text", content: "SIBLING-SURVIVES" }, ]), ), ); wrap(); // The healthy sibling rendered — the panel did not white-screen. expect(await screen.findByText("SIBLING-SURVIVES")).toBeInTheDocument(); // The broken document degraded to a fallback in place. expect(screen.getByRole("alert")).toBeInTheDocument(); expect(screen.getByText(/couldn't be displayed/i)).toBeInTheDocument(); errorSpy.mockRestore(); }); test("renders the canonical summary order and formats human text as Markdown", async () => { server.resetHandlers( http.get("http://localhost:8787/sessions/s1/documents", () => HttpResponse.json([ { phase: "—", key: "question", title: "Original question", format: "text", content: "Original **question**" }, { phase: "F7", key: "sql", title: "Final SQL", format: "sql", content: "SELECT 1" }, { phase: "finalize", key: "preview", title: "Data preview", format: "markdown", content: "| count |\n|---|\n| 42 |" }, { phase: "F3", key: "revised_question", title: "Revised question", format: "markdown", content: "# Domanda\n\nConta i pazienti **attivi**." }, { phase: "F3", key: "assumptions", title: "Assumptions", format: "markdown", content: "## Assunzioni\n\n- Solo adulti" }, { phase: "F8", key: "memories", title: "Memories", format: "memories", content: "[]" }, { phase: "F4", key: "schema_linking", title: "Schema linking", format: "schema-linking", content: '{"question":"q","candidates":[],"joins":[],"excluded":[],"open_questions":[]}' }, ]), ), ); wrap(); const panel = await screen.findByRole("complementary", { name: /session summary/i }); await within(panel).findByRole("heading", { name: "Original question", level: 3 }); expect(within(panel).getAllByRole("heading", { level: 3 }).map((heading) => heading.textContent)).toEqual([ "Original question", "Final SQL", "Data preview", "Revised question", "Assumptions", "Memories", "Schema linking", ]); expect(screen.getByText("question").tagName).toBe("STRONG"); expect(screen.getByText("attivi").tagName).toBe("STRONG"); expect(screen.getByRole("heading", { name: "Domanda", level: 1 })).toBeInTheDocument(); expect(screen.getByText("Solo adulti").closest("li")).toBeInTheDocument(); }); test("renders one approved-then-declined memory list with Markdown details", async () => { server.resetHandlers( http.get("http://localhost:8787/sessions/s1/documents", () => HttpResponse.json([ { phase: "F8", key: "memories", title: "Memories", format: "memories", content: JSON.stringify([ { status: "approved", subject: "ablazione **valida**", detail: "Stato `completed`.", rationale: "*Confermato* dal reviewer.", }, { status: "declined", subject: "paziente attivo", detail: "Usa `flag_attivo`.", rationale: "", }, ]), }, ]), ), ); wrap(); const list = await screen.findByRole("list", { name: /memories/i }); const items = within(list).getAllByRole("listitem"); expect(items).toHaveLength(2); expect(items[0]).toHaveTextContent(/approved/i); expect(items[1]).toHaveTextContent(/declined/i); expect(within(items[0]).getByText("valida").tagName).toBe("STRONG"); expect(within(items[0]).getByText("completed").tagName).toBe("CODE"); expect(within(items[0]).getByText("Confermato").tagName).toBe("EM"); expect(screen.queryByText(/memory promoted/i)).not.toBeInTheDocument(); }); test("never renders technical approval, promotion, or memory decisions", async () => { const lines = [ { type: "phase_approved", subject: "phase:1" }, { type: "table_approved", subject: "fact_a" }, { type: "table_promoted", subject: "fact_a" }, { type: "column_promoted", subject: "fact_a.id" }, { type: "cte_approved", subject: "eventi_device" }, { type: "sql_approved", subject: "phase:7" }, { type: "datamart_requested", subject: "phase:8" }, { type: "datamart_declined", subject: "phase:8" }, { type: "memory_promoted", subject: "concetto" }, { type: "column_excluded", subject: "fact_a.note", detail: "Non **pertinente**" }, ].map((decision) => JSON.stringify(decision)).join("\n"); server.resetHandlers( http.get("http://localhost:8787/sessions/s1/documents", () => HttpResponse.json([ { phase: "—", key: "decisions", title: "Decisions", format: "decisions", content: lines }, ]), ), ); wrap(); expect(await screen.findByText("column excluded")).toBeInTheDocument(); expect(screen.getByText("pertinente").tagName).toBe("STRONG"); for (const hidden of [ "phase approved", "table approved", "table promoted", "column promoted", "cte approved", "sql approved", "datamart requested", "datamart declined", "memory promoted", ]) { expect(screen.queryByText(hidden)).not.toBeInTheDocument(); } });