@@ -238,4 +238,167 @@ describe("NativeToolCallParser", () => {
238238 } )
239239 } )
240240 } )
241+
242+ describe ( "write_to_file tool - content type coercion" , ( ) => {
243+ describe ( "parseToolCall" , ( ) => {
244+ it ( "should handle content as a string (normal case)" , ( ) => {
245+ const toolCall = {
246+ id : "toolu_123" ,
247+ name : "write_to_file" as const ,
248+ arguments : JSON . stringify ( {
249+ path : "package.json" ,
250+ content : '{\n "name": "test"\n}' ,
251+ } ) ,
252+ }
253+
254+ const result = NativeToolCallParser . parseToolCall ( toolCall )
255+
256+ expect ( result ) . not . toBeNull ( )
257+ expect ( result ?. type ) . toBe ( "tool_use" )
258+ if ( result ?. type === "tool_use" ) {
259+ const nativeArgs = result . nativeArgs as { path : string ; content : string }
260+ expect ( nativeArgs . path ) . toBe ( "package.json" )
261+ expect ( nativeArgs . content ) . toBe ( '{\n "name": "test"\n}' )
262+ }
263+ } )
264+
265+ it ( "should coerce content from object to JSON string" , ( ) => {
266+ // This simulates the bug where models like GLM 4.7 pass content as an object
267+ const toolCall = {
268+ id : "toolu_456" ,
269+ name : "write_to_file" as const ,
270+ arguments : JSON . stringify ( {
271+ path : "package.json" ,
272+ content : { name : "sample-project" , version : "1.0.0" } ,
273+ } ) ,
274+ }
275+
276+ const consoleSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
277+
278+ const result = NativeToolCallParser . parseToolCall ( toolCall )
279+
280+ expect ( result ) . not . toBeNull ( )
281+ expect ( result ?. type ) . toBe ( "tool_use" )
282+ if ( result ?. type === "tool_use" ) {
283+ const nativeArgs = result . nativeArgs as { path : string ; content : string }
284+ expect ( nativeArgs . path ) . toBe ( "package.json" )
285+ // Content should be converted to a formatted JSON string
286+ expect ( nativeArgs . content ) . toBe (
287+ JSON . stringify ( { name : "sample-project" , version : "1.0.0" } , null , 2 ) ,
288+ )
289+ }
290+
291+ // Should log a warning about the type coercion
292+ expect ( consoleSpy ) . toHaveBeenCalledWith (
293+ expect . stringContaining ( "Model sent non-string content for 'write_to_file' tool" ) ,
294+ )
295+
296+ consoleSpy . mockRestore ( )
297+ } )
298+
299+ it ( "should coerce content from array to JSON string" , ( ) => {
300+ const toolCall = {
301+ id : "toolu_789" ,
302+ name : "write_to_file" as const ,
303+ arguments : JSON . stringify ( {
304+ path : "data.json" ,
305+ content : [ 1 , 2 , 3 , { key : "value" } ] ,
306+ } ) ,
307+ }
308+
309+ const consoleSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
310+
311+ const result = NativeToolCallParser . parseToolCall ( toolCall )
312+
313+ expect ( result ) . not . toBeNull ( )
314+ if ( result ?. type === "tool_use" ) {
315+ const nativeArgs = result . nativeArgs as { path : string ; content : string }
316+ expect ( nativeArgs . content ) . toBe ( JSON . stringify ( [ 1 , 2 , 3 , { key : "value" } ] , null , 2 ) )
317+ }
318+
319+ expect ( consoleSpy ) . toHaveBeenCalled ( )
320+ consoleSpy . mockRestore ( )
321+ } )
322+
323+ it ( "should coerce content from number to string" , ( ) => {
324+ const toolCall = {
325+ id : "toolu_num" ,
326+ name : "write_to_file" as const ,
327+ arguments : JSON . stringify ( {
328+ path : "number.txt" ,
329+ content : 42 ,
330+ } ) ,
331+ }
332+
333+ const consoleSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
334+
335+ const result = NativeToolCallParser . parseToolCall ( toolCall )
336+
337+ expect ( result ) . not . toBeNull ( )
338+ if ( result ?. type === "tool_use" ) {
339+ const nativeArgs = result . nativeArgs as { path : string ; content : string }
340+ expect ( nativeArgs . content ) . toBe ( "42" )
341+ }
342+
343+ expect ( consoleSpy ) . toHaveBeenCalled ( )
344+ consoleSpy . mockRestore ( )
345+ } )
346+ } )
347+
348+ describe ( "processStreamingChunk" , ( ) => {
349+ it ( "should coerce content from object to JSON string during streaming" , ( ) => {
350+ const id = "toolu_streaming_write"
351+ NativeToolCallParser . startStreamingToolCall ( id , "write_to_file" )
352+
353+ const consoleSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
354+
355+ const fullArgs = JSON . stringify ( {
356+ path : "config.json" ,
357+ content : { setting : true , value : 123 } ,
358+ } )
359+
360+ const result = NativeToolCallParser . processStreamingChunk ( id , fullArgs )
361+
362+ expect ( result ) . not . toBeNull ( )
363+ if ( result ?. nativeArgs ) {
364+ const nativeArgs = result . nativeArgs as { path : string ; content : string }
365+ expect ( nativeArgs . path ) . toBe ( "config.json" )
366+ expect ( nativeArgs . content ) . toBe ( JSON . stringify ( { setting : true , value : 123 } , null , 2 ) )
367+ }
368+
369+ expect ( consoleSpy ) . toHaveBeenCalled ( )
370+ consoleSpy . mockRestore ( )
371+ } )
372+ } )
373+
374+ describe ( "finalizeStreamingToolCall" , ( ) => {
375+ it ( "should coerce content from object to JSON string on finalize" , ( ) => {
376+ const id = "toolu_finalize_write"
377+ NativeToolCallParser . startStreamingToolCall ( id , "write_to_file" )
378+
379+ const consoleSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
380+
381+ NativeToolCallParser . processStreamingChunk (
382+ id ,
383+ JSON . stringify ( {
384+ path : "tsconfig.json" ,
385+ content : { compilerOptions : { strict : true } } ,
386+ } ) ,
387+ )
388+
389+ const result = NativeToolCallParser . finalizeStreamingToolCall ( id )
390+
391+ expect ( result ) . not . toBeNull ( )
392+ expect ( result ?. type ) . toBe ( "tool_use" )
393+ if ( result ?. type === "tool_use" ) {
394+ const nativeArgs = result . nativeArgs as { path : string ; content : string }
395+ expect ( nativeArgs . path ) . toBe ( "tsconfig.json" )
396+ expect ( nativeArgs . content ) . toBe ( JSON . stringify ( { compilerOptions : { strict : true } } , null , 2 ) )
397+ }
398+
399+ expect ( consoleSpy ) . toHaveBeenCalled ( )
400+ consoleSpy . mockRestore ( )
401+ } )
402+ } )
403+ } )
241404} )
0 commit comments