109 lines
5.2 KiB
TypeScript
109 lines
5.2 KiB
TypeScript
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(<QueryClientProvider client={client}><AppShell canLogout={false} /></QueryClientProvider>);
|
|
}
|
|
|
|
function deferred() {
|
|
let resolve!: () => void;
|
|
const promise = new Promise<void>((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");
|
|
});
|