import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { server } from "../test/msw";
import { FakeEventSource } from "../test/fakeEventSource";
import { AppShell } from "./AppShell";
import type { AuthenticatedUser } from "../api/types";
import { clearAuthState, setAuthState } from "../auth/authState";
import { useSessionStore } from "../store/sessionStore";
import { ACTIVITY_PANEL_STORAGE_KEY } from "./useActivityPanelResize";
const regularUser: AuthenticatedUser = {
issuer: "portal", subject: "alice", displayName: "Alice", roles: ["user"] as const,
permissions: ["session.use", "pi.manage"] as const, isAdmin: false, csrfToken: null, session: null,
};
const adminUser: AuthenticatedUser = {
...regularUser, subject: "alice-id", roles: ["admin"] as const,
permissions: ["session.use", "session.read_all", "pi.manage"] as const, isAdmin: true,
};
function wrap(user: AuthenticatedUser = regularUser) {
if (user !== regularUser) setAuthState(user);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render();
}
const LIST = [
{ id: "s1", status: "open", question: "Attiva uno", summary: null, created_at: "2026-01-02T00:00:00Z", updated_at: null, author: null, name: null, group: "Aritmologia", archived: false },
{ id: "s2", status: "finalized", question: "Archiviata due", summary: null, created_at: "2026-01-01T00:00:00Z", updated_at: null, author: null, name: null, group: null, archived: true },
];
const resumeResult = (id: string, alreadyActive = false) =>
HttpResponse.json({ id, alreadyActive });
class ControlledResizeObserver {
static instances: ControlledResizeObserver[] = [];
constructor(private readonly callback: ResizeObserverCallback) {
ControlledResizeObserver.instances.push(this);
}
observe = vi.fn();
disconnect = vi.fn();
trigger() {
this.callback([], this as unknown as ResizeObserver);
}
}
function setContainerWidth(element: HTMLElement, width: number) {
Object.defineProperty(element, "clientWidth", { configurable: true, value: width });
// AppShell registers more than one observer (activity split + app-area CSS vars):
// trigger them all so the layout sync isn't skipped.
act(() => ControlledResizeObserver.instances.forEach((o) => o.trigger()));
}
function deferred() {
let resolve!: () => void;
const promise = new Promise((onResolve) => { resolve = onResolve; });
return { promise, resolve };
}
beforeEach(() => {
clearAuthState();
setAuthState(regularUser);
localStorage.clear();
FakeEventSource.instances = [];
ControlledResizeObserver.instances = [];
(globalThis as any).EventSource = FakeEventSource;
(globalThis as any).PointerEvent = MouseEvent;
(globalThis as any).ResizeObserver = ControlledResizeObserver;
window.matchMedia = vi.fn().mockReturnValue({ matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() });
useSessionStore.getState().resetSession();
server.use(
http.get("/api/me", () =>
HttpResponse.json({ issuer: "portal", subject: "alice", displayName: "Alice", isAdmin: false, permissions: ["session.use", "pi.manage"] }),
),
http.get("/api/settings", () => HttpResponse.json({ workspace: "psd", provider: "zai", model: "glm-5.2", thinking: "medium" })),
http.get("/api/workspaces", () => HttpResponse.json([])),
http.get("/api/models", () => HttpResponse.json({ models: [] })),
http.get("/api/sessions", () => HttpResponse.json(LIST)),
http.get("/api/sessions/:id", () => HttpResponse.json({ id: "s1", status: "open", phase: 1 })),
http.get("/api/sessions/:id/documents", () => HttpResponse.json([
{ phase: "—", key: "question", title: "Domanda originale", format: "text", content: "Attiva uno" },
])),
http.post("/api/sessions/:id/archive", () => new HttpResponse(null, { status: 204 })),
http.post("/api/runtime/prewarm", () => new HttpResponse(null, { status: 202 })),
);
});
test("regular users load only their sessions and never see administrator controls", async () => {
let scope: string | null = null;
server.use(http.get("/api/sessions", ({ request }) => {
scope = new URL(request.url).searchParams.get("scope");
return HttpResponse.json(LIST);
}));
wrap();
await screen.findByText("Attiva uno");
expect(scope).toBe("mine");
expect(screen.queryByRole("button", { name: "All sessions" })).not.toBeInTheDocument();
expect(screen.queryByText(/administrator view/i)).not.toBeInTheDocument();
});
test("Pi management preserves the open session summary and the model activity timeline", async () => {
const user = userEvent.setup();
server.use(
http.get("/api/pi-management/status", () => HttpResponse.json({
version: "0.80.3", ready: true,
credentials: "present",
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
checkedAt: "2026-08-05T10:00:00.000Z",
})),
http.get("/api/pi-management/options", () => HttpResponse.json({
providers: ["zai"], models: [{ provider: "zai", id: "glm-5.2" }],
reasoning: ["low", "medium", "high"], checkedAt: "2026-08-05T10:00:00.000Z",
})),
http.post("/api/sessions/:id/resume", () => resumeResult("s1")),
http.get("/api/sessions/:id", () => HttpResponse.json({ phase: 4 })),
);
wrap();
await user.click(await screen.findByText("Attiva uno"));
expect(await screen.findByRole("complementary", { name: "Session summary" })).toHaveTextContent("Domanda originale");
await user.click(screen.getByRole("button", { name: "Pi management" }));
expect(await screen.findByRole("heading", { name: "Pi management" })).toBeVisible();
const dialog = screen.getByRole("dialog", { name: "Pi management" });
const workArea = screen.getByTestId("conversation-column");
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
expect(workArea).toContainElement(dialog);
expect(sessionNavigation).not.toContainElement(dialog);
const preservedSummary = screen.getByRole("complementary", { name: "Session summary" });
expect(preservedSummary).not.toHaveAttribute("aria-hidden", "true");
expect(preservedSummary).toHaveTextContent("Attiva uno");
await user.click(screen.getByRole("button", { name: "Close Pi management" }));
await waitFor(() => {
expect(screen.getByRole("complementary", { name: "Session summary" })).toHaveTextContent("Domanda originale");
});
await user.click(screen.getByRole("button", { name: "Resume" }));
await screen.findByRole("button", { name: "Show model activity" });
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "Preserved activity" }));
await user.click(screen.getByRole("button", { name: "Pi management" }));
await user.click(await screen.findByRole("button", { name: "Close Pi management" }));
await waitFor(() => expect(screen.queryByRole("heading", { name: "Pi management" })).not.toBeInTheDocument());
await user.click(screen.getByRole("button", { name: "Show model activity" }));
expect(await screen.findByRole("heading", { name: "Model activity" })).toBeVisible();
expect(screen.getByLabelText("Model activity timeline")).toHaveTextContent("Preserved activity");
});
test("administrators can explicitly switch to all sessions and see owners", async () => {
let scope = "";
server.use(
http.get("/api/me", () =>
HttpResponse.json({ issuer: "portal", subject: "alice-id", displayName: "Alice", isAdmin: true }),
),
http.get("/api/sessions", ({ request }) => {
scope = new URL(request.url).searchParams.get("scope") ?? "";
return HttpResponse.json([
{ ...LIST[0], author: "Alice" },
{ ...LIST[1], id: "s3", question: "Another owner's session", archived: false, author: "Bob" },
]);
}),
);
wrap(adminUser);
await screen.findByRole("button", { name: "All sessions" });
expect(screen.getByRole("button", { name: "My sessions" })).toHaveAttribute("aria-pressed", "true");
expect(screen.getByRole("button", { name: "All sessions" })).toHaveAttribute("aria-pressed", "false");
await userEvent.click(screen.getByRole("button", { name: "All sessions" }));
await waitFor(() => expect(scope).toBe("all"));
expect(screen.getByRole("button", { name: "My sessions" })).toHaveAttribute("aria-pressed", "false");
expect(screen.getByRole("button", { name: "All sessions" })).toHaveAttribute("aria-pressed", "true");
expect(await screen.findByText("Administrator view: all sessions")).toBeInTheDocument();
expect(screen.getByText("Owner: Bob")).toBeInTheDocument();
});
test("administrator confirms before deleting a same-named user's session", async () => {
let deletes = 0;
server.use(
http.get("/api/me", () =>
HttpResponse.json({ issuer: "portal", subject: "alice-id", displayName: "Alice", isAdmin: true }),
),
http.get("/api/sessions", () => HttpResponse.json([
{ ...LIST[0], author: "Alice" },
{ ...LIST[1], id: "s3", question: "Second session", archived: false, author: "Bob" },
])),
http.delete("/api/sessions/:id", () => {
deletes += 1;
return new HttpResponse(null, { status: 204 });
}),
);
wrap(adminUser);
await userEvent.click(await screen.findByRole("button", { name: "All sessions" }));
await screen.findByText("Owner: Alice");
await userEvent.click(screen.getByRole("checkbox", { name: "Select Attiva uno" }));
await userEvent.click(screen.getByRole("button", { name: "Delete 1 selected sessions" }));
expect(deletes).toBe(0);
expect(await screen.findByRole("heading", { name: "Delete permanently" })).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Delete" }));
await waitFor(() => expect(deletes).toBe(1));
});
test("administrator confirms before archiving a same-named user's session", async () => {
let archives = 0;
const confirm = vi.spyOn(window, "confirm").mockReturnValue(false);
server.use(
http.get("/api/me", () =>
HttpResponse.json({ issuer: "portal", subject: "alice-id", displayName: "Alice", isAdmin: true }),
),
http.get("/api/sessions", () => HttpResponse.json([
{ ...LIST[0], author: "Alice" },
])),
http.post("/api/sessions/:id/archive", () => {
archives += 1;
return new HttpResponse(null, { status: 204 });
}),
);
wrap(adminUser);
await userEvent.click(await screen.findByRole("button", { name: "All sessions" }));
await screen.findByText("Owner: Alice");
await userEvent.click(screen.getByRole("button", { name: "Session actions" }));
await userEvent.click(await screen.findByText("Archive"));
expect(confirm).toHaveBeenCalledWith("Archive Alice's session?");
expect(archives).toBe(0);
confirm.mockRestore();
});
test("active list shows group header and hides archived sessions", async () => {
wrap();
expect(await screen.findByText("Attiva uno")).toBeInTheDocument();
expect(screen.getByText("Aritmologia")).toBeInTheDocument(); // group header
expect(screen.queryByText("Archiviata due")).not.toBeInTheDocument(); // archived hidden
});
test("ungrouped sessions render after groups with no 'No group' label", async () => {
server.use(
http.get("/api/sessions", () => HttpResponse.json([
{ id: "g1", status: "open", question: "In gruppo", summary: null, created_at: "2026-01-02T00:00:00Z", updated_at: null, author: null, name: null, group: "Aritmologia", archived: false },
{ id: "u1", status: "open", question: "Senza gruppo", summary: null, created_at: "2026-01-03T00:00:00Z", updated_at: null, author: null, name: null, group: null, archived: false },
])),
);
wrap();
expect(await screen.findByText("In gruppo")).toBeInTheDocument(); // grouped
expect(screen.getByText("Senza gruppo")).toBeInTheDocument(); // ungrouped, still listed
expect(screen.queryByText(/no group/i)).not.toBeInTheDocument();
});
test("opening the panel shows the session documents", async () => {
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
expect(await screen.findByText("Domanda originale")).toBeInTheDocument();
});
test("resizes the session summary to exactly 50% and persists pointer width independently", async () => {
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await screen.findByText("Domanda originale");
const shell = screen.getByTestId("app-shell");
setContainerWidth(shell, 1600);
const separator = screen.getByRole("separator", { name: /resize session summary/i });
expect(separator).toHaveAttribute("aria-orientation", "vertical");
expect(separator).toHaveAttribute("aria-valuemin", "320");
expect(separator).toHaveAttribute("aria-valuemax", "800");
expect(shell.style.getPropertyValue("--session-panel-width")).toBe("384px");
Object.assign(separator, {
setPointerCapture: vi.fn(),
releasePointerCapture: vi.fn(),
hasPointerCapture: vi.fn(() => true),
});
fireEvent.pointerDown(separator, { pointerId: 2, clientX: 384 });
fireEvent.pointerMove(separator, { pointerId: 2, clientX: 900 });
fireEvent.pointerUp(separator, { pointerId: 2, clientX: 900 });
expect(shell.style.getPropertyValue("--session-panel-width")).toBe("800px");
expect(localStorage.getItem("thothii:session-documents-panel-width")).toBe("800");
expect(screen.getByRole("complementary", { name: /session summary/i })).toHaveClass(
"w-[var(--session-panel-width)]",
"max-w-[50%]",
);
expect(screen.queryByRole("separator", { name: /resize model activity/i })).not.toBeInTheDocument();
});
test("supports keyboard session resizing and hides its divider when the split is unusable", async () => {
localStorage.setItem("thothii:session-documents-panel-width", "448");
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await screen.findByText("Domanda originale");
const shell = screen.getByTestId("app-shell");
setContainerWidth(shell, 1200);
const separator = screen.getByRole("separator", { name: /resize session summary/i });
expect(shell.style.getPropertyValue("--session-panel-width")).toBe("448px");
fireEvent.keyDown(separator, { key: "ArrowRight", shiftKey: true });
expect(shell.style.getPropertyValue("--session-panel-width")).toBe("496px");
fireEvent.keyDown(separator, { key: "End" });
expect(shell.style.getPropertyValue("--session-panel-width")).toBe("600px");
fireEvent.keyDown(separator, { key: "Home" });
expect(shell.style.getPropertyValue("--session-panel-width")).toBe("320px");
setContainerWidth(shell, 800);
expect(screen.queryByRole("separator", { name: /resize session summary/i })).not.toBeInTheDocument();
expect(screen.getByRole("complementary", { name: /session summary/i })).toHaveClass(
"w-[30vw]",
"max-w-[30vw]",
);
});
test("clicking a session with a live runtime reconnects to its gate instead of the panel", async () => {
let resumed: string | null = null;
server.use(
http.get("/api/sessions", () =>
HttpResponse.json([{ ...LIST[0], active: true }])),
http.post("/api/sessions/:id/resume", ({ params }) => {
resumed = params.id as string;
return resumeResult(resumed, true); // warm runtime → alreadyActive
}),
http.get("/api/sessions/:id", () =>
HttpResponse.json({ id: "s1", status: "open", phase: 1 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await waitFor(() => expect(resumed).toBe("s1"));
await waitFor(() => expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s1/events"));
// Live view, not the read-only documents panel.
expect(screen.queryByText("Domanda originale")).not.toBeInTheDocument();
});
test("New session closes an open session detail panel", async () => {
server.use(http.post("/api/runtime/prewarm", () =>
HttpResponse.json({ status: "warming" }, { status: 202 })));
wrap();
await userEvent.click(await screen.findByText("Attiva uno")); // cold session → panel opens
await screen.findByText("Domanda originale");
await userEvent.click(screen.getByRole("button", { name: /^new session$/i }));
await waitFor(() => expect(screen.queryByText("Domanda originale")).not.toBeInTheDocument());
expect(screen.getByText(/type your question/i)).toBeInTheDocument();
});
test("Archive accordion expands to reveal archived sessions", async () => {
wrap();
// archived hidden until the Archive accordion is expanded
expect(await screen.findByText("Attiva uno")).toBeInTheDocument();
expect(screen.queryByText("Archiviata due")).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: /archive/i }));
expect(await screen.findByText("Archiviata due")).toBeInTheDocument();
// Active sessions remain visible (it is a separate accordion, not a swap)
expect(screen.getByText("Attiva uno")).toBeInTheDocument();
});
test("Resume from the panel activates the session and closes the panel", async () => {
let resumed: string | null = null;
useSessionStore.setState({
activityLog: [{ kind: "status", phase: "F7", text: "Stale prior activity", level: "info" }],
});
server.use(
http.post("/api/sessions/:id/resume", ({ params }) => {
resumed = params.id as string;
return resumeResult(resumed);
}),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await screen.findByText("Domanda originale"); // panel open
await userEvent.click(screen.getByRole("button", { name: /resume/i }));
expect(useSessionStore.getState().activityLog).toEqual([
{ kind: "lifecycle", phase: null, text: "Resuming session" },
]);
await waitFor(() => expect(resumed).toBe("s1"));
await waitFor(() => expect(screen.queryByText("Domanda originale")).not.toBeInTheDocument()); // panel closed
});
test("Resume paints the re-entry phase from the manifest after the cold Resume succeeds", async () => {
useSessionStore.getState().resetSession();
server.use(
http.post("/api/sessions/:id/resume", () => resumeResult("s1")),
http.get("/api/sessions/:id", () =>
HttpResponse.json({ id: "s1", status: "open", phase: 4 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await screen.findByText("Domanda originale"); // panel open
await userEvent.click(screen.getByRole("button", { name: /resume/i }));
await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F4"));
});
test("an already-active same-session Resume preserves its EventSource and store", async () => {
let resumeCalls = 0;
server.use(
http.post("/api/sessions/:id/resume", () => {
resumeCalls += 1;
return resumeResult("s1", resumeCalls > 1);
}),
http.get("/api/sessions/:id", () =>
HttpResponse.json({ id: "s1", status: "open", phase: 1 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
const first = FakeEventSource.instances[0];
act(() => first.emitNamed("info", { type: "info", text: "Live state" }, "4"));
const before = useSessionStore.getState().activityLog.map((entry) => ({ ...entry }));
await userEvent.click(screen.getByTestId("session-item-s1"));
await screen.findByText("Domanda originale");
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(screen.queryByText("Domanda originale")).not.toBeInTheDocument());
expect(FakeEventSource.instances).toHaveLength(1);
expect(first.closed).toBe(false);
expect(useSessionStore.getState().activityLog).toEqual(before);
});
test("concurrent same-id Resume invocations share one cold request and replacement source", async () => {
let resumeCalls = 0;
const coldGate = deferred();
const coldStarted = deferred();
server.use(
http.post("/api/sessions/:id/resume", async () => {
resumeCalls += 1;
if (resumeCalls === 1) return resumeResult("s1", false);
if (resumeCalls === 2) coldStarted.resolve();
await coldGate.promise;
return resumeResult("s1", false);
}),
http.get("/api/sessions/:id", () =>
HttpResponse.json({ id: "s1", status: "open", phase: 1 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
const oldSource = FakeEventSource.instances[0];
act(() => oldSource.emitNamed("info", { type: "info", text: "Old generation" }, "900"));
await userEvent.click(screen.getByTestId("session-item-s1"));
const resumeButton = await screen.findByRole("button", { name: /resume/i });
await userEvent.click(resumeButton);
await coldStarted.promise;
await userEvent.click(resumeButton);
await new Promise((resolve) => setImmediate(resolve));
expect(resumeCalls).toBe(2);
coldGate.resolve();
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(2));
const replacement = FakeEventSource.instances[1];
expect(oldSource.closed).toBe(true);
expect(replacement.url).toBe("/api/sessions/s1/events");
expect(useSessionStore.getState().activityLog).toEqual([
{ kind: "lifecycle", phase: null, text: "Resuming session" },
]);
act(() => replacement.emitNamed(
"ui_request",
{ type: "ui_request", ui_request: { id: "cold-gate", widget: "select" } },
"1",
));
expect(useSessionStore.getState().pendingWidget?.id).toBe("cold-gate");
});
test("a committed Resume releases same-id single-flight before its manifest settles", async () => {
const other = {
...LIST[0], id: "s3", question: "Attiva tre", group: null,
created_at: "2026-01-03T00:00:00Z",
};
let s1ResumeCalls = 0;
let s1ManifestCalls = 0;
const firstS1ManifestGate = deferred();
const firstS1ManifestStarted = deferred();
server.use(
http.get("/api/sessions", () => HttpResponse.json([LIST[0], other])),
http.post("/api/sessions/:id/resume", ({ params }) => {
const id = params.id as string;
if (id === "s1") s1ResumeCalls += 1;
return resumeResult(id);
}),
http.get("/api/sessions/:id", async ({ params }) => {
if (params.id === "s1") {
s1ManifestCalls += 1;
if (s1ManifestCalls === 1) {
firstS1ManifestStarted.resolve();
await firstS1ManifestGate.promise;
return HttpResponse.json({ id: "s1", status: "open", phase: 7 });
}
return HttpResponse.json({ id: "s1", status: "open", phase: 4 });
}
return HttpResponse.json({ id: "s3", status: "open", phase: 3 });
}),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await firstS1ManifestStarted.promise;
expect(s1ResumeCalls).toBe(1);
await userEvent.click(screen.getByText("Attiva tre"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F3"));
await userEvent.click(screen.getByTestId("session-item-s1"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(s1ResumeCalls).toBe(2));
await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F4"));
expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s1/events");
firstS1ManifestGate.resolve();
await new Promise((resolve) => setImmediate(resolve));
expect(useSessionStore.getState().currentPhase).toBe("F4");
expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s1/events");
});
test("cold same-session Resume keeps the old stream until success then receives post-clear events once", async () => {
server.use(
http.post("/api/sessions/:id/resume", () =>
resumeResult("s1")),
http.get("/api/sessions/:id", () =>
HttpResponse.json({ id: "s1", status: "open", phase: 1 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
const first = FakeEventSource.instances[0];
act(() => first.emitNamed("info", { type: "info", text: "Before cold Resume" }, "900"));
let releaseResume!: () => void;
let markStarted!: () => void;
const resumeStarted = new Promise((resolve) => { markStarted = resolve; });
const resumeReleased = new Promise((resolve) => { releaseResume = resolve; });
server.use(http.post("/api/sessions/:id/resume", async () => {
markStarted();
await resumeReleased;
return resumeResult("s1");
}));
await userEvent.click(screen.getByTestId("session-item-s1"));
await screen.findByText("Domanda originale");
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await resumeStarted;
expect(FakeEventSource.instances).toHaveLength(1);
expect(first.closed).toBe(false);
expect(screen.getByText("Domanda originale")).toBeInTheDocument();
expect(useSessionStore.getState().activityLog).toContainEqual({
kind: "status", phase: "F1", text: "Before cold Resume", level: "info",
});
act(() => first.emitNamed("text_delta", { type: "text_delta", text: "Still attached" }, "901"));
expect(useSessionStore.getState().transcript).toEqual([
{ role: "assistant", text: "Still attached" },
]);
releaseResume();
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(2));
expect(first.closed).toBe(true);
const replacement = FakeEventSource.instances[1];
expect(replacement.url).toBe("/api/sessions/s1/events");
expect(useSessionStore.getState().transcript).toEqual([]);
expect(useSessionStore.getState().activityLog).toEqual([
{ kind: "lifecycle", phase: null, text: "Resuming session" },
]);
act(() => replacement.emitNamed(
"text_delta",
{ type: "text_delta", text: "Post-resume delivery" },
"1",
));
expect(useSessionStore.getState().transcript).toEqual([
{ role: "assistant", text: "Post-resume delivery" },
]);
expect(useSessionStore.getState().activityLog.filter((entry) => entry.kind === "assistant"))
.toEqual([{ kind: "assistant", phase: "F1", text: "Post-resume delivery" }]);
const gate = {
type: "ui_request" as const,
ui_request: { id: "post-resume-gate", widget: "select", title: "Review once" },
};
act(() => {
replacement.emitNamed("ui_request", gate, "2");
replacement.emitNamed("ui_request", gate, "3");
});
expect(useSessionStore.getState().pendingWidget?.id).toBe("post-resume-gate");
expect(useSessionStore.getState().activityLog.filter((entry) => entry.kind === "gate"))
.toEqual([{ kind: "gate", phase: "F1", text: "Review once" }]);
});
test("a failed same-session Resume preserves its source, activity, and document panel", async () => {
server.use(
http.post("/api/sessions/:id/resume", () =>
resumeResult("s1")),
http.get("/api/sessions/:id", () =>
HttpResponse.json({ id: "s1", status: "open", phase: 1 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
const first = FakeEventSource.instances[0];
act(() => first.emitNamed("info", { type: "info", text: "Keep me" }, "4"));
const before = useSessionStore.getState().activityLog.map((entry) => ({ ...entry }));
server.use(http.post("/api/sessions/:id/resume", () =>
new HttpResponse(null, { status: 409 })));
await userEvent.click(screen.getByTestId("session-item-s1"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(screen.getByText("Domanda originale")).toBeInTheDocument());
expect(FakeEventSource.instances).toHaveLength(1);
expect(first.closed).toBe(false);
expect(useSessionStore.getState().activityLog).toEqual(before);
act(() => first.emitNamed("text_delta", { type: "text_delta", text: "After failure" }, "5"));
expect(useSessionStore.getState().transcript.at(-1)?.text).toBe("After failure");
});
test("resuming a different already-active session binds it only after success", async () => {
const other = {
...LIST[0], id: "s3", question: "Attiva tre", group: null,
created_at: "2026-01-03T00:00:00Z",
};
server.use(
http.get("/api/sessions", () => HttpResponse.json([LIST[0], other])),
http.post("/api/sessions/:id/resume", ({ params }) =>
resumeResult(params.id as string, params.id === "s3")),
http.get("/api/sessions/:id", ({ params }) =>
HttpResponse.json({ id: params.id, status: "open", phase: params.id === "s3" ? 3 : 1 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
const first = FakeEventSource.instances[0];
act(() => first.emitNamed("info", { type: "info", text: "Old target" }, "3"));
await userEvent.click(screen.getByText("Attiva tre"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F3"));
expect(FakeEventSource.instances).toHaveLength(2);
expect(first.closed).toBe(true);
expect(FakeEventSource.instances[1].url).toContain("/sessions/s3/events");
expect(FakeEventSource.instances[1].url).not.toContain("lastEventId");
expect(useSessionStore.getState().activityLog).not.toContainEqual(expect.objectContaining({
text: "Old target",
}));
});
test("competing Resume requests for different ids commit only the latest intent", async () => {
const other = {
...LIST[0], id: "s3", question: "Attiva tre", group: null,
created_at: "2026-01-03T00:00:00Z",
};
const s1Gate = deferred();
const s3Gate = deferred();
const s1Started = deferred();
const s3Started = deferred();
server.use(
http.get("/api/sessions", () => HttpResponse.json([LIST[0], other])),
http.post("/api/sessions/:id/resume", async ({ params }) => {
const id = params.id as string;
if (id === "s1") {
s1Started.resolve();
await s1Gate.promise;
} else {
s3Started.resolve();
await s3Gate.promise;
}
return resumeResult(id);
}),
http.get("/api/sessions/:id", ({ params }) =>
HttpResponse.json({ id: params.id, status: "open", phase: params.id === "s3" ? 3 : 1 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await s1Started.promise;
await userEvent.click(screen.getByText("Attiva tre"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await s3Started.promise;
s3Gate.resolve();
await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F3"));
expect(FakeEventSource.instances).toHaveLength(1);
const latestSource = FakeEventSource.instances[0];
expect(latestSource.url).toContain("/sessions/s3/events");
act(() => latestSource.emitNamed("info", { type: "info", text: "Latest target" }, "1"));
s1Gate.resolve();
await new Promise((resolve) => setImmediate(resolve));
expect(FakeEventSource.instances).toHaveLength(1);
expect(latestSource.closed).toBe(false);
expect(useSessionStore.getState().currentPhase).toBe("F3");
expect(useSessionStore.getState().activityLog).toContainEqual(expect.objectContaining({
text: "Latest target",
}));
});
test("a stale Resume manifest cannot repaint the latest session phase", async () => {
const other = {
...LIST[0], id: "s3", question: "Attiva tre", group: null,
created_at: "2026-01-03T00:00:00Z",
};
const s1ManifestGate = deferred();
const s1ManifestStarted = deferred();
server.use(
http.get("/api/sessions", () => HttpResponse.json([LIST[0], other])),
http.post("/api/sessions/:id/resume", ({ params }) =>
resumeResult(params.id as string)),
http.get("/api/sessions/:id", async ({ params }) => {
if (params.id === "s1") {
s1ManifestStarted.resolve();
await s1ManifestGate.promise;
return HttpResponse.json({ id: "s1", status: "open", phase: 7 });
}
return HttpResponse.json({ id: "s3", status: "open", phase: 3 });
}),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await s1ManifestStarted.promise;
await userEvent.click(screen.getByText("Attiva tre"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F3"));
expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s3/events");
s1ManifestGate.resolve();
await new Promise((resolve) => setImmediate(resolve));
expect(useSessionStore.getState().currentPhase).toBe("F3");
expect(FakeEventSource.instances.at(-1)?.url).toContain("/sessions/s3/events");
});
test("starting a new question invalidates a pending Resume intent", async () => {
const resumeGate = deferred();
const resumeStarted = deferred();
server.use(
http.post("/api/sessions/:id/resume", async () => {
resumeStarted.resolve();
await resumeGate.promise;
return resumeResult("s1");
}),
http.post("/api/runtime/prewarm", () =>
HttpResponse.json({ status: "warming" }, { status: 202 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await resumeStarted.promise;
await userEvent.click(screen.getByRole("button", { name: /^new session$/i }));
expect(await screen.findByText(/type your question/i)).toBeInTheDocument();
resumeGate.resolve();
await new Promise((resolve) => setImmediate(resolve));
expect(FakeEventSource.instances).toHaveLength(0);
expect(screen.getByText(/type your question/i)).toBeInTheDocument();
});
test("a delayed Resume from user A cannot activate a session after user B logs in", async () => {
const resumeGate = deferred();
const resumeStarted = deferred();
const resumeSettled = deferred();
server.use(
http.post("/api/sessions/:id/resume", async () => {
resumeStarted.resolve();
try {
await resumeGate.promise;
return resumeResult("s1");
} finally {
resumeSettled.resolve();
}
}),
http.get("/api/sessions/:id", () => HttpResponse.json({ id: "s1", status: "open", phase: 4 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await resumeStarted.promise;
act(() => setAuthState({ ...regularUser, subject: "bob", displayName: "Bob" }));
resumeGate.resolve();
await act(async () => { await resumeSettled.promise; });
expect(FakeEventSource.instances).toHaveLength(0);
expect(useSessionStore.getState().currentPhase).toBeNull();
expect(screen.queryByRole("button", { name: "Hide model activity" })).not.toBeInTheDocument();
});
test("a delayed session deletion from user A cannot refresh or toast into user B", async () => {
const deleteGate = deferred();
const deleteStarted = deferred();
const deleteSettled = deferred();
server.use(http.delete("/api/sessions/:id", async () => {
deleteStarted.resolve();
try {
await deleteGate.promise;
return new HttpResponse(null, { status: 204 });
} finally {
deleteSettled.resolve();
}
}));
wrap();
await screen.findByText("Attiva uno");
screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus();
await userEvent.keyboard(" ");
await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" }));
await deleteStarted.promise;
act(() => setAuthState({ ...regularUser, subject: "bob", displayName: "Bob" }));
deleteGate.resolve();
await act(async () => { await deleteSettled.promise; });
expect(useSessionStore.getState().toasts).toEqual([]);
expect(screen.queryByText("Deleted 1 of 1 sessions.")).not.toBeInTheDocument();
expect(screen.getByTestId("session-item-s1")).toBeInTheDocument();
});
test("a successful Delete invalidates an earlier pending Resume for the same target", async () => {
const resumeGate = deferred();
const resumeStarted = deferred();
const deleteCompleted = deferred();
server.use(
http.post("/api/sessions/:id/resume", async () => {
resumeStarted.resolve();
await resumeGate.promise;
return resumeResult("s1");
}),
http.delete("/api/sessions/:id", ({ params }) => {
expect(params.id).toBe("s1");
deleteCompleted.resolve();
return new HttpResponse(null, { status: 204 });
}),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await resumeStarted.promise;
screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus();
await userEvent.keyboard(" ");
await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" }));
await deleteCompleted.promise;
resumeGate.resolve();
await new Promise((resolve) => setImmediate(resolve));
expect(FakeEventSource.instances).toHaveLength(0);
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "false");
});
test("a successful Delete detaches a Resume that commits while Delete is pending", async () => {
const deleteGate = deferred();
const deleteStarted = deferred();
server.use(
http.delete("/api/sessions/:id", async ({ params }) => {
expect(params.id).toBe("s1");
deleteStarted.resolve();
await deleteGate.promise;
return new HttpResponse(null, { status: 204 });
}),
http.post("/api/sessions/:id/resume", () => resumeResult("s1")),
http.get("/api/sessions/:id", () =>
HttpResponse.json({ id: "s1", status: "open", phase: 1 })),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus();
await userEvent.keyboard(" ");
await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" }));
await deleteStarted.promise;
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "true");
deleteGate.resolve();
await waitFor(() => {
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "false");
});
expect(FakeEventSource.instances[0].closed).toBe(true);
});
test("deleting another session does not invalidate a pending Resume", async () => {
const resumeGate = deferred();
const resumeStarted = deferred();
const deleteCompleted = deferred();
server.use(
http.post("/api/sessions/:id/resume", async () => {
resumeStarted.resolve();
await resumeGate.promise;
return resumeResult("s1");
}),
http.delete("/api/sessions/:id", ({ params }) => {
expect(params.id).toBe("s2");
deleteCompleted.resolve();
return new HttpResponse(null, { status: 204 });
}),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await resumeStarted.promise;
await userEvent.click(screen.getByRole("button", { name: /archive/i }));
(await screen.findByRole("checkbox", { name: "Select Archiviata due" })).focus();
await userEvent.keyboard(" ");
await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" }));
await deleteCompleted.promise;
resumeGate.resolve();
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
expect(FakeEventSource.instances[0].url).toContain("/sessions/s1/events");
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "true");
});
test("deleting active A preserves a pending Resume for different session B", async () => {
const other = {
...LIST[0], id: "s3", question: "Attiva tre", group: null,
created_at: "2026-01-03T00:00:00Z",
};
const s3ResumeGate = deferred();
const s3ResumeStarted = deferred();
server.use(
http.get("/api/sessions", () => HttpResponse.json([LIST[0], other])),
http.post("/api/sessions/:id/resume", async ({ params }) => {
const id = params.id as string;
if (id === "s3") {
s3ResumeStarted.resolve();
await s3ResumeGate.promise;
}
return resumeResult(id);
}),
http.get("/api/sessions/:id", ({ params }) =>
HttpResponse.json({ id: params.id, status: "open", phase: params.id === "s3" ? 3 : 1 })),
http.delete("/api/sessions/:id", ({ params }) => {
expect(params.id).toBe("s1");
return new HttpResponse(null, { status: 204 });
}),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
const sourceA = FakeEventSource.instances[0];
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "true");
await userEvent.click(screen.getByText("Attiva tre"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await s3ResumeStarted.promise;
screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus();
await userEvent.keyboard(" ");
await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" }));
await waitFor(() => expect(sourceA.closed).toBe(true));
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "false");
s3ResumeGate.resolve();
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(2));
expect(FakeEventSource.instances[1].url).toContain("/sessions/s3/events");
expect(screen.getByTestId("session-item-s3")).toHaveAttribute("data-active", "true");
});
test("a failed Delete does not invalidate a pending Resume for its target", async () => {
const resumeGate = deferred();
const resumeStarted = deferred();
const deleteFailed = deferred();
server.use(
http.post("/api/sessions/:id/resume", async () => {
resumeStarted.resolve();
await resumeGate.promise;
return resumeResult("s1");
}),
http.delete("/api/sessions/:id", ({ params }) => {
expect(params.id).toBe("s1");
deleteFailed.resolve();
return new HttpResponse(null, { status: 500 });
}),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await resumeStarted.promise;
screen.getByRole("checkbox", { name: "Select Attiva uno" }).focus();
await userEvent.keyboard(" ");
await userEvent.click(await screen.findByRole("button", { name: "Delete 1 selected sessions" }));
await deleteFailed.promise;
resumeGate.resolve();
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
expect(FakeEventSource.instances[0].url).toContain("/sessions/s1/events");
expect(screen.getByTestId("session-item-s1")).toHaveAttribute("data-active", "true");
});
test("a failed Resume with no active session preserves the panel and existing activity", async () => {
server.use(http.post("/api/sessions/:id/resume", () => new HttpResponse(null, { status: 409 })));
useSessionStore.setState({
activityLog: [{ kind: "status", phase: "F7", text: "Preserve activity", level: "info" }],
});
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await screen.findByText("Domanda originale"); // panel open
await userEvent.click(screen.getByRole("button", { name: /resume/i }));
// panel stays open (document still visible) after the failed resume
await waitFor(() => expect(screen.getByText("Domanda originale")).toBeInTheDocument());
expect(FakeEventSource.instances).toHaveLength(0);
expect(useSessionStore.getState().activityLog).toEqual([
{ kind: "status", phase: "F7", text: "Preserve activity", level: "info" },
]);
});
test("closing and reopening Model activity preserves the complete activity log", async () => {
wrap();
server.use(http.post("/api/sessions/:id/resume", () => resumeResult("s1")));
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
act(() => {
const store = useSessionStore.getState();
store.setPhase("F4");
store.setLastUserEntry({ kind: "input", text: "Inspect patient cohort" });
store.applyEvent({
type: "activity_event",
activity: { kind: "tool", toolCallId: "tool-1", toolName: "bash", status: "completed" },
});
store.applyEvent({ type: "text_delta", text: "Cohort ready" });
store.applyEvent({ type: "activity_delta", text: "Inspecting **cohort**" });
store.applyEvent({ type: "info", level: "info", text: "Cohort status" });
});
const beforeClose = useSessionStore.getState().activityLog.map((entry) => ({ ...entry }));
expect(screen.queryByRole("heading", { name: /model activity/i })).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: /model activity/i }));
const panel = (await screen.findByRole("heading", { name: /model activity/i })).closest("aside");
expect(panel).not.toBeNull();
expect(panel).toHaveTextContent("Inspect patient cohort");
expect(panel).toHaveTextContent("Inspecting cohort");
expect(within(panel!).getByText("cohort").tagName).toBe("STRONG");
expect(within(panel!).getByText("Cohort ready")).toBeInTheDocument();
expect(within(panel!).queryByText("Cohort status")).not.toBeInTheDocument();
expect(within(panel!).queryByText("bash")).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: /close model activity/i }));
expect(screen.queryByRole("heading", { name: /model activity/i })).not.toBeInTheDocument();
expect(useSessionStore.getState().activityLog).toEqual(beforeClose);
await userEvent.click(screen.getByRole("button", { name: /show model activity/i }));
const reopened = (await screen.findByRole("heading", { name: /model activity/i })).closest("aside");
expect(reopened).not.toBeNull();
expect(reopened).toHaveTextContent("Inspect patient cohort");
expect(reopened).toHaveTextContent("Inspecting cohort");
expect(within(reopened!).getByText("cohort").tagName).toBe("STRONG");
expect(within(reopened!).getByText("Cohort ready")).toBeInTheDocument();
expect(within(reopened!).queryByText("Cohort status")).not.toBeInTheDocument();
expect(within(reopened!).queryByText("bash")).not.toBeInTheDocument();
expect(useSessionStore.getState().activityLog).toEqual(beforeClose);
});
test("session finalization shows the completion banner and returns to landing", async () => {
useSessionStore.getState().resetSession();
let finalized = false;
server.use(
http.post("/api/sessions/:id/resume", () => resumeResult("s1")),
http.get("/api/sessions", () =>
HttpResponse.json(finalized ? [{ ...LIST[0], status: "finalized" }] : LIST)),
);
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
// The final turn ends: the session is now finalized on disk and agent_end
// arrives — the shell must refetch immediately (not wait for the 10s poll)
// and surface the new-session invite.
finalized = true;
act(() => {
useSessionStore.getState().applyEvent({ type: "system_event", event: "agent_end" } as any);
});
const cta = await screen.findByRole("button", { name: /start a new question/i });
expect(screen.getByText(/session completed and finalized/i)).toBeInTheDocument();
await userEvent.click(cta);
// Landing state: the composer invites a brand-new question.
expect(await screen.findByText(/type your question/i)).toBeInTheDocument();
});
test("renaming a group reassigns its members via setSessionGroup", async () => {
const groupSets: Array<{ id: string; group: string }> = [];
server.use(
http.post("/api/sessions/:id/group", async ({ params, request }) => {
const body = (await request.json()) as { group: string };
groupSets.push({ id: params.id as string, group: body.group });
return new HttpResponse(null, { status: 204 });
}),
);
wrap();
await screen.findByText("Aritmologia");
await userEvent.click(screen.getByRole("button", { name: /rename group aritmologia/i }));
const input = await screen.findByLabelText(/name/i, { selector: "input" });
await userEvent.clear(input);
await userEvent.type(input, "Cardiologia");
await userEvent.click(screen.getByRole("button", { name: /save/i }));
await waitFor(() => expect(groupSets).toEqual([{ id: "s1", group: "Cardiologia" }]));
});
test("opens an accessible resizable activity split and persists pointer width", async () => {
server.use(http.post("/api/sessions/:id/resume", () => resumeResult("s1")));
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await userEvent.click(screen.getByRole("button", { name: /show model activity/i }));
const shell = screen.getByTestId("app-shell");
setContainerWidth(shell, 1200);
const separator = screen.getByRole("separator", { name: /resize model activity/i });
expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("384px");
expect(separator).toHaveAttribute("aria-orientation", "vertical");
expect(separator).toHaveAttribute("aria-valuemin", "288");
expect(separator).toHaveAttribute("aria-valuenow", "384");
Object.assign(separator, {
setPointerCapture: vi.fn(),
releasePointerCapture: vi.fn(),
hasPointerCapture: vi.fn(() => true),
});
fireEvent.pointerDown(separator, { pointerId: 1, clientX: 384 });
fireEvent.pointerMove(separator, { pointerId: 1, clientX: 484 });
fireEvent.pointerUp(separator, { pointerId: 1, clientX: 484 });
expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("484px");
expect(localStorage.getItem(ACTIVITY_PANEL_STORAGE_KEY)).toBe("484");
expect(screen.getByTestId("conversation-column")).toHaveClass("flex-1");
});
test("resizes the activity split with keyboard and exposes responsive drawer classes", async () => {
localStorage.setItem(ACTIVITY_PANEL_STORAGE_KEY, "448");
server.use(http.post("/api/sessions/:id/resume", () => resumeResult("s1")));
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await userEvent.click(screen.getByRole("button", { name: /show model activity/i }));
const shell = screen.getByTestId("app-shell");
setContainerWidth(shell, 1200);
const separator = screen.getByRole("separator", { name: /resize model activity/i });
expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("448px");
fireEvent.keyDown(separator, { key: "ArrowLeft" });
expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("432px");
fireEvent.keyDown(separator, { key: "ArrowRight", shiftKey: true });
expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("480px");
fireEvent.keyDown(separator, { key: "Home" });
expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("288px");
const panel = screen.getByRole("heading", { name: "Model activity" }).closest("aside");
expect(panel).toHaveClass(
"static",
"w-[var(--activity-panel-width)]",
);
expect(separator).toHaveClass("touch-none", "cursor-col-resize");
});
test("uses the measured app shell for the desktop activity split", async () => {
localStorage.setItem(ACTIVITY_PANEL_STORAGE_KEY, "576");
server.use(http.post("/api/sessions/:id/resume", () => resumeResult("s1")));
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await userEvent.click(await screen.findByRole("button", { name: /show model activity/i }));
const shell = screen.getByTestId("app-shell");
setContainerWidth(shell, 1200);
expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("576px");
expect(screen.getByRole("separator", { name: /resize model activity/i })).toBeInTheDocument();
setContainerWidth(shell, 1024);
expect(shell.style.getPropertyValue("--activity-panel-width")).toBe("512px");
setContainerWidth(shell, 799);
expect(screen.queryByRole("separator", { name: /resize model activity/i })).not.toBeInTheDocument();
expect(screen.getByTestId("conversation-column")).toHaveClass("flex-1");
expect(screen.getByRole("heading", { name: "Model activity" }).closest("aside")).toHaveClass("fixed");
});
test("cancels an active resize when the measured shell becomes too narrow", async () => {
localStorage.setItem(ACTIVITY_PANEL_STORAGE_KEY, "576");
server.use(http.post("/api/sessions/:id/resume", () => resumeResult("s1")));
wrap();
await userEvent.click(await screen.findByText("Attiva uno"));
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
await userEvent.click(await screen.findByRole("button", { name: /show model activity/i }));
const shell = screen.getByTestId("app-shell");
setContainerWidth(shell, 1200);
const separator = screen.getByRole("separator", { name: /resize model activity/i });
Object.assign(separator, {
setPointerCapture: vi.fn(), releasePointerCapture: vi.fn(), hasPointerCapture: vi.fn(() => true),
});
fireEvent.pointerDown(separator, { pointerId: 1, clientX: 576 });
fireEvent.pointerMove(separator, { pointerId: 1, clientX: 400 });
setContainerWidth(shell, 799);
fireEvent.pointerUp(separator, { pointerId: 1, clientX: 300 });
expect(localStorage.getItem(ACTIVITY_PANEL_STORAGE_KEY)).toBe("576");
expect(shell).toHaveAttribute("data-activity-resizing", "false");
});