531 lines
25 KiB
TypeScript
531 lines
25 KiB
TypeScript
// frontend/src/shell/SteerInput.test.tsx
|
|
import { render, screen, waitFor } 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 { useSessionStore } from "../store/sessionStore";
|
|
import { ComposerFooter, ContextGauge, SteerInput } from "./SteerInput";
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
server.use(
|
|
http.post("/api/sessions/:id/steer", () =>
|
|
new HttpResponse(null, { status: 204 }),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("new sessions send the browser-selected workspace, model, provider, and thinking", async () => {
|
|
let body: unknown;
|
|
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
|
|
workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "high",
|
|
}));
|
|
server.use(
|
|
http.get("/api/workspaces", () => HttpResponse.json([{
|
|
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical",
|
|
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" },
|
|
}])),
|
|
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
|
|
workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } },
|
|
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" },
|
|
})),
|
|
http.post("/api/sessions", async ({ request }) => {
|
|
body = await request.json();
|
|
return HttpResponse.json({ id: "s1" });
|
|
}),
|
|
);
|
|
render(<SteerInput sessionId={null} />);
|
|
|
|
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
|
|
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
|
|
|
await waitFor(() => expect(body).toEqual({
|
|
question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "high",
|
|
}));
|
|
});
|
|
|
|
test("renders a text input and submit button", () => {
|
|
render(<SteerInput sessionId="s1" />);
|
|
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: /invia|send|steer/i })).toBeInTheDocument();
|
|
});
|
|
|
|
test("submitting typed text POSTs to /steer and clears the input", async () => {
|
|
let captured: unknown = null;
|
|
server.use(
|
|
http.post("/api/sessions/:id/steer", async ({ request, params }) => {
|
|
captured = { id: params.id, body: await request.json() };
|
|
return new HttpResponse(null, { status: 204 });
|
|
}),
|
|
);
|
|
render(<SteerInput sessionId="s1" />);
|
|
const input = screen.getByRole("textbox");
|
|
await userEvent.type(input, "redirect to plan B");
|
|
await userEvent.click(screen.getByRole("button", { name: /invia|send|steer/i }));
|
|
await waitFor(() => {
|
|
expect(captured).toEqual({ id: "s1", body: { text: "redirect to plan B" } });
|
|
});
|
|
expect(input).toHaveValue("");
|
|
});
|
|
|
|
test("pressing Enter in the input submits the steer", async () => {
|
|
let captured: unknown = null;
|
|
server.use(
|
|
http.post("/api/sessions/:id/steer", async ({ request }) => {
|
|
captured = await request.json();
|
|
return new HttpResponse(null, { status: 204 });
|
|
}),
|
|
);
|
|
render(<SteerInput sessionId="s1" />);
|
|
const input = screen.getByRole("textbox");
|
|
await userEvent.type(input, "use approach X{Enter}");
|
|
await waitFor(() => {
|
|
expect(captured).toEqual({ text: "use approach X" });
|
|
});
|
|
});
|
|
|
|
test("does not POST when input is empty", async () => {
|
|
let called = false;
|
|
server.use(
|
|
http.post("/api/sessions/:id/steer", () => {
|
|
called = true;
|
|
return new HttpResponse(null, { status: 204 });
|
|
}),
|
|
);
|
|
render(<SteerInput sessionId="s1" />);
|
|
await userEvent.click(screen.getByRole("button", { name: /invia|send|steer/i }));
|
|
// small wait to ensure the call didn't go through
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
expect(called).toBe(false);
|
|
});
|
|
|
|
|
|
test("keeps the new-question composer white by default", () => {
|
|
render(<SteerInput sessionId={null} />);
|
|
const input = screen.getByRole("textbox", { name: /new question/i });
|
|
expect(input).not.toHaveAttribute("data-awaiting-input");
|
|
expect(input).not.toHaveClass("thot-awaiting-input");
|
|
});
|
|
|
|
test("marks the composer as awaiting input only when requested", () => {
|
|
render(<SteerInput sessionId={null} awaitingInput />);
|
|
const input = screen.getByRole("textbox", { name: /new question/i });
|
|
expect(input).toHaveAttribute("data-awaiting-input", "true");
|
|
expect(input).toHaveClass("thot-awaiting-input");
|
|
});
|
|
|
|
test("the context gauge uses green, yellow, and red at the requested thresholds", () => {
|
|
const { rerender } = render(<ContextGauge pct={0.699} />);
|
|
expect(screen.getByLabelText("Context usage 70%")).toHaveAttribute("data-level", "green");
|
|
|
|
rerender(<ContextGauge pct={0.7} />);
|
|
expect(screen.getByLabelText("Context usage 70%")).toHaveAttribute("data-level", "yellow");
|
|
|
|
rerender(<ContextGauge pct={0.85} />);
|
|
expect(screen.getByLabelText("Context usage 85%")).toHaveAttribute("data-level", "yellow");
|
|
|
|
rerender(<ContextGauge pct={0.851} />);
|
|
expect(screen.getByLabelText("Context usage 85%")).toHaveAttribute("data-level", "red");
|
|
});
|
|
|
|
test("footer shows cumulative k-token counters after workspace and context gauge after thinking", async () => {
|
|
server.use(
|
|
http.get("/api/settings", () => HttpResponse.json({
|
|
workspace: "psd", provider: "zai", model: "glm-5.2", thinking: "medium",
|
|
})),
|
|
http.get("/api/workspaces", () => HttpResponse.json([{ name: "psd" }])),
|
|
http.get("/api/workspaces/psd", () => HttpResponse.json({
|
|
workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } },
|
|
revision: { id: "psd", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" },
|
|
})),
|
|
http.get("/api/models", () => HttpResponse.json({
|
|
models: [{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true }],
|
|
})),
|
|
);
|
|
useSessionStore.getState().resetSession();
|
|
useSessionStore.getState().applyEvent({
|
|
type: "usage",
|
|
usage: {
|
|
input: 110_348,
|
|
cacheRead: 633_344,
|
|
output: 33_652,
|
|
totalTokens: 55_935,
|
|
contextWindow: 200_000,
|
|
},
|
|
});
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><ComposerFooter /></QueryClientProvider>);
|
|
|
|
const counters = screen.getByText("110k/633k/34k");
|
|
const workspace = await screen.findByRole("combobox", { name: "Workspace" });
|
|
const gauge = screen.getByLabelText("Context usage 28%");
|
|
const thinking = await screen.findByRole("combobox", { name: "Thinking level" });
|
|
expect(workspace.compareDocumentPosition(counters) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
expect(thinking.compareDocumentPosition(gauge) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
});
|
|
|
|
test("footer limits model choices to the selected workspace policy", async () => {
|
|
server.use(
|
|
http.get("/api/settings", () => HttpResponse.json({
|
|
workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
|
|
})),
|
|
http.get("/api/workspaces", () => HttpResponse.json([{
|
|
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical",
|
|
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" },
|
|
}])),
|
|
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
|
|
workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } },
|
|
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" },
|
|
})),
|
|
http.get("/api/models", () => HttpResponse.json({ models: [
|
|
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
|
|
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
|
|
] })),
|
|
);
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><ComposerFooter /></QueryClientProvider>);
|
|
|
|
const selector = await screen.findByRole("combobox", { name: "Model" });
|
|
await waitFor(() => expect(selector).toHaveTextContent("GLM-5.2"));
|
|
await waitFor(() => expect(selector).not.toHaveTextContent("DeepSeek V4 Pro"));
|
|
});
|
|
|
|
test("switching workspaces replaces an out-of-policy model before session creation", async () => {
|
|
let body: unknown;
|
|
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
|
|
workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
|
|
}));
|
|
const revision = (id: string) => ({
|
|
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" as const,
|
|
});
|
|
server.use(
|
|
http.get("/api/settings", () => HttpResponse.json({
|
|
workspace: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
|
|
})),
|
|
http.get("/api/workspaces", () => HttpResponse.json([
|
|
{ id: "research", name: "research", file: "research.yaml", displayName: "Research", revision: revision("research") },
|
|
{ id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", revision: revision("psd-clinical") },
|
|
])),
|
|
http.get("/api/workspaces/research", () => HttpResponse.json({
|
|
workspace: { workspace: { id: "research" }, llm_policy: { allowed: ["deepseek/deepseek-v4-pro"] } },
|
|
revision: revision("research"),
|
|
})),
|
|
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
|
|
workspace: { workspace: { id: "psd-clinical" }, llm_policy: { default: "zai/glm-5.2", allowed: ["zai/glm-5.2"] } },
|
|
revision: revision("psd-clinical"),
|
|
})),
|
|
http.get("/api/models", () => HttpResponse.json({ models: [
|
|
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
|
|
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
|
|
] })),
|
|
http.post("/api/sessions", async ({ request }) => {
|
|
body = await request.json();
|
|
return HttpResponse.json({ id: "s1" });
|
|
}),
|
|
);
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><ComposerFooter /><SteerInput sessionId={null} /></QueryClientProvider>);
|
|
|
|
const workspaceSelector = await screen.findByRole("combobox", { name: "Workspace" });
|
|
await waitFor(() => expect(workspaceSelector).toHaveTextContent("psd-clinical"));
|
|
await userEvent.selectOptions(workspaceSelector, "psd-clinical");
|
|
await waitFor(() => expect(screen.getByRole("combobox", { name: "Model" })).toHaveValue("glm-5.2"));
|
|
expect(screen.getByRole("combobox", { name: "Model" })).not.toHaveTextContent("DeepSeek V4 Pro");
|
|
|
|
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
|
|
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
|
|
|
await waitFor(() => expect(body).toEqual({
|
|
question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
|
|
}));
|
|
});
|
|
|
|
test("immediate submit waits for a switched workspace policy before creating a session", async () => {
|
|
let body: unknown;
|
|
let releasePolicy!: () => void;
|
|
let policyRequestStarted = false;
|
|
const policyMayFinish = new Promise<void>((resolve) => { releasePolicy = resolve; });
|
|
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
|
|
workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
|
|
}));
|
|
const revision = (id: string) => ({
|
|
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" as const,
|
|
});
|
|
server.use(
|
|
http.get("/api/settings", () => HttpResponse.json({ workspace: "research" })),
|
|
http.get("/api/workspaces", () => HttpResponse.json([
|
|
{ id: "research", name: "research", file: "research.yaml", displayName: "Research", revision: revision("research") },
|
|
{ id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", revision: revision("psd-clinical") },
|
|
])),
|
|
http.get("/api/workspaces/research", () => HttpResponse.json({
|
|
workspace: { llm_policy: { allowed: ["deepseek/deepseek-v4-pro"] } }, revision: revision("research"),
|
|
})),
|
|
http.get("/api/workspaces/psd-clinical", async () => {
|
|
policyRequestStarted = true;
|
|
await policyMayFinish;
|
|
return HttpResponse.json({
|
|
workspace: { llm_policy: { default: "zai/glm-5.2", allowed: ["zai/glm-5.2"] } },
|
|
revision: revision("psd-clinical"),
|
|
});
|
|
}),
|
|
http.get("/api/models", () => HttpResponse.json({ models: [
|
|
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
|
|
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
|
|
] })),
|
|
http.post("/api/sessions", async ({ request }) => {
|
|
body = await request.json();
|
|
return HttpResponse.json({ id: "s1" });
|
|
}),
|
|
);
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><ComposerFooter /><SteerInput sessionId={null} /></QueryClientProvider>);
|
|
|
|
const workspaceSelector = await screen.findByRole("combobox", { name: "Workspace" });
|
|
await waitFor(() => expect(workspaceSelector).toHaveTextContent("psd-clinical"));
|
|
await userEvent.selectOptions(workspaceSelector, "psd-clinical");
|
|
await waitFor(() => expect(policyRequestStarted).toBe(true));
|
|
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
|
|
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
|
|
|
expect(body).toBeUndefined();
|
|
expect(screen.getByRole("button", { name: /send/i })).toBeDisabled();
|
|
releasePolicy();
|
|
|
|
await waitFor(() => expect(body).toEqual({
|
|
question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
|
|
}));
|
|
});
|
|
|
|
test("initial restored workspace waits for its delayed policy before creating a session", async () => {
|
|
let body: unknown;
|
|
let releasePolicy!: () => void;
|
|
let policyRequestStarted = false;
|
|
const policyMayFinish = new Promise<void>((resolve) => { releasePolicy = resolve; });
|
|
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
|
|
workspaceId: "psd-clinical", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
|
|
}));
|
|
const revision = (id: string) => ({
|
|
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" as const,
|
|
});
|
|
server.use(
|
|
http.get("/api/settings", () => HttpResponse.json({ workspace: "psd-clinical" })),
|
|
http.get("/api/workspaces", () => HttpResponse.json([
|
|
{ id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", revision: revision("psd-clinical") },
|
|
])),
|
|
http.get("/api/workspaces/psd-clinical", async () => {
|
|
policyRequestStarted = true;
|
|
await policyMayFinish;
|
|
return HttpResponse.json({
|
|
workspace: { llm_policy: { default: "zai/glm-5.2", allowed: ["zai/glm-5.2"] } },
|
|
revision: revision("psd-clinical"),
|
|
});
|
|
}),
|
|
http.get("/api/models", () => HttpResponse.json({ models: [
|
|
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
|
|
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
|
|
] })),
|
|
http.post("/api/sessions", async ({ request }) => {
|
|
body = await request.json();
|
|
return HttpResponse.json({ id: "s1" });
|
|
}),
|
|
);
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><ComposerFooter /><SteerInput sessionId={null} /></QueryClientProvider>);
|
|
|
|
await waitFor(() => expect(policyRequestStarted).toBe(true));
|
|
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
|
|
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
|
|
|
expect(body).toBeUndefined();
|
|
expect(screen.getByRole("button", { name: /send/i })).toBeDisabled();
|
|
releasePolicy();
|
|
|
|
await waitFor(() => expect(body).toEqual({
|
|
question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
|
|
}));
|
|
});
|
|
|
|
test("initial submit rejects a migration-required workspace after summaries load", async () => {
|
|
let body: unknown;
|
|
let failure: string | undefined;
|
|
let releaseSummaries!: () => void;
|
|
let summaryRequestStarted = false;
|
|
const summariesMayFinish = new Promise<void>((resolve) => { releaseSummaries = resolve; });
|
|
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
|
|
workspaceId: "legacy-workspace", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
|
|
}));
|
|
server.use(
|
|
http.get("/api/settings", () => HttpResponse.json({ workspace: "legacy-workspace" })),
|
|
http.get("/api/workspaces", async () => {
|
|
summaryRequestStarted = true;
|
|
await summariesMayFinish;
|
|
return HttpResponse.json([{
|
|
id: "legacy-workspace", name: "legacy-workspace", file: "legacy-workspace.yaml", displayName: "Legacy workspace",
|
|
revision: { id: "legacy-workspace", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "migration_required" },
|
|
}]);
|
|
}),
|
|
http.get("/api/workspaces/legacy-workspace", () => new HttpResponse(null, { status: 409 })),
|
|
http.get("/api/models", () => HttpResponse.json({ models: [
|
|
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
|
|
] })),
|
|
http.post("/api/sessions", async ({ request }) => {
|
|
body = await request.json();
|
|
return HttpResponse.json({ id: "s1" });
|
|
}),
|
|
);
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><ComposerFooter /><SteerInput sessionId={null} onSessionCreateFailed={(message) => { failure = message; }} /></QueryClientProvider>);
|
|
|
|
await waitFor(() => expect(summaryRequestStarted).toBe(true));
|
|
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
|
|
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
|
|
|
expect(body).toBeUndefined();
|
|
expect(screen.getByRole("button", { name: /send/i })).toBeDisabled();
|
|
releaseSummaries();
|
|
|
|
await waitFor(() => expect(failure).toBe("Could not load selected workspace policy. Please retry."));
|
|
expect(body).toBeUndefined();
|
|
});
|
|
|
|
test("initial submit rejects a workspace summary that omits the canonical revision", async () => {
|
|
let body: unknown;
|
|
let failure: string | undefined;
|
|
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
|
|
workspaceId: "broken-workspace", provider: "zai", model: "glm-5.2", thinking: "medium",
|
|
}));
|
|
server.use(
|
|
http.get("/api/settings", () => HttpResponse.json({ workspace: "broken-workspace" })),
|
|
http.get("/api/workspaces", () => HttpResponse.json([{
|
|
id: "broken-workspace", name: "broken-workspace", file: "broken-workspace.yaml", displayName: "Broken workspace",
|
|
}])),
|
|
http.get("/api/models", () => HttpResponse.json({ models: [
|
|
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
|
|
] })),
|
|
http.post("/api/sessions", async ({ request }) => {
|
|
body = await request.json();
|
|
return HttpResponse.json({ id: "s1" });
|
|
}),
|
|
);
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><ComposerFooter /><SteerInput sessionId={null} onSessionCreateFailed={(message) => { failure = message; }} /></QueryClientProvider>);
|
|
|
|
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
|
|
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
|
|
|
await waitFor(() => expect(failure).toBe("Could not load selected workspace policy. Please retry."));
|
|
expect(body).toBeUndefined();
|
|
});
|
|
|
|
test("failed workspace summaries block creation and report a safe error", async () => {
|
|
let body: unknown;
|
|
let failure: string | undefined;
|
|
let summaryRequestFailed = false;
|
|
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
|
|
workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
|
|
}));
|
|
server.use(
|
|
http.get("/api/settings", () => HttpResponse.json({ workspace: "psd-clinical" })),
|
|
http.get("/api/workspaces", () => {
|
|
summaryRequestFailed = true;
|
|
return new HttpResponse(null, { status: 503 });
|
|
}),
|
|
http.get("/api/models", () => HttpResponse.json({ models: [
|
|
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
|
|
] })),
|
|
http.post("/api/sessions", async ({ request }) => {
|
|
body = await request.json();
|
|
return HttpResponse.json({ id: "s1" });
|
|
}),
|
|
);
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(
|
|
<QueryClientProvider client={client}>
|
|
<ComposerFooter />
|
|
<SteerInput sessionId={null} onSessionCreateFailed={(message) => { failure = message; }} />
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
await waitFor(() => expect(summaryRequestFailed).toBe(true));
|
|
await waitFor(() => expect(client.getQueryState(["workspaces"])?.status).toBe("error"));
|
|
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
|
|
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
|
|
|
await waitFor(() => expect(failure).toBe("Could not load workspace registry. Please retry."));
|
|
expect(body).toBeUndefined();
|
|
});
|
|
|
|
test("submit follows a rapid workspace switch instead of waiting for an abandoned policy", async () => {
|
|
let body: unknown;
|
|
let releaseC!: () => void;
|
|
let bPolicyRequestStarted = false;
|
|
let cPolicyRequestStarted = false;
|
|
const cPolicyMayFinish = new Promise<void>((resolve) => { releaseC = resolve; });
|
|
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
|
|
workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
|
|
}));
|
|
const revision = (id: string) => ({
|
|
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" as const,
|
|
});
|
|
server.use(
|
|
http.get("/api/settings", () => HttpResponse.json({ workspace: "research" })),
|
|
http.get("/api/workspaces", () => HttpResponse.json([
|
|
{ id: "research", name: "research", file: "research.yaml", displayName: "Research", revision: revision("research") },
|
|
{ id: "workspace-b", name: "workspace-b", file: "workspace-b.yaml", displayName: "Workspace B", revision: revision("workspace-b") },
|
|
{ id: "workspace-c", name: "workspace-c", file: "workspace-c.yaml", displayName: "Workspace C", revision: revision("workspace-c") },
|
|
])),
|
|
http.get("/api/workspaces/research", () => HttpResponse.json({
|
|
workspace: { llm_policy: { allowed: ["deepseek/deepseek-v4-pro"] } }, revision: revision("research"),
|
|
})),
|
|
http.get("/api/workspaces/workspace-b", async () => {
|
|
bPolicyRequestStarted = true;
|
|
await new Promise(() => undefined);
|
|
return HttpResponse.json({});
|
|
}),
|
|
http.get("/api/workspaces/workspace-c", async () => {
|
|
cPolicyRequestStarted = true;
|
|
await cPolicyMayFinish;
|
|
return HttpResponse.json({
|
|
workspace: { llm_policy: { default: "zai/glm-5.2", allowed: ["zai/glm-5.2"] } },
|
|
revision: revision("workspace-c"),
|
|
});
|
|
}),
|
|
http.get("/api/models", () => HttpResponse.json({ models: [
|
|
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
|
|
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
|
|
] })),
|
|
http.post("/api/sessions", async ({ request }) => {
|
|
body = await request.json();
|
|
return HttpResponse.json({ id: "s1" });
|
|
}),
|
|
);
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
render(<QueryClientProvider client={client}><ComposerFooter /><SteerInput sessionId={null} /></QueryClientProvider>);
|
|
|
|
const workspaceSelector = await screen.findByRole("combobox", { name: "Workspace" });
|
|
await waitFor(() => expect(screen.getByRole("option", { name: "workspace-b" })).toBeInTheDocument());
|
|
await userEvent.selectOptions(workspaceSelector, "workspace-b");
|
|
await waitFor(() => expect(bPolicyRequestStarted).toBe(true));
|
|
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
|
|
await userEvent.click(screen.getByRole("button", { name: /send/i }));
|
|
await userEvent.selectOptions(workspaceSelector, "workspace-c");
|
|
await waitFor(() => expect(cPolicyRequestStarted).toBe(true));
|
|
|
|
expect(body).toBeUndefined();
|
|
releaseC();
|
|
|
|
await waitFor(() => expect(body).toEqual({
|
|
question: "q", workspaceId: "workspace-c", provider: "zai", model: "glm-5.2", thinking: "medium",
|
|
}));
|
|
});
|
|
|
|
test("pulses the stop dot only while the harness is working", () => {
|
|
const { rerender } = render(<SteerInput sessionId="s1" working />);
|
|
const dot = () =>
|
|
screen.getByRole("button", { name: /stop and save session/i }).querySelector("span");
|
|
expect(dot()).toHaveClass("thot-pulse");
|
|
rerender(<SteerInput sessionId="s1" working={false} />);
|
|
expect(dot()).not.toHaveClass("thot-pulse");
|
|
});
|