@@ -375,6 +375,56 @@ describe('AiRepository', () => {
375375 expect ( result ) . toBeUndefined ( ) ;
376376 expect ( mockFetch ) . toHaveBeenCalledTimes ( OPENCODE_MAX_RETRIES * 2 ) ;
377377 } ) ;
378+
379+ it ( 'returns undefined when expectJson is true but response has no JSON object (no curly brace)' , async ( ) => {
380+ const ai = createAi ( ) ;
381+ const sessionOk = { ok : true , text : async ( ) => JSON . stringify ( { id : 's1' } ) } ;
382+ const messageNoJson = {
383+ ok : true ,
384+ status : 200 ,
385+ text : async ( ) =>
386+ JSON . stringify ( {
387+ parts : [ { type : 'text' , text : 'No JSON here, just plain text.' } ] ,
388+ } ) ,
389+ } ;
390+ for ( let i = 0 ; i < OPENCODE_MAX_RETRIES ; i ++ ) {
391+ mockFetch . mockResolvedValueOnce ( sessionOk ) . mockResolvedValueOnce ( messageNoJson ) ;
392+ }
393+ const promise = repo . askAgent ( ai , 'plan' , 'P' , { expectJson : true , schema : { } } ) ;
394+ await jest . advanceTimersByTimeAsync ( ( OPENCODE_MAX_RETRIES - 1 ) * OPENCODE_RETRY_DELAY_MS ) ;
395+ const result = await promise ;
396+ expect ( result ) . toBeUndefined ( ) ;
397+ } ) ;
398+
399+ it ( 'returns undefined when session create returns invalid JSON (error with cause)' , async ( ) => {
400+ const ai = createAi ( ) ;
401+ mockFetch . mockResolvedValue ( { ok : true , text : async ( ) => 'not valid json' } ) ;
402+ const promise = repo . askAgent ( ai , 'plan' , 'P' , { } ) ;
403+ await jest . advanceTimersByTimeAsync ( ( OPENCODE_MAX_RETRIES - 1 ) * OPENCODE_RETRY_DELAY_MS ) ;
404+ const result = await promise ;
405+ expect ( result ) . toBeUndefined ( ) ;
406+ expect ( mockFetch ) . toHaveBeenCalledTimes ( OPENCODE_MAX_RETRIES ) ;
407+ } ) ;
408+
409+ it ( 'hits single-quote path in extractor when response has single-quoted object (invalid JSON)' , async ( ) => {
410+ const ai = createAi ( ) ;
411+ const sessionOk = { ok : true , text : async ( ) => JSON . stringify ( { id : 's1' } ) } ;
412+ const messageSingleQuote = {
413+ ok : true ,
414+ status : 200 ,
415+ text : async ( ) =>
416+ JSON . stringify ( {
417+ parts : [ { type : 'text' , text : "Note { 'a': 1 }" } ] ,
418+ } ) ,
419+ } ;
420+ for ( let i = 0 ; i < OPENCODE_MAX_RETRIES ; i ++ ) {
421+ mockFetch . mockResolvedValueOnce ( sessionOk ) . mockResolvedValueOnce ( messageSingleQuote ) ;
422+ }
423+ const promise = repo . askAgent ( ai , 'plan' , 'P' , { expectJson : true , schema : { } } ) ;
424+ await jest . advanceTimersByTimeAsync ( ( OPENCODE_MAX_RETRIES - 1 ) * OPENCODE_RETRY_DELAY_MS ) ;
425+ const result = await promise ;
426+ expect ( result ) . toBeUndefined ( ) ;
427+ } ) ;
378428 } ) ;
379429
380430 describe ( 'copilotMessage' , ( ) => {
0 commit comments