@@ -20,6 +20,7 @@ import {
2020 mock ,
2121 spyOn ,
2222} from 'bun:test'
23+ import { APICallError } from 'ai'
2324import { z } from 'zod/v4'
2425
2526import { loopAgentSteps } from '../run-agent-step'
@@ -931,4 +932,89 @@ describe('loopAgentSteps - runAgentStep vs runProgrammaticStep behavior', () =>
931932 expect ( llmCallCount ) . toBe ( 0 )
932933 } )
933934 } )
935+
936+ describe ( 'API error handling' , ( ) => {
937+ it ( 'should propagate error code and server message from 403 APICallError responseBody' , async ( ) => {
938+ const llmOnlyTemplate = {
939+ ...mockTemplate ,
940+ handleSteps : undefined ,
941+ }
942+
943+ const localAgentTemplates = {
944+ 'test-agent' : llmOnlyTemplate ,
945+ }
946+
947+ // Mock promptAiSdkStream to throw an APICallError with a 403 status
948+ // and a responseBody containing the server's structured error
949+ loopAgentStepsBaseParams . promptAiSdkStream = async function * ( ) {
950+ throw new APICallError ( {
951+ statusCode : 403 ,
952+ message : 'Forbidden' ,
953+ url : 'https://api.codebuff.com/v1/chat/completions' ,
954+ requestBodyValues : { } ,
955+ responseBody : JSON . stringify ( {
956+ error : 'free_mode_unavailable' ,
957+ message : 'Free mode is not available in your country.' ,
958+ } ) ,
959+ isRetryable : false ,
960+ } )
961+ }
962+
963+ const result = await loopAgentSteps ( {
964+ ...loopAgentStepsBaseParams ,
965+ agentType : 'test-agent' ,
966+ localAgentTemplates,
967+ } )
968+
969+ expect ( result . output . type ) . toBe ( 'error' )
970+ if ( result . output . type === 'error' ) {
971+ // Should use the server's message, NOT the generic "Forbidden"
972+ expect ( result . output . message ) . toBe ( 'Free mode is not available in your country.' )
973+ // Should NOT have the 'Agent run error: ' prefix since message came from responseBody
974+ expect ( result . output . message ) . not . toContain ( 'Agent run error:' )
975+ // Should propagate the error code so the CLI can match on it
976+ expect ( result . output . error ) . toBe ( 'free_mode_unavailable' )
977+ // Should propagate the status code
978+ expect ( result . output . statusCode ) . toBe ( 403 )
979+ }
980+ } )
981+
982+ it ( 'should prefix with "Agent run error:" when responseBody has no parseable message' , async ( ) => {
983+ const llmOnlyTemplate = {
984+ ...mockTemplate ,
985+ handleSteps : undefined ,
986+ }
987+
988+ const localAgentTemplates = {
989+ 'test-agent' : llmOnlyTemplate ,
990+ }
991+
992+ // APICallError with no responseBody
993+ loopAgentStepsBaseParams . promptAiSdkStream = async function * ( ) {
994+ throw new APICallError ( {
995+ statusCode : 500 ,
996+ message : 'Internal Server Error' ,
997+ url : 'https://api.codebuff.com/v1/chat/completions' ,
998+ requestBodyValues : { } ,
999+ responseBody : undefined ,
1000+ isRetryable : true ,
1001+ } )
1002+ }
1003+
1004+ const result = await loopAgentSteps ( {
1005+ ...loopAgentStepsBaseParams ,
1006+ agentType : 'test-agent' ,
1007+ localAgentTemplates,
1008+ } )
1009+
1010+ expect ( result . output . type ) . toBe ( 'error' )
1011+ if ( result . output . type === 'error' ) {
1012+ // Should have the prefix since there's no server message
1013+ expect ( result . output . message ) . toContain ( 'Agent run error:' )
1014+ expect ( result . output . message ) . toContain ( 'Internal Server Error' )
1015+ // No error code since responseBody wasn't parseable
1016+ expect ( result . output . error ) . toBeUndefined ( )
1017+ }
1018+ } )
1019+ } )
9341020} )
0 commit comments