Download packages/agent-core-v2/test/session/subagent/spawn.test.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 22.4 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/agent-core-v2/test/session/subagent/spawn.test.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/agent-core-v2/test/session/subagent/spawn.test.ts
-
curl -L -o spawn.test.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/agent-core-v2/test/session/subagent/spawn.test.ts
22.4 kB
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | |
| import { SyncDescriptor } from '#/_base/di/descriptors'; | |
| import { DisposableStore } from '#/_base/di/lifecycle'; | |
| import { TestInstantiationService } from '#/_base/di/test'; | |
| import { Event } from '#/_base/event'; | |
| import { LifecycleScope } from '#/app/scopes'; | |
| import type { IAgentScopeHandle } from '#/_base/di/scope'; | |
| import { IConfigService } from '#/app/config/config'; | |
| import { IFlagService } from '#/app/flag/flag'; | |
| import { ILogService } from '#/_base/log/log'; | |
| import { | |
| normalizeAgentProfile, | |
| type AgentProfile, | |
| } from '#/app/agentProfileCatalog/agentProfileCatalog'; | |
| import { IAgentProfileService, type ProfileData } from '#/agent/profile/profile'; | |
| import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode'; | |
| import { IAgentUserToolService } from '#/agent/userTool/userTool'; | |
| import { IAgentRuntimeService } from '#/agent/runtimeBinding/agentRuntime'; | |
| import { Error2, ErrorCodes, isError2 } from '#/errors'; | |
| import { UNKNOWN_CAPABILITY } from '#/llm-adapter/contract/capability'; | |
| import { IModelCatalog, type Model } from '#/llm-adapter/model/catalog'; | |
| import { FakeRuntime } from '#/runtime/fakeRuntime'; | |
| import type { RuntimeLease } from '#/runtime/runtime'; | |
| import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle'; | |
| import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; | |
| import { ISessionAgentProfileCatalog } from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalog'; | |
| import { ISessionContext } from '#/session/sessionContext/sessionContext'; | |
| import { SECONDARY_MODEL_SECTION } from '#/session/subagent/configSection'; | |
| import { ISessionSubagentService } from '#/session/subagent/subagent'; | |
| import { SessionSubagentService } from '#/session/subagent/subagentService'; | |
| import { | |
| FORK_CONTEXT_NOTICE, | |
| type SpawnedSubagent, | |
| type SpawnSubagentOptions, | |
| type SubagentSpawnPlan, | |
| type SubagentSpawnPlanInput, | |
| } from '#/session/subagent/spawn'; | |
| import { IAgentReminderService } from '#/features/reminder/reminderService'; | |
| import { stubLog } from '../../_base/log/stubs'; | |
| import { stubFlag } from '../../app/flag/stubs'; | |
| import { StubConfigService } from '../../stubs'; | |
| import { stubAgentContext } from '../../agent/agentContext/stubs'; | |
| const CALLER_ID = 'main'; | |
| describe('SessionSubagentService planSpawn and spawn', () => { | |
| let disposables: DisposableStore; | |
| let ix: TestInstantiationService; | |
| let callerData: ProfileData; | |
| let profiles: AgentProfile[]; | |
| let modelIds: Set<string>; | |
| let modelMeta: Map<string, Partial<Model>>; | |
| let caller: IAgentScopeHandle; | |
| let createdHandles: Map<string, IAgentScopeHandle>; | |
| let createAgent: ReturnType<typeof vi.fn>; | |
| let forkAgent: ReturnType<typeof vi.fn>; | |
| let acquireRuntime: ReturnType<typeof vi.fn>; | |
| let callerPermissionMode: { mode: string; setMode: ReturnType<typeof vi.fn> }; | |
| let createdPermissionMode: { mode: string; setMode: ReturnType<typeof vi.fn> }; | |
| let callerUserTools: IAgentUserToolService; | |
| let createdUserTools: IAgentUserToolService; | |
| let createdReminder: { notify: ReturnType<typeof vi.fn> }; | |
| let lease: RuntimeLease; | |
| function userToolsStub(): IAgentUserToolService { | |
| return { | |
| _serviceBrand: undefined, | |
| list: () => [], | |
| inheritUserTools: vi.fn(), | |
| register: vi.fn(), | |
| unregister: vi.fn(), | |
| } as unknown as IAgentUserToolService; | |
| } | |
| function profileServiceStub(data: ProfileData): IAgentProfileService { | |
| return { | |
| _serviceBrand: undefined, | |
| data: () => data, | |
| getActiveToolNames: () => data.activeToolNames, | |
| } as unknown as IAgentProfileService; | |
| } | |
| function createdHandle(agentId: string): IAgentScopeHandle { | |
| return { | |
| id: agentId, | |
| kind: LifecycleScope.Agent, | |
| accessor: { | |
| get: (serviceId: unknown) => { | |
| if (serviceId === IAgentProfileService) { | |
| return profileServiceStub({ ...callerData, modelCapabilities: {} as never }); | |
| } | |
| if (serviceId === IAgentPermissionModeService) return createdPermissionMode; | |
| if (serviceId === IAgentUserToolService) return createdUserTools; | |
| if (serviceId === IAgentReminderService) return createdReminder; | |
| return undefined; | |
| }, | |
| } as IAgentScopeHandle['accessor'], | |
| dispose: () => {}, | |
| }; | |
| } | |
| beforeEach(() => { | |
| disposables = new DisposableStore(); | |
| ix = disposables.add(new TestInstantiationService()); | |
| ix.stub(IFlagService, stubFlag(true)); | |
| callerData = { | |
| profileName: 'orchestrator', | |
| modelAlias: 'main-model', | |
| thinkingLevel: 'high', | |
| systemPrompt: 'caller prompt', | |
| modelCapabilities: {} as never, | |
| }; | |
| profiles = [ | |
| normalizeAgentProfile({ | |
| name: 'coder', | |
| description: 'Coder', | |
| systemPrompt: () => 'coder', | |
| }), | |
| normalizeAgentProfile({ | |
| name: 'explore', | |
| description: 'Explorer', | |
| systemPrompt: () => 'explore', | |
| }), | |
| ]; | |
| modelIds = new Set(['main-model']); | |
| modelMeta = new Map(); | |
| callerPermissionMode = { mode: 'auto', setMode: vi.fn() }; | |
| createdPermissionMode = { mode: 'manual', setMode: vi.fn() }; | |
| callerUserTools = userToolsStub(); | |
| createdUserTools = userToolsStub(); | |
| createdReminder = { notify: vi.fn() }; | |
| lease = { | |
| runtime: new FakeRuntime({ workspaceId: 'w1', runtimeId: 'acp:s1', generation: 'g1' }), | |
| track: (resource) => resource, | |
| dispose: vi.fn(), | |
| }; | |
| acquireRuntime = vi.fn(() => lease); | |
| caller = { | |
| id: CALLER_ID, | |
| kind: LifecycleScope.Agent, | |
| accessor: { | |
| get: (serviceId: unknown) => { | |
| if (serviceId === IAgentProfileService) return profileServiceStub(callerData); | |
| if (serviceId === IAgentPermissionModeService) return callerPermissionMode; | |
| if (serviceId === IAgentUserToolService) return callerUserTools; | |
| if (serviceId === IAgentRuntimeService) { | |
| return { | |
| _serviceBrand: undefined, | |
| acquire: acquireRuntime, | |
| }; | |
| } | |
| if (serviceId === IAgentScopeContext) { | |
| return { | |
| _serviceBrand: undefined, | |
| agentId: CALLER_ID, | |
| agentContext: stubAgentContext(CALLER_ID, 1), | |
| scope: () => '', | |
| }; | |
| } | |
| return undefined; | |
| }, | |
| } as IAgentScopeHandle['accessor'], | |
| dispose: () => {}, | |
| }; | |
| createdHandles = new Map(); | |
| createAgent = vi.fn(async (input: { readonly agentId?: string } = {}) => { | |
| const agentId = input.agentId ?? 'agent-child'; | |
| createdHandles.set(agentId, createdHandle(agentId)); | |
| return stubAgentContext(agentId, 1); | |
| }); | |
| forkAgent = vi.fn(async () => { | |
| createdHandles.set('agent-fork', createdHandle('agent-fork')); | |
| return stubAgentContext('agent-fork', 1); | |
| }); | |
| ix.stub(IAgentLifecycleService, { | |
| _serviceBrand: undefined, | |
| onDidCreate: Event.None, | |
| onDidCreateScope: Event.None, | |
| onWillClose: Event.None, | |
| onDidClose: Event.None, | |
| create: createAgent, | |
| fork: forkAgent, | |
| get: (agentId: string) => (agentId === CALLER_ID ? stubAgentContext(CALLER_ID, 1) : undefined), | |
| handleOf: (agentId: string) => | |
| agentId === CALLER_ID ? caller : createdHandles.get(agentId), | |
| list: () => [stubAgentContext(CALLER_ID, 1)], | |
| remove: async () => {}, | |
| broadcastPermissionMode: () => {}, | |
| } as unknown as IAgentLifecycleService); | |
| ix.stub(ISessionAgentProfileCatalog, { | |
| _serviceBrand: undefined, | |
| ready: Promise.resolve(), | |
| onDidChange: Event.None, | |
| get: (name: string) => profiles.find((profile) => profile.name === name), | |
| getDefault: () => profiles[0]!, | |
| list: () => profiles, | |
| inspect: (name: string) => | |
| profiles.some((profile) => profile.name === name) ? { sourceId: 'builtin' } : undefined, | |
| } as unknown as ISessionAgentProfileCatalog); | |
| ix.stub(IModelCatalog, { | |
| _serviceBrand: undefined, | |
| get: (alias: string) => { | |
| if (!modelIds.has(alias)) { | |
| throw new Error2( | |
| ErrorCodes.CONFIG_INVALID, | |
| `Model "${alias}" is not configured in config.toml.`, | |
| { details: { model: alias } }, | |
| ); | |
| } | |
| return { id: alias, ...modelMeta.get(alias) } as Model; | |
| }, | |
| } as unknown as IModelCatalog); | |
| ix.stub(ISessionContext, { _serviceBrand: undefined, cwd: '/repo' } as unknown as ISessionContext); | |
| ix.stub(ILogService, stubLog()); | |
| }); | |
| afterEach(() => { | |
| disposables.dispose(); | |
| }); | |
| function service(configValues: Record<string, unknown> = {}): ISessionSubagentService { | |
| ix.stub(IConfigService, new StubConfigService(configValues)); | |
| ix.set(ISessionSubagentService, new SyncDescriptor(SessionSubagentService)); | |
| return ix.get(ISessionSubagentService); | |
| } | |
| async function planSpawnError( | |
| svc: ISessionSubagentService, | |
| input: SubagentSpawnPlanInput, | |
| ): Promise<Error2> { | |
| try { | |
| await svc.planSpawn(input); | |
| } catch (error) { | |
| if (!isError2(error)) throw error; | |
| return error; | |
| } | |
| throw new Error('planSpawn did not throw'); | |
| } | |
| async function spawnError( | |
| svc: ISessionSubagentService, | |
| options: SpawnSubagentOptions, | |
| ): Promise<Error2> { | |
| try { | |
| await svc.spawn(options); | |
| } catch (error) { | |
| if (!isError2(error)) throw error; | |
| return error; | |
| } | |
| throw new Error('spawn did not throw'); | |
| } | |
| function spawnNonForkChild(svc: ISessionSubagentService): Promise<SpawnedSubagent> { | |
| return svc.spawn({ | |
| callerAgentId: CALLER_ID, | |
| plan: { profileName: 'coder', model: 'provider/fast', modelSource: 'secondary_pool', thinking: 'low', fork: false }, | |
| labels: { parentAgentId: 'main' }, | |
| prompt: 'Review the file', | |
| }); | |
| } | |
| function spawnForkChild(svc: ISessionSubagentService): Promise<SpawnedSubagent> { | |
| return svc.spawn({ | |
| callerAgentId: CALLER_ID, | |
| plan: { profileName: 'orchestrator', model: 'main-model', modelSource: 'inherited', thinking: 'high', fork: true }, | |
| labels: { parentAgentId: 'main' }, | |
| prompt: 'Continue the analysis', | |
| }); | |
| } | |
| it('rejects an unknown subagent type', async () => { | |
| const svc = service(); | |
| const error = await planSpawnError(svc, { callerAgentId: CALLER_ID, profileName: 'ghost' }); | |
| expect(error.code).toBe(ErrorCodes.PROFILE_UNKNOWN); | |
| expect(error.message).toBe('Unknown agent type: "ghost"'); | |
| }); | |
| it('rejects a subagent type outside the caller allowlist', async () => { | |
| callerData = { ...callerData, subagents: ['explore'] }; | |
| const svc = service(); | |
| const error = await planSpawnError(svc, { callerAgentId: CALLER_ID, profileName: 'coder' }); | |
| expect(error.code).toBe(ErrorCodes.AGENT_TYPE_NOT_ALLOWED); | |
| expect(error.message).toBe( | |
| 'Subagent type "coder" is not allowed for this agent. Allowed subagent types: explore.', | |
| ); | |
| }); | |
| it('rejects when the caller agent has no model bound', async () => { | |
| callerData = { ...callerData, modelAlias: undefined }; | |
| const svc = service(); | |
| const error = await planSpawnError(svc, { callerAgentId: CALLER_ID, profileName: 'coder' }); | |
| expect(error.code).toBe(ErrorCodes.MODEL_NOT_CONFIGURED); | |
| expect(error.message).toBe('Caller agent has no model bound'); | |
| }); | |
| it('wraps an unresolvable pool model with the secondary-model config hint', async () => { | |
| const svc = service( | |
| { | |
| [SECONDARY_MODEL_SECTION]: { | |
| defaultModel: 'provider/bad', | |
| models: { 'provider/bad': 'broken' }, | |
| }, | |
| }, | |
| ); | |
| const error = await planSpawnError(svc, { callerAgentId: CALLER_ID, profileName: 'coder' }); | |
| expect(error.code).toBe(ErrorCodes.CONFIG_INVALID); | |
| expect(error.message).toContain('Model "provider/bad" is not configured in config.toml.'); | |
| expect(error.message).toContain('comes from [secondary_model.models]'); | |
| }); | |
| it('passes [secondary_model].default_effort as the explicit subagent thinking', async () => { | |
| modelIds.add('provider/fast'); | |
| const svc = service( | |
| { | |
| [SECONDARY_MODEL_SECTION]: { | |
| defaultModel: 'provider/fast', | |
| models: { 'provider/fast': 'fast model' }, | |
| defaultEffort: 'max', | |
| }, | |
| thinking: { enabled: false }, | |
| }, | |
| ); | |
| const plan = await svc.planSpawn({ callerAgentId: CALLER_ID, profileName: 'coder' }); | |
| expect(plan).toEqual({ | |
| profileName: 'coder', | |
| model: 'provider/fast', | |
| modelSource: 'secondary_pool', | |
| thinking: 'max', | |
| fork: false, | |
| }); | |
| }); | |
| it('prefers [secondary_model].default_effort over the bound model default_effort', async () => { | |
| modelIds.add('provider/fast'); | |
| modelMeta.set('provider/fast', { | |
| capabilities: { ...UNKNOWN_CAPABILITY, thinking: true }, | |
| supportEfforts: ['low', 'high', 'max'], | |
| defaultEffort: 'high', | |
| }); | |
| const svc = service( | |
| { | |
| [SECONDARY_MODEL_SECTION]: { | |
| defaultModel: 'provider/fast', | |
| models: { 'provider/fast': 'fast model' }, | |
| defaultEffort: 'max', | |
| }, | |
| }, | |
| ); | |
| const plan = await svc.planSpawn({ callerAgentId: CALLER_ID, profileName: 'coder' }); | |
| expect(plan.thinking).toBe('max'); | |
| }); | |
| it('falls back to the bound model default_effort when the section declares none', async () => { | |
| modelIds.add('provider/fast'); | |
| modelMeta.set('provider/fast', { | |
| capabilities: { ...UNKNOWN_CAPABILITY, thinking: true }, | |
| supportEfforts: ['low', 'high', 'max'], | |
| defaultEffort: 'max', | |
| }); | |
| const svc = service( | |
| { | |
| [SECONDARY_MODEL_SECTION]: { | |
| defaultModel: 'provider/fast', | |
| models: { 'provider/fast': 'fast model' }, | |
| }, | |
| }, | |
| ); | |
| const plan = await svc.planSpawn({ callerAgentId: CALLER_ID, profileName: 'coder' }); | |
| expect(plan.thinking).toBe('max'); | |
| }); | |
| it('leaves thinking unset for global resolution when thinking is disabled', async () => { | |
| modelIds.add('provider/fast'); | |
| modelMeta.set('provider/fast', { | |
| capabilities: { ...UNKNOWN_CAPABILITY, thinking: true }, | |
| supportEfforts: ['low', 'high', 'max'], | |
| defaultEffort: 'max', | |
| }); | |
| const svc = service( | |
| { | |
| [SECONDARY_MODEL_SECTION]: { | |
| defaultModel: 'provider/fast', | |
| models: { 'provider/fast': 'fast model' }, | |
| }, | |
| thinking: { enabled: false }, | |
| }, | |
| ); | |
| const plan = await svc.planSpawn({ callerAgentId: CALLER_ID, profileName: 'coder' }); | |
| expect(plan.thinking).toBeUndefined(); | |
| }); | |
| it('leaves the subagent thinking unset when the bound model declares no valid default_effort', async () => { | |
| modelIds.add('provider/fast'); | |
| modelMeta.set('provider/fast', { | |
| capabilities: { ...UNKNOWN_CAPABILITY, thinking: true }, | |
| supportEfforts: ['low', 'high'], | |
| defaultEffort: 'max', | |
| }); | |
| const svc = service( | |
| { | |
| [SECONDARY_MODEL_SECTION]: { | |
| defaultModel: 'provider/fast', | |
| models: { 'provider/fast': 'fast model' }, | |
| }, | |
| }, | |
| ); | |
| const plan = await svc.planSpawn({ callerAgentId: CALLER_ID, profileName: 'coder' }); | |
| expect(plan.thinking).toBeUndefined(); | |
| }); | |
| it('passes [secondary_model].default_effort with the forced model', async () => { | |
| modelIds.add('provider/fast'); | |
| const svc = service( | |
| { | |
| [SECONDARY_MODEL_SECTION]: { | |
| force: true, | |
| defaultModel: 'provider/fast', | |
| defaultEffort: 'max', | |
| }, | |
| }, | |
| ); | |
| const plan = await svc.planSpawn({ callerAgentId: CALLER_ID, profileName: 'coder' }); | |
| expect(plan).toEqual({ | |
| profileName: 'coder', | |
| model: 'provider/fast', | |
| modelSource: 'forced', | |
| thinking: 'max', | |
| fork: false, | |
| }); | |
| }); | |
| it('inherits the caller model and thinking when no pool is configured', async () => { | |
| const svc = service({}); | |
| const plan = await svc.planSpawn({ callerAgentId: CALLER_ID, profileName: 'coder' }); | |
| expect(plan).toEqual({ | |
| profileName: 'coder', | |
| model: 'main-model', | |
| modelSource: 'inherited', | |
| thinking: 'high', | |
| fork: false, | |
| }); | |
| }); | |
| it('skips the allowlist check when forking', async () => { | |
| callerData = { ...callerData, profileName: 'coder', subagents: ['explore'] }; | |
| const svc = service(); | |
| const plan = await svc.planSpawn({ callerAgentId: CALLER_ID, fork: true }); | |
| expect(plan.profileName).toBe('coder'); | |
| }); | |
| it('skips the unknown-profile check when forking', async () => { | |
| callerData = { ...callerData, profileName: 'ghost' }; | |
| const svc = service(); | |
| const plan = await svc.planSpawn({ callerAgentId: CALLER_ID, fork: true }); | |
| expect(plan.profileName).toBe('ghost'); | |
| }); | |
| it('returns the caller binding when forking', async () => { | |
| const svc = service(); | |
| const plan = await svc.planSpawn({ callerAgentId: CALLER_ID, fork: true }); | |
| expect(plan).toEqual({ | |
| profileName: 'orchestrator', | |
| model: 'main-model', | |
| modelSource: 'inherited', | |
| thinking: 'high', | |
| fork: true, | |
| }); | |
| }); | |
| it('creates the child with the plan binding when the plan is not a fork', async () => { | |
| const svc = service(); | |
| await spawnNonForkChild(svc); | |
| expect(createAgent).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| binding: { profile: 'coder', model: 'provider/fast', thinking: 'low' }, | |
| }), | |
| ); | |
| expect(forkAgent).not.toHaveBeenCalled(); | |
| }); | |
| it('creates the child with the task labels when the plan is not a fork', async () => { | |
| const svc = service(); | |
| await spawnNonForkChild(svc); | |
| expect(createAgent).toHaveBeenCalledWith( | |
| expect.objectContaining({ labels: { parentAgentId: 'main' } }), | |
| ); | |
| }); | |
| it('creates the child on the acquired runtime lease', async () => { | |
| const svc = service(); | |
| await spawnNonForkChild(svc); | |
| expect(createAgent).toHaveBeenCalledWith(expect.objectContaining({ runtimeId: 'acp:s1' })); | |
| }); | |
| it('inherits the caller permission mode and user tools', async () => { | |
| const svc = service(); | |
| await spawnNonForkChild(svc); | |
| expect(createdPermissionMode.setMode).toHaveBeenCalledWith('auto'); | |
| expect(createdUserTools.inheritUserTools).toHaveBeenCalledWith(callerUserTools); | |
| }); | |
| it('applies the profile prompt prefix to the spawned prompt', async () => { | |
| profiles = [ | |
| normalizeAgentProfile({ | |
| name: 'coder', | |
| description: 'Coder', | |
| promptPrefix: async () => 'FIXED-PREFIX', | |
| systemPrompt: () => 'coder', | |
| }), | |
| ]; | |
| const svc = service(); | |
| const spawned = await spawnNonForkChild(svc); | |
| expect(spawned).toEqual({ | |
| agentId: 'agent-child', | |
| profileName: 'coder', | |
| model: 'provider/fast', | |
| modelSource: 'secondary_pool', | |
| promptText: 'FIXED-PREFIX\n\nReview the file', | |
| }); | |
| }); | |
| it('releases the runtime lease after spawn', async () => { | |
| const svc = service(); | |
| await spawnNonForkChild(svc); | |
| expect(lease.dispose).toHaveBeenCalled(); | |
| }); | |
| it('delegates to manager.fork with the caller labels when the plan is a fork', async () => { | |
| const svc = service(); | |
| await spawnForkChild(svc); | |
| expect(forkAgent).toHaveBeenCalledWith( | |
| expect.objectContaining({ agentId: 'main' }), | |
| { labels: { parentAgentId: 'main' } }, | |
| ); | |
| expect(createAgent).not.toHaveBeenCalled(); | |
| }); | |
| it('preserves the fork snapshot active tool names when inheriting user tools', async () => { | |
| callerData = { ...callerData, activeToolNames: ['Agent', 'Read'] }; | |
| const svc = service(); | |
| await spawnForkChild(svc); | |
| expect(createdUserTools.inheritUserTools).toHaveBeenCalledWith(callerUserTools, [ | |
| 'Agent', | |
| 'Read', | |
| ]); | |
| }); | |
| it('delivers the fork notice as a reminder injection when the plan is a fork', async () => { | |
| const svc = service(); | |
| const spawned = await spawnForkChild(svc); | |
| expect(spawned).toEqual({ | |
| agentId: 'agent-fork', | |
| profileName: 'orchestrator', | |
| model: 'main-model', | |
| modelSource: 'inherited', | |
| promptText: 'Continue the analysis', | |
| }); | |
| expect(createdReminder.notify).toHaveBeenCalledWith(FORK_CONTEXT_NOTICE, { | |
| variant: 'fork_context', | |
| }); | |
| }); | |
| it('does not require the process capability when forking', async () => { | |
| acquireRuntime.mockImplementation(() => { | |
| throw new Error('process capability is no longer available'); | |
| }); | |
| const svc = service(); | |
| await expect(spawnForkChild(svc)).resolves.toMatchObject({ agentId: 'agent-fork' }); | |
| expect(acquireRuntime).not.toHaveBeenCalled(); | |
| expect(forkAgent).toHaveBeenCalledWith( | |
| expect.objectContaining({ agentId: 'main' }), | |
| { labels: { parentAgentId: 'main' } }, | |
| ); | |
| }); | |
| it('wraps a create rejection with the secondary-model config hint', async () => { | |
| createAgent.mockRejectedValueOnce( | |
| new Error2( | |
| ErrorCodes.CONFIG_INVALID, | |
| 'Model "provider/bad" is not configured in config.toml.', | |
| { details: { model: 'provider/bad' } }, | |
| ), | |
| ); | |
| const svc = service(); | |
| const error = await spawnError(svc, { | |
| callerAgentId: CALLER_ID, | |
| plan: { profileName: 'coder', model: 'provider/bad', modelSource: 'secondary_pool', thinking: 'low', fork: false }, | |
| prompt: 'Review the file', | |
| }); | |
| expect(error.code).toBe(ErrorCodes.CONFIG_INVALID); | |
| expect(error.message).toContain('Model "provider/bad" is not configured in config.toml.'); | |
| expect(error.message).toContain('comes from [secondary_model.models]'); | |
| }); | |
| it('spawn throws before creating anything when the caller runtime lease fails', async () => { | |
| acquireRuntime.mockImplementation(() => { | |
| throw new Error('process capability is no longer available'); | |
| }); | |
| const svc = service(); | |
| const plan: SubagentSpawnPlan = { | |
| profileName: 'coder', | |
| model: 'main-model', | |
| thinking: 'high', | |
| fork: false, | |
| }; | |
| await expect( | |
| svc.spawn({ callerAgentId: CALLER_ID, plan, prompt: 'Review the file' }), | |
| ).rejects.toThrow('process capability is no longer available'); | |
| expect(createAgent).not.toHaveBeenCalled(); | |
| expect(forkAgent).not.toHaveBeenCalled(); | |
| }); | |
| }); | |