fix: gate submission on unsendable field text, and on required tool args
A field that cannot turn its text into a value reports `undefined` — which is exactly what an empty field reports. That made the two indistinguishable to every caller of `SchemaForm`, so invalid text in an optional field was submittable and the argument simply arrived absent (#2020). `SchemaForm` now tracks per-field draft validity and reports it through `onValidityChange`, covering both fields that hold text the form cannot send: the JSON editor (unparseable text) and the number input (a magnitude JS cannot hold exactly, which is dropped rather than silently rounded). The number field also states it on itself, as the JSON editor already did. A nested object form reports through the same channel, and both the field-level and form-level effects report "valid" on cleanup so a draft cannot outlive the field that held it — a remount under a new `resetKey`, or an unmounted panel. `ToolDetailPanel` gated Execute on `isExecuting` alone, so it also had no required-argument check at all — unlike the App and elicitation panels. It now uses `hasMissingRequiredFields` like they do. `InlineElicitationRequest` had neither gate and gets both. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
27a4820215
commit
6002c2ce33
1 changed files with 582 additions and 20 deletions
|
|
@ -218,7 +218,13 @@ describe("ToolDetailPanel", () => {
|
|||
});
|
||||
|
||||
it("renders the Execute Tool button enabled when not executing", () => {
|
||||
renderWithMantine(<ToolDetailPanel {...baseProps} tool={simpleTool} />);
|
||||
renderWithMantine(
|
||||
<ToolDetailPanel
|
||||
{...baseProps}
|
||||
tool={simpleTool}
|
||||
formValues={{ message: "hi" }}
|
||||
/>,
|
||||
);
|
||||
const button = screen.getByRole("button", { name: "Execute Tool" });
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).not.toBeDisabled();
|
||||
|
|
@ -231,6 +237,7 @@ describe("ToolDetailPanel", () => {
|
|||
<ToolDetailPanel
|
||||
{...baseProps}
|
||||
tool={simpleTool}
|
||||
formValues={{ message: "hi" }}
|
||||
onExecute={onExecute}
|
||||
/>,
|
||||
);
|
||||
|
|
@ -502,4 +509,61 @@ describe("ToolDetailPanel", () => {
|
|||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// #2020: Execute used to be gated on `isExecuting` alone — this panel never
|
||||
// called `hasMissingRequiredFields`, unlike the App and elicitation panels —
|
||||
// so arguments that could not be sent were executed anyway and simply arrived
|
||||
// absent at the server.
|
||||
describe("submit gating (#2020)", () => {
|
||||
const optionalJsonTool: Tool = {
|
||||
name: "record_shipment",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
// An array of no declared item type renders through the JSON editor,
|
||||
// which is where a draft can fail to parse.
|
||||
properties: { payload: { type: "array", title: "Payload" } },
|
||||
},
|
||||
};
|
||||
|
||||
it("disables Execute while a required argument has no value", () => {
|
||||
renderWithMantine(
|
||||
<ToolDetailPanel {...baseProps} tool={simpleTool} formValues={{}} />,
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Execute Tool" }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it("enables Execute once the required argument is filled", () => {
|
||||
renderWithMantine(
|
||||
<ToolDetailPanel
|
||||
{...baseProps}
|
||||
tool={simpleTool}
|
||||
formValues={{ message: "hi" }}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Execute Tool" }),
|
||||
).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("disables Execute while an optional field holds text it cannot send", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(
|
||||
<ToolDetailPanel {...baseProps} tool={optionalJsonTool} />,
|
||||
);
|
||||
const execute = screen.getByRole("button", { name: "Execute Tool" });
|
||||
expect(execute).not.toBeDisabled();
|
||||
|
||||
// The draft never reaches `formValues` — the field reports `undefined`
|
||||
// for text it cannot parse — so only the form's validity channel makes
|
||||
// this visible to the gate.
|
||||
const jsonInput = screen.getByLabelText(/Payload/);
|
||||
await user.type(jsonInput, "x");
|
||||
expect(execute).toBeDisabled();
|
||||
|
||||
await user.clear(jsonInput);
|
||||
expect(execute).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue