import { render, screen, waitFor } 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"; function renderShell() { const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return render(); } beforeEach(() => { FakeEventSource.instances = []; (globalThis as any).EventSource = FakeEventSource; useSessionStore.getState().resetSession(); server.use( http.get("http://localhost:8787/sessions", () => HttpResponse.json([])), http.get("http://localhost:8787/settings", () => HttpResponse.json({ workspace: "default", provider: "test", model: "test", thinking: "low" })), http.get("http://localhost:8787/workspaces", () => HttpResponse.json([])), http.get("http://localhost:8787/models", () => HttpResponse.json({ models: [] })), ); }); test("New session starts prewarm without delaying composer focus", async () => { let prewarmStarted = false; server.use( http.post("http://localhost:8787/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()); }); test("shows the question and creation timer before POST /sessions resolves, then opens SSE", async () => { let releaseCreate!: () => void; const createMayFinish = new Promise((resolve) => { releaseCreate = resolve; }); server.use( http.post("http://localhost:8787/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(await screen.findByText("How many patients?", { selector: "p" })).toBeInTheDocument(); expect(screen.getByText("Creating session…")).toBeInTheDocument(); expect(screen.getByText(/\d+s/)).toBeInTheDocument(); expect(FakeEventSource.instances).toHaveLength(0); releaseCreate(); await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); expect(screen.getByText("How many patients?", { selector: "p" })).toBeInTheDocument(); expect(screen.getByText("Analyzing question…")).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("http://localhost:8787/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); });