import { act, render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { http, HttpResponse } from "msw"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { server } from "../test/msw"; import { FakeEventSource } from "../test/fakeEventSource"; import { AppShell } from "./AppShell"; import { useSessionStore } from "../store/sessionStore"; function wrap() { const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return render(); } const LIST = [ { id: "s1", status: "open", question: "Attiva uno", summary: null, created_at: "2026-01-02T00:00:00Z", updated_at: null, author: null, name: null, group: "Aritmologia", archived: false }, { id: "s2", status: "finalized", question: "Archiviata due", summary: null, created_at: "2026-01-01T00:00:00Z", updated_at: null, author: null, name: null, group: null, archived: true }, ]; const resumeResult = (id: string, alreadyActive = false) => HttpResponse.json({ id, alreadyActive }); function deferred() { let resolve!: () => void; const promise = new Promise((onResolve) => { resolve = onResolve; }); return { promise, resolve }; } beforeEach(() => { FakeEventSource.instances = []; (globalThis as any).EventSource = FakeEventSource; (globalThis as any).PointerEvent = MouseEvent; useSessionStore.getState().resetSession(); server.use( http.get("http://localhost:8787/sessions", () => HttpResponse.json(LIST)), http.get("http://localhost:8787/sessions/:id/documents", () => HttpResponse.json([ { phase: "—", key: "question", title: "Domanda originale", format: "text", content: "Attiva uno" }, ])), http.post("http://localhost:8787/sessions/:id/archive", () => new HttpResponse(null, { status: 204 })), ); }); test("active list shows group header and hides archived sessions", async () => { wrap(); expect(await screen.findByText("Attiva uno")).toBeInTheDocument(); expect(screen.getByText("Aritmologia")).toBeInTheDocument(); // group header expect(screen.queryByText("Archiviata due")).not.toBeInTheDocument(); // archived hidden }); test("ungrouped sessions render after groups with no 'No group' label", async () => { server.use( http.get("http://localhost:8787/sessions", () => HttpResponse.json([ { id: "g1", status: "open", question: "In gruppo", summary: null, created_at: "2026-01-02T00:00:00Z", updated_at: null, author: null, name: null, group: "Aritmologia", archived: false }, { id: "u1", status: "open", question: "Senza gruppo", summary: null, created_at: "2026-01-03T00:00:00Z", updated_at: null, author: null, name: null, group: null, archived: false }, ])), ); wrap(); expect(await screen.findByText("In gruppo")).toBeInTheDocument(); // grouped expect(screen.getByText("Senza gruppo")).toBeInTheDocument(); // ungrouped, still listed expect(screen.queryByText(/no group/i)).not.toBeInTheDocument(); }); test("opening the panel shows the session documents", async () => { wrap(); await userEvent.click(await screen.findByText("Attiva uno")); expect(await screen.findByText("Domanda originale")).toBeInTheDocument(); }); test("Archive accordion expands to reveal archived sessions", async () => { wrap(); // archived hidden until the Archive accordion is expanded expect(await screen.findByText("Attiva uno")).toBeInTheDocument(); expect(screen.queryByText("Archiviata due")).not.toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: /archive/i })); expect(await screen.findByText("Archiviata due")).toBeInTheDocument(); // Active sessions remain visible (it is a separate accordion, not a swap) expect(screen.getByText("Attiva uno")).toBeInTheDocument(); }); test("Resume from the panel activates the session and closes the panel", async () => { let resumed: string | null = null; useSessionStore.setState({ activityLog: [{ kind: "status", phase: "F7", text: "Stale prior activity", level: "info" }], }); server.use( http.post("http://localhost:8787/sessions/:id/resume", ({ params }) => { resumed = params.id as string; return resumeResult(resumed); }), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await screen.findByText("Domanda originale"); // panel open await userEvent.click(screen.getByRole("button", { name: /resume/i })); expect(useSessionStore.getState().activityLog).toEqual([ { kind: "lifecycle", phase: null, text: "Resuming session" }, ]); await waitFor(() => expect(resumed).toBe("s1")); await waitFor(() => expect(screen.queryByText("Domanda originale")).not.toBeInTheDocument()); // panel closed }); test("Resume paints the re-entry phase from the manifest after the cold Resume succeeds", async () => { useSessionStore.getState().resetSession(); server.use( http.post("http://localhost:8787/sessions/:id/resume", () => resumeResult("s1")), http.get("http://localhost:8787/sessions/:id", () => HttpResponse.json({ id: "s1", status: "open", phase: 4 })), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await screen.findByText("Domanda originale"); // panel open await userEvent.click(screen.getByRole("button", { name: /resume/i })); await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F4")); }); test("an already-active same-session Resume preserves its EventSource and store", async () => { let resumeCalls = 0; server.use( http.post("http://localhost:8787/sessions/:id/resume", () => { resumeCalls += 1; return resumeResult("s1", resumeCalls > 1); }), http.get("http://localhost:8787/sessions/:id", () => HttpResponse.json({ id: "s1", status: "open", phase: 1 })), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); const first = FakeEventSource.instances[0]; act(() => first.emitNamed("info", { type: "info", text: "Live state" }, "4")); const before = useSessionStore.getState().activityLog.map((entry) => ({ ...entry })); await userEvent.click(screen.getByText("Attiva uno")); await screen.findByText("Domanda originale"); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(screen.queryByText("Domanda originale")).not.toBeInTheDocument()); expect(FakeEventSource.instances).toHaveLength(1); expect(first.closed).toBe(false); expect(useSessionStore.getState().activityLog).toEqual(before); }); test("concurrent same-id Resume invocations share one cold request and replacement source", async () => { let resumeCalls = 0; const coldGate = deferred(); const coldStarted = deferred(); server.use( http.post("http://localhost:8787/sessions/:id/resume", async () => { resumeCalls += 1; if (resumeCalls === 1) return resumeResult("s1", false); if (resumeCalls === 2) coldStarted.resolve(); await coldGate.promise; return resumeResult("s1", false); }), http.get("http://localhost:8787/sessions/:id", () => HttpResponse.json({ id: "s1", status: "open", phase: 1 })), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); const oldSource = FakeEventSource.instances[0]; act(() => oldSource.emitNamed("info", { type: "info", text: "Old generation" }, "900")); await userEvent.click(screen.getByText("Attiva uno")); const resumeButton = await screen.findByRole("button", { name: /resume/i }); await userEvent.click(resumeButton); await coldStarted.promise; await userEvent.click(resumeButton); await new Promise((resolve) => setImmediate(resolve)); expect(resumeCalls).toBe(2); coldGate.resolve(); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(2)); const replacement = FakeEventSource.instances[1]; expect(oldSource.closed).toBe(true); expect(replacement.url).toBe("http://localhost:8787/sessions/s1/events"); expect(useSessionStore.getState().activityLog).toEqual([ { kind: "lifecycle", phase: null, text: "Resuming session" }, ]); act(() => replacement.emitNamed( "ui_request", { type: "ui_request", ui_request: { id: "cold-gate", widget: "select" } }, "1", )); expect(useSessionStore.getState().pendingWidget?.id).toBe("cold-gate"); }); test("a committed Resume releases same-id single-flight before its manifest settles", async () => { const other = { ...LIST[0], id: "s3", question: "Attiva tre", group: null, created_at: "2026-01-03T00:00:00Z", }; let s1ResumeCalls = 0; let s1ManifestCalls = 0; const firstS1ManifestGate = deferred(); const firstS1ManifestStarted = deferred(); server.use( http.get("http://localhost:8787/sessions", () => HttpResponse.json([LIST[0], other])), http.post("http://localhost:8787/sessions/:id/resume", ({ params }) => { const id = params.id as string; if (id === "s1") s1ResumeCalls += 1; return resumeResult(id); }), http.get("http://localhost:8787/sessions/:id", async ({ params }) => { if (params.id === "s1") { s1ManifestCalls += 1; if (s1ManifestCalls === 1) { firstS1ManifestStarted.resolve(); await firstS1ManifestGate.promise; return HttpResponse.json({ id: "s1", status: "open", phase: 7 }); } return HttpResponse.json({ id: "s1", status: "open", phase: 4 }); } return HttpResponse.json({ id: "s3", status: "open", phase: 3 }); }), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await firstS1ManifestStarted.promise; expect(s1ResumeCalls).toBe(1); await userEvent.click(screen.getByText("Attiva tre")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F3")); await userEvent.click(screen.getByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(s1ResumeCalls).toBe(2)); await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F4")); expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s1/events"); firstS1ManifestGate.resolve(); await new Promise((resolve) => setImmediate(resolve)); expect(useSessionStore.getState().currentPhase).toBe("F4"); expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s1/events"); }); test("cold same-session Resume keeps the old stream until success then receives post-clear events once", async () => { server.use( http.post("http://localhost:8787/sessions/:id/resume", () => resumeResult("s1")), http.get("http://localhost:8787/sessions/:id", () => HttpResponse.json({ id: "s1", status: "open", phase: 1 })), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); const first = FakeEventSource.instances[0]; act(() => first.emitNamed("info", { type: "info", text: "Before cold Resume" }, "900")); let releaseResume!: () => void; let markStarted!: () => void; const resumeStarted = new Promise((resolve) => { markStarted = resolve; }); const resumeReleased = new Promise((resolve) => { releaseResume = resolve; }); server.use(http.post("http://localhost:8787/sessions/:id/resume", async () => { markStarted(); await resumeReleased; return resumeResult("s1"); })); await userEvent.click(screen.getByText("Attiva uno")); await screen.findByText("Domanda originale"); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await resumeStarted; expect(FakeEventSource.instances).toHaveLength(1); expect(first.closed).toBe(false); expect(screen.getByText("Domanda originale")).toBeInTheDocument(); expect(useSessionStore.getState().activityLog).toContainEqual({ kind: "status", phase: "F1", text: "Before cold Resume", level: "info", }); act(() => first.emitNamed("text_delta", { type: "text_delta", text: "Still attached" }, "901")); expect(useSessionStore.getState().transcript).toEqual([ { role: "assistant", text: "Still attached" }, ]); releaseResume(); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(2)); expect(first.closed).toBe(true); const replacement = FakeEventSource.instances[1]; expect(replacement.url).toBe("http://localhost:8787/sessions/s1/events"); expect(useSessionStore.getState().transcript).toEqual([]); expect(useSessionStore.getState().activityLog).toEqual([ { kind: "lifecycle", phase: null, text: "Resuming session" }, ]); act(() => replacement.emitNamed( "text_delta", { type: "text_delta", text: "Post-resume delivery" }, "1", )); expect(useSessionStore.getState().transcript).toEqual([ { role: "assistant", text: "Post-resume delivery" }, ]); expect(useSessionStore.getState().activityLog.filter((entry) => entry.kind === "assistant")) .toEqual([{ kind: "assistant", phase: "F1", text: "Post-resume delivery" }]); const gate = { type: "ui_request" as const, ui_request: { id: "post-resume-gate", widget: "select", title: "Review once" }, }; act(() => { replacement.emitNamed("ui_request", gate, "2"); replacement.emitNamed("ui_request", gate, "3"); }); expect(useSessionStore.getState().pendingWidget?.id).toBe("post-resume-gate"); expect(useSessionStore.getState().activityLog.filter((entry) => entry.kind === "gate")) .toEqual([{ kind: "gate", phase: "F1", text: "Review once" }]); }); test("a failed same-session Resume preserves its source, activity, and document panel", async () => { server.use( http.post("http://localhost:8787/sessions/:id/resume", () => resumeResult("s1")), http.get("http://localhost:8787/sessions/:id", () => HttpResponse.json({ id: "s1", status: "open", phase: 1 })), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); const first = FakeEventSource.instances[0]; act(() => first.emitNamed("info", { type: "info", text: "Keep me" }, "4")); const before = useSessionStore.getState().activityLog.map((entry) => ({ ...entry })); server.use(http.post("http://localhost:8787/sessions/:id/resume", () => new HttpResponse(null, { status: 409 }))); await userEvent.click(screen.getByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(screen.getByText("Domanda originale")).toBeInTheDocument()); expect(FakeEventSource.instances).toHaveLength(1); expect(first.closed).toBe(false); expect(useSessionStore.getState().activityLog).toEqual(before); act(() => first.emitNamed("text_delta", { type: "text_delta", text: "After failure" }, "5")); expect(useSessionStore.getState().transcript.at(-1)?.text).toBe("After failure"); }); test("resuming a different already-active session binds it only after success", async () => { const other = { ...LIST[0], id: "s3", question: "Attiva tre", group: null, created_at: "2026-01-03T00:00:00Z", }; server.use( http.get("http://localhost:8787/sessions", () => HttpResponse.json([LIST[0], other])), http.post("http://localhost:8787/sessions/:id/resume", ({ params }) => resumeResult(params.id as string, params.id === "s3")), http.get("http://localhost:8787/sessions/:id", ({ params }) => HttpResponse.json({ id: params.id, status: "open", phase: params.id === "s3" ? 3 : 1 })), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); const first = FakeEventSource.instances[0]; act(() => first.emitNamed("info", { type: "info", text: "Old target" }, "3")); await userEvent.click(screen.getByText("Attiva tre")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F3")); expect(FakeEventSource.instances).toHaveLength(2); expect(first.closed).toBe(true); expect(FakeEventSource.instances[1].url).toContain("/sessions/s3/events"); expect(FakeEventSource.instances[1].url).not.toContain("lastEventId"); expect(useSessionStore.getState().activityLog).not.toContainEqual(expect.objectContaining({ text: "Old target", })); }); test("competing Resume requests for different ids commit only the latest intent", async () => { const other = { ...LIST[0], id: "s3", question: "Attiva tre", group: null, created_at: "2026-01-03T00:00:00Z", }; const s1Gate = deferred(); const s3Gate = deferred(); const s1Started = deferred(); const s3Started = deferred(); server.use( http.get("http://localhost:8787/sessions", () => HttpResponse.json([LIST[0], other])), http.post("http://localhost:8787/sessions/:id/resume", async ({ params }) => { const id = params.id as string; if (id === "s1") { s1Started.resolve(); await s1Gate.promise; } else { s3Started.resolve(); await s3Gate.promise; } return resumeResult(id); }), http.get("http://localhost:8787/sessions/:id", ({ params }) => HttpResponse.json({ id: params.id, status: "open", phase: params.id === "s3" ? 3 : 1 })), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await s1Started.promise; await userEvent.click(screen.getByText("Attiva tre")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await s3Started.promise; s3Gate.resolve(); await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F3")); expect(FakeEventSource.instances).toHaveLength(1); const latestSource = FakeEventSource.instances[0]; expect(latestSource.url).toContain("/sessions/s3/events"); act(() => latestSource.emitNamed("info", { type: "info", text: "Latest target" }, "1")); s1Gate.resolve(); await new Promise((resolve) => setImmediate(resolve)); expect(FakeEventSource.instances).toHaveLength(1); expect(latestSource.closed).toBe(false); expect(useSessionStore.getState().currentPhase).toBe("F3"); expect(useSessionStore.getState().activityLog).toContainEqual(expect.objectContaining({ text: "Latest target", })); }); test("a stale Resume manifest cannot repaint the latest session phase", async () => { const other = { ...LIST[0], id: "s3", question: "Attiva tre", group: null, created_at: "2026-01-03T00:00:00Z", }; const s1ManifestGate = deferred(); const s1ManifestStarted = deferred(); server.use( http.get("http://localhost:8787/sessions", () => HttpResponse.json([LIST[0], other])), http.post("http://localhost:8787/sessions/:id/resume", ({ params }) => resumeResult(params.id as string)), http.get("http://localhost:8787/sessions/:id", async ({ params }) => { if (params.id === "s1") { s1ManifestStarted.resolve(); await s1ManifestGate.promise; return HttpResponse.json({ id: "s1", status: "open", phase: 7 }); } return HttpResponse.json({ id: "s3", status: "open", phase: 3 }); }), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await s1ManifestStarted.promise; await userEvent.click(screen.getByText("Attiva tre")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F3")); expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s3/events"); s1ManifestGate.resolve(); await new Promise((resolve) => setImmediate(resolve)); expect(useSessionStore.getState().currentPhase).toBe("F3"); expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s3/events"); }); test("starting a new question invalidates a pending Resume intent", async () => { const resumeGate = deferred(); const resumeStarted = deferred(); server.use( http.post("http://localhost:8787/sessions/:id/resume", async () => { resumeStarted.resolve(); await resumeGate.promise; return resumeResult("s1"); }), http.post("http://localhost:8787/runtime/prewarm", () => HttpResponse.json({ status: "warming" }, { status: 202 })), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await resumeStarted.promise; await userEvent.click(screen.getByRole("button", { name: /^new session$/i })); expect(await screen.findByText(/type your question/i)).toBeInTheDocument(); resumeGate.resolve(); await new Promise((resolve) => setImmediate(resolve)); expect(FakeEventSource.instances).toHaveLength(0); expect(screen.getByText(/type your question/i)).toBeInTheDocument(); }); test("a successful Delete invalidates an earlier pending Resume for the same target", async () => { const resumeGate = deferred(); const resumeStarted = deferred(); const deleteCompleted = deferred(); server.use( http.post("http://localhost:8787/sessions/:id/resume", async () => { resumeStarted.resolve(); await resumeGate.promise; return resumeResult("s1"); }), http.delete("http://localhost:8787/sessions/:id", ({ params }) => { expect(params.id).toBe("s1"); deleteCompleted.resolve(); return new HttpResponse(null, { status: 204 }); }), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await resumeStarted.promise; screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus(); await userEvent.keyboard(" "); await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" })); await deleteCompleted.promise; resumeGate.resolve(); await new Promise((resolve) => setImmediate(resolve)); expect(FakeEventSource.instances).toHaveLength(0); expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "false"); }); test("a successful Delete detaches a Resume that commits while Delete is pending", async () => { const deleteGate = deferred(); const deleteStarted = deferred(); server.use( http.delete("http://localhost:8787/sessions/:id", async ({ params }) => { expect(params.id).toBe("s1"); deleteStarted.resolve(); await deleteGate.promise; return new HttpResponse(null, { status: 204 }); }), http.post("http://localhost:8787/sessions/:id/resume", () => resumeResult("s1")), http.get("http://localhost:8787/sessions/:id", () => HttpResponse.json({ id: "s1", status: "open", phase: 1 })), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus(); await userEvent.keyboard(" "); await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" })); await deleteStarted.promise; await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "true"); deleteGate.resolve(); await waitFor(() => { expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "false"); }); expect(FakeEventSource.instances[0].closed).toBe(true); }); test("deleting another session does not invalidate a pending Resume", async () => { const resumeGate = deferred(); const resumeStarted = deferred(); const deleteCompleted = deferred(); server.use( http.post("http://localhost:8787/sessions/:id/resume", async () => { resumeStarted.resolve(); await resumeGate.promise; return resumeResult("s1"); }), http.delete("http://localhost:8787/sessions/:id", ({ params }) => { expect(params.id).toBe("s2"); deleteCompleted.resolve(); return new HttpResponse(null, { status: 204 }); }), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await resumeStarted.promise; await userEvent.click(screen.getByRole("button", { name: /archive/i })); (await screen.findByRole("checkbox", { name: "Select Archiviata due" })).focus(); await userEvent.keyboard(" "); await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" })); await deleteCompleted.promise; resumeGate.resolve(); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); expect(FakeEventSource.instances[0].url).toContain("/sessions/s1/events"); expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "true"); }); test("deleting active A preserves a pending Resume for different session B", async () => { const other = { ...LIST[0], id: "s3", question: "Attiva tre", group: null, created_at: "2026-01-03T00:00:00Z", }; const s3ResumeGate = deferred(); const s3ResumeStarted = deferred(); server.use( http.get("http://localhost:8787/sessions", () => HttpResponse.json([LIST[0], other])), http.post("http://localhost:8787/sessions/:id/resume", async ({ params }) => { const id = params.id as string; if (id === "s3") { s3ResumeStarted.resolve(); await s3ResumeGate.promise; } return resumeResult(id); }), http.get("http://localhost:8787/sessions/:id", ({ params }) => HttpResponse.json({ id: params.id, status: "open", phase: params.id === "s3" ? 3 : 1 })), http.delete("http://localhost:8787/sessions/:id", ({ params }) => { expect(params.id).toBe("s1"); return new HttpResponse(null, { status: 204 }); }), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); const sourceA = FakeEventSource.instances[0]; expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "true"); await userEvent.click(screen.getByText("Attiva tre")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await s3ResumeStarted.promise; screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus(); await userEvent.keyboard(" "); await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" })); await waitFor(() => expect(sourceA.closed).toBe(true)); expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "false"); s3ResumeGate.resolve(); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(2)); expect(FakeEventSource.instances[1].url).toContain("/sessions/s3/events"); expect(screen.getByTestId("session-item-s3")).toHaveAttribute("data-active", "true"); }); test("a failed Delete does not invalidate a pending Resume for its target", async () => { const resumeGate = deferred(); const resumeStarted = deferred(); const deleteFailed = deferred(); server.use( http.post("http://localhost:8787/sessions/:id/resume", async () => { resumeStarted.resolve(); await resumeGate.promise; return resumeResult("s1"); }), http.delete("http://localhost:8787/sessions/:id", ({ params }) => { expect(params.id).toBe("s1"); deleteFailed.resolve(); return new HttpResponse(null, { status: 500 }); }), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await resumeStarted.promise; screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus(); await userEvent.keyboard(" "); await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" })); await deleteFailed.promise; resumeGate.resolve(); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); expect(FakeEventSource.instances[0].url).toContain("/sessions/s1/events"); expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "true"); }); test("a failed Resume with no active session preserves the panel and existing activity", async () => { server.use(http.post("http://localhost:8787/sessions/:id/resume", () => new HttpResponse(null, { status: 409 }))); useSessionStore.setState({ activityLog: [{ kind: "status", phase: "F7", text: "Preserve activity", level: "info" }], }); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await screen.findByText("Domanda originale"); // panel open await userEvent.click(screen.getByRole("button", { name: /resume/i })); // panel stays open (document still visible) after the failed resume await waitFor(() => expect(screen.getByText("Domanda originale")).toBeInTheDocument()); expect(FakeEventSource.instances).toHaveLength(0); expect(useSessionStore.getState().activityLog).toEqual([ { kind: "status", phase: "F7", text: "Preserve activity", level: "info" }, ]); }); test("closing and reopening Model activity preserves the complete activity log", async () => { wrap(); server.use(http.post("http://localhost:8787/sessions/:id/resume", () => resumeResult("s1"))); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); act(() => { const store = useSessionStore.getState(); store.setPhase("F4"); store.setLastUserEntry({ kind: "input", text: "Inspect patient cohort" }); store.applyEvent({ type: "activity_event", activity: { kind: "tool", toolCallId: "tool-1", toolName: "bash", status: "completed" }, }); store.applyEvent({ type: "text_delta", text: "Cohort ready" }); store.applyEvent({ type: "activity_delta", text: "Inspecting **cohort**" }); store.applyEvent({ type: "info", level: "info", text: "Cohort status" }); }); const beforeClose = useSessionStore.getState().activityLog.map((entry) => ({ ...entry })); expect(screen.queryByRole("heading", { name: /model activity/i })).not.toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: /model activity/i })); const panel = (await screen.findByRole("heading", { name: /model activity/i })).closest("aside"); expect(panel).not.toBeNull(); expect(within(panel!).getByText("cohort").tagName).toBe("STRONG"); expect(within(panel!).getByText("Cohort status")).toBeInTheDocument(); expect(within(panel!).queryByText("Inspect patient cohort")).not.toBeInTheDocument(); expect(within(panel!).queryByText("bash")).not.toBeInTheDocument(); expect(within(panel!).queryByText("Cohort ready")).not.toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: /close model activity/i })); expect(screen.queryByRole("heading", { name: /model activity/i })).not.toBeInTheDocument(); expect(useSessionStore.getState().activityLog).toEqual(beforeClose); await userEvent.click(screen.getByRole("button", { name: /show model activity/i })); const reopened = (await screen.findByRole("heading", { name: /model activity/i })).closest("aside"); expect(reopened).not.toBeNull(); expect(within(reopened!).getByText("cohort").tagName).toBe("STRONG"); expect(within(reopened!).getByText("Cohort status")).toBeInTheDocument(); expect(within(reopened!).queryByText("Inspect patient cohort")).not.toBeInTheDocument(); expect(within(reopened!).queryByText("bash")).not.toBeInTheDocument(); expect(within(reopened!).queryByText("Cohort ready")).not.toBeInTheDocument(); expect(useSessionStore.getState().activityLog).toEqual(beforeClose); }); test("session finalization shows the completion banner and returns to landing", async () => { useSessionStore.getState().resetSession(); let finalized = false; server.use( http.post("http://localhost:8787/sessions/:id/resume", () => resumeResult("s1")), http.get("http://localhost:8787/sessions", () => HttpResponse.json(finalized ? [{ ...LIST[0], status: "finalized" }] : LIST)), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); // The final turn ends: the session is now finalized on disk and agent_end // arrives — the shell must refetch immediately (not wait for the 10s poll) // and surface the new-session invite. finalized = true; act(() => { useSessionStore.getState().applyEvent({ type: "system_event", event: "agent_end" } as any); }); const cta = await screen.findByRole("button", { name: /start a new question/i }); expect(screen.getByText(/session completed and finalized/i)).toBeInTheDocument(); await userEvent.click(cta); // Landing state: the composer invites a brand-new question. expect(await screen.findByText(/type your question/i)).toBeInTheDocument(); }); test("renaming a group reassigns its members via setSessionGroup", async () => { const groupSets: Array<{ id: string; group: string }> = []; server.use( http.post("http://localhost:8787/sessions/:id/group", async ({ params, request }) => { const body = (await request.json()) as { group: string }; groupSets.push({ id: params.id as string, group: body.group }); return new HttpResponse(null, { status: 204 }); }), ); wrap(); await screen.findByText("Aritmologia"); await userEvent.click(screen.getByRole("button", { name: /rename group aritmologia/i })); const input = await screen.findByLabelText(/name/i, { selector: "input" }); await userEvent.clear(input); await userEvent.type(input, "Cardiologia"); await userEvent.click(screen.getByRole("button", { name: /save/i })); await waitFor(() => expect(groupSets).toEqual([{ id: "s1", group: "Cardiologia" }])); }); test("opening Model activity replaces the session rail with a 40/60 activity and chat layout", async () => { server.use(http.post("http://localhost:8787/sessions/:id/resume", () => resumeResult("s1"))); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); expect(screen.getByText("Sessions")).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: /show model activity/i })); const shell = screen.getByTestId("app-shell"); expect(shell).toHaveAttribute("data-activity-layout", "open"); expect(screen.getByRole("heading", { name: "Model activity" }).closest("aside")).toHaveClass("w-2/5"); expect(screen.queryByText("Sessions")).not.toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: /hide model activity/i })); expect(shell).toHaveAttribute("data-activity-layout", "closed"); expect(screen.getByText("Sessions")).toBeInTheDocument(); });