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 () => {
setAuthState({
issuer: "test", subject: "admin", roles: ["admin"],
permissions: ["session.use", "workspace.manage", "workspace.secrets.manage", "database.manage", "pi.manage"],
isAdmin: true, csrfToken: null, session: null,
});
server.use(
http.get("/api/workspace-registry/status", () =>
HttpResponse.json({ branch: "main", ahead: 0, behind: 0, degraded: false })),
http.get("/api/health/dwh", () => HttpResponse.json({ ok: false })),
);
renderShell();
await userEvent.click(screen.getByRole("button", { name: "Administration" }));
await userEvent.click(screen.getByRole("button", { name: "Workspace management" }));
expect(await screen.findByRole("heading", { name: "Workspace management" })).toBeVisible();
const dialog = screen.getByRole("dialog", { name: "Workspace management" });
const workArea = screen.getByTestId("conversation-column");
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
const workspaceManagement = within(sessionNavigation).getByRole("button", { name: "Workspace management" });
const newSession = within(sessionNavigation).getByRole("button", { name: "New session" });
expect(workArea).toContainElement(dialog);
expect(sessionNavigation).not.toContainElement(dialog);
expect(screen.getByTestId("app-shell")).toHaveAttribute("data-activity-layout", "closed");
expect(workspaceManagement).toHaveAttribute("aria-current", "page");
expect(workspaceManagement).toHaveAttribute("aria-expanded", "true");
expect(workspaceManagement).toHaveAttribute("data-navigation-state", "current");
expect(workspaceManagement).toHaveClass("bg-[oklch(var(--nav-active))]");
expect(newSession).toHaveAttribute("data-navigation-state", "available");
expect(newSession).not.toHaveAttribute("aria-current");
await userEvent.click(screen.getByRole("button", { name: "Close workspace management" }));
await waitFor(() => expect(screen.queryByRole("dialog", { name: "Workspace management" })).not.toBeInTheDocument());
expect(newSession).toHaveAttribute("aria-current", "page");
expect(newSession).toHaveAttribute("data-navigation-state", "current");
expect(workspaceManagement).toHaveAttribute("aria-expanded", "false");
expect(workspaceManagement).toHaveAttribute("data-navigation-state", "available");
});
test("does not block the shell when the DWH is unavailable at startup", async () => {
let healthChecks = 0;
server.use(
http.get("/api/health/dwh", () => {
healthChecks += 1;
return HttpResponse.json({ ok: false });
}),
);
renderShell();
expect(await screen.findByRole("button", { name: "New session" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument();
expect(screen.queryByRole("heading", { name: "Connection unavailable" })).not.toBeInTheDocument();
expect(healthChecks).toBe(0);
});
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");
});