fix: address Copilot review round 27 — unconstrained null branches, typed discriminators
- anyOfAdmitsNull recognizes a branch that constrains nothing (`{}`, or
annotations only): it admits every value, null among them, so a field pinned
to `const: null` beside such a union stays submittable. `admitsNull`
cannot say that on its own — an unconstrained schema tells it nothing either
way and it declines rather than guesses — but here the question is narrower.
- convertToolParameters sends a matched `const` as the schema's own typed
value: `kind=2` selects a branch pinned to `const: 2` and was then sent
`"2"`, which that same branch rejects. Only an exact match is
substituted; anything else is the user's input and is left alone.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
f31589a8f9
commit
c8c5442dcf
2 changed files with 75 additions and 1 deletions
|
|
@ -215,6 +215,36 @@ describe("JSON Utils", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("sends a non-string discriminator as its typed constant (#2123)", () => {
|
||||
const numericallyPinned: Tool = {
|
||||
name: "numerically-pinned",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
oneOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: { kind: { const: 1 }, a: { type: "string" } },
|
||||
required: ["kind"],
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: { kind: { const: 2 }, b: { type: "string" } },
|
||||
required: ["kind"],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
// `kind=2` selects the second branch, which then rejects `"2"` — the
|
||||
// schema's own typed value is what goes on the wire.
|
||||
expect(convertToolParameters(numericallyPinned, { kind: "2" })).toEqual({
|
||||
kind: 2,
|
||||
});
|
||||
// Text that matches no constant is the user's input and is left alone.
|
||||
expect(convertToolParameters(numericallyPinned, { kind: "9" })).toEqual({
|
||||
kind: "9",
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves an ambiguously typed argument uncoerced (#2123)", () => {
|
||||
const ambiguous: Tool = {
|
||||
name: "ambiguous",
|
||||
|
|
|
|||
|
|
@ -759,6 +759,15 @@ describe("admitsNull", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("admits null when a sibling branch constrains nothing", () => {
|
||||
// `{}` is the equivalent of `true` — it admits every value, null among
|
||||
// them — so the pinned null stays reachable and its field submittable.
|
||||
expect(admitsNull({ const: null, anyOf: [{}] })).toBe(true);
|
||||
expect(admitsNull({ const: null, anyOf: [{ title: "anything" }] })).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
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