import { act, render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { http, HttpResponse, delay } from "msw"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { server } from "../test/msw"; import { FakeEventSource } from "../test/fakeEventSource"; import { useSessionStore } from "../store/sessionStore"; import { AppShell } from "./AppShell"; import { clearAuthState, setAuthState } from "../auth/authState"; import { workspacePreferences } from "../workspaces/preferences"; function renderShell() { const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return render(); } beforeEach(() => { clearAuthState(); setAuthState({ issuer: "test", subject: "test", roles: ["user"], permissions: ["session.use"], isAdmin: false, csrfToken: null, session: null, }); localStorage.clear(); FakeEventSource.instances = []; (globalThis as any).EventSource = FakeEventSource; useSessionStore.getState().resetSession(); server.use( http.get("/api/me", () => HttpResponse.json({ issuer: "test", subject: "test", displayName: "Test", isAdmin: false })), http.get("/api/sessions", () => HttpResponse.json([])), http.get("/api/settings", () => HttpResponse.json({ workspace: "default", provider: "test", model: "test", thinking: "low" })), http.get("/api/workspaces", () => HttpResponse.json([])), http.get("/api/models", () => HttpResponse.json({ models: [] })), ); }); test("New session starts prewarm without delaying composer focus", async () => { let prewarmStarted = false; server.use( http.post("/api/runtime/prewarm", async () => { prewarmStarted = true; await delay(100); return new HttpResponse(null, { status: 202 }); }), ); renderShell(); await userEvent.click(screen.getByRole("button", { name: /^new session$/i })); const composer = screen.getByRole("textbox", { name: /new question/i }); await waitFor(() => expect(prewarmStarted).toBe(true)); await waitFor(() => expect(composer).toHaveFocus()); expect(composer).toHaveAttribute("data-awaiting-input", "true"); expect(composer).toHaveClass("thot-awaiting-input"); }); test("records the prompt without central duplication, then opens the live log", async () => { let releaseCreate!: () => void; const createMayFinish = new Promise((resolve) => { releaseCreate = resolve; }); server.use( http.post("/api/sessions", async () => { await createMayFinish; return HttpResponse.json({ id: "s-new" }); }), ); renderShell(); const composer = screen.getByRole("textbox", { name: /new question/i }); await userEvent.type(composer, "How many patients?"); await userEvent.click(screen.getByRole("button", { name: /send/i })); expect(screen.queryByText("How many patients?", { selector: "p" })).not.toBeInTheDocument(); expect(screen.queryByText("Creating session…")).not.toBeInTheDocument(); expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument(); expect(useSessionStore.getState().activityLog[0]).toEqual({ kind: "prompt", phase: "F1", text: "How many patients?", }); releaseCreate(); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); act(() => FakeEventSource.instances[0].emit({ type: "text_delta", text: "Inspecting cohort" })); expect(await screen.findByRole("log", { name: "Live model activity" })) .toHaveTextContent("Inspecting cohort"); expect(screen.queryByText("Analyzing question…")).not.toBeInTheDocument(); expect(useSessionStore.getState().currentPhase).toBe("F1"); }); test("a failed create restores the landing view and preserves the question for retry", async () => { server.use( http.post("/api/sessions", () => new HttpResponse("unavailable", { status: 503 })), ); renderShell(); const composer = screen.getByRole("textbox", { name: /new question/i }); await userEvent.type(composer, "Keep this question"); await userEvent.click(screen.getByRole("button", { name: /send/i })); expect(await screen.findByText(/failed to create session/i)).toBeInTheDocument(); expect(screen.getByText(/type your question/i)).toBeInTheDocument(); expect(composer).toHaveValue("Keep this question"); expect(useSessionStore.getState().lastUserEntry).toBeNull(); expect(FakeEventSource.instances).toHaveLength(0); }); test("a DWH-unreachable precheck shows a specific alert and preserves the question", async () => { server.use( http.post("/api/sessions", () => HttpResponse.json( { error: "Cannot start a session: the database is unreachable. Check the VPN connection and try again.", code: "dwh_unreachable", }, { status: 503 }, )), ); renderShell(); const composer = screen.getByRole("textbox", { name: /new question/i }); await userEvent.type(composer, "quanti pazienti?"); await userEvent.click(screen.getByRole("button", { name: /send/i })); // Specific alert, not the generic "failed to create session" hint. expect(await screen.findByText(/database is unreachable/i)).toBeInTheDocument(); expect(screen.queryByText(/failed to create session/i)).not.toBeInTheDocument(); // No session was created: landing view stays and the question is kept for retry. expect(screen.getByText(/type your question/i)).toBeInTheDocument(); expect(composer).toHaveValue("quanti pazienti?"); expect(FakeEventSource.instances).toHaveLength(0); }); test("model selector shows the three Pi-enabled models and stores the selected provider locally", async () => { server.use( http.get("/api/settings", () => HttpResponse.json({ workspace: "default", provider: "zai", model: "glm-5.2", thinking: "low", })), http.get("/api/models", () => HttpResponse.json({ models: [ { provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true }, { provider: "deepseek", id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: true }, { provider: "local-qwen", id: "qwen3.6-35b-a3b", name: "Qwen3.6 35B A3B Local", reasoning: false }, ] })), ); renderShell(); const select = await screen.findByRole("combobox", { name: "Model" }); await waitFor(() => { expect(within(select).getAllByRole("option").map((option) => option.textContent)).toEqual([ "GLM-5.2", "DeepSeek V4 Flash", "Qwen3.6 35B A3B Local", ]); }); await userEvent.selectOptions(select, "qwen3.6-35b-a3b"); await waitFor(() => expect(workspacePreferences.load()).toEqual({ workspaceId: "default", provider: "local-qwen", model: "qwen3.6-35b-a3b", thinking: "low", })); }); test("opens Workspace management from the right sidebar without interrupting the shell", async () => { server.use( http.get("/api/workspace-registry/status", () => HttpResponse.json({ branch: "main", ahead: 0, behind: 0, degraded: false })), ); renderShell(); await userEvent.click(screen.getByRole("button", { name: "Workspace management" })); expect(await screen.findByRole("heading", { name: "Workspace management" })).toBeVisible(); expect(screen.getByTestId("app-shell")).toHaveAttribute("data-activity-layout", "closed"); }); test("marks the composer as awaiting input for a pending freetext gate", () => { useSessionStore.setState({ pendingWidget: { id: "free-1", widget: "freetext", title: "Clarify" }, }); renderShell(); expect(screen.getByRole("textbox", { name: /new question/i })).toHaveAttribute("data-awaiting-input", "true"); });