@@ -177,6 +177,103 @@ describe("ToolsTab", () => {
177177 } ) ;
178178 } ) ;
179179
180+ it ( "should support tri-state nullable boolean (null -> false -> true -> null)" , async ( ) => {
181+ const mockCallTool = jest . fn ( ) ;
182+ const toolWithNullableBoolean : Tool = {
183+ name : "testTool" ,
184+ description : "Tool with nullable boolean" ,
185+ inputSchema : {
186+ type : "object" as const ,
187+ properties : {
188+ optionalBoolean : {
189+ type : [ "boolean" , "null" ] as const ,
190+ default : null ,
191+ } ,
192+ } ,
193+ } ,
194+ } ;
195+
196+ renderToolsTab ( {
197+ tools : [ toolWithNullableBoolean ] ,
198+ selectedTool : toolWithNullableBoolean ,
199+ callTool : mockCallTool ,
200+ } ) ;
201+
202+ const nullCheckbox = screen . getByRole ( "checkbox" , { name : / n u l l / i } ) ;
203+ const runButton = screen . getByRole ( "button" , { name : / r u n t o o l / i } ) ;
204+
205+ // State 1: Initial state should be null (input disabled)
206+ const wrapper = screen . getByRole ( "toolinputwrapper" ) ;
207+ expect ( wrapper . classList ) . toContain ( "pointer-events-none" ) ;
208+ expect ( wrapper . classList ) . toContain ( "opacity-50" ) ;
209+
210+ // Verify tool is called with null initially
211+ await act ( async ( ) => {
212+ fireEvent . click ( runButton ) ;
213+ } ) ;
214+ expect ( mockCallTool ) . toHaveBeenCalledWith ( toolWithNullableBoolean . name , {
215+ optionalBoolean : null ,
216+ } ) ;
217+
218+ // State 2: Uncheck null checkbox -> should set value to false and enable input
219+ await act ( async ( ) => {
220+ fireEvent . click ( nullCheckbox ) ;
221+ } ) ;
222+ expect ( wrapper . classList ) . not . toContain ( "pointer-events-none" ) ;
223+
224+ // Clear previous calls to make assertions clearer
225+ mockCallTool . mockClear ( ) ;
226+
227+ // Verify tool can be called with false
228+ await act ( async ( ) => {
229+ fireEvent . click ( runButton ) ;
230+ } ) ;
231+ expect ( mockCallTool ) . toHaveBeenLastCalledWith (
232+ toolWithNullableBoolean . name ,
233+ {
234+ optionalBoolean : false ,
235+ } ,
236+ ) ;
237+
238+ // State 3: Check boolean checkbox -> should set value to true
239+ // Find the boolean checkbox within the input wrapper (to avoid ID conflict with null checkbox)
240+ const booleanCheckbox = within ( wrapper ) . getByRole ( "checkbox" ) ;
241+
242+ mockCallTool . mockClear ( ) ;
243+
244+ await act ( async ( ) => {
245+ fireEvent . click ( booleanCheckbox ) ;
246+ } ) ;
247+
248+ // Verify tool can be called with true
249+ await act ( async ( ) => {
250+ fireEvent . click ( runButton ) ;
251+ } ) ;
252+ expect ( mockCallTool ) . toHaveBeenLastCalledWith (
253+ toolWithNullableBoolean . name ,
254+ {
255+ optionalBoolean : true ,
256+ } ,
257+ ) ;
258+
259+ // State 4: Check null checkbox again -> should set value back to null and disable input
260+ await act ( async ( ) => {
261+ fireEvent . click ( nullCheckbox ) ;
262+ } ) ;
263+ expect ( wrapper . classList ) . toContain ( "pointer-events-none" ) ;
264+
265+ // Verify tool can be called with null again
266+ await act ( async ( ) => {
267+ fireEvent . click ( runButton ) ;
268+ } ) ;
269+ expect ( mockCallTool ) . toHaveBeenLastCalledWith (
270+ toolWithNullableBoolean . name ,
271+ {
272+ optionalBoolean : null ,
273+ } ,
274+ ) ;
275+ } ) ;
276+
180277 it ( "should disable button and change text while tool is running" , async ( ) => {
181278 // Create a promise that we can resolve later
182279 let resolvePromise : ( ( value : unknown ) => void ) | undefined ;
0 commit comments