fix: address Copilot review round 31 — proto-safe results, set-valued types, core keywords
- convertToolParameters defines its result keys rather than assigning them, so a discriminator legally named `__proto__` reaches the call instead of the prototype setter. It also collapses a nullable declaration before dispatching, so `type: ["number","null"]` coerces as a number rather than being sent as text. - typeNameOf sorts an array `type`: JSON Schema reads it as a SET, so `["number","null"]` and `["null","number"]` are one declaration and must not read as branches disagreeing. - nullableUnion's non-asserting keyword set covers `$defs`, `$id`, `$schema`, `$anchor`, `$dynamicAnchor` and `$vocabulary` — they identify or declare rather than assert. `$ref` stays out: it applies whatever it points at. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
67e7a4ca1a
commit
c9162d0e73
2 changed files with 87 additions and 11 deletions
|
|
@ -245,6 +245,42 @@ describe("JSON Utils", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("keeps a converted argument named __proto__ (#2123)", () => {
|
||||
const protoNamed: Tool = {
|
||||
name: "proto-named",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: Object.fromEntries([["__proto__", { type: "number" }]]),
|
||||
},
|
||||
};
|
||||
const converted = convertToolParameters(protoNamed, {
|
||||
["__proto__"]: "3",
|
||||
});
|
||||
expect(Object.hasOwn(converted, "__proto__")).toBe(true);
|
||||
});
|
||||
|
||||
it("reads an array type as a set when branches agree (#2123)", () => {
|
||||
const setTyped: Tool = {
|
||||
name: "set-typed",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
anyOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: { v: { type: ["number", "null"] }, a: {} },
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: { v: { type: ["null", "number"] }, b: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
// The same declaration written in the other order — not a disagreement,
|
||||
// so the coercion survives.
|
||||
expect(convertToolParameters(setTyped, { v: "2" })).toEqual({ v: 2 });
|
||||
});
|
||||
|
||||
it("leaves an ambiguously typed argument uncoerced (#2123)", () => {
|
||||
const ambiguous: Tool = {
|
||||
name: "ambiguous",
|
||||
|
|
@ -293,8 +329,9 @@ describe("JSON Utils", () => {
|
|||
],
|
||||
},
|
||||
};
|
||||
// Both spell the type the same way, so it is not ambiguous.
|
||||
expect(convertToolParameters(arrayTyped, { v: "2" })).toEqual({ v: "2" });
|
||||
// Both spell the type the same way, so it is not ambiguous — and the
|
||||
// nullable declaration collapses to `number`, which is what coerces.
|
||||
expect(convertToolParameters(arrayTyped, { v: "2" })).toEqual({ v: 2 });
|
||||
});
|
||||
|
||||
it("ignores a malformed branch declaration when matching constants (#2123)", () => {
|
||||
|
|
|
|||
|
|
@ -774,6 +774,16 @@ describe("admitsNull", () => {
|
|||
expect(admitsNull({ const: null, anyOf: [false] })).toBe(false);
|
||||
});
|
||||
|
||||
it("treats core identifier keywords as asserting nothing", () => {
|
||||
// `$defs` declares subschemas for reference; it constrains nothing on its
|
||||
// own, so this branch is unconstrained and admits null.
|
||||
expect(admitsNull({ const: null, anyOf: [{ $defs: {} }] })).toBe(true);
|
||||
// `$ref` is different — it applies whatever it points at.
|
||||
expect(admitsNull({ const: null, anyOf: [{ $ref: "#/$defs/T" }] })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not override a sibling that rejects null", () => {
|
||||
// `const` is conjunctive with its siblings, not an override: both of
|
||||
// these reject every value, so claiming nullability would let the
|
||||
|
|
|
|||
Loading…
Reference in a new issue