import { act, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { beforeEach, expect, test, vi } from "vitest"; import { http, HttpResponse } from "msw"; import { AppShell } from "./AppShell"; import { server } from "../test/msw"; import { workspaceSummaryFixture } from "../test/workspace-fixtures"; import { FakeEventSource } from "../test/fakeEventSource"; import { clearAuthState, setAuthState } from "../auth/authState"; import { useSessionStore } from "../store/sessionStore"; function renderShell() { const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return render(); } function deferred() { let resolve!: () => void; const promise = new Promise((onResolve) => { resolve = onResolve; }); return { promise, resolve }; } beforeEach(() => { clearAuthState(); setAuthState({ issuer: "local", subject: "user-a", roles: ["user"], permissions: ["session.use"], isAdmin: false, csrfToken: null, session: null, }); FakeEventSource.instances = []; (globalThis as { EventSource: typeof EventSource }).EventSource = FakeEventSource as unknown as typeof EventSource; useSessionStore.getState().resetSession(); server.use( http.get("/api/me", () => HttpResponse.json({ issuer: "local", subject: "user-a", isAdmin: false })), http.get("/api/settings", () => HttpResponse.json({ workspace: "default", provider: "test", model: "test", thinking: "low" })), http.get("/api/workspaces", () => HttpResponse.json([workspaceSummaryFixture("default")])), http.get("/api/models", () => HttpResponse.json({ models: [{ provider: "test", id: "test", name: "Test LLM", reasoning: true }] })), http.post("/api/runtime/prewarm", () => new HttpResponse(null, { status: 202 })), ); }); test("a held stop for s1 cannot reset the newer active s2 session", async () => { const closeGate = deferred(); const closeStarted = deferred(); const active = (id: string, question: string) => ({ id, status: "open", question, summary: null, created_at: "2026-01-02T00:00:00Z", updated_at: null, author: null, name: null, group: null, archived: false, active: true, }); server.use( http.get("/api/sessions", () => HttpResponse.json([active("s1", "Active one"), active("s2", "Active two")])), http.post("/api/sessions/:id/resume", ({ params }) => HttpResponse.json({ id: params.id, alreadyActive: false })), http.get("/api/sessions/:id", ({ params }) => HttpResponse.json({ id: params.id, status: "open", phase: 1 })), http.post("/api/sessions/s1/close", async () => { closeStarted.resolve(); await closeGate.promise; return new HttpResponse(null, { status: 204 }); }), ); renderShell(); await userEvent.click(await screen.findByRole("button", { name: "Active sessions" })); await userEvent.click(await screen.findByTestId("session-item-s1")); await waitFor(() => expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s1/events")); await userEvent.click(screen.getByRole("button", { name: /stop and save session/i })); await userEvent.click((await screen.findAllByRole("button", { name: "Stop & save" }))[0]); await closeStarted.promise; await userEvent.click(screen.getByTestId("session-item-s2")); await waitFor(() => expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s2/events")); act(() => useSessionStore.setState({ currentPhase: "F2" })); closeGate.resolve(); await new Promise((resolve) => setImmediate(resolve)); expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s2/events"); expect(FakeEventSource.instances.at(-1)?.closed).toBe(false); expect(useSessionStore.getState().currentPhase).toBe("F2"); }); test("a held new-session completion cannot replace the newer active s2 target", async () => { const createGate = deferred(); const createStarted = deferred(); server.use( http.get("/api/sessions", () => HttpResponse.json([{ id: "s2", status: "open", question: "Current session", summary: null, created_at: "2026-01-02T00:00:00Z", updated_at: null, author: null, name: null, group: null, archived: false, active: true, }])), http.post("/api/sessions", async () => { createStarted.resolve(); await createGate.promise; return HttpResponse.json({ id: "s3" }); }), http.post("/api/sessions/:id/resume", ({ params }) => HttpResponse.json({ id: params.id, alreadyActive: false })), http.get("/api/sessions/:id", ({ params }) => HttpResponse.json({ id: params.id, status: "open", phase: 1 })), ); renderShell(); const composer = await screen.findByRole("textbox", { name: /new question/i }); await userEvent.type(composer, "Held new question"); await userEvent.click(screen.getByRole("button", { name: /send/i })); await createStarted.promise; await userEvent.click(screen.getByRole("button", { name: "Active sessions" })); await userEvent.click(screen.getByTestId("session-item-s2")); await waitFor(() => expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s2/events")); act(() => useSessionStore.setState({ currentPhase: "F2" })); createGate.resolve(); await new Promise((resolve) => setImmediate(resolve)); expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s2/events"); expect(FakeEventSource.instances.at(-1)?.closed).toBe(false); expect(useSessionStore.getState().currentPhase).toBe("F2"); });