Support tri-state nullable boolean

This commit is contained in:
olaservo 2025-10-19 05:47:33 -07:00
commit 93fc8cc97e
2 changed files with 102 additions and 1 deletions

View file

@ -179,7 +179,11 @@ const ToolsTab = ({
onCheckedChange={(checked: boolean) =>
setParams({
...params,
[key]: checked ? null : prop.default,
[key]: checked
? null
: prop.type === "boolean"
? false
: prop.default,
})
}
/>

View file

@ -177,6 +177,103 @@ describe("ToolsTab", () => {
});
});
it("should support tri-state nullable boolean (null -> false -> true -> null)", async () => {
const mockCallTool = jest.fn();
const toolWithNullableBoolean: Tool = {
name: "testTool",
description: "Tool with nullable boolean",
inputSchema: {
type: "object" as const,
properties: {
optionalBoolean: {
type: ["boolean", "null"] as const,
default: null,
},
},
},
};
renderToolsTab({
tools: [toolWithNullableBoolean],
selectedTool: toolWithNullableBoolean,
callTool: mockCallTool,
});
const nullCheckbox = screen.getByRole("checkbox", { name: /null/i });
const runButton = screen.getByRole("button", { name: /run tool/i });
// State 1: Initial state should be null (input disabled)
const wrapper = screen.getByRole("toolinputwrapper");
expect(wrapper.classList).toContain("pointer-events-none");
expect(wrapper.classList).toContain("opacity-50");
// Verify tool is called with null initially
await act(async () => {
fireEvent.click(runButton);
});
expect(mockCallTool).toHaveBeenCalledWith(toolWithNullableBoolean.name, {
optionalBoolean: null,
});
// State 2: Uncheck null checkbox -> should set value to false and enable input
await act(async () => {
fireEvent.click(nullCheckbox);
});
expect(wrapper.classList).not.toContain("pointer-events-none");
// Clear previous calls to make assertions clearer
mockCallTool.mockClear();
// Verify tool can be called with false
await act(async () => {
fireEvent.click(runButton);
});
expect(mockCallTool).toHaveBeenLastCalledWith(
toolWithNullableBoolean.name,
{
optionalBoolean: false,
},
);
// State 3: Check boolean checkbox -> should set value to true
// Find the boolean checkbox within the input wrapper (to avoid ID conflict with null checkbox)
const booleanCheckbox = within(wrapper).getByRole("checkbox");
mockCallTool.mockClear();
await act(async () => {
fireEvent.click(booleanCheckbox);
});
// Verify tool can be called with true
await act(async () => {
fireEvent.click(runButton);
});
expect(mockCallTool).toHaveBeenLastCalledWith(
toolWithNullableBoolean.name,
{
optionalBoolean: true,
},
);
// State 4: Check null checkbox again -> should set value back to null and disable input
await act(async () => {
fireEvent.click(nullCheckbox);
});
expect(wrapper.classList).toContain("pointer-events-none");
// Verify tool can be called with null again
await act(async () => {
fireEvent.click(runButton);
});
expect(mockCallTool).toHaveBeenLastCalledWith(
toolWithNullableBoolean.name,
{
optionalBoolean: null,
},
);
});
it("should disable button and change text while tool is running", async () => {
// Create a promise that we can resolve later
let resolvePromise: ((value: unknown) => void) | undefined;