fix: address Copilot review rounds 7 and 8
- rootUnion: a `oneOf` demands that EXACTLY one alternative match, which flattening cannot preserve, so it is offered only when a discriminator makes the alternatives mutually exclusive; `anyOf` makes no such claim and is unaffected. An empty `additionalProperties` schema is the equivalent of `true` and no longer declines a union. - selectBranchIndex: falls back to the branch whose own required fields the values supply, so an undiscriminated union's values open the picker on the shape they describe rather than on the first branch. - admitsNull: honors a sibling `nullable: true` beside `const: null`. - SchemaForm: a value is carried only where the incoming branch leaves the root's declaration as it found it; anything it declares itself is reset, since it may type the name differently from where the value was typed. `hasOwn`/`fromEntries` throughout, so `constructor` is not read as a supplied value and `__proto__` is not dropped into the prototype setter. - TUI: the same prototype-safe construction on the way out of the form, and the chosen branch's `required` list is checked at submit — rendering a branch's fields optional makes the FORM satisfiable, not the call, so the missing names are reported rather than a known-invalid call being sent. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
c15e30414f
commit
85b8e4601c
1 changed files with 492 additions and 42 deletions
|
|
@ -74,6 +74,81 @@ const renderAndSubmit = async (
|
|||
};
|
||||
|
||||
describe("ToolTestModal", () => {
|
||||
it("reports a missing required argument instead of calling the tool (#2123)", async () => {
|
||||
// A union branch's fields render optional — a static form cannot demand
|
||||
// every branch's — so the chosen shape's own requirements are checked here.
|
||||
const callTool = vi.fn();
|
||||
const tool = makeTool({
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
oneOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
kind: { type: "string", const: "email" },
|
||||
address: { type: "string" },
|
||||
},
|
||||
required: ["kind", "address"],
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
kind: { type: "string", const: "sms" },
|
||||
phone: { type: "string" },
|
||||
},
|
||||
required: ["kind", "phone"],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as Partial<Tool>);
|
||||
const api = render(
|
||||
<ToolTestModal
|
||||
tool={tool}
|
||||
inspectorClient={fakeClient(callTool)}
|
||||
width={80}
|
||||
height={24}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await tick();
|
||||
setSubmitValue({ __variant: "0", __b0__kind: "email" });
|
||||
api.stdin.write("\r");
|
||||
await tick();
|
||||
await tick();
|
||||
// The assertion is the transition, not the frame — this suite drives state,
|
||||
// not rendered text (see the note at the top of the file).
|
||||
expect(callTool).not.toHaveBeenCalled();
|
||||
api.unmount();
|
||||
});
|
||||
|
||||
it("names every missing required argument (#2123)", async () => {
|
||||
const callTool = vi.fn();
|
||||
const tool = makeTool({
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { a: { type: "string" }, b: { type: "string" } },
|
||||
required: ["a", "b"],
|
||||
},
|
||||
} as unknown as Partial<Tool>);
|
||||
const api = render(
|
||||
<ToolTestModal
|
||||
tool={tool}
|
||||
inspectorClient={fakeClient(callTool)}
|
||||
width={80}
|
||||
height={24}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await tick();
|
||||
setSubmitValue({});
|
||||
api.stdin.write("\r");
|
||||
await tick();
|
||||
await tick();
|
||||
// Two missing names, which is also the plural branch of the message.
|
||||
expect(callTool).not.toHaveBeenCalled();
|
||||
api.unmount();
|
||||
});
|
||||
|
||||
it("renders the form initially without invoking the client", async () => {
|
||||
const callTool = vi.fn();
|
||||
const api = render(
|
||||
|
|
|
|||
Loading…
Reference in a new issue