@@ -4,8 +4,35 @@ import "@testing-library/jest-dom";
4
4
import ToolsTab from "../ToolsTab" ;
5
5
import { Tool } from "@modelcontextprotocol/sdk/types.js" ;
6
6
import { Tabs } from "@/components/ui/tabs" ;
7
+ import * as schemaUtils from "@/utils/schemaUtils" ;
8
+
9
+ // Mock the schemaUtils module
10
+ // Note: hasOutputSchema checks if a tool's output schema validator has been compiled and cached
11
+ // by cacheToolOutputSchemas. In these tests, we mock it to avoid needing to call
12
+ // cacheToolOutputSchemas for every test that uses tools with output schemas.
13
+ // This keeps the tests focused on the component's behavior rather than schema compilation.
14
+ jest . mock ( "@/utils/schemaUtils" , ( ) => ( {
15
+ ...jest . requireActual ( "@/utils/schemaUtils" ) ,
16
+ hasOutputSchema : jest . fn ( ) ,
17
+ validateToolOutput : jest . fn ( ) ,
18
+ } ) ) ;
7
19
8
20
describe ( "ToolsTab" , ( ) => {
21
+ beforeEach ( ( ) => {
22
+ jest . clearAllMocks ( ) ;
23
+ // Reset to default behavior
24
+ ( schemaUtils . hasOutputSchema as jest . Mock ) . mockImplementation (
25
+ ( toolName ) => {
26
+ // Only tools with outputSchema property should return true
27
+ return false ;
28
+ } ,
29
+ ) ;
30
+ ( schemaUtils . validateToolOutput as jest . Mock ) . mockReturnValue ( {
31
+ isValid : true ,
32
+ error : null ,
33
+ } ) ;
34
+ } ) ;
35
+
9
36
const mockTools : Tool [ ] = [
10
37
{
11
38
name : "tool1" ,
@@ -141,4 +168,226 @@ describe("ToolsTab", () => {
141
168
142
169
expect ( submitButton . getAttribute ( "disabled" ) ) . toBeNull ( ) ;
143
170
} ) ;
171
+
172
+ describe ( "Output Schema Display" , ( ) => {
173
+ const toolWithOutputSchema : Tool = {
174
+ name : "weatherTool" ,
175
+ description : "Get weather" ,
176
+ inputSchema : {
177
+ type : "object" as const ,
178
+ properties : {
179
+ city : { type : "string" as const } ,
180
+ } ,
181
+ } ,
182
+ outputSchema : {
183
+ type : "object" as const ,
184
+ properties : {
185
+ temperature : { type : "number" as const } ,
186
+ humidity : { type : "number" as const } ,
187
+ } ,
188
+ required : [ "temperature" , "humidity" ] ,
189
+ } ,
190
+ } ;
191
+
192
+ it ( "should display output schema when tool has one" , ( ) => {
193
+ renderToolsTab ( {
194
+ tools : [ toolWithOutputSchema ] ,
195
+ selectedTool : toolWithOutputSchema ,
196
+ } ) ;
197
+
198
+ expect ( screen . getByText ( "Output Schema:" ) ) . toBeInTheDocument ( ) ;
199
+ // Check for expand/collapse button
200
+ expect (
201
+ screen . getByRole ( "button" , { name : / e x p a n d / i } ) ,
202
+ ) . toBeInTheDocument ( ) ;
203
+ } ) ;
204
+
205
+ it ( "should not display output schema section when tool doesn't have one" , ( ) => {
206
+ renderToolsTab ( {
207
+ selectedTool : mockTools [ 0 ] , // Tool without outputSchema
208
+ } ) ;
209
+
210
+ expect ( screen . queryByText ( "Output Schema:" ) ) . not . toBeInTheDocument ( ) ;
211
+ } ) ;
212
+
213
+ it ( "should toggle output schema expansion" , ( ) => {
214
+ renderToolsTab ( {
215
+ tools : [ toolWithOutputSchema ] ,
216
+ selectedTool : toolWithOutputSchema ,
217
+ } ) ;
218
+
219
+ const toggleButton = screen . getByRole ( "button" , { name : / e x p a n d / i } ) ;
220
+
221
+ // Click to expand
222
+ fireEvent . click ( toggleButton ) ;
223
+ expect (
224
+ screen . getByRole ( "button" , { name : / c o l l a p s e / i } ) ,
225
+ ) . toBeInTheDocument ( ) ;
226
+
227
+ // Click to collapse
228
+ fireEvent . click ( toggleButton ) ;
229
+ expect (
230
+ screen . getByRole ( "button" , { name : / e x p a n d / i } ) ,
231
+ ) . toBeInTheDocument ( ) ;
232
+ } ) ;
233
+ } ) ;
234
+
235
+ describe ( "Structured Output Results" , ( ) => {
236
+ const toolWithOutputSchema : Tool = {
237
+ name : "weatherTool" ,
238
+ description : "Get weather" ,
239
+ inputSchema : {
240
+ type : "object" as const ,
241
+ properties : { } ,
242
+ } ,
243
+ outputSchema : {
244
+ type : "object" as const ,
245
+ properties : {
246
+ temperature : { type : "number" as const } ,
247
+ } ,
248
+ required : [ "temperature" ] ,
249
+ } ,
250
+ } ;
251
+
252
+ it ( "should display structured content when present" , ( ) => {
253
+ // Mock hasOutputSchema to return true for this tool
254
+ ( schemaUtils . hasOutputSchema as jest . Mock ) . mockReturnValue ( true ) ;
255
+
256
+ const structuredResult = {
257
+ content : [ ] ,
258
+ structuredContent : {
259
+ temperature : 25 ,
260
+ } ,
261
+ } ;
262
+
263
+ renderToolsTab ( {
264
+ selectedTool : toolWithOutputSchema ,
265
+ toolResult : structuredResult ,
266
+ } ) ;
267
+
268
+ expect ( screen . getByText ( "Structured Content:" ) ) . toBeInTheDocument ( ) ;
269
+ expect (
270
+ screen . getByText ( / V a l i d a c c o r d i n g t o o u t p u t s c h e m a / ) ,
271
+ ) . toBeInTheDocument ( ) ;
272
+ } ) ;
273
+
274
+ it ( "should show validation error for invalid structured content" , ( ) => {
275
+ // Mock hasOutputSchema to return true for this tool
276
+ ( schemaUtils . hasOutputSchema as jest . Mock ) . mockReturnValue ( true ) ;
277
+ // Mock the validation to fail
278
+ ( schemaUtils . validateToolOutput as jest . Mock ) . mockReturnValue ( {
279
+ isValid : false ,
280
+ error : "temperature must be number" ,
281
+ } ) ;
282
+
283
+ const invalidResult = {
284
+ content : [ ] ,
285
+ structuredContent : {
286
+ temperature : "25" , // String instead of number
287
+ } ,
288
+ } ;
289
+
290
+ renderToolsTab ( {
291
+ selectedTool : toolWithOutputSchema ,
292
+ toolResult : invalidResult ,
293
+ } ) ;
294
+
295
+ expect ( screen . getByText ( / V a l i d a t i o n E r r o r : / ) ) . toBeInTheDocument ( ) ;
296
+ } ) ;
297
+
298
+ it ( "should show error when tool with output schema doesn't return structured content" , ( ) => {
299
+ // Mock hasOutputSchema to return true for this tool
300
+ ( schemaUtils . hasOutputSchema as jest . Mock ) . mockReturnValue ( true ) ;
301
+
302
+ const resultWithoutStructured = {
303
+ content : [ { type : "text" , text : "some result" } ] ,
304
+ // No structuredContent
305
+ } ;
306
+
307
+ renderToolsTab ( {
308
+ selectedTool : toolWithOutputSchema ,
309
+ toolResult : resultWithoutStructured ,
310
+ } ) ;
311
+
312
+ expect (
313
+ screen . getByText (
314
+ / T o o l h a s a n o u t p u t s c h e m a b u t d i d n o t r e t u r n s t r u c t u r e d c o n t e n t / ,
315
+ ) ,
316
+ ) . toBeInTheDocument ( ) ;
317
+ } ) ;
318
+
319
+ it ( "should show unstructured content title when both structured and unstructured exist" , ( ) => {
320
+ // Mock hasOutputSchema to return true for this tool
321
+ ( schemaUtils . hasOutputSchema as jest . Mock ) . mockReturnValue ( true ) ;
322
+
323
+ const resultWithBoth = {
324
+ content : [ { type : "text" , text : '{"temperature": 25}' } ] ,
325
+ structuredContent : { temperature : 25 } ,
326
+ } ;
327
+
328
+ renderToolsTab ( {
329
+ selectedTool : toolWithOutputSchema ,
330
+ toolResult : resultWithBoth ,
331
+ } ) ;
332
+
333
+ expect ( screen . getByText ( "Structured Content:" ) ) . toBeInTheDocument ( ) ;
334
+ expect ( screen . getByText ( "Unstructured Content:" ) ) . toBeInTheDocument ( ) ;
335
+ } ) ;
336
+
337
+ it ( "should not show unstructured content title when only unstructured exists" , ( ) => {
338
+ const resultWithUnstructuredOnly = {
339
+ content : [ { type : "text" , text : "some result" } ] ,
340
+ } ;
341
+
342
+ renderToolsTab ( {
343
+ selectedTool : mockTools [ 0 ] , // Tool without output schema
344
+ toolResult : resultWithUnstructuredOnly ,
345
+ } ) ;
346
+
347
+ expect (
348
+ screen . queryByText ( "Unstructured Content:" ) ,
349
+ ) . not . toBeInTheDocument ( ) ;
350
+ } ) ;
351
+
352
+ it ( "should show compatibility check when tool has output schema" , ( ) => {
353
+ // Mock hasOutputSchema to return true for this tool
354
+ ( schemaUtils . hasOutputSchema as jest . Mock ) . mockReturnValue ( true ) ;
355
+
356
+ const compatibleResult = {
357
+ content : [ { type : "text" , text : '{"temperature": 25}' } ] ,
358
+ structuredContent : { temperature : 25 } ,
359
+ } ;
360
+
361
+ renderToolsTab ( {
362
+ selectedTool : toolWithOutputSchema ,
363
+ toolResult : compatibleResult ,
364
+ } ) ;
365
+
366
+ // Should show compatibility result
367
+ expect (
368
+ screen . getByText (
369
+ / m a t c h e s s t r u c t u r e d c o n t e n t | n o t a s i n g l e t e x t b l o c k | n o t v a l i d J S O N | d o e s n o t m a t c h / ,
370
+ ) ,
371
+ ) . toBeInTheDocument ( ) ;
372
+ } ) ;
373
+
374
+ it ( "should not show compatibility check when tool has no output schema" , ( ) => {
375
+ const resultWithBoth = {
376
+ content : [ { type : "text" , text : '{"data": "value"}' } ] ,
377
+ structuredContent : { different : "data" } ,
378
+ } ;
379
+
380
+ renderToolsTab ( {
381
+ selectedTool : mockTools [ 0 ] , // Tool without output schema
382
+ toolResult : resultWithBoth ,
383
+ } ) ;
384
+
385
+ // Should not show any compatibility messages
386
+ expect (
387
+ screen . queryByText (
388
+ / m a t c h e s s t r u c t u r e d c o n t e n t | n o t a s i n g l e t e x t b l o c k | n o t v a l i d J S O N | d o e s n o t m a t c h / ,
389
+ ) ,
390
+ ) . not . toBeInTheDocument ( ) ;
391
+ } ) ;
392
+ } ) ;
144
393
} ) ;
0 commit comments