mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-22 21:45:08 +00:00
fix(structured): crash-safe validation façade — deep model reply → refusal, not a 500 (closes #181)
#153's cyclic-$ref guard caps the REF-chain depth but not the DATA depth: validateJsonSchema recurses on the value's nesting (properties/items/additionalProps), so a model reply nested ~2000+ levels overflowed the stack with a RangeError, which handleChatCompletions caught as a generic HTTP 500 instead of the spec-correct refusal. (Found in the #153 final review, filed as #181; ≤1 spawn, no crash, no client-only trigger — the value always comes from the model reply.) New exported validateJsonSchemaSafe() wraps the validator: ANY throw (the deep-data RangeError, or any future recursion hazard) becomes a single validation error, so the structured-output retry loop treats a pathological reply as "did not validate" → refusal. A well-formed reply is byte-identical (passes the inner errors through). runStructuredCompletion calls the safe façade. Chose the wrapper over threading a data-depth counter through six recursive call sites: it protects every internal path at once (impossible to miss one) and stays deterministically testable — a 6000-deep fixture reliably overflows on any platform. Tests: +2, mutation-proven (revert the wrapper to a bare call → the deep test throws RED). Suite 431/0. Rule 2: OCP-internal validation, no wire change, no cli.js citation. Closes #181 Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude <claude-opus-4-8> <noreply@anthropic.com>
parent
788cbbcd99
commit
4143993e3b
+23
-1
@@ -4331,7 +4331,7 @@ test("stream: /health block is additive and exposes the divergence counter", ()
|
||||
});
|
||||
|
||||
// ── OpenAI Structured Outputs (response_format) — lib/structured-output.mjs ──
|
||||
import { detectStructuredOutput, validateJsonSchema, extractJsonPayload, structuredSystemInstruction, StructuredOutputError, resolveMaxAttempts } from "./lib/structured-output.mjs";
|
||||
import { detectStructuredOutput, validateJsonSchema, validateJsonSchemaSafe, extractJsonPayload, structuredSystemInstruction, StructuredOutputError, resolveMaxAttempts } from "./lib/structured-output.mjs";
|
||||
|
||||
test("detectStructuredOutput: json_schema shape", () => {
|
||||
const d = detectStructuredOutput({ response_format: { type: "json_schema", json_schema: { name: "x", strict: true, schema: { type: "object" } } } });
|
||||
@@ -4358,6 +4358,28 @@ test("cacheHash: structured marker isolates JSON requests from the conversationa
|
||||
assert.equal(plain, cacheHash("m", msgs, { keyId: "k" })); // unchanged for normal requests
|
||||
});
|
||||
|
||||
// ── validateJsonSchemaSafe (#181): deep value must NOT crash the handler ─────
|
||||
// A recursive schema + a model reply nested ~thousands deep overflows the value-
|
||||
// depth recursion → RangeError → the handler used to surface a generic 500. The
|
||||
// safe façade turns it into a validation miss (→ retry → refusal). Mutation-proof:
|
||||
// replace the wrapper body with a bare `validateJsonSchema(...)` call and the deep
|
||||
// test throws instead of returning errors.
|
||||
test("validateJsonSchemaSafe: pathologically deep value → errors, never throws", () => {
|
||||
const schema = { $defs: { node: { type: "object", properties: { child: { $ref: "#/$defs/node" } } } }, $ref: "#/$defs/node" };
|
||||
let deep = {};
|
||||
let cur = deep;
|
||||
for (let i = 0; i < 6000; i++) { cur.child = {}; cur = cur.child; } // way past any stack limit
|
||||
let out;
|
||||
assert.doesNotThrow(() => { out = validateJsonSchemaSafe(deep, schema, "$", true); }, "must not throw a RangeError out to the handler");
|
||||
assert.ok(Array.isArray(out) && out.length > 0, "returns a non-empty validation error, so the retry loop yields a refusal not a 500");
|
||||
});
|
||||
|
||||
test("validateJsonSchemaSafe: well-formed value passes through unchanged (byte-identical to the raw validator)", () => {
|
||||
const schema = { type: "object", required: ["name", "age"], properties: { name: { type: "string" }, age: { type: "integer" } } };
|
||||
assert.deepEqual(validateJsonSchemaSafe({ name: "a", age: 3 }, schema), validateJsonSchema({ name: "a", age: 3 }, schema));
|
||||
assert.deepEqual(validateJsonSchemaSafe({ name: "a" }, schema), validateJsonSchema({ name: "a" }, schema)); // error case matches too
|
||||
});
|
||||
|
||||
test("validateJsonSchema: valid object passes", () => {
|
||||
assert.deepEqual(validateJsonSchema({ name: "a", age: 3 }, { type: "object", required: ["name", "age"], properties: { name: { type: "string" }, age: { type: "integer" } } }), []);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user