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 { 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([])),
http.get("/api/models", () => HttpResponse.json({ models: [] })),
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.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.findByRole("button", { name: "Stop & save" }));
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 = screen.getByRole("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.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");
});