fix: harden Pi management readiness

This commit is contained in:
2026-08-05 06:20:25 +02:00
parent 7a8bcacbfe
commit 7df21b5f21
24 changed files with 577 additions and 274 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ import { apiFetch } from "./client";
test("body-less POST omits content-type (avoids Fastify empty-body 400)", async () => {
let contentType: string | null = "unset";
server.use(
http.post("http://localhost:8787/sessions/s1/resume", ({ request }) => {
http.post("/api/sessions/s1/resume", ({ request }) => {
contentType = request.headers.get("content-type");
return HttpResponse.json({ id: "s1" });
}),
@@ -17,7 +17,7 @@ test("body-less POST omits content-type (avoids Fastify empty-body 400)", async
test("POST with a body sends application/json content-type", async () => {
let contentType: string | null = null;
server.use(
http.post("http://localhost:8787/sessions/s1/steer", ({ request }) => {
http.post("/api/sessions/s1/steer", ({ request }) => {
contentType = request.headers.get("content-type");
return new HttpResponse(null, { status: 204 });
}),
+2 -2
View File
@@ -14,7 +14,7 @@ test("Pi management client calls only the sanctioned sanitized endpoints", async
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" });
return HttpResponse.json({ ready: true, version: "0.80.3", credentials: "present", 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 });
@@ -34,7 +34,7 @@ test("Pi management client calls only the sanctioned sanitized endpoints", async
}),
);
await expect(getPiManagementStatus()).resolves.toMatchObject({ version: "0.80.3", ready: true });
await expect(getPiManagementStatus()).resolves.toMatchObject({ version: "0.80.3", ready: true, credentials: "present" });
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 });
+1
View File
@@ -11,6 +11,7 @@ export interface PiInstallationConfig {
export interface PiManagementStatus {
version?: string;
ready: boolean;
credentials: "present" | "missing";
config: Partial<PiInstallationConfig>;
checkedAt: string;
message?: string;
+14 -14
View File
@@ -10,13 +10,13 @@ test("createSession migrates legacy selections and POSTs browser preferences", a
localStorage.clear();
let body: unknown = null;
server.use(
http.get("http://localhost:8787/settings", () => HttpResponse.json({
http.get("/api/settings", () => HttpResponse.json({
workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low",
})),
http.get("http://localhost:8787/workspaces", () => HttpResponse.json([{
http.get("/api/workspaces", () => HttpResponse.json([{
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical",
}])),
http.post("http://localhost:8787/sessions", async ({ request }) => {
http.post("/api/sessions", async ({ request }) => {
body = await request.json();
return HttpResponse.json({ id: "s1" });
}),
@@ -33,7 +33,7 @@ test("createSession migrates legacy selections and POSTs browser preferences", a
test.each([202, 204])("prewarmRuntime accepts a body-less %s response", async (status) => {
let called = false;
server.use(
http.post("http://localhost:8787/runtime/prewarm", () => {
http.post("/api/runtime/prewarm", () => {
called = true;
return new HttpResponse(null, { status });
}),
@@ -44,7 +44,7 @@ test.each([202, 204])("prewarmRuntime accepts a body-less %s response", async (s
test("listSessions defaults to the current user's scope", async () => {
let scope: string | null = null;
server.use(http.get("http://localhost:8787/sessions", ({ request }) => {
server.use(http.get("/api/sessions", ({ request }) => {
scope = new URL(request.url).searchParams.get("scope");
return HttpResponse.json([{ id: "s1", status: "open", question: "q", summary: null, created_at: "t", updated_at: null, author: null }]);
}));
@@ -55,7 +55,7 @@ test("listSessions defaults to the current user's scope", async () => {
test("listSessions requests the selected administrator scope", async () => {
let scope: string | null = null;
server.use(http.get("http://localhost:8787/sessions", ({ request }) => {
server.use(http.get("/api/sessions", ({ request }) => {
scope = new URL(request.url).searchParams.get("scope");
return HttpResponse.json([]);
}));
@@ -64,7 +64,7 @@ test("listSessions requests the selected administrator scope", async () => {
});
test("getMe fetches the typed authenticated principal", async () => {
server.use(http.get("http://localhost:8787/me", () =>
server.use(http.get("/api/me", () =>
HttpResponse.json({ issuer: "portal", subject: "42", displayName: "Alice", isAdmin: true }),
));
await expect(getMe()).resolves.toEqual({
@@ -73,7 +73,7 @@ test("getMe fetches the typed authenticated principal", async () => {
});
test("resumeSession returns the typed runtime disposition", async () => {
server.use(http.post("http://localhost:8787/sessions/s1/resume", () =>
server.use(http.post("/api/sessions/s1/resume", () =>
HttpResponse.json({ id: "s1", alreadyActive: false })));
const result: { id: string; alreadyActive: boolean } = await resumeSession("s1");
@@ -82,7 +82,7 @@ test("resumeSession returns the typed runtime disposition", async () => {
test("renameSession POSTs {name}", async () => {
let body: unknown = null;
server.use(http.post("http://localhost:8787/sessions/s1/rename", async ({ request }) => {
server.use(http.post("/api/sessions/s1/rename", async ({ request }) => {
body = await request.json();
return new HttpResponse(null, { status: 204 });
}));
@@ -92,7 +92,7 @@ test("renameSession POSTs {name}", async () => {
test("setSessionGroup POSTs {group}", async () => {
let body: unknown = null;
server.use(http.post("http://localhost:8787/sessions/s1/group", async ({ request }) => {
server.use(http.post("/api/sessions/s1/group", async ({ request }) => {
body = await request.json();
return new HttpResponse(null, { status: 204 });
}));
@@ -103,9 +103,9 @@ test("setSessionGroup POSTs {group}", async () => {
test("archive / unarchive / delete hit the right verbs+paths", async () => {
const hits: string[] = [];
server.use(
http.post("http://localhost:8787/sessions/s1/archive", () => { hits.push("archive"); return new HttpResponse(null, { status: 204 }); }),
http.post("http://localhost:8787/sessions/s1/unarchive", () => { hits.push("unarchive"); return new HttpResponse(null, { status: 204 }); }),
http.delete("http://localhost:8787/sessions/s1", () => { hits.push("delete"); return new HttpResponse(null, { status: 204 }); }),
http.post("/api/sessions/s1/archive", () => { hits.push("archive"); return new HttpResponse(null, { status: 204 }); }),
http.post("/api/sessions/s1/unarchive", () => { hits.push("unarchive"); return new HttpResponse(null, { status: 204 }); }),
http.delete("/api/sessions/s1", () => { hits.push("delete"); return new HttpResponse(null, { status: 204 }); }),
);
await archiveSession("s1");
await unarchiveSession("s1");
@@ -114,7 +114,7 @@ test("archive / unarchive / delete hit the right verbs+paths", async () => {
});
test("getSessionDocuments GETs the array", async () => {
server.use(http.get("http://localhost:8787/sessions/s1/documents", () =>
server.use(http.get("/api/sessions/s1/documents", () =>
HttpResponse.json([{ phase: "—", key: "question", title: "t", format: "text", content: "q" }]),
));
const docs = await getSessionDocuments("s1");
+4 -4
View File
@@ -15,7 +15,7 @@ const workspace: CanonicalWorkspace = {
test("uploads a workspace bundle without JSON content type", async () => {
let contentType: string | null = null;
server.use(http.post("http://localhost:8787/workspaces/import", ({ request }) => {
server.use(http.post("/api/workspaces/import", ({ request }) => {
contentType = request.headers.get("content-type");
return HttpResponse.json({ draft: { workspace: {} } });
}));
@@ -29,7 +29,7 @@ test("uploads a workspace bundle without JSON content type", async () => {
});
test("rejects a conflict payload that attempts to surface a secret field", async () => {
server.use(http.post("http://localhost:8787/workspaces/publish", () => HttpResponse.json({
server.use(http.post("/api/workspaces/publish", () => HttpResponse.json({
code: "workspace_conflict", message: "Workspace changed in the registry.", fields: ["dwh.password"],
base: { ...workspace, dwh: { ...workspace.dwh, password: "secret" } }, local: workspace, remote: workspace,
}, { status: 409 })));
@@ -73,7 +73,7 @@ const diagnosticsWorkspace: CanonicalWorkspace = {
};
test.each(optionalDiagnosticsConflictFields)("accepts optional diagnostics conflict branch %s", async (field) => {
server.use(http.post("http://localhost:8787/workspaces/publish", () => HttpResponse.json({
server.use(http.post("/api/workspaces/publish", () => HttpResponse.json({
code: "workspace_conflict", message: "Workspace changed in the registry.", fields: [field],
expected: { commit: "a".repeat(40), blob: "b".repeat(40) },
actual: { commit: "c".repeat(40), blob: "d".repeat(40) },
@@ -99,7 +99,7 @@ test.each(diagnosticConflictFields)("accepts canonical diagnostic conflict leaf
embedding: { method: "GET", path: "/models", auth: "none", response: { model: "model", dimensions: "dimensions" } },
},
};
server.use(http.post("http://localhost:8787/workspaces/publish", () => HttpResponse.json({
server.use(http.post("/api/workspaces/publish", () => HttpResponse.json({
code: "workspace_conflict", message: "Workspace changed in the registry.",
fields: [field],
expected: { commit: "a".repeat(40), blob: "b".repeat(40) },