src/vs/platform/agentHost/test/node/mockAgent.ts

1115 LOC · 531 covered · 584 uncovered · 151 ranges · 707 concepts · 46 introducers · 325 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- mockAgent.ts ×55
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { timeout } from '../../../../base/common/async.js';
7 > import { Emitter } from '../../../../base/common/event.js';
8 > import { observableValue } from '../../../../base/common/observable.js';
9 > import type { IAuthorizationProtectedResourceMetadata } from '../../../../base/common/oauth.js';
10 > import { URI } from '../../../../base/common/uri.js';
11 > import { type ISyncedCustomization } from '../../common/agentPluginManager.js';
12 > import { AgentSession, type AgentProvider, type AgentSignal, type IActiveClient, type IAgent, type IAgentActionSignal, type IAgentChats, type IAgentCreateChatForkSource, type IAgentCreateChatOptions, type IAgentCreateChatResult, type IAgentCreateSessionConfig, type IAgentCreateSessionResult, type IAgentDescriptor, type IAgentModelInfo, type IAgentResolveSessionConfigParams, type IAgentSessionConfigCompletionsParams, type IAgentSessionMetadata, type IAgentToolPendingConfirmationSignal } from '../../common/agentService.js';
13 > import { buildSubagentTurnsFromHistory, buildTurnsFromHistory, type IHistoryRecord } from './historyRecordFixtures.js';
14 > import { ProtectedResourceMetadata, ToolCallContributorKind, type AgentSelection, type MessageAttachment, type ModelSelection, type ToolDefinition } from '../../common/state/protocol/state.js';
15 > import type { ResolveSessionConfigResult, SessionConfigCompletionsResult } from '../../common/state/protocol/commands.js';
16 > import { ActionType } from '../../common/state/sessionActions.js';
17 > import { ResponsePartKind, ToolCallConfirmationReason, ToolCallStatus, ToolResultContentType, CustomizationLoadStatus, buildDefaultChatUri, isAhpChatChannel, parseChatUri, parseSubagentSessionUri, type ClientPluginCustomization, type Customization, type PendingMessage, type StringOrMarkdown, type ToolCallResult, type Turn, type UsageInfo } from '../../common/state/sessionState.js';
18 > import { hasKey } from '../../../../base/common/types.js';
19 >
20 > /** Well-known auto-generated title used by the 'with-title' prompt. */
21 > export const MOCK_AUTO_TITLE = 'Automatically generated title';
22 >
23 > function uriKey(session: URI): string { mockAgent.ts ×3
24 > // Build a stable key from raw URI fields without invoking `toString()`,
25 > // which would mutate the URI's `_formatted` cache and break
26 > // `assert.deepStrictEqual` comparisons in tests that capture the URI
27 > // before it is observed elsewhere.
28 > return `${session.scheme}://${session.authority}${session.path}${session.query ? '?' + session.query : ''}${session.fragment ? '#' + session.fragment : ''}`;
29 > }
31 > function mockProject(provider: AgentProvider) { mockAgent.ts ×1
32 > return { uri: URI.from({ scheme: 'mock-project', path: `/${provider}` }), displayName: `Agent ${provider}` };
33 > }
35 > interface IMockSendMessageCall {
36 > readonly session: URI;
37 > readonly prompt: string;
38 > readonly attachments?: readonly MessageAttachment[];
39 > readonly chat?: URI;
40 > readonly senderClientId?: string;
41 > }
42 >
43 > /**
44 > * General-purpose mock agent for unit tests. Tracks all method calls
45 > * for assertion and exposes {@link fireProgress} to inject progress events.
46 > */
47 > export class MockAgent implements IAgent {
48 > private readonly _onDidSessionProgress = new Emitter<AgentSignal>();
49 > readonly onDidSessionProgress = this._onDidSessionProgress.event;
50 > private readonly _onDidSendMessage = new Emitter<IMockSendMessageCall>();
51 > readonly onDidSendMessage = this._onDidSendMessage.event;
52 > private readonly _models = observableValue<readonly IAgentModelInfo[]>(this, []);
53 > readonly models = this._models;
54 >
55 > private readonly _sessions = new Map<string, URI>();
56 > private _nextId = 1;
57 > /** Active turn IDs per session, captured from sendMessage(). */
58 > private readonly _activeTurnIds = new Map<string, string>();
59 >
60 >
61 > readonly sendMessageCalls: IMockSendMessageCall[] = [];
62 > readonly setPendingMessagesCalls: { chat: URI; steeringMessage: PendingMessage | undefined; queuedMessages: readonly PendingMessage[] }[] = [];
63 > readonly disposeSessionCalls: URI[] = [];
64 > readonly releaseSessionCalls: URI[] = [];
65 > readonly abortSessionCalls: URI[] = [];
66 > readonly respondToPermissionCalls: { requestId: string; approved: boolean }[] = [];
67 > readonly changeModelCalls: { session: URI; model: ModelSelection; chat?: URI }[] = [];
68 > readonly changeAgentCalls: { session: URI; agent: AgentSelection | undefined; chat?: URI }[] = [];
69 > readonly authenticateCalls: { resource: string; token: string }[] = [];
70 > readonly setClientCustomizationsCalls: { clientId: string; customizations: ClientPluginCustomization[] }[] = [];
71 > readonly setClientToolsCalls: { clientId: string; tools: readonly ToolDefinition[] }[] = [];
72 > readonly removeActiveClientCalls: { clientId: string }[] = [];
73 > readonly clientToolCallCompleteCalls: { session: URI; chat: URI; toolCallId: string; result: ToolCallResult }[] = [];
74 > readonly truncateSessionCalls: { session: URI; turnId: string | undefined; chat: URI | undefined }[] = [];
75 > /** Configurable return value for getCustomizations. */
76 > customizations: Customization[] = [];
77 > private readonly _onDidCustomizationsChange = new Emitter<void>();
78 > readonly onDidCustomizationsChange = this._onDidCustomizationsChange.event;
79 > getSessionCustomizations?: (session: URI) => Promise<readonly Customization[]>;
80 >
81 > /**
82 > * Configurable session history. Tests construct {@link IHistoryRecord}
83 > * entries (the agent-internal intermediate shape) and the mock converts
84 > * them to {@link Turn}s on demand. Subagent URIs are routed to filtered
85 > * subagent turns via {@link buildSubagentTurnsFromHistory}.
86 > */
87 > sessionMessages: IHistoryRecord[] = [];
88 >
89 > /** Optional overrides applied to session metadata from listSessions. */
90 > sessionMetadataOverrides: Partial<Omit<IAgentSessionMetadata, 'session'>> = {};
91 >
92 > constructor(readonly id: AgentProvider = 'mock') { }
94 > getDescriptor(): IAgentDescriptor {
95 > return { provider: this.id, displayName: `Agent ${this.id}`, description: `Test ${this.id} agent` }; mockAgent.ts ×2
96 > }
98 > getProtectedResources(): ProtectedResourceMetadata[] {
99 > if (this.id === 'copilot') { mockAgent.ts ×2
100 > return [{ resource: 'https://api.github.com', authorization_servers: ['https://github.com/login/oauth'], required: true }]; mockAgent.ts ×1
101 > }
102 > return []; mockAgent.ts ×1
105 > setModels(models: readonly IAgentModelInfo[]): void {
106 > this._models.set(models, undefined); agentSideEffects.ts ×1
107 > }
109 > async listSessions(): Promise<IAgentSessionMetadata[]> {
110 > return [...this._sessions.values()].map(s => ({ session: s, startTime: Date.now(), modifiedTime: Date.now(), project: mockProject(this.id), ...this.sessionMetadataOverrides })); mockAgent.ts ×1
111 > }
113 > async getSessionMetadata(session: URI): Promise<IAgentSessionMetadata | undefined> {
114 > if (!this._sessions.has(AgentSession.id(session))) { mockAgent.ts ×2
115 > return undefined; agentService.ts ×1
116 > }
117 > return { session, startTime: Date.now(), modifiedTime: Date.now(), project: mockProject(this.id), ...this.sessionMetadataOverrides }; mockAgent.ts ×1
120 > /** Optional override for the working directory returned by createSession. */
121 > resolvedWorkingDirectory: URI | undefined;
122 >
123 > /**
124 > * When set, {@link sendMessage} rejects with this error after recording the
125 > * call — used to simulate a failed first-turn materialization (e.g. worktree
126 > * or branch setup throwing).
127 > */
128 > sendMessageError: Error | undefined;
129 > async createSession(config?: IAgentCreateSessionConfig): Promise<IAgentCreateSessionResult> {
130 > const session = config?.session ?? AgentSession.uri(this.id, `${this.id}-session-${this._nextId++}`); mockAgent.ts ×1
131 > const rawId = AgentSession.id(session);
132 > this._sessions.set(rawId, session);
133 > return { session, project: mockProject(this.id), workingDirectory: this.resolvedWorkingDirectory };
134 > }
136 > async resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult> {
137 > return { schema: { type: 'object', properties: {} }, values: params.config ?? {} }; mockAgent.ts ×1
138 > }
140 > async sessionConfigCompletions(_params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult> {
141 return { items: [] };
142 }
144 > async sendMessage(session: URI, chat: URI, prompt: string, attachments?: readonly MessageAttachment[], turnId?: string, senderClientId?: string): Promise<void> {
145 > const call = { session, prompt, attachments, chat, ...(senderClientId ? { senderClientId } : {}) }; mockAgent.ts ×3
146 > this.sendMessageCalls.push(call);
147 > this._onDidSendMessage.fire(call);
148 > if (turnId) {
149 > this._activeTurnIds.set(uriKey(session), turnId);
150 > }
151 > if (this.sendMessageError) {
152 > throw this.sendMessageError; mockAgent.ts ×1
153 > }
156 > setPendingMessages(chat: URI, steeringMessage: PendingMessage | undefined, queuedMessages: readonly PendingMessage[]): void {
157 > this.setPendingMessagesCalls.push({ chat, steeringMessage, queuedMessages }); agentSideEffects.ts ×4
158 > }
160 > readonly onSessionConfigChangedCalls: { session: URI; values: Record<string, unknown> }[] = [];
161 > onSessionConfigChanged(session: URI, values: Record<string, unknown>): void {
162 > this.onSessionConfigChangedCalls.push({ session, values }); agentSideEffects.ts ×2
163 > }
165 > async getSessionMessages(session: URI): Promise<readonly Turn[]> {
166 > const subagentInfo = parseSubagentSessionUri(session); historyRecordFixtures.ts ×5
167 > if (subagentInfo) {
168 > return buildSubagentTurnsFromHistory(this.sessionMessages, subagentInfo.toolCallId, session.toString()); historyRecordFixtures.ts ×10
169 > }
170 > return buildTurnsFromHistory(this.sessionMessages); historyRecordFixtures.ts ×5
171 > }
173 > async disposeSession(session: URI): Promise<void> {
174 > this.disposeSessionCalls.push(session); agentService.ts ×4
175 > this._sessions.delete(AgentSession.id(session));
176 > }
178 > async releaseSession(session: URI): Promise<void> {
179 > // Non-destructive: record the call but keep the session in the catalog agentService.ts ×10
180 > // so a later restore/resume still finds its durable data.
181 > this.releaseSessionCalls.push(session);
182 > }
184 > async abortSession(session: URI): Promise<void> {
185 > this.abortSessionCalls.push(session); agentSideEffects.ts ×5
186 > }
188 > async truncateSession(session: URI, turnId?: string, chat?: URI): Promise<void> {
189 > this.truncateSessionCalls.push({ session, turnId, chat }); agentSideEffects.ts ×4
190 > }
192 > respondToPermissionRequest(requestId: string, approved: boolean): void {
193 > this.respondToPermissionCalls.push({ requestId, approved }); mockAgent.ts ×1
194 > }
196 > respondToUserInputRequest(): void {
197 // no-op for tests
198 }
200 > async changeModel(session: URI, model: ModelSelection, chat?: URI): Promise<void> {
201 > this.changeModelCalls.push({ session, model, chat }); mockAgent.ts ×1
202 > }
204 > async changeAgent(session: URI, agent: AgentSelection | undefined, chat?: URI): Promise<void> {
205 > this.changeAgentCalls.push({ session, agent, chat }); agentSideEffects.ts ×3
206 > }
208 > /**
209 > * Create an additional (peer) chat. The base mock is single-chat and
210 > * rejects; multi-chat test subclasses override this.
211 > */
212 > async createChat(_session: URI, _chat: URI, _options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> {
213 > throw new Error(`Agent ${this.id} does not support multiple chats`); mockAgent.ts ×1
214 > }
216 > /** Dispose an additional (peer) chat. Overridden by multi-chat subclasses. */
217 > async disposeChat(_session: URI, _chat: URI): Promise<void> { }
218 >
219 > /**
220 > * Map an already-resolved chat URI to the `(session, chat)` pair the
221 > * mock records calls against (mirroring the real agents).
222 > */
223 > private _resolveChatTarget(chat: URI): { session: URI; chat: URI } {
224 > const parsed = parseChatUri(chat); mockAgent.ts ×2
225 > if (!parsed) {
226 throw new Error(`Mock agent chat operation requires an AHP chat URI: ${chat.toString()}`);
227 }
228 > return { session: URI.parse(parsed.session), chat: URI.parse(chat.toString()) }; mockAgent.ts ×2
229 > }
231 > readonly chats: IAgentChats = {
232 > createChat: (chatUri: URI, options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> => {
233 > const { session, chat } = this._resolveChatTarget(chatUri); mockAgent.ts ×1
234 > return this.createChat(session, chat, options);
235 > },
236 > fork: (chatUri: URI, source: IAgentCreateChatForkSource, options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> => { mockAgent.ts ×31
237 > const { session, chat } = this._resolveChatTarget(chatUri); agentService.ts ×1
238 > return this.createChat(session, chat, { ...options, fork: source });
239 > },
240 > disposeChat: (chatUri: URI): Promise<void> => { mockAgent.ts ×31
241 > const { session, chat } = this._resolveChatTarget(chatUri); mockAgent.ts ×1
242 > return this.disposeChat(session, chat);
243 > },
244 > sendMessage: (chatUri: URI, prompt: string, _workingDirectory: URI | undefined, attachments?: readonly MessageAttachment[], turnId?: string, senderClientId?: string): Promise<void> => { mockAgent.ts ×31
245 > const { session, chat } = this._resolveChatTarget(chatUri); agentSideEffects.ts ×1
246 > return this.sendMessage(session, chat, prompt, attachments, turnId, senderClientId);
247 > },
248 > abort: (chat: URI): Promise<void> => { mockAgent.ts ×31
249 > const { session } = this._resolveChatTarget(chat); agentSideEffects.ts ×5
250 > return this.abortSession(session);
251 > },
252 > changeModel: (chatUri: URI, model: ModelSelection): Promise<void> => { mockAgent.ts ×31
253 > const { session, chat } = this._resolveChatTarget(chatUri); agentSideEffects.ts ×1
254 > return this.changeModel(session, model, chat);
255 > },
256 > changeAgent: (chatUri: URI, agent: AgentSelection | undefined): Promise<void> => { mockAgent.ts ×31
257 > const { session, chat } = this._resolveChatTarget(chatUri); agentSideEffects.ts ×3
258 > return this.changeAgent(session, agent, chat);
259 > },
260 > getMessages: (chat: URI): Promise<readonly Turn[]> => { mockAgent.ts ×31
261 > return this.getSessionMessages(chat); mockAgent.ts ×1
262 > },
265 > async authenticate(resource: string, token: string): Promise<boolean> {
266 > this.authenticateCalls.push({ resource, token }); agentHostAuthenticationService.ts ×2
267 > return true;
268 > }
270 > getCustomizations(): Customization[] {
271 > return this.customizations; mockAgent.ts ×2
272 > }
274 > syncClientCustomizations(session: URI, clientId: string, customizations: ClientPluginCustomization[]): ISyncedCustomization[] {
275 > this.setClientCustomizationsCalls.push({ clientId, customizations }); mockAgent.ts ×2
276 > const results: ISyncedCustomization[] = customizations.map(c => ({
277 > customization: { mockAgent.ts ×1
278 > ...c,
279 > load: { kind: CustomizationLoadStatus.Loaded },
280 > },
281 > })); mockAgent.ts ×2
282 > this._onDidSessionProgress.fire({
283 > kind: 'action',
284 > resource: session,
285 > action: {
286 > type: ActionType.SessionCustomizationsChanged,
287 > customizations: results.map(result => result.customization),
288 > },
289 > });
290 > return results;
291 > }
293 > getOrCreateActiveClient(session: URI, client: { readonly clientId: string; readonly displayName?: string }): IActiveClient {
294 > const self = this; agentSideEffects.ts ×2
295 > let tools: readonly ToolDefinition[] = [];
296 > let customizations: readonly ClientPluginCustomization[] = [];
297 > return {
298 > clientId: client.clientId,
299 > displayName: client.displayName,
300 > get tools() { return tools; },
301 > set tools(value: readonly ToolDefinition[]) {
302 > tools = value;
303 > self.setClientToolsCalls.push({ clientId: client.clientId, tools: value });
304 > },
305 > get customizations() { return customizations; },
306 > set customizations(value: readonly ClientPluginCustomization[]) {
307 > customizations = value;
308 > self.syncClientCustomizations(session, client.clientId, [...value]);
309 > },
310 > };
311 > }
313 > removeActiveClient(_session: URI, clientId: string): void {
314 > this.removeActiveClientCalls.push({ clientId }); agentSideEffects.ts ×1
315 > }
317 > onClientToolCallComplete(session: URI, chat: URI, toolCallId: string, result: ToolCallResult): void {
318 > this.clientToolCallCompleteCalls.push({ session, chat, toolCallId, result }); agentSideEffects.ts ×4
319 > }
321 > async shutdown(): Promise<void> { }
322 >
323 > /**
324 > * Fires an {@link AgentSignal} on this agent.
325 > */
326 > fireProgress(signal: AgentSignal): void {
327 > this._onDidSessionProgress.fire(signal); mockAgent.ts ×1
328 > }
330 > /**
331 > * Looks up the active turn id captured from the most recent
332 > * {@link sendMessage} call for a given session. Returns `undefined` if
333 > * the session has no active turn yet (e.g. tests that fire progress
334 > * without first calling sendMessage).
335 > */
336 > getActiveTurnId(session: URI): string | undefined {
337 return this._activeTurnIds.get(uriKey(session));
338 }
340 > fireCustomizationsChange(): void {
341 > this._onDidCustomizationsChange.fire(); mockAgent.ts ×1
342 > }
344 > dispose(): void {
345 > this._onDidSessionProgress.dispose(); mockAgent.ts ×1
346 > this._onDidSendMessage.dispose();
347 > this._onDidCustomizationsChange.dispose();
348 > }
350 >
351 > /**
352 > * Well-known URI of a pre-existing session seeded in {@link ScriptedMockAgent}.
353 > * This session appears in `listSessions()` and has message history via
354 > * `getSessionMessages()`, but was never created through the server's
355 > * `handleCreateSession`. It simulates a session from a previous server
356 > * lifetime for testing the restore-on-subscribe path.
357 > */
358 > export const PRE_EXISTING_SESSION_URI = AgentSession.uri('mock', 'pre-existing-session');
359 >
360 > export class ScriptedMockAgent implements IAgent {
361 > readonly id: AgentProvider = 'mock';
362 >
363 > private readonly _onDidSessionProgress = new Emitter<AgentSignal>();
364 > readonly onDidSessionProgress = this._onDidSessionProgress.event;
365 > private readonly _models = observableValue<readonly IAgentModelInfo[]>(this, [{ provider: 'mock', id: 'mock-model', name: 'Mock Model', maxContextWindow: 128000, supportsVision: false }]);
366 > readonly models = this._models;
367 >
368 > private readonly _sessions = new Map<string, URI>();
369 > private _nextId = 1;
370 >
371 > /**
372 > * Message history for the pre-existing session: a single user→assistant
373 > * turn with a tool call.
374 > */
375 > private readonly _preExistingMessages: IHistoryRecord[] = [
376 > { type: 'message', role: 'user', session: PRE_EXISTING_SESSION_URI, messageId: 'h-msg-1', content: 'What files are here?' },
377 > { type: 'tool_start', session: PRE_EXISTING_SESSION_URI, toolCallId: 'h-tc-1', toolName: 'list_files', displayName: 'List Files', invocationMessage: 'Listing files...' },
378 > { type: 'tool_complete', session: PRE_EXISTING_SESSION_URI, toolCallId: 'h-tc-1', result: { pastTenseMessage: 'Listed files', content: [{ type: ToolResultContentType.Text, text: 'file1.ts\nfile2.ts' }], success: true } satisfies ToolCallResult },
379 > { type: 'message', role: 'assistant', session: PRE_EXISTING_SESSION_URI, messageId: 'h-msg-2', content: 'Here are the files: file1.ts and file2.ts' },
380 > ];
381 >
382 > // Track pending permission requests
383 > private readonly _pendingPermissions = new Map<string, (approved: boolean) => void>();
384 > // Track the active turn ID per session, captured from sendMessage().
385 > private readonly _activeTurnIds = new Map<string, string>();
386 > // Track pending abort callbacks for slow responses
387 > private readonly _pendingAborts = new Map<string, () => void>();
388 >
389 > constructor() {
390 > // Seed the pre-existing session so it appears in listSessions() mockAgent.ts ×5
391 > this._sessions.set(AgentSession.id(PRE_EXISTING_SESSION_URI), PRE_EXISTING_SESSION_URI);
392 >
393 > // Allow integration tests to seed additional pre-existing sessions across
394 > // server restarts via env var. The value is a comma-separated list of
395 > // session URIs (e.g. `mock://pre-1,mock://pre-2`).
396 > const seeded = process.env['VSCODE_AGENT_HOST_MOCK_SEED_SESSIONS'];
397 > if (seeded) {
398 for (const raw of seeded.split(',')) {
399 const trimmed = raw.trim();
400 if (!trimmed) {
401 continue;
402 }
403 const uri = URI.parse(trimmed);
404 this._sessions.set(AgentSession.id(uri), uri);
405 }
406 }
409 > getDescriptor(): IAgentDescriptor {
410 return { provider: 'mock', displayName: 'Mock Agent', description: 'Scripted test agent' };
411 }
413 > getProtectedResources(): IAuthorizationProtectedResourceMetadata[] {
414 return [];
415 }
417 > async listSessions(): Promise<IAgentSessionMetadata[]> {
418 > return [...this._sessions.values()].map(s => ({ mockAgent.ts ×5
419 > session: s,
420 > startTime: Date.now(),
421 > modifiedTime: Date.now(),
422 > project: mockProject(this.id),
423 > summary: s.toString() === PRE_EXISTING_SESSION_URI.toString() ? 'Pre-existing session' : undefined,
424 > }));
425 > }
427 > async getSessionMetadata(session: URI): Promise<IAgentSessionMetadata | undefined> {
428 if (!this._sessions.has(AgentSession.id(session))) {
429 return undefined;
430 }
431 return {
432 session,
433 startTime: Date.now(),
434 modifiedTime: Date.now(),
435 project: mockProject(this.id),
436 summary: session.toString() === PRE_EXISTING_SESSION_URI.toString() ? 'Pre-existing session' : undefined,
437 };
438 }
440 > async createSession(config?: IAgentCreateSessionConfig): Promise<IAgentCreateSessionResult> {
441 > const session = config?.session ?? AgentSession.uri('mock', `mock-session-${this._nextId++}`); mockAgent.ts ×5
442 > const rawId = AgentSession.id(session);
443 > this._sessions.set(rawId, session);
444 > return { session, project: mockProject(this.id) };
445 > }
447 > async resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise<ResolveSessionConfigResult> {
448 const isolation = params.config?.isolation === 'folder' || params.config?.isolation === 'worktree' ? params.config.isolation : 'worktree';
449 const branch = isolation === 'worktree' && typeof params.config?.branch === 'string' ? params.config.branch : 'main';
450 return {
451 schema: {
452 type: 'object',
453 properties: {
454 isolation: {
455 type: 'string',
456 title: 'Isolation',
457 description: 'Where the mock agent should make changes',
458 enum: ['folder', 'worktree'],
459 enumLabels: ['Folder', 'Worktree'],
460 default: 'worktree',
461 },
462 branch: {
463 type: 'string',
464 title: 'Branch',
465 description: 'Base branch to work from',
466 enum: ['main'],
467 enumLabels: ['main'],
468 default: 'main',
469 enumDynamic: isolation === 'worktree',
470 readOnly: isolation === 'folder',
471 },
472 },
473 },
474 values: { isolation, branch },
475 };
476 }
478 > async sessionConfigCompletions(params: IAgentSessionConfigCompletionsParams): Promise<SessionConfigCompletionsResult> {
479 if (params.property !== 'branch') {
480 return { items: [] };
481 }
482 const query = params.query?.toLowerCase() ?? '';
483 const branches = ['main', 'feature/config', 'release'].filter(branch => branch.toLowerCase().includes(query));
484 return { items: branches.map(branch => ({ value: branch, label: branch })) };
485 }
487 > async sendMessage(session: URI, chat: URI, prompt: string, _attachments?: readonly MessageAttachment[], turnId?: string): Promise<void> {
488 if (turnId) {
489 this._activeTurnIds.set(uriKey(session), turnId);
490 this._activeTurnIds.set(uriKey(chat), turnId);
491 }
492 const { sessionStr, turnId: tid } = this._ctx(chat);
493 switch (prompt) {
494 case 'hello':
495 this._fireSequence([
496 _markdown(chat, sessionStr, tid, 'Hello, world!'),
497 _idle(chat, sessionStr, tid),
498 ]);
499 break;
500
501 case 'use-tool':
502 this._fireSequence([
503 ..._toolStart(chat, sessionStr, tid, 'tc-1', 'echo_tool', 'Echo Tool', 'Running echo tool...'),
504 _toolComplete(chat, sessionStr, tid, 'tc-1', { pastTenseMessage: 'Ran echo tool', content: [{ type: ToolResultContentType.Text, text: 'echoed' }], success: true }),
505 _markdown(chat, sessionStr, tid, 'Tool done.'),
506 _idle(chat, sessionStr, tid),
507 ]);
508 break;
509
510 case 'error':
511 this._fireSequence([
512 _error(chat, sessionStr, tid, 'test_error', 'Something went wrong'),
513 ]);
514 break;
515
516 case 'permission': {
517 // Fire tool_start to create the tool, then pending_confirmation to request confirmation
518 (async () => {
519 await timeout(10);
520 for (const s of _toolStart(chat, sessionStr, tid, 'tc-perm-1', 'shell', 'Shell', 'Run a test command')) {
521 this._onDidSessionProgress.fire(s);
522 }
523 await timeout(5);
524 this._onDidSessionProgress.fire(_pendingConfirmation(chat, 'tc-perm-1', 'Run a test command', { toolInput: 'echo test', confirmationTitle: 'Run a test command' }));
525 })();
526 this._pendingPermissions.set('tc-perm-1', (approved) => {
527 if (approved) {
528 this._fireSequence([
529 _markdown(chat, sessionStr, tid, 'Allowed.'),
530 _idle(chat, sessionStr, tid),
531 ]);
532 }
533 });
534 break;
535 }
536
537 case 'write-file': {
538 // Fire tool_start + pending_confirmation with write permission for a regular file (should be auto-approved)
539 (async () => {
540 await timeout(10);
541 for (const s of _toolStart(chat, sessionStr, tid, 'tc-write-1', 'create', 'Create File', 'Create file')) {
542 this._onDidSessionProgress.fire(s);
543 }
544 await timeout(5);
545 this._onDidSessionProgress.fire(_pendingConfirmation(chat, 'tc-write-1', 'Write src/app.ts', { permissionKind: 'write', permissionPath: '/workspace/src/app.ts' }));
546 // Auto-approved writes resolve immediately — complete the tool and turn
547 await timeout(10);
548 this._fireSequence([
549 _toolComplete(chat, sessionStr, tid, 'tc-write-1', { pastTenseMessage: 'Wrote file', content: [{ type: ToolResultContentType.Text, text: 'ok' }], success: true }),
550 _idle(chat, sessionStr, tid),
551 ]);
552 })();
553 break;
554 }
555
556 case 'write-env': {
557 // Fire tool_start + pending_confirmation with write permission for .env (should be blocked)
558 (async () => {
559 await timeout(10);
560 for (const s of _toolStart(chat, sessionStr, tid, 'tc-write-env-1', 'create', 'Create File', 'Create file')) {
561 this._onDidSessionProgress.fire(s);
562 }
563 await timeout(5);
564 this._onDidSessionProgress.fire(_pendingConfirmation(chat, 'tc-write-env-1', 'Write .env', { permissionKind: 'write', permissionPath: '/workspace/.env', confirmationTitle: 'Write .env' }));
565 })();
566 this._pendingPermissions.set('tc-write-env-1', (approved) => {
567 if (approved) {
568 this._fireSequence([
569 _toolComplete(chat, sessionStr, tid, 'tc-write-env-1', { pastTenseMessage: 'Wrote .env', content: [{ type: ToolResultContentType.Text, text: 'ok' }], success: true }),
570 _idle(chat, sessionStr, tid),
571 ]);
572 }
573 });
574 break;
575 }
576
577 case 'run-safe-command': {
578 // Fire tool_start + pending_confirmation with shell permission for an allowed command (should be auto-approved)
579 (async () => {
580 await timeout(10);
581 for (const s of _toolStart(chat, sessionStr, tid, 'tc-shell-1', 'bash', 'Run Command', 'Run command')) {
582 this._onDidSessionProgress.fire(s);
583 }
584 await timeout(5);
585 this._onDidSessionProgress.fire(_pendingConfirmation(chat, 'tc-shell-1', 'ls -la', { permissionKind: 'shell', toolInput: 'ls -la' }));
586 // Auto-approved shell commands resolve immediately
587 await timeout(10);
588 this._fireSequence([
589 _toolComplete(chat, sessionStr, tid, 'tc-shell-1', { pastTenseMessage: 'Ran command', content: [{ type: ToolResultContentType.Text, text: 'file1.ts\nfile2.ts' }], success: true }),
590 _idle(chat, sessionStr, tid),
591 ]);
592 })();
593 break;
594 }
595
596 case 'run-dangerous-command': {
597 // Fire tool_start + pending_confirmation with shell permission for a denied command (should require confirmation)
598 (async () => {
599 await timeout(10);
600 for (const s of _toolStart(chat, sessionStr, tid, 'tc-shell-deny-1', 'bash', 'Run Command', 'Run command')) {
601 this._onDidSessionProgress.fire(s);
602 }
603 await timeout(5);
604 this._onDidSessionProgress.fire(_pendingConfirmation(chat, 'tc-shell-deny-1', 'rm -rf /', { permissionKind: 'shell', toolInput: 'rm -rf /', confirmationTitle: 'Run in terminal' }));
605 })();
606 this._pendingPermissions.set('tc-shell-deny-1', (approved) => {
607 if (approved) {
608 this._fireSequence([
609 _toolComplete(chat, sessionStr, tid, 'tc-shell-deny-1', { pastTenseMessage: 'Ran command', content: [{ type: ToolResultContentType.Text, text: '' }], success: true }),
610 _idle(chat, sessionStr, tid),
611 ]);
612 }
613 });
614 break;
615 }
616
617 case 'orphan-confirmation': {
618 // Regression scenario for a `pending_confirmation` that
619 // arrives without an active protocol turn (the session would
620 // otherwise hang forever). Reproduces a hook-triggered
621 // continuation that runs *after* the protocol turn has
622 // already completed:
623 // 1. A tool runs and the turn completes — the state manager
624 // no longer has an active turn.
625 // 2. The continuation dispatches a new tool with an empty
626 // turnId and emits `pending_confirmation` while there is
627 // no active turn.
628 // The read targets a path inside the working directory, so the
629 // host auto-approves it and calls `respondToPermissionRequest`,
630 // which resolves the callback below and lets the session
631 // continue. Without the fix the signal is dropped, the callback
632 // never fires, and the session hangs.
633 (async () => {
634 await timeout(10);
635 for (const s of _toolStart(chat, sessionStr, tid, 'tc-orphan-initial', 'bash', 'Run Command', 'Run command')) {
636 this._onDidSessionProgress.fire(s);
637 }
638 await timeout(5);
639 this._onDidSessionProgress.fire(_toolComplete(chat, sessionStr, tid, 'tc-orphan-initial', { pastTenseMessage: 'Ran command', content: [{ type: ToolResultContentType.Text, text: 'ok' }], success: true }));
640 await timeout(5);
641 // Complete the turn — the state manager clears the active turn.
642 this._onDidSessionProgress.fire(_idle(chat, sessionStr, tid));
643
644 // Hook-triggered continuation: a new tool starts with an
645 // empty turnId and `pending_confirmation` arrives while
646 // there is no active turn.
647 await timeout(10);
648 for (const s of _toolStart(chat, sessionStr, '', 'tc-orphan', 'view', 'Read', 'Read file')) {
649 this._onDidSessionProgress.fire(s);
650 }
651 await timeout(5);
652 this._onDidSessionProgress.fire(_pendingConfirmation(chat, 'tc-orphan', 'Read file', { permissionKind: 'read', permissionPath: '/workspace/file.ts' }));
653 })();
654 this._pendingPermissions.set('tc-orphan', (approved) => {
655 if (approved) {
656 this._fireSequence([
657 _toolComplete(chat, sessionStr, tid, 'tc-orphan', { pastTenseMessage: 'Read file', content: [{ type: ToolResultContentType.Text, text: 'contents' }], success: true }),
658 _markdown(chat, sessionStr, tid, 'continued-after-hook'),
659 _idle(chat, sessionStr, tid),
660 ]);
661 }
662 });
663 break;
664 }
665
666 case 'with-usage':
667 this._fireSequence([
668 _markdown(chat, sessionStr, tid, 'Usage response.'),
669 _usage(chat, sessionStr, tid, { inputTokens: 100, outputTokens: 50, model: 'mock-model', _meta: { cost: 0.5 } }),
670 _idle(chat, sessionStr, tid),
671 ]);
672 break;
673
674 case 'with-reasoning': {
675 const initialReasoning = _reasoning(chat, sessionStr, tid, 'Let me think');
676 const partId = initialReasoning.action.type === ActionType.ChatResponsePart
677 && hasKey(initialReasoning.action.part, { id: true })
678 ? initialReasoning.action.part.id
679 : '';
680 this._fireSequence([
681 initialReasoning,
682 _action(chat, {
683 type: ActionType.ChatReasoning,
684 turnId: tid,
685 partId,
686 content: ' about this...',
687 }),
688 _markdown(chat, sessionStr, tid, 'Reasoned response.'),
689 _idle(chat, sessionStr, tid),
690 ]);
691 break;
692 }
693
694 case 'with-title':
695 this._fireSequence([
696 _markdown(chat, sessionStr, tid, 'Title response.'),
697 _titleChanged(session, sessionStr, MOCK_AUTO_TITLE),
698 _idle(chat, sessionStr, tid),
699 ]);
700 break;
701
702 case 'slow': {
703 // Slow response for cancel testing — fires delta after a long delay
704 const timer = setTimeout(() => {
705 const ctx = this._ctx(chat);
706 this._fireSequence([
707 _markdown(chat, ctx.sessionStr, ctx.turnId, 'Slow response.'),
708 _idle(chat, ctx.sessionStr, ctx.turnId),
709 ]);
710 }, 5000);
711 this._pendingAborts.set(session.toString(), () => clearTimeout(timer));
712 break;
713 }
714
715 case 'client-tool': {
716 // Fires tool_start with toolClientId followed by pending_confirmation
717 // (without confirmationTitle) to simulate a client-provided tool
718 // that is ready for execution. The real SDK handler fires
719 // tool_ready once its deferred is in place.
720 (async () => {
721 await timeout(10);
722 // Client tools don't get auto-ready — toolStart with toolClientId only emits tool_start
723 this._onDidSessionProgress.fire(_action(chat, {
724 type: ActionType.ChatToolCallStart,
725 turnId: tid,
726 toolCallId: 'tc-client-1',
727 toolName: 'runTests',
728 displayName: 'Run Tests',
729 contributor: { kind: ToolCallContributorKind.Client, clientId: 'test-client-tool' },
730 }));
731 await timeout(5);
732 this._onDidSessionProgress.fire(_pendingConfirmation(chat, 'tc-client-1', 'Running tests...', { toolInput: '{}' }));
733 })();
734 // The tool stays pending — the client is responsible for dispatching toolCallComplete.
735 // Once complete, fire a response delta and idle.
736 this._pendingPermissions.set('tc-client-1', () => {
737 this._fireSequence([
738 _markdown(chat, sessionStr, tid, 'Client tool done.'),
739 _idle(chat, sessionStr, tid),
740 ]);
741 });
742 break;
743 }
744
745 case 'client-tool-with-permission': {
746 // Fires tool_start with toolClientId followed by a permission request.
747 (async () => {
748 await timeout(10);
749 this._onDidSessionProgress.fire(_action(chat, {
750 type: ActionType.ChatToolCallStart,
751 turnId: tid,
752 toolCallId: 'tc-client-perm-1',
753 toolName: 'runTests',
754 displayName: 'Run Tests',
755 contributor: { kind: ToolCallContributorKind.Client, clientId: 'test-client-tool' },
756 }));
757 await timeout(5);
758 this._onDidSessionProgress.fire(_pendingConfirmation(chat, 'tc-client-perm-1', 'Run tests on project', { confirmationTitle: 'Allow Run Tests?' }));
759 })();
760 this._pendingPermissions.set('tc-client-perm-1', (approved) => {
761 if (approved) {
762 this._fireSequence([
763 _toolComplete(chat, sessionStr, tid, 'tc-client-perm-1', { pastTenseMessage: 'Ran tests', content: [{ type: ToolResultContentType.Text, text: 'all passed' }], success: true }),
764 _markdown(chat, sessionStr, tid, 'Permission granted, tool done.'),
765 _idle(chat, sessionStr, tid),
766 ]);
767 }
768 });
769 break;
770 }
771
772 case 'subagent': {
773 // Spawns a subagent: parent `task` tool starts (emits start +
774 // auto-ready as a pair), then `subagent_started` creates the
775 // child session, then an inner tool runs in the child session
776 // (routed via `parentToolCallId`).
777 this._fireSequence([
778 ..._toolStart(chat, sessionStr, tid, 'tc-task-1', 'task', 'Task', 'Spawning subagent', { toolKind: 'subagent', subagentAgentName: 'explore', subagentDescription: 'Explore' }),
779 { kind: 'subagent_started', chat, toolCallId: 'tc-task-1', agentName: 'explore', agentDisplayName: 'Explore', agentDescription: 'Exploration helper' },
780 ..._toolStart(chat, sessionStr, tid, 'tc-inner-1', 'echo_tool', 'Echo Tool', 'Inner tool running...', { parentToolCallId: 'tc-task-1' }),
781 _toolComplete(chat, sessionStr, tid, 'tc-inner-1', { pastTenseMessage: 'Ran inner tool', content: [{ type: ToolResultContentType.Text, text: 'inner-ok' }], success: true }, 'tc-task-1'),
782 { kind: 'subagent_completed', chat, toolCallId: 'tc-task-1' },
783 _toolComplete(chat, sessionStr, tid, 'tc-task-1', { pastTenseMessage: 'Subagent done', content: [{ type: ToolResultContentType.Text, text: 'task-ok' }], success: true }),
784 _markdown(chat, sessionStr, tid, 'Subagent finished.'),
785 _idle(chat, sessionStr, tid),
786 ]);
787 break;
788 }
789
790 default:
791 if (prompt.startsWith('terminal-edit:')) {
792 // Test prompt: simulate a terminal command that edits a file on disk
793 // without emitting any ToolResultFileEditContent. The test relies on the
794 // git-driven diff path to pick this up. Format: `terminal-edit:<absPath>`.
795 const filePath = prompt.slice('terminal-edit:'.length);
796 void (async () => {
797 for (const s of _toolStart(chat, sessionStr, tid, 'tc-term-edit-1', 'bash', 'Run Command', 'Edit file via shell')) {
798 this._onDidSessionProgress.fire(s);
799 }
800 const fs = await import('fs/promises');
801 await fs.writeFile(filePath, 'edited-from-terminal\n');
802 this._fireSequence([
803 _toolComplete(chat, sessionStr, tid, 'tc-term-edit-1', { pastTenseMessage: 'Edited file', content: [{ type: ToolResultContentType.Text, text: 'ok' }], success: true }),
804 _idle(chat, sessionStr, tid),
805 ]);
806 })().catch(err => {
807 // Surface failures deterministically — an unhandled rejection
808 // would make the test suite flaky.
809 this._fireSequence([
810 _markdown(chat, sessionStr, tid, 'terminal-edit failed: ' + (err instanceof Error ? err.message : String(err))),
811 _idle(chat, sessionStr, tid),
812 ]);
813 });
814 break;
815 }
816 this._fireSequence([
817 _markdown(chat, sessionStr, tid, 'Unknown prompt: ' + prompt),
818 _idle(chat, sessionStr, tid),
819 ]);
820 break;
821 }
822 }
824 > setPendingMessages(chat: URI, steeringMessage: PendingMessage | undefined, _queuedMessages: readonly PendingMessage[]): void {
825 // When steering is set, consume it on the next tick
826 if (steeringMessage) {
827 timeout(20).then(() => {
828 this._onDidSessionProgress.fire({ kind: 'steering_consumed', chat: isAhpChatChannel(chat.toString()) ? chat : URI.parse(buildDefaultChatUri(chat)), id: steeringMessage.id });
829 });
830 }
831 }
833 > getOrCreateActiveClient(_session: URI, client: { readonly clientId: string; readonly displayName?: string }): IActiveClient {
834 let tools: readonly ToolDefinition[] = [];
835 let customizations: readonly ClientPluginCustomization[] = [];
836 return {
837 clientId: client.clientId,
838 displayName: client.displayName,
839 get tools() { return tools; },
840 set tools(value: readonly ToolDefinition[]) { tools = value; },
841 get customizations() { return customizations; },
842 set customizations(value: readonly ClientPluginCustomization[]) { customizations = value; },
843 };
844 }
846 > removeActiveClient(): void { }
847 >
848 > private didCompleteToolCalls = new Set<string>();
849 >
850 > onClientToolCallComplete(session: URI, chat: URI, toolCallId: string, result: ToolCallResult): void {
851 // The mock's event model is chat-channel oriented (sendMessage fires
852 // every turn signal on the chat URI). Emit the completion on the chat
853 // channel the tool was started on so the parked turn callback — which
854 // captured that same chat URI — resolves on the right channel.
855 const key = `${chat.toString()}:${toolCallId}`;
856 if (this.didCompleteToolCalls.has(key)) {
857 return;
858 }
859 this.didCompleteToolCalls.add(key);
860 // Fire tool_complete action signal and resolve any pending callback.
861 const { sessionStr, turnId } = this._ctx(chat);
862 this._onDidSessionProgress.fire(_toolComplete(chat, sessionStr, turnId, toolCallId, result));
863 const callback = this._pendingPermissions.get(toolCallId);
864 if (callback) {
865 this._pendingPermissions.delete(toolCallId);
866 callback(true);
867 }
868 }
870 > async getSessionMessages(session: URI): Promise<readonly Turn[]> {
871 const subagentInfo = parseSubagentSessionUri(session);
872 if (subagentInfo) {
873 return buildSubagentTurnsFromHistory(this._preExistingMessages, subagentInfo.toolCallId, session.toString());
874 }
875 // Restore addresses the default chat by its channel URI; normalize it
876 // back to the session URI (mirroring the real agents' getSessionMessages).
877 const parsed = parseChatUri(session);
878 const normalized = parsed && buildDefaultChatUri(parsed.session) === session.toString() ? URI.parse(parsed.session) : session;
879 if (normalized.toString() === PRE_EXISTING_SESSION_URI.toString()) {
880 return buildTurnsFromHistory(this._preExistingMessages);
881 }
882 return [];
883 }
885 > async disposeSession(session: URI): Promise<void> {
886 this._sessions.delete(AgentSession.id(session));
887 }
889 > async abortSession(session: URI): Promise<void> {
890 const callback = this._pendingAborts.get(session.toString());
891 if (callback) {
892 this._pendingAborts.delete(session.toString());
893 callback();
894 }
895 }
897 > async changeModel(_session: URI, _model: ModelSelection): Promise<void> {
898 // Mock agent doesn't track model state
899 }
901 > /**
902 > * Map an already-resolved chat URI to the `(session, chat)` pair the
903 > * scripted mock's per-chat context is keyed by.
904 > */
905 > private _resolveChatTarget(chat: URI): { session: URI; chat: URI } {
906 const parsed = parseChatUri(chat);
907 if (!parsed) {
908 throw new Error(`Scripted mock chat operation requires an AHP chat URI: ${chat.toString()}`);
909 }
910 return { session: URI.parse(parsed.session), chat: URI.parse(chat.toString()) };
911 }
913 > readonly chats: IAgentChats = {
914 > createChat: (_chat: URI, _options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> => {
915 > throw new Error('Scripted mock agent does not support multiple chats'); mockAgent.ts ×8
916 > },
917 > fork: (_chat: URI, _source: IAgentCreateChatForkSource, _options?: IAgentCreateChatOptions): Promise<IAgentCreateChatResult | void> => { mockAgent.ts ×55
918 > throw new Error('Scripted mock agent does not support chat forking'); mockAgent.ts ×8
919 > },
920 > disposeChat: (_chat: URI): Promise<void> => { mockAgent.ts ×55
921 > return Promise.resolve(); mockAgent.ts ×8
922 > },
923 > sendMessage: (chatUri: URI, prompt: string, _workingDirectory: URI | undefined, attachments?: readonly MessageAttachment[], turnId?: string, _senderClientId?: string): Promise<void> => { mockAgent.ts ×55
924 > const { session, chat } = this._resolveChatTarget(chatUri); mockAgent.ts ×8
925 > return this.sendMessage(session, chat, prompt, attachments, turnId);
926 > },
927 > abort: (chat: URI): Promise<void> => { mockAgent.ts ×55
928 > const { session } = this._resolveChatTarget(chat); mockAgent.ts ×8
929 > return this.abortSession(session);
930 > },
931 > changeModel: (chat: URI, model: ModelSelection): Promise<void> => { mockAgent.ts ×55
932 > const { session } = this._resolveChatTarget(chat); mockAgent.ts ×8
933 > return this.changeModel(session, model);
934 > },
935 > changeAgent: (_chat: URI, _agent: AgentSelection | undefined): Promise<void> => { mockAgent.ts ×55
936 > // Scripted mock does not track agent selection. mockAgent.ts ×8
937 > return Promise.resolve();
938 > },
939 > getMessages: (chat: URI): Promise<readonly Turn[]> => { mockAgent.ts ×55
940 > return this.getSessionMessages(chat); mockAgent.ts ×8
941 > },
943 >
944 > async truncateSession(_session: URI, _turnId?: string): Promise<void> {
945 // Mock agent accepts truncation without side effects
946 }
948 > respondToPermissionRequest(toolCallId: string, approved: boolean): void {
949 const callback = this._pendingPermissions.get(toolCallId);
950 if (callback) {
951 this._pendingPermissions.delete(toolCallId);
952 callback(approved);
953 }
954 }
956 > respondToUserInputRequest(): void {
957 // no-op for tests
958 }
960 > async authenticate(_resource: string, _token: string): Promise<boolean> {
961 return true;
962 }
964 > async shutdown(): Promise<void> { }
965 >
966 > dispose(): void {
967 > this._onDidSessionProgress.dispose(); mockAgent.ts ×5
968 > }
970 > /**
971 > * Fires a sequence of {@link AgentSignal}s with staggered 10 ms delays
972 > * so the state manager processes them in order.
973 > */
974 > private _fireSequence(signals: AgentSignal[]): void {
975 let delay = 0;
976 for (const signal of signals) {
977 delay += 10;
978 setTimeout(() => this._onDidSessionProgress.fire(signal), delay);
979 }
980 }
982 > /** Builds the session-string + turnId context for signal construction. */
983 > private _ctx(session: URI): { sessionStr: string; turnId: string } {
984 return {
985 sessionStr: session.toString(),
986 turnId: this._activeTurnIds.get(uriKey(session)) ?? 'mock-turn',
987 };
988 }
990 >
991 > // =============================================================================
992 > // Test-event helpers
993 > // =============================================================================
994 >
995 > // =============================================================================
996 > // Signal factory helpers
997 > // =============================================================================
998 >
999 > let _mockPartIdCounter = 0;
1000 >
1001 > /** Wraps a session action into an {@link IAgentActionSignal}. */
1002 function _action(session: URI, action: import('../../common/state/sessionActions.js').SessionAction | import('../../common/state/sessionActions.js').ChatAction, parentToolCallId?: string): IAgentActionSignal {
1003 return { kind: 'action', resource: session, action, parentToolCallId };
1004 }
1006 > /** Creates a markdown {@link ResponsePartKind.Markdown} response part signal. */
1007 function _markdown(session: URI, sessionStr: string, turnId: string, content: string, parentToolCallId?: string): IAgentActionSignal {
1008 return _action(session, {
1009 type: ActionType.ChatResponsePart,
1010 turnId,
1011 part: { kind: ResponsePartKind.Markdown, id: `mock-md-${++_mockPartIdCounter}`, content },
1012 }, parentToolCallId);
1013 }
1015 > /** Creates a reasoning {@link ResponsePartKind.Reasoning} response part signal. */
1016 function _reasoning(session: URI, sessionStr: string, turnId: string, content: string): IAgentActionSignal {
1017 return _action(session, {
1018 type: ActionType.ChatResponsePart,
1019 turnId,
1020 part: { kind: ResponsePartKind.Reasoning, id: `mock-rs-${++_mockPartIdCounter}`, content },
1021 });
1022 }
1024 > /** Creates a {@link ActionType.ChatTurnComplete} signal. */
1025 function _idle(session: URI, sessionStr: string, turnId: string): IAgentActionSignal {
1026 return _action(session, { type: ActionType.ChatTurnComplete, turnId, duration: 1 });
1027 }
1029 > /** Creates a {@link ActionType.ChatError} signal. */
1030 function _error(session: URI, sessionStr: string, turnId: string, errorType: string, message: string, stack?: string): IAgentActionSignal {
1031 return _action(session, { type: ActionType.ChatError, turnId, duration: 1, error: { errorType, message, stack } });
1032 }
1034 > /** Creates a {@link ActionType.SessionTitleChanged} signal. */
1035 function _titleChanged(session: URI, sessionStr: string, title: string): IAgentActionSignal {
1036 return _action(session, { type: ActionType.SessionTitleChanged, title });
1037 }
1039 > /** Creates a {@link ActionType.ChatUsage} signal. */
1040 function _usage(session: URI, sessionStr: string, turnId: string, usage: UsageInfo): IAgentActionSignal {
1041 return _action(session, { type: ActionType.ChatUsage, turnId, usage });
1042 }
1044 > /**
1045 > * Creates tool-start signals: a {@link ActionType.ChatToolCallStart} and,
1046 > * for non-client tools, an auto-ready {@link ActionType.ChatToolCallReady}.
1047 > */
1048 function _toolStart(session: URI, sessionStr: string, turnId: string, toolCallId: string, toolName: string, displayName: string, invocationMessage: StringOrMarkdown, opts?: {
1049 toolInput?: string;
1050 toolKind?: string;
1051 toolClientId?: string;
1052 subagentAgentName?: string;
1053 subagentDescription?: string;
1054 parentToolCallId?: string;
1055 }): IAgentActionSignal[] {
1056 const meta: Record<string, unknown> = {};
1057 if (opts?.toolKind) {
1058 meta.toolKind = opts.toolKind;
1059 }
1060 if (opts?.subagentAgentName) {
1061 meta.subagentAgentName = opts.subagentAgentName;
1062 }
1063 if (opts?.subagentDescription) {
1064 meta.subagentDescription = opts.subagentDescription;
1065 }
1066 const signals: IAgentActionSignal[] = [_action(session, {
1067 type: ActionType.ChatToolCallStart,
1068 turnId,
1069 toolCallId,
1070 toolName,
1071 displayName,
1072 contributor: opts?.toolClientId ? { kind: ToolCallContributorKind.Client, clientId: opts.toolClientId } : undefined,
1073 _meta: Object.keys(meta).length ? meta : undefined,
1074 }, opts?.parentToolCallId)];
1075 if (!opts?.toolClientId) {
1076 signals.push(_action(session, {
1077 type: ActionType.ChatToolCallReady,
1078 turnId,
1079 toolCallId,
1080 invocationMessage,
1081 toolInput: opts?.toolInput,
1082 confirmed: ToolCallConfirmationReason.NotNeeded,
1083 }, opts?.parentToolCallId));
1084 }
1085 return signals;
1086 }
1088 > /** Creates a {@link ActionType.ChatToolCallComplete} signal. */
1089 function _toolComplete(session: URI, sessionStr: string, turnId: string, toolCallId: string, result: ToolCallResult, parentToolCallId?: string): IAgentActionSignal {
1090 return _action(session, { type: ActionType.ChatToolCallComplete, turnId, toolCallId, result }, parentToolCallId);
1091 }
1093 > /** Creates a {@link IAgentToolPendingConfirmationSignal}. */
1094 function _pendingConfirmation(session: URI, toolCallId: string, invocationMessage: StringOrMarkdown, opts?: {
1095 toolInput?: string;
1096 confirmationTitle?: StringOrMarkdown;
1097 permissionKind?: IAgentToolPendingConfirmationSignal['permissionKind'];
1098 permissionPath?: IAgentToolPendingConfirmationSignal['permissionPath'];
1099 }): IAgentToolPendingConfirmationSignal {
1100 return {
1101 kind: 'pending_confirmation',
1102 chat: session,
1103 state: {
1104 status: ToolCallStatus.PendingConfirmation,
1105 toolCallId,
1106 toolName: '',
1107 displayName: '',
1108 invocationMessage,
1109 toolInput: opts?.toolInput,
1110 confirmationTitle: opts?.confirmationTitle,
1111 },
1112 permissionKind: opts?.permissionKind,
1113 permissionPath: opts?.permissionPath,
1114 };
1115 }