fix(studio): recover transient AI failures and report provider diagnostics
This commit is contained in:
1 parent
98b3c5ff2f
commit
e8c0f06d8f
4 files changed
+320
-41
No files matched your search
+120
-1
@@ -3,6 +3,7 @@ import { CORE_DESIGN_SYSTEMS } from "@wrnexus/design-system";
|
||||
import { designRevision, DEFAULT_DESIGN_BRIEF } from "@wrnexus/design-studio";
|
||||
import {
|
||||
AI_TIMEOUT_MS,
|
||||
AI_OUTPUT_TOKENS,
|
||||
apimartProvider,
|
||||
studioAI,
|
||||
} from "../app/lib/studio-ai.ts";
|
||||
@@ -57,7 +58,7 @@ test("APIMart uses the specified model, bearer authentication, non-streaming JSO
|
||||
const body = JSON.parse(init?.body as string);
|
||||
expect(body.model).toBe("gpt-6-luna");
|
||||
expect(body.stream).toBe(false);
|
||||
expect(body.max_tokens).toBe(4096);
|
||||
expect(body.max_tokens).toBe(AI_OUTPUT_TOKENS);
|
||||
expect(body.messages).toEqual([
|
||||
{ role: "system", content: "rules" },
|
||||
{ role: "user", content: "brief" },
|
||||
@@ -200,3 +201,121 @@ test("a public AI endpoint enforces a bounded generation rate", async () => {
|
||||
expect((await handler(await request()))!.status).toBe(200);
|
||||
expect((await handler(await request()))!.status).toBe(429);
|
||||
});
|
||||
|
||||
test("temporary APIMart failures retry once and return three validated designs", async () => {
|
||||
let calls = 0;
|
||||
const handler = studioAI(
|
||||
"test-secret",
|
||||
mockFetch(async (_url, init) => {
|
||||
calls++;
|
||||
expect(JSON.parse(init!.body as string).max_tokens).toBe(8192);
|
||||
return calls === 1
|
||||
? new Response("sensitive upstream context test-secret", {
|
||||
status: 503,
|
||||
})
|
||||
: completion();
|
||||
}),
|
||||
);
|
||||
const batch = await (await handler(await request()))!.json();
|
||||
expect(calls).toBe(2);
|
||||
expect(batch.alternatives).toHaveLength(3);
|
||||
expect(batch.provenance.attempts).toBe(2);
|
||||
expect(batch.provenance.maxOutputTokens).toBe(8192);
|
||||
});
|
||||
|
||||
test("authentication and quota failures provide actionable redacted diagnostics without retrying", async () => {
|
||||
for (const [status, code, hint] of [
|
||||
[401, "invalid_api_key", "server API key"],
|
||||
[402, "insufficient_quota", "credit or quota"],
|
||||
[404, "model_not_found", "gpt-6-luna"],
|
||||
] as const) {
|
||||
let calls = 0;
|
||||
const handler = studioAI(
|
||||
"test-secret",
|
||||
mockFetch(async () => {
|
||||
calls++;
|
||||
return Response.json(
|
||||
{ error: { code, message: "test-secret private brief" } },
|
||||
{ status },
|
||||
);
|
||||
}),
|
||||
);
|
||||
const body = await (await handler(await request()))!.text();
|
||||
expect(calls).toBe(1);
|
||||
expect(body).toContain(hint);
|
||||
expect(body).not.toContain("test-secret");
|
||||
expect(body).not.toContain("private brief");
|
||||
}
|
||||
});
|
||||
|
||||
test("truncated and empty completions retry but never exceed two total upstream calls", async () => {
|
||||
for (const kind of ["length", "empty", "invalid-then-outage"]) {
|
||||
let calls = 0;
|
||||
const handler = studioAI(
|
||||
"test-secret",
|
||||
mockFetch(async () => {
|
||||
calls++;
|
||||
if (kind === "invalid-then-outage")
|
||||
return calls === 1
|
||||
? Response.json({
|
||||
choices: [{ message: { content: "invalid JSON" } }],
|
||||
})
|
||||
: new Response("private upstream body", { status: 502 });
|
||||
return calls === 1
|
||||
? Response.json({
|
||||
choices: [
|
||||
{
|
||||
finish_reason: kind === "length" ? "length" : "stop",
|
||||
message: { content: "" },
|
||||
},
|
||||
],
|
||||
})
|
||||
: completion();
|
||||
}),
|
||||
);
|
||||
const batch = await (await handler(await request()))!.json();
|
||||
expect(calls).toBe(2);
|
||||
expect(batch.provenance.attempts).toBe(2);
|
||||
expect(batch.alternatives).toHaveLength(
|
||||
kind === "invalid-then-outage" ? 0 : 3,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("network and repeated rate failures remain bounded and describe the actual failure category", async () => {
|
||||
for (const network of [true, false]) {
|
||||
let calls = 0;
|
||||
const handler = studioAI(
|
||||
"test-secret",
|
||||
mockFetch(async () => {
|
||||
calls++;
|
||||
if (network) throw Error("private test-secret TLS details");
|
||||
return new Response("private test-secret", { status: 429 });
|
||||
}),
|
||||
);
|
||||
const body = await (await handler(await request()))!.text();
|
||||
expect(calls).toBe(2);
|
||||
expect(body).toContain(
|
||||
network ? "could not reach APIMart" : "rate limiting",
|
||||
);
|
||||
expect(body).not.toContain("test-secret");
|
||||
}
|
||||
});
|
||||
|
||||
test("a retry followed by an invalid design does not spend a third request or mislabel a schema error", async () => {
|
||||
let calls = 0;
|
||||
const handler = studioAI(
|
||||
"test-secret",
|
||||
mockFetch(async () => {
|
||||
calls++;
|
||||
return calls === 1
|
||||
? new Response("unavailable", { status: 503 })
|
||||
: Response.json({ choices: [{ message: { content: "not JSON" } }] });
|
||||
}),
|
||||
);
|
||||
const batch = await (await handler(await request()))!.json();
|
||||
expect(calls).toBe(2);
|
||||
expect(batch.alternatives).toHaveLength(0);
|
||||
expect(batch.diagnostics[0].code).toBe("WRN-STUDIO-PROPOSAL");
|
||||
expect(batch.diagnostics[0].message).toContain("schema");
|
||||
});
|
||||
Reference in new issue
Block a user