The sidecar registered a closed Zod schema, so the SDK judged arguments before Rust ever saw them: an extra key was stripped, a missing required property was refused with an MCP error, and the call never reached the handler. The connector path decodes the same argument in Rust, where `deny_unknown_fields` and required-field decoding turn a bad argument into the gap the evaluator records. `toolInputSchema` now builds a loose object whose every property catches, so nothing is stripped and nothing is refused. The conversion survives only to advertise: the in-process MCP server derives the schema the model sees from this object, and that schema still carries every property, type, bound, and `required` name the emitter produced. What is deleted is the strictness that made it a second validator -- `.strict()`, the closed-object requirement, and the every-property-is-required requirement. One case cannot be carried through as written: a value outside a property's own type empties that property, so it reaches Rust as an absent field rather than a wrong one. Rust still names the gap; the SDK still does not answer the call. The undispatched-tool_use protocol violation stays, because no argument reaches it any more but a call never offered to the handler still does. `resultFault` is the result-subtype mapping lifted out of `runQuery`, so `error_max_turns` becoming an ordinary round and every other subtype becoming its named fault are both asserted without a credential. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
// Which `result` message becomes a round and which becomes a named fault. The
|
|
// mapping is the sidecar's whole judgement of a finished query, so it is
|
|
// asserted directly against the exported decision, with no credential, no
|
|
// network, and no `query()`.
|
|
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { apiRetryReason, resultFault, resultReason, SidecarFault } from "../src/main.ts";
|
|
|
|
function reasonOf(message: Record<string, unknown>): string | null {
|
|
const fault = resultFault(message);
|
|
if (fault === null) {
|
|
return null;
|
|
}
|
|
assert.ok(fault instanceof SidecarFault);
|
|
return fault.reason;
|
|
}
|
|
|
|
test("a spent turn cap is an ordinary output, not a fault", () => {
|
|
// The cap is the lane's own remaining round budget, so reaching it means the
|
|
// model spent turns the evaluator would also have allowed.
|
|
assert.equal(resultFault({ subtype: "error_max_turns", num_turns: 24 }), null);
|
|
assert.equal(resultFault({ subtype: "success" }), null);
|
|
});
|
|
|
|
test("every other result subtype carries its named fault", () => {
|
|
assert.equal(reasonOf({ subtype: "error_max_budget_usd" }), "max_budget_usd");
|
|
assert.equal(reasonOf({ subtype: "error_during_execution" }), "execution_error");
|
|
assert.equal(
|
|
reasonOf({ subtype: "error_max_structured_output_retries" }),
|
|
"execution_error",
|
|
);
|
|
assert.equal(reasonOf({ subtype: "error_nobody_has_named_yet" }), "unknown");
|
|
});
|
|
|
|
test("an API status outranks the subtype it arrived on", () => {
|
|
assert.equal(
|
|
reasonOf({ subtype: "error_during_execution", api_error_status: 429 }),
|
|
"rate_limited",
|
|
);
|
|
assert.equal(
|
|
reasonOf({ subtype: "error_during_execution", api_error_status: 503 }),
|
|
"server_error",
|
|
);
|
|
assert.equal(
|
|
reasonOf({ subtype: "error_during_execution", api_error_status: 400 }),
|
|
"execution_error",
|
|
);
|
|
assert.equal(resultReason("error_during_execution", "429"), "execution_error");
|
|
});
|
|
|
|
test("a retry category names the fault when the result carries one", () => {
|
|
const categories: [string, string][] = [
|
|
["rate_limit", "rate_limited"],
|
|
["overloaded", "overloaded"],
|
|
["server_error", "server_error"],
|
|
["authentication_failed", "authentication_failed"],
|
|
["oauth_org_not_allowed", "org_not_allowed"],
|
|
["billing_error", "billing_error"],
|
|
["invalid_request", "invalid_request"],
|
|
["model_not_found", "model_not_found"],
|
|
["max_output_tokens", "max_output_tokens"],
|
|
["something_new", "unknown"],
|
|
];
|
|
for (const [category, reason] of categories) {
|
|
assert.equal(apiRetryReason(category), reason, category);
|
|
assert.equal(
|
|
reasonOf({ subtype: "error_during_execution", error: category }),
|
|
reason,
|
|
category,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("a fault's detail carries the subtype and the reported errors", () => {
|
|
const fault = resultFault({
|
|
subtype: "error_during_execution",
|
|
errors: ["the pipe closed", "and stayed closed"],
|
|
});
|
|
assert.ok(fault instanceof SidecarFault);
|
|
assert.equal(fault.reason, "execution_error");
|
|
assert.equal(fault.message, "error_during_execution: the pipe closed; and stayed closed");
|
|
});
|