@@ -298,6 +298,36 @@ describe('isFailoverError', () => {
298298 } ) ,
299299 ) . toBe ( false ) ;
300300 } ) ;
301+
302+ test ( 'returns true for OpenCode ProviderModelNotFoundError "Model not found" errors' , ( ) => {
303+ // Issue #1034: OpenCode's ProviderModelNotFoundError ("Model not found:
304+ // <model>") was not classified as a failover error, so a missing primary
305+ // model failed the task outright instead of advancing the fallback chain.
306+ // The reporter's error string always carries the message; the bare
307+ // camelCase class name "ProviderModelNotFoundError" (no spaces) does not
308+ // match /\bmodel not found\b/i and is intentionally not covered here.
309+ expect (
310+ isFailoverError (
311+ 'ProviderModelNotFoundError: Model not found: custom/missing-model.' ,
312+ ) ,
313+ ) . toBe ( true ) ;
314+ expect (
315+ isFailoverError ( { message : 'Model not found: custom/missing-model' } ) ,
316+ ) . toBe ( true ) ;
317+ } ) ;
318+
319+ test ( 'returns true for existing model-outage patterns (regression guard)' , ( ) => {
320+ expect ( isFailoverError ( 'model not available' ) ) . toBe ( true ) ;
321+ expect ( isFailoverError ( 'unsupported model' ) ) . toBe ( true ) ;
322+ expect ( isFailoverError ( 'unknown model' ) ) . toBe ( true ) ;
323+ } ) ;
324+
325+ test ( 'returns false for normal errors and model mentions without outage wording' , ( ) => {
326+ expect ( isFailoverError ( 'Cannot connect to database' ) ) . toBe ( false ) ;
327+ expect ( isFailoverError ( { message : 'invalid model configuration' } ) ) . toBe (
328+ false ,
329+ ) ;
330+ } ) ;
301331} ) ;
302332
303333// ---------------------------------------------------------------------------
@@ -483,6 +513,42 @@ describe('ForegroundFallbackManager session.error', () => {
483513 expect ( call [ 0 ] . body . model . modelID ) . toBe ( 'gpt-4o' ) ;
484514 } ) ;
485515
516+ test ( 'triggers fallback on ProviderModelNotFoundError session.error' , async ( ) => {
517+ await mgr . handleEvent ( {
518+ type : 'message.updated' ,
519+ properties : {
520+ info : {
521+ sessionID : 'sess-1' ,
522+ providerID : 'anthropic' ,
523+ modelID : 'claude-opus-4-5' ,
524+ role : 'assistant' ,
525+ } ,
526+ } ,
527+ } ) ;
528+
529+ await mgr . handleEvent ( {
530+ type : 'session.error' ,
531+ properties : {
532+ sessionID : 'sess-1' ,
533+ error : {
534+ message :
535+ 'ProviderModelNotFoundError: Model not found: custom/missing-model.' ,
536+ } ,
537+ } ,
538+ } ) ;
539+
540+ expect ( mocks . abort ) . not . toHaveBeenCalled ( ) ;
541+ expect ( mocks . promptAsync ) . toHaveBeenCalledTimes ( 1 ) ;
542+
543+ const call = mocks . promptAsync . mock . calls [ 0 ] as [
544+ {
545+ model : { providerID : string ; modelID : string } ;
546+ } ,
547+ ] ;
548+ expect ( call [ 0 ] . body . model . providerID ) . toBe ( 'openai' ) ;
549+ expect ( call [ 0 ] . body . model . modelID ) . toBe ( 'gpt-4o' ) ;
550+ } ) ;
551+
486552 test ( 'marks the replayed user prompt as an internal initiator' , async ( ) => {
487553 await mgr . handleEvent ( {
488554 type : 'message.updated' ,
0 commit comments