Skip to content

Commit 1da28be

Browse files
committed
test(agent-core-v2): cover cancellation during fallback setModel
Add a deferred-setModel profile subclass test asserting the switch returns false and rolls back to the previous model when the step aborts mid-switch; assert exact token_budget trigger values.
1 parent 1bde4b7 commit 1da28be

1 file changed

Lines changed: 87 additions & 3 deletions

File tree

packages/agent-core-v2/test/agent/turnRecovery/modelFallback.test.ts

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { afterEach, describe, expect, it } from 'vitest';
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
22

33
import { APIConnectionError } from '#/kosong/contract/errors';
44
import { emptyUsage } from '#/kosong/contract/usage';
@@ -7,19 +7,48 @@ import { IEventBus } from '#/app/event/eventBus';
77
import { IAgentLoopService, type LoopErrorContext, type Step } from '#/agent/loop/loop';
88
import { ContinuationStepRequest } from '#/agent/loop/stepRequest';
99
import { 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';
1116
import { MODEL_FALLBACK_FLAG_ID } from '#/agent/turnRecovery/flag';
1217
import { IAgentModelFallbackService, ModelFallbackSwitched } from '#/agent/turnRecovery/modelFallback';
1318

1419
import { 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

1728
const FALLBACK_MODEL = 'fallback-model';
1829

1930
function 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+
2352
function 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

Comments
 (0)