1- import { afterEach , describe , expect , it } from 'vitest' ;
1+ import { afterEach , describe , expect , it , vi } from 'vitest' ;
22
33import { APIConnectionError } from '#/kosong/contract/errors' ;
44import { emptyUsage } from '#/kosong/contract/usage' ;
@@ -7,19 +7,48 @@ import { IEventBus } from '#/app/event/eventBus';
77import { IAgentLoopService , type LoopErrorContext , type Step } from '#/agent/loop/loop' ;
88import { ContinuationStepRequest } from '#/agent/loop/stepRequest' ;
99import { TurnStarted } from '#/agent/loop/turnEvents' ;
10- import { IAgentProfileService } from '#/agent/profile/profile' ;
10+ import {
11+ type ProfileSetModelResult ,
12+ IAgentProfileService ,
13+ } from '#/agent/profile/profile' ;
14+ import { AgentProfileService } from '#/agent/profile/profileService' ;
15+ import { SyncDescriptor } from '#/_base/di/descriptors' ;
1116import { MODEL_FALLBACK_FLAG_ID } from '#/agent/turnRecovery/flag' ;
1217import { IAgentModelFallbackService , ModelFallbackSwitched } from '#/agent/turnRecovery/modelFallback' ;
1318
1419import { stubFlag } from '../../app/flag/stubs' ;
15- import { appService , createTestAgent , llmGenerateServices , type TestAgentContext } from '../../harness' ;
20+ import {
21+ agentService ,
22+ appService ,
23+ createTestAgent ,
24+ llmGenerateServices ,
25+ type TestAgentContext ,
26+ } from '../../harness' ;
1627
1728const FALLBACK_MODEL = 'fallback-model' ;
1829
1930function fallbackFlags ( enabled = true ) : ReturnType < typeof appService > {
2031 return appService ( IFlagService , stubFlag ( ( id ) => enabled && id === MODEL_FALLBACK_FLAG_ID ) ) ;
2132}
2233
34+ class DeferredSetModelProfile extends AgentProfileService {
35+ override async setModel ( model : string ) : Promise < ProfileSetModelResult > {
36+ setModelCalls . push ( model ) ;
37+ if ( deferFirstSetModel ) {
38+ deferFirstSetModel = false ;
39+ await new Promise < void > ( ( resolve ) => {
40+ resolveFirstSetModel = resolve ;
41+ } ) ;
42+ return { model } ;
43+ }
44+ return super . setModel ( model ) ;
45+ }
46+ }
47+
48+ let deferFirstSetModel = false ;
49+ let resolveFirstSetModel : ( ( ) => void ) | undefined ;
50+ const setModelCalls : string [ ] = [ ] ;
51+
2352function fallbackTestConfig ( ) {
2453 return {
2554 loopControl : { maxAttemptsPerStep : 1 , maxStepsPerTurn : 20 , fallbackModel : FALLBACK_MODEL } ,
@@ -259,4 +288,59 @@ describe('modelFallback plugin', () => {
259288 expect ( switched ) . toBe ( false ) ;
260289 expect ( ctx . get ( IAgentProfileService ) . getModel ( ) ) . toBe ( 'mock-model' ) ;
261290 } ) ;
291+
292+ it ( 'rolls back the model when the step aborts during setModel' , async ( ) => {
293+ const stepController = new AbortController ( ) ;
294+ deferFirstSetModel = true ;
295+ resolveFirstSetModel = undefined ;
296+ setModelCalls . length = 0 ;
297+ ctx = createTestAgent (
298+ fallbackFlags ( ) ,
299+ llmGenerateServices ( async ( ) => ( {
300+ id : 'mid-abort' ,
301+ message : {
302+ role : 'assistant' as const ,
303+ content : [ { type : 'text' as const , text : 'ok' } ] ,
304+ toolCalls : [ ] ,
305+ } ,
306+ usage : emptyUsage ( ) ,
307+ finishReason : 'completed' as const ,
308+ rawFinishReason : 'stop' ,
309+ } ) ) ,
310+ { initialConfig : fallbackTestConfig ( ) } ,
311+ agentService (
312+ IAgentProfileService ,
313+ new SyncDescriptor ( DeferredSetModelProfile ) ,
314+ ) ,
315+ ) ;
316+
317+ const stepStub : Step = {
318+ id : 'step-1' ,
319+ turnId : 1 ,
320+ state : 'running' ,
321+ signal : stepController . signal ,
322+ result : Promise . resolve ( { type : 'cancelled' , reason : new Error ( 'cancelled' ) } ) ,
323+ cancel : ( ) => false ,
324+ } ;
325+ const context : LoopErrorContext = {
326+ turnId : 1 ,
327+ step : 1 ,
328+ signal : new AbortController ( ) . signal ,
329+ currentStep : stepStub ,
330+ error : new APIConnectionError ( 'terminated' ) ,
331+ failedDriver : new ContinuationStepRequest ( ) ,
332+ retry : ( ) => stepStub ,
333+ } ;
334+
335+ const pending = ctx
336+ . get ( IAgentModelFallbackService )
337+ . tryFallbackSwitch ( context ) ;
338+ await vi . waitFor ( ( ) => expect ( resolveFirstSetModel ) . toBeDefined ( ) ) ;
339+ stepController . abort ( ) ;
340+ resolveFirstSetModel ! ( ) ;
341+
342+ await expect ( pending ) . resolves . toBe ( false ) ;
343+ expect ( setModelCalls ) . toEqual ( [ 'fallback-model' , 'mock-model' ] ) ;
344+ expect ( ctx . get ( IAgentProfileService ) . getModel ( ) ) . toBe ( 'mock-model' ) ;
345+ } ) ;
262346} ) ;
0 commit comments