@@ -457,4 +457,195 @@ test.describe("SchemaRenderer widgets", () => {
457457 // Clean up
458458 await createdRow . getByRole ( "button" , { name : "Delete" } ) . click ( ) ;
459459 } ) ;
460+
461+ test ( "renders bar-chart and pie-chart from x-ui annotations" , async ( { page } ) => {
462+ const agentName = `e2e-charts-${ Date . now ( ) } ` ;
463+ const mockAgentId = `agent-charts-${ Date . now ( ) } ` ;
464+ const outputSchema = {
465+ type : "object" ,
466+ title : "ChartOutput" ,
467+ properties : {
468+ message : { type : "string" , "x-ui" : { widget : "markdown" } } ,
469+ status_distribution : {
470+ type : "object" ,
471+ description : "Tickets per status" ,
472+ "x-ui" : { widget : "pie-chart" } ,
473+ } ,
474+ tickets_by_city : {
475+ type : "array" ,
476+ items : { type : "object" , properties : { city : { type : "string" } , count : { type : "integer" } } } ,
477+ "x-ui" : { widget : "bar-chart" , indexBy : "city" , keys : [ "count" ] } ,
478+ } ,
479+ total : { type : "integer" , "x-ui" : { widget : "stat-card" , label : "Total Tickets" } } ,
480+ ticket_ids : { type : "array" , items : { type : "string" } , "x-ui" : { widget : "badge-list" } } ,
481+ } ,
482+ } ;
483+
484+ await page . route ( "**/api/workbench/agents/*/runs" , async ( route ) => {
485+ await route . fulfill ( {
486+ status : 200 ,
487+ contentType : "application/json" ,
488+ body : JSON . stringify ( {
489+ id : "run-charts-1" ,
490+ agent_id : mockAgentId ,
491+ input_prompt : "show charts" ,
492+ status : "completed" ,
493+ output : JSON . stringify ( {
494+ message : "## Dashboard\n\nTicket statistics overview." ,
495+ status_distribution : { assigned : 43 , in_progress : 45 , pending : 115 } ,
496+ tickets_by_city : [
497+ { city : "Bern" , count : 103 } ,
498+ { city : "Zollikofen" , count : 26 } ,
499+ { city : "Ittigen" , count : 20 } ,
500+ ] ,
501+ total : 206 ,
502+ ticket_ids : [ "INC-100" , "INC-200" , "INC-300" ] ,
503+ } , null , 2 ) ,
504+ agent_snapshot : { tool_names : [ "csv_ticket_stats" ] } ,
505+ tools_used : [ "csv_ticket_stats" ] ,
506+ error : null ,
507+ created_at : "2026-03-04T10:00:00Z" ,
508+ completed_at : "2026-03-04T10:00:02Z" ,
509+ } ) ,
510+ } ) ;
511+ } ) ;
512+
513+ // Inject mock agent with output_schema into agent list
514+ let realAgents = null ;
515+ await page . route ( "**/api/workbench/agents" , async ( route ) => {
516+ if ( route . request ( ) . method ( ) === "GET" ) {
517+ if ( ! realAgents ) {
518+ const resp = await route . fetch ( ) ;
519+ realAgents = ( await resp . json ( ) ) . agents || [ ] ;
520+ }
521+ const agents = [ ...realAgents ] ;
522+ if ( ! agents . find ( a => a . id === mockAgentId ) ) {
523+ agents . push ( {
524+ id : mockAgentId , name : agentName , description : "" , system_prompt : "charts" ,
525+ tool_names : [ "csv_ticket_stats" ] , output_schema : outputSchema ,
526+ requires_input : false , required_input_description : "" ,
527+ model : "" , temperature : 0 , recursion_limit : 3 , max_tokens : 4096 ,
528+ output_instructions : "" , success_criteria : [ ] ,
529+ created_at : "2026-03-04T10:00:00Z" , updated_at : "2026-03-04T10:00:00Z" ,
530+ } ) ;
531+ }
532+ await route . fulfill ( { status : 200 , contentType : "application/json" , body : JSON . stringify ( { agents } ) } ) ;
533+ } else {
534+ await route . continue ( ) ;
535+ }
536+ } ) ;
537+
538+ await page . goto ( `${ APP_URL } /workbench` , { waitUntil : "load" } ) ;
539+ await expect ( page . getByTestId ( "workbench-page-title" ) ) . toBeVisible ( ) ;
540+
541+ // Select mock agent and run
542+ await page . locator ( '[data-testid="workbench-run-agent-select"]' ) . selectOption ( mockAgentId ) ;
543+ await page . getByTestId ( "workbench-run-agent-button" ) . click ( ) ;
544+
545+ const renderer = page . getByTestId ( "schema-renderer" ) ;
546+ await expect ( renderer ) . toBeVisible ( { timeout : 10000 } ) ;
547+
548+ // Verify markdown widget
549+ await expect ( renderer . getByRole ( "heading" , { name : "Dashboard" } ) ) . toBeVisible ( ) ;
550+
551+ // Verify stat-card with label
552+ const statField = renderer . getByTestId ( "schema-field-total" ) ;
553+ await expect ( statField ) . toBeVisible ( ) ;
554+ await expect ( statField . getByText ( "206" ) ) . toBeVisible ( ) ;
555+ await expect ( statField . getByText ( "Total Tickets" ) . first ( ) ) . toBeVisible ( ) ;
556+
557+ // Verify badge-list
558+ await expect ( renderer . getByText ( "INC-100" ) ) . toBeVisible ( ) ;
559+ await expect ( renderer . getByText ( "INC-300" ) ) . toBeVisible ( ) ;
560+
561+ // Verify pie-chart (Nivo renders SVG)
562+ const pieField = renderer . getByTestId ( "schema-field-status_distribution" ) ;
563+ await expect ( pieField ) . toBeVisible ( ) ;
564+ await expect ( pieField . locator ( "svg" ) ) . toBeVisible ( ) ;
565+
566+ // Verify bar-chart (Nivo renders SVG)
567+ const barField = renderer . getByTestId ( "schema-field-tickets_by_city" ) ;
568+ await expect ( barField ) . toBeVisible ( ) ;
569+ await expect ( barField . locator ( "svg" ) ) . toBeVisible ( ) ;
570+ } ) ;
571+
572+ test ( "renders raw JSON for object data (auto-detected)" , async ( { page } ) => {
573+ const agentName = `e2e-json-${ Date . now ( ) } ` ;
574+
575+ await page . route ( "**/api/workbench/agents/*/runs" , async ( route ) => {
576+ await route . fulfill ( {
577+ status : 200 ,
578+ contentType : "application/json" ,
579+ body : JSON . stringify ( {
580+ id : "run-json-1" , agent_id : "agent-json-1" , input_prompt : "raw" ,
581+ status : "completed" ,
582+ output : JSON . stringify ( {
583+ message : "Here is raw data." ,
584+ metadata : { version : "1.0" , source : "csv" , processed_at : "2026-03-04" } ,
585+ } , null , 2 ) ,
586+ agent_snapshot : { tool_names : [ "csv_ticket_stats" ] } ,
587+ tools_used : [ "csv_ticket_stats" ] , error : null ,
588+ created_at : "2026-03-04T10:00:00Z" , completed_at : "2026-03-04T10:00:01Z" ,
589+ } ) ,
590+ } ) ;
591+ } ) ;
592+
593+ await page . goto ( `${ APP_URL } /workbench` , { waitUntil : "load" } ) ;
594+ await page . getByTestId ( "workbench-agent-name-input" ) . fill ( agentName ) ;
595+ await page . getByTestId ( "workbench-agent-system-prompt-input" ) . fill ( "Raw data" ) ;
596+ await page . getByTestId ( "workbench-create-agent-button" ) . click ( ) ;
597+
598+ const createdRow = page . locator ( '[data-testid="workbench-agents-table"] tbody tr' , { hasText : agentName } ) ;
599+ await expect ( createdRow ) . toBeVisible ( { timeout : 10000 } ) ;
600+ await page . getByTestId ( "workbench-run-agent-button" ) . click ( ) ;
601+
602+ const renderer = page . getByTestId ( "schema-renderer" ) ;
603+ await expect ( renderer ) . toBeVisible ( { timeout : 10000 } ) ;
604+ await expect ( renderer . getByText ( "Here is raw data." ) ) . toBeVisible ( ) ;
605+
606+ // metadata auto-detected as json (object → pre block)
607+ const metaField = renderer . getByTestId ( "schema-field-metadata" ) ;
608+ await expect ( metaField ) . toBeVisible ( ) ;
609+ await expect ( metaField . locator ( "pre" ) ) . toBeVisible ( ) ;
610+ await expect ( metaField . getByText ( "csv" ) ) . toBeVisible ( ) ;
611+
612+ await createdRow . getByRole ( "button" , { name : "Delete" } ) . click ( ) ;
613+ } ) ;
614+
615+ test ( "falls back gracefully for non-JSON output" , async ( { page } ) => {
616+ const agentName = `e2e-fallback-${ Date . now ( ) } ` ;
617+
618+ await page . route ( "**/api/workbench/agents/*/runs" , async ( route ) => {
619+ await route . fulfill ( {
620+ status : 200 ,
621+ contentType : "application/json" ,
622+ body : JSON . stringify ( {
623+ id : "run-fb-1" , agent_id : "agent-fb-1" , input_prompt : "test" ,
624+ status : "completed" ,
625+ output : "# Plain Markdown\n\nThis is **not JSON** — just regular markdown." ,
626+ agent_snapshot : { tool_names : [ "csv_ticket_stats" ] } ,
627+ tools_used : [ "csv_ticket_stats" ] , error : null ,
628+ created_at : "2026-03-04T10:00:00Z" , completed_at : "2026-03-04T10:00:01Z" ,
629+ } ) ,
630+ } ) ;
631+ } ) ;
632+
633+ await page . goto ( `${ APP_URL } /workbench` , { waitUntil : "load" } ) ;
634+ await page . getByTestId ( "workbench-agent-name-input" ) . fill ( agentName ) ;
635+ await page . getByTestId ( "workbench-agent-system-prompt-input" ) . fill ( "Fallback" ) ;
636+ await page . getByTestId ( "workbench-create-agent-button" ) . click ( ) ;
637+
638+ const createdRow = page . locator ( '[data-testid="workbench-agents-table"] tbody tr' , { hasText : agentName } ) ;
639+ await expect ( createdRow ) . toBeVisible ( { timeout : 10000 } ) ;
640+ await page . getByTestId ( "workbench-run-agent-button" ) . click ( ) ;
641+
642+ const renderer = page . getByTestId ( "schema-renderer" ) ;
643+ await expect ( renderer ) . toBeVisible ( { timeout : 10000 } ) ;
644+
645+ // Non-JSON falls back: wrapped as {message: raw_text} → markdown
646+ await expect ( renderer . getByRole ( "heading" , { name : "Plain Markdown" } ) ) . toBeVisible ( ) ;
647+ await expect ( renderer . getByText ( "not JSON" ) ) . toBeVisible ( ) ;
648+
649+ await createdRow . getByRole ( "button" , { name : "Delete" } ) . click ( ) ;
650+ } ) ;
460651} ) ;
0 commit comments