276 lines
13 KiB
TypeScript
276 lines
13 KiB
TypeScript
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { http, HttpResponse } from "msw";
|
|
import { server } from "../test/msw";
|
|
import { PiManagement } from "./PiManagement";
|
|
|
|
const readyStatus = {
|
|
version: "0.80.3",
|
|
ready: true,
|
|
credentials: "present",
|
|
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
|
|
checkedAt: "2026-08-05T10:00:00.000Z",
|
|
};
|
|
|
|
const options = {
|
|
providers: ["zai", "deepseek"],
|
|
models: [
|
|
{ provider: "zai", id: "glm-5.2" },
|
|
{ provider: "deepseek", id: "deepseek-v4" },
|
|
],
|
|
reasoning: ["low", "medium", "high"],
|
|
checkedAt: "2026-08-05T10:00:00.000Z",
|
|
};
|
|
|
|
function renderManagement() {
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
return render(<QueryClientProvider client={client}><PiManagement open onClose={() => undefined} /></QueryClientProvider>);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
server.use(
|
|
http.get("/api/pi-management/status", () => HttpResponse.json(readyStatus)),
|
|
http.get("/api/pi-management/options", () => HttpResponse.json(options)),
|
|
);
|
|
});
|
|
|
|
test("loads Pi version and readiness as an accessible operational rail", async () => {
|
|
renderManagement();
|
|
|
|
expect(screen.getByText("Loading Pi management…")).toBeVisible();
|
|
expect(await screen.findByRole("heading", { name: "Pi management" })).toBeVisible();
|
|
await screen.findByLabelText("Provider");
|
|
expect(screen.getByRole("dialog", { name: "Pi management" })).toHaveClass("sm:max-w-[min(94vw,58rem)]");
|
|
expect(screen.getByTestId("pi-readiness-rail")).toHaveTextContent("Runtime ready");
|
|
expect(screen.getByRole("status", { name: "Pi readiness" })).toHaveTextContent("Ready");
|
|
expect(screen.getByText("Pi 0.80.3")).toBeVisible();
|
|
expect(screen.getByText("Defaults ready")).toBeVisible();
|
|
expect(screen.getByRole("button", { name: "Test saved defaults" })).toBeVisible();
|
|
});
|
|
|
|
test("uses closed provider, model, and reasoning choices without a secret field or terminal", async () => {
|
|
renderManagement();
|
|
|
|
const provider = await screen.findByLabelText("Provider");
|
|
expect(provider).toHaveValue("zai");
|
|
expect(screen.getByLabelText("Model")).toHaveValue("glm-5.2");
|
|
expect(screen.getByLabelText("Reasoning level")).toHaveValue("medium");
|
|
expect(within(provider).getAllByRole("option").map((option) => option.textContent)).toEqual(["zai", "deepseek"]);
|
|
expect(screen.getByLabelText("Model")).toHaveTextContent("GLM 5.2");
|
|
expect(screen.queryByRole("textbox", { name: /provider|model|reasoning|credential/i })).not.toBeInTheDocument();
|
|
expect(document.querySelector('input[type="password"]')).toBeNull();
|
|
expect(screen.queryByText(/terminal|shell access/i)).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: "Update Pi" })).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("saves only a selected non-secret configuration", async () => {
|
|
const user = userEvent.setup();
|
|
let saved: unknown;
|
|
server.use(http.put("/api/pi-management/config", async ({ request }) => {
|
|
saved = await request.json();
|
|
return HttpResponse.json({ ...saved as object, updatedAt: "2026-08-05T10:02:00.000Z" });
|
|
}));
|
|
renderManagement();
|
|
|
|
await user.selectOptions(await screen.findByLabelText("Provider"), "deepseek");
|
|
await user.selectOptions(screen.getByLabelText("Reasoning level"), "high");
|
|
await user.click(screen.getByRole("button", { name: "Save defaults" }));
|
|
|
|
await waitFor(() => expect(saved).toEqual({ provider: "deepseek", model: "deepseek-v4", reasoning: "high" }));
|
|
expect(screen.getByRole("status", { name: "Pi management feedback" })).toHaveTextContent("Defaults saved");
|
|
});
|
|
|
|
test("runs the saved-configuration test without changing credential presence", async () => {
|
|
const user = userEvent.setup();
|
|
let tests = 0;
|
|
server.use(http.post("/api/pi-management/test", () => {
|
|
tests += 1;
|
|
return HttpResponse.json(tests === 1
|
|
? { ready: true, checkedAt: "2026-08-05T10:02:00.000Z" }
|
|
: { ready: false, message: "Pi provider smoke check failed", checkedAt: "2026-08-05T10:03:00.000Z" });
|
|
}));
|
|
renderManagement();
|
|
|
|
const testButton = await screen.findByRole("button", { name: "Test saved defaults" });
|
|
expect(screen.getByText("Defaults ready")).toBeVisible();
|
|
expect(screen.queryByText("Changes are not saved yet.")).not.toBeInTheDocument();
|
|
expect(testButton).toBeEnabled();
|
|
await user.click(testButton);
|
|
await waitFor(() => expect(tests).toBe(1));
|
|
expect(await screen.findByText("Saved configuration test passed")).toBeVisible();
|
|
expect(screen.getByText("Credentials present")).toBeVisible();
|
|
await user.click(screen.getByRole("button", { name: "Test saved defaults" }));
|
|
expect(await screen.findByText("Saved configuration test failed")).toBeVisible();
|
|
expect(screen.getByText("Credentials present")).toBeVisible();
|
|
});
|
|
|
|
test("fetches and displays bounded sanitized diagnostic logs only on request", async () => {
|
|
const user = userEvent.setup();
|
|
let logRequests = 0;
|
|
server.use(http.get("/api/pi-management/logs", () => {
|
|
logRequests += 1;
|
|
return HttpResponse.json({ lines: ["Pi smoke check succeeded", "provider token=[REDACTED]"], checkedAt: "2026-08-05T10:04:00.000Z" });
|
|
}));
|
|
renderManagement();
|
|
|
|
await screen.findByLabelText("Provider");
|
|
expect(logRequests).toBe(0);
|
|
await user.click(screen.getByRole("button", { name: "Show sanitized logs" }));
|
|
expect(await screen.findByLabelText("Sanitized Pi diagnostics")).toHaveTextContent("provider token=[REDACTED]");
|
|
expect(logRequests).toBe(1);
|
|
expect(screen.queryByText("raw-provider-token")).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("explains forbidden management access without offering mutation controls", async () => {
|
|
server.use(
|
|
http.get("/api/pi-management/status", () =>
|
|
HttpResponse.json({ code: "pi_management_forbidden", error: "Pi management is not permitted" }, { status: 403 })),
|
|
);
|
|
renderManagement();
|
|
|
|
expect(await screen.findByRole("alert", { name: "Pi management unavailable" })).toHaveTextContent("Pi management is not permitted");
|
|
expect(screen.queryByLabelText("Provider")).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: "Save defaults" })).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: "Test saved defaults" })).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("offers a copyable host-side Pi update instruction without an update action", async () => {
|
|
const user = userEvent.setup();
|
|
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } });
|
|
renderManagement();
|
|
|
|
expect(await screen.findByText("thothctl pi update")).toBeVisible();
|
|
await user.click(screen.getByRole("button", { name: "Copy update command" }));
|
|
expect(writeText).toHaveBeenCalledWith("thothctl pi update");
|
|
expect(screen.getByRole("status", { name: "Pi management feedback" })).toHaveTextContent("Update command copied");
|
|
expect(screen.queryByRole("button", { name: /update Pi/i })).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("reports when the browser cannot copy the host-side update command", async () => {
|
|
const user = userEvent.setup();
|
|
Object.defineProperty(navigator, "clipboard", { configurable: true, value: undefined });
|
|
renderManagement();
|
|
|
|
await user.click(await screen.findByRole("button", { name: "Copy update command" }));
|
|
expect(screen.getByRole("status", { name: "Pi management feedback" })).toHaveTextContent("Could not copy the update command");
|
|
});
|
|
|
|
test("reloads installation defaults when the panel is reopened", async () => {
|
|
let statusCalls = 0;
|
|
server.use(http.get("/api/pi-management/status", () => {
|
|
statusCalls += 1;
|
|
return HttpResponse.json(statusCalls === 1
|
|
? readyStatus
|
|
: { ...readyStatus, config: { provider: "deepseek", model: "deepseek-v4", reasoning: "high" } });
|
|
}));
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
const view = render(<QueryClientProvider client={client}><PiManagement open onClose={() => undefined} /></QueryClientProvider>);
|
|
|
|
expect(await screen.findByLabelText("Provider")).toHaveValue("zai");
|
|
view.rerender(<QueryClientProvider client={client}><PiManagement open={false} onClose={() => undefined} /></QueryClientProvider>);
|
|
await waitFor(() => expect(screen.queryByRole("dialog", { name: "Pi management" })).not.toBeInTheDocument());
|
|
view.rerender(<QueryClientProvider client={client}><PiManagement open onClose={() => undefined} /></QueryClientProvider>);
|
|
|
|
await waitFor(() => expect(screen.getByLabelText("Provider")).toHaveValue("deepseek"));
|
|
});
|
|
|
|
test("shows an explicit recoverable incomplete state when no provider model is available", async () => {
|
|
server.use(
|
|
http.get("/api/pi-management/status", () => HttpResponse.json({ ...readyStatus, credentials: "present" })),
|
|
http.get("/api/pi-management/options", () => HttpResponse.json({
|
|
...options,
|
|
providers: ["zai"],
|
|
models: [],
|
|
})),
|
|
);
|
|
renderManagement();
|
|
|
|
const incomplete = await screen.findByRole("alert", { name: "Pi configuration incomplete" });
|
|
expect(incomplete).toHaveTextContent("No enabled provider and model choices are available");
|
|
expect(incomplete).toHaveTextContent("Check the host-managed Pi model configuration");
|
|
expect(screen.queryByText("Loading Pi management…")).not.toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: "Save defaults" })).toBeDisabled();
|
|
expect(screen.getByRole("button", { name: "Test saved defaults" })).toBeDisabled();
|
|
});
|
|
|
|
test("keeps suggested draft choices distinct from invalid persisted defaults until save succeeds", async () => {
|
|
const persisted = {
|
|
...readyStatus,
|
|
credentials: "missing",
|
|
config: { provider: "retired", model: "old-model", reasoning: "medium" },
|
|
};
|
|
server.use(
|
|
http.get("/api/pi-management/status", () => HttpResponse.json(persisted)),
|
|
http.put("/api/pi-management/config", async ({ request }) => HttpResponse.json({
|
|
...await request.json() as object,
|
|
updatedAt: "2026-08-05T10:05:00.000Z",
|
|
})),
|
|
);
|
|
const user = userEvent.setup();
|
|
renderManagement();
|
|
|
|
expect(await screen.findByLabelText("Provider")).toHaveValue("zai");
|
|
expect(screen.getByText("Defaults incomplete")).toBeVisible();
|
|
expect(screen.getByText("Suggested choices are not saved yet.")).toBeVisible();
|
|
expect(screen.getByRole("button", { name: "Test saved defaults" })).toBeDisabled();
|
|
|
|
await user.click(screen.getByRole("button", { name: "Save defaults" }));
|
|
expect(await screen.findByText("Defaults ready")).toBeVisible();
|
|
expect(screen.getByRole("button", { name: "Test saved defaults" })).toBeEnabled();
|
|
});
|
|
|
|
test.each(["present", "missing"] as const)(
|
|
"shows credentials %s from status without inferring them from smoke",
|
|
async (credentials) => {
|
|
server.use(http.get("/api/pi-management/status", () => HttpResponse.json({
|
|
...readyStatus,
|
|
credentials,
|
|
})));
|
|
renderManagement();
|
|
|
|
expect(await screen.findByText(`Credentials ${credentials}`)).toBeVisible();
|
|
},
|
|
);
|
|
|
|
test("labels smoke only as a saved-configuration test and resets it when the draft changes", async () => {
|
|
server.use(
|
|
http.get("/api/pi-management/status", () => HttpResponse.json({ ...readyStatus, credentials: "present" })),
|
|
http.post("/api/pi-management/test", () => HttpResponse.json({
|
|
ready: true,
|
|
checkedAt: "2026-08-05T10:06:00.000Z",
|
|
})),
|
|
);
|
|
const user = userEvent.setup();
|
|
renderManagement();
|
|
|
|
await user.click(await screen.findByRole("button", { name: "Test saved defaults" }));
|
|
expect(await screen.findByText("Saved configuration test passed")).toBeVisible();
|
|
expect(screen.getByText("Credentials present")).toBeVisible();
|
|
|
|
await user.selectOptions(screen.getByLabelText("Provider"), "deepseek");
|
|
expect(screen.getByText("Saved configuration test not run")).toBeVisible();
|
|
expect(screen.getByText("Save these changes before testing. The test always uses saved defaults.")).toBeVisible();
|
|
expect(screen.getByRole("button", { name: "Test saved defaults" })).toBeDisabled();
|
|
expect(screen.getByText("Credentials present")).toBeVisible();
|
|
});
|
|
|
|
test("a failed saved-configuration test does not change backend credential presence", async () => {
|
|
server.use(
|
|
http.get("/api/pi-management/status", () => HttpResponse.json({ ...readyStatus, credentials: "present" })),
|
|
http.post("/api/pi-management/test", () => HttpResponse.json({
|
|
ready: false,
|
|
message: "Pi runtime is unavailable",
|
|
checkedAt: "2026-08-05T10:07:00.000Z",
|
|
})),
|
|
);
|
|
const user = userEvent.setup();
|
|
renderManagement();
|
|
|
|
await user.click(await screen.findByRole("button", { name: "Test saved defaults" }));
|
|
expect(await screen.findByText("Saved configuration test failed")).toBeVisible();
|
|
expect(screen.getByText("Credentials present")).toBeVisible();
|
|
expect(screen.queryByText("Credentials missing")).not.toBeInTheDocument();
|
|
});
|