1+ import { describe , expect , it , vi } from 'vitest'
2+ import {
3+ formatOutputForDisplay ,
4+ formatOutputForChat ,
5+ formatOutputForWorkflow ,
6+ formatOutputRaw ,
7+ formatOutputSafe ,
8+ isOutputSafe
9+ } from './format-output'
10+
11+ describe ( 'format-output utilities' , ( ) => {
12+ describe ( 'formatOutputForDisplay' , ( ) => {
13+ // Basic types
14+ it ( 'handles null and undefined' , ( ) => {
15+ expect ( formatOutputForDisplay ( null ) ) . toBe ( '' )
16+ expect ( formatOutputForDisplay ( undefined ) ) . toBe ( '' )
17+ } )
18+
19+ it ( 'handles primitive types' , ( ) => {
20+ expect ( formatOutputForDisplay ( 'hello' ) ) . toBe ( 'hello' )
21+ expect ( formatOutputForDisplay ( 123 ) ) . toBe ( '123' )
22+ expect ( formatOutputForDisplay ( true ) ) . toBe ( 'true' )
23+ expect ( formatOutputForDisplay ( false ) ) . toBe ( 'false' )
24+ expect ( formatOutputForDisplay ( 0 ) ) . toBe ( '0' )
25+ expect ( formatOutputForDisplay ( BigInt ( 999 ) ) ) . toBe ( '999' )
26+ } )
27+
28+ // Object with text property
29+ it ( 'extracts text from objects with text property' , ( ) => {
30+ expect ( formatOutputForDisplay ( { text : 'Hello World' , type : 'response' } ) ) . toBe ( 'Hello World' )
31+ expect ( formatOutputForDisplay ( { text : ' spaced ' , other : 'data' } ) ) . toBe ( 'spaced' )
32+ } )
33+
34+ // Nested objects
35+ it ( 'handles deeply nested text properties' , ( ) => {
36+ const nested = {
37+ data : {
38+ response : {
39+ message : {
40+ content : 'Deep text'
41+ }
42+ }
43+ }
44+ }
45+ expect ( formatOutputForDisplay ( nested ) ) . toBe ( 'Deep text' )
46+ } )
47+
48+ // Arrays
49+ it ( 'handles arrays of text objects' , ( ) => {
50+ const arr = [
51+ { text : 'Line 1' } ,
52+ { text : 'Line 2' } ,
53+ { content : 'Line 3' }
54+ ]
55+ expect ( formatOutputForDisplay ( arr ) ) . toBe ( 'Line 1 Line 2 Line 3' )
56+ } )
57+
58+ it ( 'handles mixed arrays' , ( ) => {
59+ const mixed = [
60+ 'String' ,
61+ { text : 'Object text' } ,
62+ 123 ,
63+ null ,
64+ { message : 'Message text' }
65+ ]
66+ expect ( formatOutputForDisplay ( mixed ) ) . toBe ( 'String Object text 123 Message text' )
67+ } )
68+
69+ // Special objects
70+ it ( 'handles Date objects' , ( ) => {
71+ const date = new Date ( '2024-01-01T00:00:00Z' )
72+ expect ( formatOutputForDisplay ( date ) ) . toBe ( '2024-01-01T00:00:00.000Z' )
73+ } )
74+
75+ it ( 'handles Error objects' , ( ) => {
76+ const error = new Error ( 'Test error' )
77+ expect ( formatOutputForDisplay ( error ) ) . toBe ( 'Test error' )
78+ } )
79+
80+ it ( 'handles RegExp objects' , ( ) => {
81+ const regex = / t e s t .* p a t t e r n / gi
82+ expect ( formatOutputForDisplay ( regex ) ) . toBe ( '/test.*pattern/gi' )
83+ } )
84+
85+ // Circular references
86+ it ( 'handles circular references' , ( ) => {
87+ const obj : any = { a : 1 }
88+ obj . self = obj
89+ const result = formatOutputForDisplay ( obj , { mode : 'raw' } )
90+ expect ( result ) . toContain ( '[Circular]' )
91+ expect ( result ) . not . toThrow ( )
92+ } )
93+
94+ // Large arrays
95+ it ( 'handles large arrays gracefully' , ( ) => {
96+ const bigArray = new Array ( 2000 ) . fill ( 'item' )
97+ const result = formatOutputForDisplay ( bigArray )
98+ expect ( result ) . toContain ( '[Large Array: 2000 items]' )
99+ } )
100+
101+ // Binary data
102+ it ( 'handles Buffer data' , ( ) => {
103+ const buffer = Buffer . from ( 'Hello Buffer' )
104+ expect ( formatOutputForDisplay ( buffer ) ) . toBe ( 'Hello Buffer' )
105+
106+ const binaryBuffer = Buffer . from ( [ 0xFF , 0xFE , 0x00 , 0x01 ] )
107+ expect ( formatOutputForDisplay ( binaryBuffer ) ) . toBe ( '[Binary Data]' )
108+ } )
109+
110+ // Truncation
111+ it ( 'truncates long strings when specified' , ( ) => {
112+ const longText = 'x' . repeat ( 10000 )
113+ const result = formatOutputForDisplay ( longText , { maxLength : 100 , truncate : true } )
114+ expect ( result . length ) . toBeLessThan ( 150 )
115+ expect ( result ) . toContain ( '... [truncated]' )
116+ } )
117+
118+ // Whitespace handling
119+ it ( 'preserves whitespace when requested' , ( ) => {
120+ const spaced = 'Line 1\n\nLine 2\t\tTabbed'
121+ expect ( formatOutputForDisplay ( spaced , { preserveWhitespace : true } ) )
122+ . toBe ( 'Line 1\n\nLine 2\t\tTabbed' )
123+ expect ( formatOutputForDisplay ( spaced , { preserveWhitespace : false } ) )
124+ . toBe ( 'Line 1 Line 2 Tabbed' )
125+ } )
126+
127+ // Mode-specific formatting
128+ it ( 'formats correctly for different modes' , ( ) => {
129+ const obj = { data : 'test' }
130+
131+ const chatFormat = formatOutputForDisplay ( obj , { mode : 'chat' } )
132+ expect ( chatFormat ) . toContain ( 'test' )
133+
134+ const workflowFormat = formatOutputForDisplay ( obj , { mode : 'workflow' } )
135+ expect ( workflowFormat ) . toMatch ( / ` ` ` j s o n / )
136+
137+ const rawFormat = formatOutputForDisplay ( obj , { mode : 'raw' } )
138+ expect ( rawFormat ) . toBe ( '{"data":"test"}' )
139+ } )
140+
141+ // Edge cases
142+ it ( 'handles objects with toString method' , ( ) => {
143+ const customObj = {
144+ toString ( ) {
145+ return 'Custom String'
146+ }
147+ }
148+ expect ( formatOutputForDisplay ( customObj ) ) . toBe ( 'Custom String' )
149+ } )
150+
151+ it ( 'handles undefined and function properties' , ( ) => {
152+ const obj = {
153+ func : ( ) => console . log ( 'test' ) ,
154+ undef : undefined ,
155+ sym : Symbol ( 'test' )
156+ }
157+ const result = formatOutputForDisplay ( obj , { mode : 'raw' } )
158+ expect ( result ) . toContain ( '[Function]' )
159+ expect ( result ) . toContain ( '[undefined]' )
160+ expect ( result ) . toContain ( '[Symbol]' )
161+ } )
162+ } )
163+
164+ describe ( 'specialized formatters' , ( ) => {
165+ it ( 'formatOutputForChat limits length' , ( ) => {
166+ const longText = 'x' . repeat ( 10000 )
167+ const result = formatOutputForChat ( longText )
168+ expect ( result . length ) . toBeLessThanOrEqual ( 5100 ) // 5000 + truncation message
169+ } )
170+
171+ it ( 'formatOutputForWorkflow wraps in code block' , ( ) => {
172+ const obj = { test : 'data' }
173+ const result = formatOutputForWorkflow ( obj )
174+ expect ( result ) . toMatch ( / ^ ` ` ` j s o n / )
175+ expect ( result ) . toMatch ( / ` ` ` $ / )
176+ } )
177+
178+ it ( 'formatOutputRaw preserves everything' , ( ) => {
179+ const text = ' \n\t spaced \n\t '
180+ const result = formatOutputRaw ( text )
181+ expect ( result ) . toBe ( text )
182+ } )
183+ } )
184+
185+ describe ( 'security features' , ( ) => {
186+ it ( 'detects unsafe content' , ( ) => {
187+ expect ( isOutputSafe ( '<script>alert("xss")</script>' ) ) . toBe ( false )
188+ expect ( isOutputSafe ( 'javascript:void(0)' ) ) . toBe ( false )
189+ expect ( isOutputSafe ( '<div onclick="alert(1)">' ) ) . toBe ( false )
190+ expect ( isOutputSafe ( '<iframe src="evil">' ) ) . toBe ( false )
191+ expect ( isOutputSafe ( 'Normal text' ) ) . toBe ( true )
192+ } )
193+
194+ it ( 'escapes HTML in unsafe content' , ( ) => {
195+ const unsafe = '<script>alert("xss")</script>'
196+ const result = formatOutputSafe ( unsafe )
197+ expect ( result ) . not . toContain ( '<script>' )
198+ expect ( result ) . toContain ( '<script' )
199+ expect ( result ) . toContain ( '>' )
200+ } )
201+
202+ it ( 'leaves safe content unescaped' , ( ) => {
203+ const safe = 'Normal text with no HTML'
204+ const result = formatOutputSafe ( safe )
205+ expect ( result ) . toBe ( safe )
206+ } )
207+ } )
208+
209+ describe ( 'error handling' , ( ) => {
210+ it ( 'handles errors gracefully' , ( ) => {
211+ // Create object that throws on property access
212+ const evil = new Proxy ( { } , {
213+ get ( ) {
214+ throw new Error ( 'Evil object!' )
215+ }
216+ } )
217+
218+ const result = formatOutputForDisplay ( evil )
219+ expect ( result ) . toContain ( '[' )
220+ expect ( ( ) => formatOutputForDisplay ( evil ) ) . not . toThrow ( )
221+ } )
222+
223+ it ( 'handles very deep recursion' , ( ) => {
224+ let deep : any = { text : 'Found it!' }
225+ for ( let i = 0 ; i < 20 ; i ++ ) {
226+ deep = { nested : deep }
227+ }
228+
229+ const result = formatOutputForDisplay ( deep )
230+ // Should stop at MAX_DEPTH but not crash
231+ expect ( result ) . toBeTruthy ( )
232+ expect ( ( ) => formatOutputForDisplay ( deep ) ) . not . toThrow ( )
233+ } )
234+ } )
235+
236+ describe ( 'real-world LLM outputs' , ( ) => {
237+ it ( 'handles OpenAI format' , ( ) => {
238+ const openAIResponse = {
239+ choices : [ {
240+ message : {
241+ content : 'AI response here'
242+ }
243+ } ]
244+ }
245+ expect ( formatOutputForDisplay ( openAIResponse ) ) . toBe ( 'AI response here' )
246+ } )
247+
248+ it ( 'handles Anthropic format' , ( ) => {
249+ const anthropicResponse = {
250+ content : [ {
251+ text : 'Claude response'
252+ } ]
253+ }
254+ expect ( formatOutputForDisplay ( anthropicResponse ) ) . toBe ( 'Claude response' )
255+ } )
256+
257+ it ( 'handles streaming chunks' , ( ) => {
258+ const chunk = {
259+ delta : {
260+ content : 'Streaming text'
261+ }
262+ }
263+ expect ( formatOutputForDisplay ( chunk ) ) . toBe ( 'Streaming text' )
264+ } )
265+
266+ it ( 'handles tool outputs' , ( ) => {
267+ const toolOutput = {
268+ result : {
269+ data : {
270+ output : 'Tool execution result'
271+ }
272+ }
273+ }
274+ expect ( formatOutputForDisplay ( toolOutput ) ) . toBe ( 'Tool execution result' )
275+ } )
276+ } )
277+ } )
0 commit comments