import { act, fireEvent, 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 { workspaceSummaryFixture } from "../test/workspace-fixtures"; import { FakeEventSource } from "../test/fakeEventSource"; import { AppShell } from "./AppShell"; import type { AuthenticatedUser } from "../api/types"; import { clearAuthState, setAuthState } from "../auth/authState"; import { useSessionStore } from "../store/sessionStore"; import { ACTIVITY_PANEL_STORAGE_KEY } from "./useActivityPanelResize"; const regularUser: AuthenticatedUser = { issuer: "portal", subject: "alice", displayName: "Alice", roles: ["user"] as const, permissions: ["session.use", "pi.manage"] as const, isAdmin: false, csrfToken: null, session: null, }; const adminUser: AuthenticatedUser = { ...regularUser, subject: "alice-id", roles: ["admin"] as const, permissions: ["session.use", "session.read_all", "workspace.manage", "workspace.secrets.manage", "database.manage", "pi.manage"] as const, isAdmin: true, }; function wrap(user: AuthenticatedUser = regularUser) { if (user !== regularUser) setAuthState(user); 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 }); class ControlledResizeObserver { static instances: ControlledResizeObserver[] = []; private targets = new Set(); constructor(private readonly callback: ResizeObserverCallback) { ControlledResizeObserver.instances.push(this); } observe = vi.fn((target: Element) => { this.targets.add(target); }); disconnect = vi.fn(() => { this.targets.clear(); }); trigger() { const entries: ResizeObserverEntry[] = [...this.targets].map(target => ({ target, contentRect: new DOMRect(0, 0, target.clientWidth, target.clientHeight), borderBoxSize: [], contentBoxSize: [], devicePixelContentBoxSize: [], })); if (entries.length) this.callback(entries, this as unknown as ResizeObserver); } } function setContainerWidth(element: HTMLElement, width: number) { Object.defineProperty(element, "clientWidth", { configurable: true, value: width }); // AppShell registers more than one observer (activity split + app-area CSS vars): // trigger them all so the layout sync isn't skipped. act(() => ControlledResizeObserver.instances.forEach((o) => o.trigger())); } function deferred() { let resolve!: () => void; const promise = new Promise((onResolve) => { resolve = onResolve; }); return { promise, resolve }; } beforeEach(() => { clearAuthState(); setAuthState(regularUser); localStorage.clear(); FakeEventSource.instances = []; ControlledResizeObserver.instances = []; (globalThis as any).EventSource = FakeEventSource; (globalThis as any).PointerEvent = MouseEvent; (globalThis as any).ResizeObserver = ControlledResizeObserver; window.matchMedia = vi.fn().mockReturnValue({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() }); useSessionStore.getState().resetSession(); server.use( http.get("/api/me", () => HttpResponse.json({ issuer: "portal", subject: "alice", displayName: "Alice", isAdmin: false, permissions: ["session.use", "pi.manage"] }), ), http.get("/api/settings", () => HttpResponse.json({ workspace: "psd", provider: "zai", model: "glm-5.2", thinking: "medium" })), http.get("/api/workspaces", () => HttpResponse.json([workspaceSummaryFixture("psd")])), http.get("/api/models", () => HttpResponse.json({ models: [{ provider: "zai", id: "glm-5.2", name: "Test LLM", reasoning: true }] })), http.get("/api/sessions", () => HttpResponse.json(LIST)), http.get("/api/sessions/:id", () => HttpResponse.json({ id: "s1", status: "open", phase: 1 })), http.get("/api/sessions/:id/documents", () => HttpResponse.json([ { phase: "—", key: "question", title: "Domanda originale", format: "text", content: "Attiva uno" }, ])), http.post("/api/sessions/:id/archive", () => new HttpResponse(null, { status: 204 })), http.post("/api/runtime/prewarm", () => new HttpResponse(null, { status: 202 })), ); }); test("regular users load only their sessions and never see administrator controls", async () => { let scope: string | null = null; server.use(http.get("/api/sessions", ({ request }) => { scope = new URL(request.url).searchParams.get("scope"); return HttpResponse.json(LIST); })); wrap(); await screen.findByText("Attiva uno"); expect(scope).toBe("mine"); expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument(); expect(screen.queryByRole("tab", { name: "All sessions" })).not.toBeInTheDocument(); expect(screen.queryByText(/administrator view/i)).not.toBeInTheDocument(); }); test("Pi management preserves the open session summary and the model activity timeline", async () => { const user = userEvent.setup(); server.use( http.get("/api/pi-management/status", () => HttpResponse.json({ version: "0.80.3", ready: true, credentials: "present", config: { provider: "zai", model: "glm-5.2", reasoning: "medium" }, checkedAt: "2026-08-05T10:00:00.000Z", })), http.get("/api/pi-management/options", () => HttpResponse.json({ providers: ["zai"], models: [{ provider: "zai", id: "glm-5.2" }], reasoning: ["low", "medium", "high"], checkedAt: "2026-08-05T10:00:00.000Z", })), http.post("/api/sessions/:id/resume", () => resumeResult("s1")), http.get("/api/sessions/:id", () => HttpResponse.json({ phase: 4 })), ); wrap(adminUser); await user.click(await screen.findByText("Attiva uno")); expect(await screen.findByRole("complementary", { name: "Session summary" })).toHaveTextContent("Domanda originale"); await user.click(screen.getByRole("button", { name: "Administration" })); await user.click(screen.getByRole("button", { name: "Pi configuration" })); expect(await screen.findByRole("heading", { name: "Pi management" })).toBeVisible(); const dialog = screen.getByRole("main", { name: "Pi management" }); const workArea = screen.getByTestId("conversation-column"); const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" }); const piManagement = within(sessionNavigation).getByRole("button", { name: "Pi configuration" }); const newSession = within(sessionNavigation).getByRole("button", { name: "New session" }); expect(workArea).toContainElement(dialog); expect(sessionNavigation).not.toContainElement(dialog); expect(piManagement).toHaveAttribute("aria-current", "page"); expect(screen.queryByRole("dialog", { name: "Pi management" })).not.toBeInTheDocument(); expect(piManagement).toHaveAttribute("data-navigation-state", "current"); expect(newSession).toHaveAttribute("data-navigation-state", "available"); expect(newSession).not.toHaveAttribute("aria-current"); const preservedSummary = document.querySelector('[aria-label="Session summary"]'); expect(preservedSummary).toHaveAttribute("aria-hidden", "true"); expect(preservedSummary).toHaveTextContent("Attiva uno"); await user.click(screen.getByRole("button", { name: "Return to session" })); await waitFor(() => { expect(screen.getByRole("complementary", { name: "Session summary" })).toHaveTextContent("Domanda originale"); }); expect(newSession).toHaveAttribute("aria-current", "page"); expect(newSession).toHaveAttribute("data-navigation-state", "current"); expect(new URLSearchParams(window.location.search).has("thoth_route")).toBe(false); expect(piManagement).toHaveAttribute("data-navigation-state", "available"); await user.click(screen.getByRole("button", { name: "Resume" })); await screen.findByRole("button", { name: "Show model activity" }); act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "Preserved activity" })); await user.click(screen.getByRole("button", { name: "Pi configuration" })); await user.click(await screen.findByRole("button", { name: "Return to session" })); await waitFor(() => expect(screen.queryByRole("heading", { name: "Pi management" })).not.toBeInTheDocument()); await user.click(screen.getByRole("button", { name: "Show model activity" })); expect(await screen.findByRole("heading", { name: "Model activity" })).toBeVisible(); expect(screen.getByLabelText("Model activity timeline")).toHaveTextContent("Preserved activity"); }); test("administrators can explicitly switch to all sessions and see owners", async () => { let scope = ""; server.use( http.get("/api/me", () => HttpResponse.json({ issuer: "portal", subject: "alice-id", displayName: "Alice", isAdmin: true }), ), http.get("/api/sessions", ({ request }) => { scope = new URL(request.url).searchParams.get("scope") ?? ""; return HttpResponse.json([ { ...LIST[0], author: "Alice" }, { ...LIST[1], id: "s3", question: "Another owner's session", archived: false, author: "Bob" }, ]); }), ); wrap(adminUser); const tablist = await screen.findByRole("tablist", { name: "Session scope" }); const mySessions = within(tablist).getByRole("tab", { name: "My sessions" }); const allSessions = within(tablist).getByRole("tab", { name: "All sessions" }); expect(mySessions).toHaveAttribute("aria-selected", "true"); expect(mySessions).toHaveAttribute("aria-controls", "session-scope-panel"); expect(mySessions).toHaveAttribute("tabindex", "0"); expect(mySessions).toHaveAttribute("data-tab-state", "active"); expect(mySessions).toHaveClass("bg-[oklch(var(--nav-active))]"); expect(mySessions).toHaveClass("border-b-0"); expect(allSessions).toHaveClass("border-b-0"); expect(allSessions).toHaveAttribute("aria-selected", "false"); expect(allSessions).toHaveAttribute("aria-controls", "session-scope-panel"); expect(allSessions).toHaveAttribute("tabindex", "-1"); expect(allSessions).toHaveAttribute("data-tab-state", "inactive"); expect(allSessions).toHaveClass("border-border", "bg-card"); expect(allSessions).not.toHaveClass("border-transparent", "bg-transparent"); expect(screen.getByRole("tabpanel")).toHaveAttribute("aria-labelledby", "session-scope-mine-tab"); await userEvent.click(allSessions); await waitFor(() => expect(scope).toBe("all")); expect(mySessions).toHaveAttribute("aria-selected", "false"); expect(mySessions).toHaveAttribute("data-tab-state", "inactive"); expect(allSessions).toHaveAttribute("aria-selected", "true"); expect(allSessions).toHaveAttribute("data-tab-state", "active"); expect(allSessions).toHaveClass("bg-[oklch(var(--nav-active))]"); expect(screen.getByRole("tabpanel")).toHaveAttribute("aria-labelledby", "session-scope-all-tab"); expect(await screen.findByText("Administrator view: all sessions")).toBeInTheDocument(); expect(screen.getByText("Owner: Bob")).toBeInTheDocument(); }); test("session scope tabs support roving keyboard navigation", async () => { let scope = ""; server.use(http.get("/api/sessions", ({ request }) => { scope = new URL(request.url).searchParams.get("scope") ?? ""; return HttpResponse.json(LIST); })); wrap(adminUser); const user = userEvent.setup(); const tablist = await screen.findByRole("tablist", { name: "Session scope" }); const mySessions = within(tablist).getByRole("tab", { name: "My sessions" }); const allSessions = within(tablist).getByRole("tab", { name: "All sessions" }); mySessions.focus(); await user.keyboard("{ArrowRight}"); await waitFor(() => expect(scope).toBe("all")); expect(allSessions).toHaveFocus(); expect(allSessions).toHaveAttribute("aria-selected", "true"); expect(mySessions).toHaveAttribute("tabindex", "-1"); await user.keyboard("{ArrowLeft}"); await waitFor(() => expect(scope).toBe("mine")); expect(mySessions).toHaveFocus(); expect(mySessions).toHaveAttribute("aria-selected", "true"); await user.keyboard("{End}"); expect(allSessions).toHaveFocus(); expect(allSessions).toHaveAttribute("aria-selected", "true"); await user.keyboard("{Home}"); expect(mySessions).toHaveFocus(); expect(mySessions).toHaveAttribute("aria-selected", "true"); }); test("administrator confirms before deleting a same-named user's session", async () => { let deletes = 0; server.use( http.get("/api/me", () => HttpResponse.json({ issuer: "portal", subject: "alice-id", displayName: "Alice", isAdmin: true }), ), http.get("/api/sessions", () => HttpResponse.json([ { ...LIST[0], author: "Alice" }, { ...LIST[1], id: "s3", question: "Second session", archived: false, author: "Bob" }, ])), http.delete("/api/sessions/:id", () => { deletes += 1; return new HttpResponse(null, { status: 204 }); }), ); wrap(adminUser); await userEvent.click(await screen.findByRole("tab", { name: "All sessions" })); await screen.findByText("Owner: Alice"); await userEvent.click(screen.getByRole("checkbox", { name: "Select Attiva uno" })); await userEvent.click(screen.getByRole("button", { name: "Delete 1 selected sessions" })); expect(deletes).toBe(0); expect(await screen.findByRole("heading", { name: "Delete permanently" })).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Delete" })); await waitFor(() => expect(deletes).toBe(1)); }); test("administrator confirms before archiving a same-named user's session", async () => { let archives = 0; const confirm = vi.spyOn(window, "confirm").mockReturnValue(false); server.use( http.get("/api/me", () => HttpResponse.json({ issuer: "portal", subject: "alice-id", displayName: "Alice", isAdmin: true }), ), http.get("/api/sessions", () => HttpResponse.json([ { ...LIST[0], author: "Alice" }, ])), http.post("/api/sessions/:id/archive", () => { archives += 1; return new HttpResponse(null, { status: 204 }); }), ); wrap(adminUser); await userEvent.click(await screen.findByRole("tab", { name: "All sessions" })); await screen.findByText("Owner: Alice"); await userEvent.click(screen.getByRole("button", { name: "Session actions" })); await userEvent.click(await screen.findByText("Archive")); expect(confirm).toHaveBeenCalledWith("Archive Alice's session?"); expect(archives).toBe(0); confirm.mockRestore(); }); 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("/api/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("resizes the session summary to exactly 50% and persists pointer width independently", async () => { wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await screen.findByText("Domanda originale"); const shell = screen.getByTestId("app-shell"); setContainerWidth(shell, 1600); const separator = screen.getByRole("separator", { name: /resize session summary/i }); expect(separator).toHaveAttribute("aria-orientation", "vertical"); expect(separator).toHaveAttribute("aria-valuemin", "320"); expect(separator).toHaveAttribute("aria-valuemax", "800"); expect(shell.style.getPropertyValue("--session-panel-width")).toBe("384px"); Object.assign(separator, { setPointerCapture: vi.fn(), releasePointerCapture: vi.fn(), hasPointerCapture: vi.fn(() => true), }); fireEvent.pointerDown(separator, { pointerId: 2, clientX: 384 }); fireEvent.pointerMove(separator, { pointerId: 2, clientX: 900 }); fireEvent.pointerUp(separator, { pointerId: 2, clientX: 900 }); expect(shell.style.getPropertyValue("--session-panel-width")).toBe("800px"); expect(localStorage.getItem("thothii:session-documents-panel-width")).toBe("800"); expect(screen.getByRole("complementary", { name: /session summary/i })).toHaveClass( "w-[var(--session-panel-width)]", "max-w-[50%]", ); expect(screen.queryByRole("separator", { name: /resize model activity/i })).not.toBeInTheDocument(); }); test("supports keyboard session resizing and hides its divider when the split is unusable", async () => { localStorage.setItem("thothii:session-documents-panel-width", "448"); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await screen.findByText("Domanda originale"); const shell = screen.getByTestId("app-shell"); setContainerWidth(shell, 1200); const separator = screen.getByRole("separator", { name: /resize session summary/i }); expect(shell.style.getPropertyValue("--session-panel-width")).toBe("448px"); fireEvent.keyDown(separator, { key: "ArrowRight", shiftKey: true }); expect(shell.style.getPropertyValue("--session-panel-width")).toBe("496px"); fireEvent.keyDown(separator, { key: "End" }); expect(shell.style.getPropertyValue("--session-panel-width")).toBe("600px"); fireEvent.keyDown(separator, { key: "Home" }); expect(shell.style.getPropertyValue("--session-panel-width")).toBe("320px"); setContainerWidth(shell, 800); expect(screen.queryByRole("separator", { name: /resize session summary/i })).not.toBeInTheDocument(); expect(screen.getByRole("complementary", { name: /session summary/i })).toHaveClass( "w-[30vw]", "max-w-[30vw]", ); }); test("clicking a session with a live runtime reconnects to its gate instead of the panel", async () => { let resumed: string | null = null; server.use( http.get("/api/sessions", () => HttpResponse.json([{ ...LIST[0], active: true }])), http.post("/api/sessions/:id/resume", ({ params }) => { resumed = params.id as string; return resumeResult(resumed, true); // warm runtime → alreadyActive }), http.get("/api/sessions/:id", () => HttpResponse.json({ id: "s1", status: "open", phase: 1 })), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await waitFor(() => expect(resumed).toBe("s1")); await waitFor(() => expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s1/events")); // Live view, not the read-only documents panel. expect(screen.queryByText("Domanda originale")).not.toBeInTheDocument(); }); test("New session closes an open session detail panel", async () => { server.use(http.post("/api/runtime/prewarm", () => HttpResponse.json({ status: "warming" }, { status: 202 }))); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); // cold session → panel opens await screen.findByText("Domanda originale"); await userEvent.click(screen.getByRole("button", { name: /^new session$/i })); await waitFor(() => expect(screen.queryByText("Domanda originale")).not.toBeInTheDocument()); expect(screen.getByText(/type your question/i)).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("/api/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("/api/sessions/:id/resume", () => resumeResult("s1")), http.get("/api/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("/api/sessions/:id/resume", () => { resumeCalls += 1; return resumeResult("s1", resumeCalls > 1); }), http.get("/api/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.getByTestId("session-item-s1")); 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("/api/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("/api/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.getByTestId("session-item-s1")); 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("/api/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("/api/sessions", () => HttpResponse.json([LIST[0], other])), http.post("/api/sessions/:id/resume", ({ params }) => { const id = params.id as string; if (id === "s1") s1ResumeCalls += 1; return resumeResult(id); }), http.get("/api/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.getByTestId("session-item-s1")); 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("/api/sessions/:id/resume", () => resumeResult("s1")), http.get("/api/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("/api/sessions/:id/resume", async () => { markStarted(); await resumeReleased; return resumeResult("s1"); })); await userEvent.click(screen.getByTestId("session-item-s1")); 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("/api/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("/api/sessions/:id/resume", () => resumeResult("s1")), http.get("/api/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("/api/sessions/:id/resume", () => new HttpResponse(null, { status: 409 }))); await userEvent.click(screen.getByTestId("session-item-s1")); 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("/api/sessions", () => HttpResponse.json([LIST[0], other])), http.post("/api/sessions/:id/resume", ({ params }) => resumeResult(params.id as string, params.id === "s3")), http.get("/api/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("/api/sessions", () => HttpResponse.json([LIST[0], other])), http.post("/api/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("/api/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("/api/sessions", () => HttpResponse.json([LIST[0], other])), http.post("/api/sessions/:id/resume", ({ params }) => resumeResult(params.id as string)), http.get("/api/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("/api/sessions/:id/resume", async () => { resumeStarted.resolve(); await resumeGate.promise; return resumeResult("s1"); }), http.post("/api/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 delayed Resume from user A cannot activate a session after user B logs in", async () => { const resumeGate = deferred(); const resumeStarted = deferred(); const resumeSettled = deferred(); server.use( http.post("/api/sessions/:id/resume", async () => { resumeStarted.resolve(); try { await resumeGate.promise; return resumeResult("s1"); } finally { resumeSettled.resolve(); } }), http.get("/api/sessions/:id", () => HttpResponse.json({ id: "s1", status: "open", phase: 4 })), ); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await resumeStarted.promise; act(() => setAuthState({ ...regularUser, subject: "bob", displayName: "Bob" })); resumeGate.resolve(); await act(async () => { await resumeSettled.promise; }); expect(FakeEventSource.instances).toHaveLength(0); expect(useSessionStore.getState().currentPhase).toBeNull(); expect(screen.queryByRole("button", { name: "Hide model activity" })).not.toBeInTheDocument(); }); test("a delayed session deletion from user A cannot refresh or toast into user B", async () => { const deleteGate = deferred(); const deleteStarted = deferred(); const deleteSettled = deferred(); server.use(http.delete("/api/sessions/:id", async () => { deleteStarted.resolve(); try { await deleteGate.promise; return new HttpResponse(null, { status: 204 }); } finally { deleteSettled.resolve(); } })); wrap(); 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; act(() => setAuthState({ ...regularUser, subject: "bob", displayName: "Bob" })); deleteGate.resolve(); await act(async () => { await deleteSettled.promise; }); expect(useSessionStore.getState().toasts).toEqual([]); expect(screen.queryByText("Deleted 1 of 1 sessions.")).not.toBeInTheDocument(); expect(screen.getByTestId("session-item-s1")).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("/api/sessions/:id/resume", async () => { resumeStarted.resolve(); await resumeGate.promise; return resumeResult("s1"); }), http.delete("/api/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("/api/sessions/:id", async ({ params }) => { expect(params.id).toBe("s1"); deleteStarted.resolve(); await deleteGate.promise; return new HttpResponse(null, { status: 204 }); }), http.post("/api/sessions/:id/resume", () => resumeResult("s1")), http.get("/api/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("/api/sessions/:id/resume", async () => { resumeStarted.resolve(); await resumeGate.promise; return resumeResult("s1"); }), http.delete("/api/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("/api/sessions", () => HttpResponse.json([LIST[0], other])), http.post("/api/sessions/:id/resume", async ({ params }) => { const id = params.id as string; if (id === "s3") { s3ResumeStarted.resolve(); await s3ResumeGate.promise; } return resumeResult(id); }), http.get("/api/sessions/:id", ({ params }) => HttpResponse.json({ id: params.id, status: "open", phase: params.id === "s3" ? 3 : 1 })), http.delete("/api/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("/api/sessions/:id/resume", async () => { resumeStarted.resolve(); await resumeGate.promise; return resumeResult("s1"); }), http.delete("/api/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("/api/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("/api/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(panel).toHaveTextContent("Inspect patient cohort"); expect(panel).toHaveTextContent("Inspecting cohort"); expect(within(panel!).getByText("cohort").tagName).toBe("STRONG"); expect(within(panel!).getByText("Cohort ready")).toBeInTheDocument(); expect(within(panel!).queryByText("Cohort status")).not.toBeInTheDocument(); expect(within(panel!).queryByText("bash")).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(reopened).toHaveTextContent("Inspect patient cohort"); expect(reopened).toHaveTextContent("Inspecting cohort"); expect(within(reopened!).getByText("cohort").tagName).toBe("STRONG"); expect(within(reopened!).getByText("Cohort ready")).toBeInTheDocument(); expect(within(reopened!).queryByText("Cohort status")).not.toBeInTheDocument(); expect(within(reopened!).queryByText("bash")).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("/api/sessions/:id/resume", () => resumeResult("s1")), http.get("/api/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("/api/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("opens an accessible resizable activity split and persists pointer width", async () => { server.use(http.post("/api/sessions/:id/resume", () => resumeResult("s1"))); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await userEvent.click(screen.getByRole("button", { name: /show model activity/i })); const shell = screen.getByTestId("app-shell"); setContainerWidth(shell, 1200); const separator = screen.getByRole("separator", { name: /resize model activity/i }); expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("384px"); expect(separator).toHaveAttribute("aria-orientation", "vertical"); expect(separator).toHaveAttribute("aria-valuemin", "288"); expect(separator).toHaveAttribute("aria-valuenow", "384"); Object.assign(separator, { setPointerCapture: vi.fn(), releasePointerCapture: vi.fn(), hasPointerCapture: vi.fn(() => true), }); fireEvent.pointerDown(separator, { pointerId: 1, clientX: 384 }); fireEvent.pointerMove(separator, { pointerId: 1, clientX: 484 }); fireEvent.pointerUp(separator, { pointerId: 1, clientX: 484 }); expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("484px"); expect(localStorage.getItem(ACTIVITY_PANEL_STORAGE_KEY)).toBe("484"); expect(screen.getByTestId("conversation-column")).toHaveClass("flex-1"); }); test("resizes the activity split with keyboard and exposes responsive drawer classes", async () => { localStorage.setItem(ACTIVITY_PANEL_STORAGE_KEY, "448"); server.use(http.post("/api/sessions/:id/resume", () => resumeResult("s1"))); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await userEvent.click(screen.getByRole("button", { name: /show model activity/i })); const shell = screen.getByTestId("app-shell"); setContainerWidth(shell, 1200); const separator = screen.getByRole("separator", { name: /resize model activity/i }); expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("448px"); fireEvent.keyDown(separator, { key: "ArrowLeft" }); expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("432px"); fireEvent.keyDown(separator, { key: "ArrowRight", shiftKey: true }); expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("480px"); fireEvent.keyDown(separator, { key: "Home" }); expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("288px"); const panel = screen.getByRole("heading", { name: "Model activity" }).closest("aside"); expect(panel).toHaveClass( "static", "w-[var(--activity-panel-width)]", ); expect(separator).toHaveClass("touch-none", "cursor-col-resize"); }); test("uses the measured app shell for the desktop activity split", async () => { localStorage.setItem(ACTIVITY_PANEL_STORAGE_KEY, "576"); server.use(http.post("/api/sessions/:id/resume", () => resumeResult("s1"))); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await userEvent.click(await screen.findByRole("button", { name: /show model activity/i })); const shell = screen.getByTestId("app-shell"); setContainerWidth(shell, 1200); expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("576px"); expect(screen.getByRole("separator", { name: /resize model activity/i })).toBeInTheDocument(); setContainerWidth(shell, 1024); expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("512px"); setContainerWidth(shell, 799); expect(screen.queryByRole("separator", { name: /resize model activity/i })).not.toBeInTheDocument(); expect(screen.getByTestId("conversation-column")).toHaveClass("flex-1"); expect(screen.getByRole("heading", { name: "Model activity" }).closest("aside")).toHaveClass("fixed"); }); test("cancels an active resize when the measured shell becomes too narrow", async () => { localStorage.setItem(ACTIVITY_PANEL_STORAGE_KEY, "576"); server.use(http.post("/api/sessions/:id/resume", () => resumeResult("s1"))); wrap(); await userEvent.click(await screen.findByText("Attiva uno")); await userEvent.click(await screen.findByRole("button", { name: /resume/i })); await userEvent.click(await screen.findByRole("button", { name: /show model activity/i })); const shell = screen.getByTestId("app-shell"); setContainerWidth(shell, 1200); const separator = screen.getByRole("separator", { name: /resize model activity/i }); Object.assign(separator, { setPointerCapture: vi.fn(), releasePointerCapture: vi.fn(), hasPointerCapture: vi.fn(() => true), }); fireEvent.pointerDown(separator, { pointerId: 1, clientX: 576 }); fireEvent.pointerMove(separator, { pointerId: 1, clientX: 400 }); setContainerWidth(shell, 799); fireEvent.pointerUp(separator, { pointerId: 1, clientX: 300 }); expect(localStorage.getItem(ACTIVITY_PANEL_STORAGE_KEY)).toBe("576"); expect(shell).toHaveAttribute("data-activity-resizing", "false"); });