@@ -4,8 +4,35 @@ import "@testing-library/jest-dom";
44import ToolsTab from "../ToolsTab" ;
55import { Tool } from "@modelcontextprotocol/sdk/types.js" ;
66import { 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+ } ) ) ;
719
820describe ( "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+
936 const mockTools : Tool [ ] = [
1037 {
1138 name : "tool1" ,
@@ -141,4 +168,226 @@ describe("ToolsTab", () => {
141168
142169 expect ( submitButton . getAttribute ( "disabled" ) ) . toBeNull ( ) ;
143170 } ) ;
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+ } ) ;
144393} ) ;
0 commit comments