feat: add Pi management interface
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { server } from "../test/msw";
|
||||
import {
|
||||
asPiManagementApiError,
|
||||
getPiManagementLogs,
|
||||
getPiManagementOptions,
|
||||
getPiManagementStatus,
|
||||
runPiManagementTest,
|
||||
savePiManagementConfig,
|
||||
} from "./pi-management";
|
||||
|
||||
test("Pi management client calls only the sanctioned sanitized endpoints", async () => {
|
||||
const calls: Array<{ method: string; path: string; body?: unknown }> = [];
|
||||
server.use(
|
||||
http.get("/api/pi-management/status", ({ request }) => {
|
||||
calls.push({ method: request.method, path: new URL(request.url).pathname });
|
||||
return HttpResponse.json({ ready: true, version: "0.80.3", config: {}, checkedAt: "2026-08-05T10:00:00.000Z" });
|
||||
}),
|
||||
http.get("/api/pi-management/options", ({ request }) => {
|
||||
calls.push({ method: request.method, path: new URL(request.url).pathname });
|
||||
return HttpResponse.json({ providers: ["zai"], models: [{ provider: "zai", id: "glm-5.2" }], reasoning: ["low", "medium", "high"], checkedAt: "2026-08-05T10:00:00.000Z" });
|
||||
}),
|
||||
http.put("/api/pi-management/config", async ({ request }) => {
|
||||
calls.push({ method: request.method, path: new URL(request.url).pathname, body: await request.json() });
|
||||
return HttpResponse.json({ provider: "zai", model: "glm-5.2", reasoning: "high", updatedAt: "2026-08-05T10:00:00.000Z" });
|
||||
}),
|
||||
http.post("/api/pi-management/test", ({ request }) => {
|
||||
calls.push({ method: request.method, path: new URL(request.url).pathname });
|
||||
return HttpResponse.json({ ready: true, checkedAt: "2026-08-05T10:00:00.000Z" });
|
||||
}),
|
||||
http.get("/api/pi-management/logs", ({ request }) => {
|
||||
calls.push({ method: request.method, path: new URL(request.url).pathname });
|
||||
return HttpResponse.json({ lines: ["Pi smoke check succeeded"], checkedAt: "2026-08-05T10:00:00.000Z" });
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(getPiManagementStatus()).resolves.toMatchObject({ version: "0.80.3", ready: true });
|
||||
await expect(getPiManagementOptions()).resolves.toMatchObject({ providers: ["zai"] });
|
||||
await expect(savePiManagementConfig({ provider: "zai", model: "glm-5.2", reasoning: "high" })).resolves.toMatchObject({ reasoning: "high" });
|
||||
await expect(runPiManagementTest()).resolves.toMatchObject({ ready: true });
|
||||
await expect(getPiManagementLogs()).resolves.toMatchObject({ lines: ["Pi smoke check succeeded"] });
|
||||
|
||||
expect(calls).toEqual([
|
||||
{ method: "GET", path: "/api/pi-management/status" },
|
||||
{ method: "GET", path: "/api/pi-management/options" },
|
||||
{ method: "PUT", path: "/api/pi-management/config", body: { provider: "zai", model: "glm-5.2", reasoning: "high" } },
|
||||
{ method: "POST", path: "/api/pi-management/test" },
|
||||
{ method: "GET", path: "/api/pi-management/logs" },
|
||||
]);
|
||||
expect(calls.some((call) => /update|terminal|shell/i.test(call.path))).toBe(false);
|
||||
});
|
||||
|
||||
test("Pi management client exposes the stable forbidden message without raw response text", async () => {
|
||||
server.use(http.get("/api/pi-management/status", () =>
|
||||
HttpResponse.json({ code: "pi_management_forbidden", error: "Pi management is not permitted", raw: "token=do-not-show" }, { status: 403 }),
|
||||
));
|
||||
|
||||
await expect(getPiManagementStatus()).rejects.toSatisfy((error: unknown) => {
|
||||
expect(asPiManagementApiError(error)).toEqual({
|
||||
status: 403,
|
||||
code: "pi_management_forbidden",
|
||||
message: "Pi management is not permitted",
|
||||
});
|
||||
expect(asPiManagementApiError(error)?.message).not.toContain("token");
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ApiError, apiFetch } from "./client";
|
||||
|
||||
export type PiReasoning = "low" | "medium" | "high";
|
||||
|
||||
export interface PiInstallationConfig {
|
||||
provider: string;
|
||||
model: string;
|
||||
reasoning: PiReasoning;
|
||||
}
|
||||
|
||||
export interface PiManagementStatus {
|
||||
version?: string;
|
||||
ready: boolean;
|
||||
config: Partial<PiInstallationConfig>;
|
||||
checkedAt: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface PiManagementOptions {
|
||||
providers: string[];
|
||||
models: Array<{ provider: string; id: string }>;
|
||||
reasoning: PiReasoning[];
|
||||
checkedAt: string;
|
||||
}
|
||||
|
||||
export interface PiManagementTestResult {
|
||||
ready: boolean;
|
||||
checkedAt: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface PiManagementLogs {
|
||||
lines: string[];
|
||||
checkedAt: string;
|
||||
}
|
||||
|
||||
export type PiManagementApiErrorCode =
|
||||
| "pi_management_forbidden"
|
||||
| "pi_management_unavailable"
|
||||
| "pi_management_invalid_config"
|
||||
| "pi_management_write_failed";
|
||||
|
||||
export interface PiManagementApiError {
|
||||
status: number;
|
||||
code: PiManagementApiErrorCode;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const errorCodes = new Set<PiManagementApiErrorCode>([
|
||||
"pi_management_forbidden",
|
||||
"pi_management_unavailable",
|
||||
"pi_management_invalid_config",
|
||||
"pi_management_write_failed",
|
||||
]);
|
||||
|
||||
function object(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Narrows the sanctioned error payload without ever surfacing its raw response body. */
|
||||
export function asPiManagementApiError(error: unknown): PiManagementApiError | undefined {
|
||||
if (!(error instanceof ApiError)) return undefined;
|
||||
const payload = object(error.payload);
|
||||
const code = payload?.code;
|
||||
const message = payload?.error;
|
||||
if (typeof code !== "string" || !errorCodes.has(code as PiManagementApiErrorCode) || typeof message !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
return { status: error.status, code: code as PiManagementApiErrorCode, message };
|
||||
}
|
||||
|
||||
export const getPiManagementStatus = () => apiFetch<PiManagementStatus>("/pi-management/status");
|
||||
export const getPiManagementOptions = () => apiFetch<PiManagementOptions>("/pi-management/options");
|
||||
export const savePiManagementConfig = (config: PiInstallationConfig) =>
|
||||
apiFetch<PiInstallationConfig & { updatedAt: string }>("/pi-management/config", {
|
||||
method: "PUT", body: JSON.stringify(config),
|
||||
});
|
||||
export const runPiManagementTest = () =>
|
||||
apiFetch<PiManagementTestResult>("/pi-management/test", { method: "POST" });
|
||||
export const getPiManagementLogs = () => apiFetch<PiManagementLogs>("/pi-management/logs");
|
||||
Reference in New Issue
Block a user