comments and tests

This commit is contained in:
Connor Peet
2026-03-31 15:33:59 -07:00
parent bf954f4ebd
commit 12fa930036
8 changed files with 130 additions and 64 deletions

View File

@@ -24,6 +24,7 @@ import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, type IJsonR
import { ContentEncoding } from '../common/state/protocol/commands.js';
import type { ISessionSummary } from '../common/state/sessionState.js';
import { WebSocketClientTransport } from './webSocketClientTransport.js';
import { encodeBase64 } from '../../../base/common/buffer.js';
/**
* A protocol-level client for a single remote agent host connection.
@@ -288,9 +289,8 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
if (!p.uri) { sendError('Missing uri'); return; }
this._fileService.readFile(URI.parse(p.uri)).then(content => {
sendResult({
data: content.value.toString(),
encoding: ContentEncoding.Utf8,
contentType: 'text/plain',
data: encodeBase64(content.value),
encoding: ContentEncoding.Base64,
});
}).catch(err => sendError(err instanceof Error ? err.message : String(err)));
return;

View File

@@ -5,7 +5,6 @@
import { VSBuffer } from '../../../base/common/buffer.js';
import { SequencerByKey } from '../../../base/common/async.js';
import { ResourceMap } from '../../../base/common/map.js';
import { URI } from '../../../base/common/uri.js';
import { IFileService } from '../../files/common/files.js';
import { ILogService } from '../../log/common/log.js';
@@ -42,11 +41,11 @@ export class AgentPluginManager implements IAgentPluginManager {
/** Serializes concurrent sync operations per plugin URI. */
private readonly _sequencer = new SequencerByKey<string>();
/** Nonces for plugins on disk, keyed by plugin URI. */
private readonly _cachedNonces = new ResourceMap<string>();
/** Nonces for plugins on disk, keyed by original customization URI string. */
private readonly _cachedNonces = new Map<string, string>();
/** LRU order: most recently used plugin URIs at the end. */
private readonly _lruOrder: URI[] = [];
/** LRU order: most recently used original customization URI strings at the end. */
private readonly _lruOrder: string[] = [];
/** Whether the on-disk cache has been loaded. */
private _cacheLoaded = false;
@@ -110,8 +109,8 @@ export class AgentPluginManager implements IAgentPluginManager {
const destDir = URI.joinPath(this._basePath, key);
// Nonce cache hit — skip copy
if (ref.nonce && this._cachedNonces.get(pluginUri) === ref.nonce) {
this._touchLru(pluginUri);
if (ref.nonce && this._cachedNonces.get(ref.uri) === ref.nonce) {
this._touchLru(ref.uri);
this._logService.trace(`[AgentPluginManager] Nonce match for ${ref.uri}, skipping copy`);
return destDir;
}
@@ -121,9 +120,9 @@ export class AgentPluginManager implements IAgentPluginManager {
await this._fileService.copy(pluginUri, destDir, true);
if (ref.nonce) {
this._cachedNonces.set(pluginUri, ref.nonce);
this._cachedNonces.set(ref.uri, ref.nonce);
}
this._touchLru(pluginUri);
this._touchLru(ref.uri);
await this._evictIfNeeded();
await this._persistCache();
@@ -134,8 +133,8 @@ export class AgentPluginManager implements IAgentPluginManager {
return uri.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 128);
}
private _touchLru(uri: URI): void {
const idx = this._lruOrder.findIndex(u => u.toString() === uri.toString());
private _touchLru(uri: string): void {
const idx = this._lruOrder.indexOf(uri);
if (idx !== -1) {
this._lruOrder.splice(idx, 1);
}
@@ -149,13 +148,13 @@ export class AgentPluginManager implements IAgentPluginManager {
break;
}
this._cachedNonces.delete(evictUri);
const evictKey = this._keyForUri(evictUri.toString());
const evictKey = this._keyForUri(evictUri);
const evictDir = URI.joinPath(this._basePath, evictKey);
this._logService.info(`[AgentPluginManager] Evicting plugin: ${evictUri.toString()}`);
this._logService.info(`[AgentPluginManager] Evicting plugin: ${evictUri}`);
try {
await this._fileService.del(evictDir, { recursive: true });
} catch (err) {
this._logService.warn(`[AgentPluginManager] Failed to evict plugin: ${evictUri.toString()}`, err);
this._logService.warn(`[AgentPluginManager] Failed to evict plugin: ${evictUri}`, err);
}
}
}
@@ -181,9 +180,8 @@ export class AgentPluginManager implements IAgentPluginManager {
// Entries are stored in LRU order (oldest first)
for (const entry of entries) {
if (typeof entry.uri === 'string' && typeof entry.nonce === 'string') {
const uri = URI.parse(entry.uri);
this._cachedNonces.set(uri, entry.nonce);
this._lruOrder.push(uri);
this._cachedNonces.set(entry.uri, entry.nonce);
this._lruOrder.push(entry.uri);
}
}
this._logService.trace(`[AgentPluginManager] Loaded ${entries.length} cache entries from disk`);
@@ -199,7 +197,7 @@ export class AgentPluginManager implements IAgentPluginManager {
for (const uri of this._lruOrder) {
const nonce = this._cachedNonces.get(uri);
if (nonce) {
entries.push({ uri: uri.toString(), nonce });
entries.push({ uri, nonce });
}
}
await this._fileService.createFolder(this._basePath);

View File

@@ -201,6 +201,7 @@ export class ProtocolServerHandler extends Disposable {
if (client && this._clients.get(client.clientId) === client) {
this._logService.info(`[ProtocolServer] Client disconnected: ${client.clientId}`);
this._clients.delete(client.clientId);
this._rejectPendingReverseRequests(client.clientId);
this._onDidChangeConnectionCount.fire(this._clients.size);
}
disposables.dispose();
@@ -403,11 +404,12 @@ export class ProtocolServerHandler extends Disposable {
// ---- Reverse RPC (server → client requests) ----------------------------
private _reverseRequestId = 0;
private readonly _pendingReverseRequests = new Map<number, { resolve: (value: unknown) => void; reject: (reason: unknown) => void }>();
private readonly _pendingReverseRequests = new Map<number, { clientId: string; resolve: (value: unknown) => void; reject: (reason: unknown) => void }>();
/**
* Sends a JSON-RPC request to a connected client and waits for the response.
* Used for reverse-RPC operations like reading client-side files.
* Rejects if the client disconnects or the server is disposed.
*/
private _sendReverseRequest<T>(clientId: string, method: string, params: unknown): Promise<T> {
const client = this._clients.get(clientId);
@@ -416,12 +418,24 @@ export class ProtocolServerHandler extends Disposable {
}
const id = ++this._reverseRequestId;
return new Promise<T>((resolve, reject) => {
this._pendingReverseRequests.set(id, { resolve: resolve as (value: unknown) => void, reject });
this._pendingReverseRequests.set(id, { clientId, resolve: resolve as (value: unknown) => void, reject });
const request: IJsonRpcRequest = { jsonrpc: '2.0', id, method, params };
client.transport.send(request);
});
}
/**
* Rejects and clears all pending reverse-RPC requests for a given client.
*/
private _rejectPendingReverseRequests(clientId: string): void {
for (const [id, pending] of this._pendingReverseRequests) {
if (pending.clientId === clientId) {
this._pendingReverseRequests.delete(id);
pending.reject(new Error(`Client ${clientId} disconnected`));
}
}
}
private _handleRequest(client: IConnectedClient, method: string, params: unknown, id: number): void {
const handler = this._requestHandlers.hasOwnProperty(method) ? this._requestHandlers[method as RequestMethod] : undefined;
if (handler) {
@@ -512,6 +526,10 @@ export class ProtocolServerHandler extends Disposable {
client.disposables.dispose();
}
this._clients.clear();
for (const [, pending] of this._pendingReverseRequests) {
pending.reject(new Error('ProtocolServerHandler disposed'));
}
this._pendingReverseRequests.clear();
this._replayBuffer.length = 0;
super.dispose();
}

View File

@@ -12,6 +12,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c
import { FileService } from '../../../files/common/fileService.js';
import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js';
import { NullLogService } from '../../../log/common/log.js';
import { AGENT_CLIENT_SCHEME, toAgentClientUri } from '../../common/agentClientUri.js';
import { CustomizationStatus, type ICustomizationRef, type ISessionCustomization } from '../../common/state/sessionState.js';
import { AgentPluginManager } from '../../node/agentPluginManager.js';
@@ -25,6 +26,7 @@ suite('AgentPluginManager', () => {
setup(() => {
fileService = disposables.add(new FileService(new NullLogService()));
disposables.add(fileService.registerProvider(Schemas.inMemory, disposables.add(new InMemoryFileSystemProvider())));
disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, disposables.add(new InMemoryFileSystemProvider())));
manager = new AgentPluginManager(basePath, fileService, new NullLogService());
});
@@ -40,10 +42,11 @@ suite('AgentPluginManager', () => {
}
async function seedPluginDir(name: string, files: Record<string, string>): Promise<void> {
const dir = URI.from({ scheme: Schemas.inMemory, path: `/plugins/${name}` });
await fileService.createFolder(dir);
const originalUri = URI.from({ scheme: Schemas.inMemory, path: `/plugins/${name}` });
const agentClientDir = toAgentClientUri(originalUri, 'test-client');
await fileService.createFolder(agentClientDir);
for (const [fileName, content] of Object.entries(files)) {
await fileService.writeFile(URI.joinPath(dir, fileName), VSBuffer.fromString(content));
await fileService.writeFile(URI.joinPath(agentClientDir, fileName), VSBuffer.fromString(content));
}
}

View File

@@ -79,8 +79,8 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements
// When a session is active, prefer session-level data (includes status)
if (this._sessionCustomizations) {
return this._sessionCustomizations.map(sc => ({
uri: URI.isUri(sc.customization.uri) ? sc.customization.uri : URI.parse(sc.customization.uri as unknown as string),
type: 'customization',
uri: URI.isUri(sc.customization.uri) ? sc.customization.uri : URI.parse(sc.customization.uri),
type: 'plugin',
name: sc.customization.displayName,
description: sc.customization.description,
status: toStatusString(sc.status),
@@ -92,7 +92,7 @@ export class RemoteAgentCustomizationItemProvider extends Disposable implements
// Baseline: agent-level customizations (no status info)
return this._agentCustomizations.map(ref => ({
uri: URI.isUri(ref.uri) ? ref.uri : URI.parse(ref.uri as unknown as string),
type: 'customization',
type: 'plugin',
name: ref.displayName,
description: ref.description,
}));

View File

@@ -46,7 +46,13 @@ export class AgentCustomizationSyncProvider extends Disposable implements ICusto
const stored = this._storageService.get(this._storageKey, StorageScope.PROFILE);
this._entries = new Map();
if (stored) {
const parsed = JSON.parse(stored) as (string | ISyncEntry)[];
let parsed: (string | ISyncEntry)[] | undefined;
try {
parsed = JSON.parse(stored) as (string | ISyncEntry)[];
} catch {
// ignored
}
if (Array.isArray(parsed)) {
for (const item of parsed) {
if (typeof item === 'string') {

View File

@@ -407,13 +407,14 @@ export function getEffectiveCommandSource(hook: IHookCommand, os: OperatingSyste
* Returns the actual field name from the JSON (e.g., 'bash' instead of 'osx' if bash was used).
* This is used for editor focus to highlight the correct field.
*/
export function getEffectiveCommandFieldKey(hook: IHookCommand, os: OperatingSystem): string {
export function getEffectiveCommandFieldKey(hook: IHookCommand | IParsedHookCommand, os: OperatingSystem): string {
const h = hook as Partial<IHookCommand>;
if (os === OperatingSystem.Windows && hook.windows) {
return hook.windowsSource ?? 'windows';
return h.windowsSource ?? 'windows';
} else if (os === OperatingSystem.Macintosh && hook.osx) {
return hook.osxSource ?? 'osx';
return h.osxSource ?? 'osx';
} else if (os === OperatingSystem.Linux && hook.linux) {
return hook.linuxSource ?? 'linux';
return h.linuxSource ?? 'linux';
}
return 'command';
}

View File

@@ -40,6 +40,9 @@ import { TestFileService } from '../../../../../test/common/workbenchTestService
import { ILabelService } from '../../../../../../platform/label/common/label.js';
import { MockLabelService } from '../../../../../services/label/test/common/mockLabelService.js';
import { IAgentHostFileSystemService } from '../../../../../services/agentHost/common/agentHostFileSystemService.js';
import { ICustomizationHarnessService } from '../../../common/customizationHarnessService.js';
import { IAgentPluginService } from '../../../common/plugins/agentPluginService.js';
import { IStorageService, InMemoryStorageService } from '../../../../../../platform/storage/common/storage.js';
// ---- Mock agent host service ------------------------------------------------
@@ -86,6 +89,12 @@ class MockAgentHostService extends mock<IAgentHostService>() {
// Protocol methods
public override readonly clientId = 'test-window-1';
public dispatchedActions: { action: ISessionAction; clientId: string; clientSeq: number }[] = [];
/** Returns dispatched actions filtered to turn-related types only
* (excludes lifecycle actions like activeClientChanged). */
get turnActions() {
return this.dispatchedActions.filter(d => d.action.type === 'session/turnStarted');
}
public sessionStates = new Map<string, ISessionState>();
override async subscribe(resource: URI): Promise<IStateSnapshot> {
const resourceStr = resource.toString();
@@ -192,6 +201,13 @@ function createTestServices(disposables: DisposableStore) {
registerAuthority: () => toDisposable(() => { }),
ensureSyncedCustomizationProvider: () => { },
});
instantiationService.stub(IStorageService, disposables.add(new InMemoryStorageService()));
instantiationService.stub(ICustomizationHarnessService, {
registerExternalHarness: () => toDisposable(() => { }),
});
instantiationService.stub(IAgentPluginService, {
plugins: observableValue('plugins', []),
});
return { instantiationService, agentHostService, chatAgentService };
}
@@ -255,6 +271,10 @@ async function startTurn(
const chatSession = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None);
ds.add(toDisposable(() => chatSession.dispose()));
// Clear any lifecycle actions (e.g. activeClientChanged from customization setup)
// so tests only see turn-related dispatches.
agentHostService.dispatchedActions.length = 0;
const collected: IChatProgress[][] = [];
const seq = { v: 1 };
@@ -272,7 +292,9 @@ async function startTurn(
await timeout(10);
const lastDispatch = agentHostService.dispatchedActions[agentHostService.dispatchedActions.length - 1];
// Filter for turn-related dispatches only (skip activeClientChanged etc.)
const turnDispatches = agentHostService.dispatchedActions.filter(d => d.action.type === 'session/turnStarted');
const lastDispatch = turnDispatches[turnDispatches.length - 1] ?? agentHostService.dispatchedActions[agentHostService.dispatchedActions.length - 1];
const session = (lastDispatch?.action as ITurnStartedAction)?.session;
const turnId = (lastDispatch?.action as ITurnStartedAction)?.turnId;
@@ -365,9 +387,9 @@ suite('AgentHostChatContribution', () => {
fire({ type: 'session/turnComplete', session, turnId } as ISessionAction);
await turnPromise;
assert.strictEqual(agentHostService.dispatchedActions.length, 1);
assert.strictEqual(agentHostService.dispatchedActions[0].action.type, 'session/turnStarted');
assert.strictEqual((agentHostService.dispatchedActions[0].action as ITurnStartedAction).userMessage.text, 'Hello');
assert.strictEqual(agentHostService.turnActions.length, 1);
assert.strictEqual(agentHostService.turnActions[0].action.type, 'session/turnStarted');
assert.strictEqual((agentHostService.turnActions[0].action as ITurnStartedAction).userMessage.text, 'Hello');
assert.ok(AgentSession.id(URI.parse(session)).startsWith('sdk-session-'));
}));
@@ -378,13 +400,16 @@ suite('AgentHostChatContribution', () => {
const chatSession = await sessionHandler.provideChatSessionContent(resource, CancellationToken.None);
disposables.add(toDisposable(() => chatSession.dispose()));
// Clear lifecycle actions so only turn dispatches are counted
agentHostService.dispatchedActions.length = 0;
// First turn
const turn1Promise = chatSession.requestHandler!(
makeRequest({ message: 'First', sessionResource: resource }),
() => { }, [], CancellationToken.None,
);
await timeout(10);
const dispatch1 = agentHostService.dispatchedActions[0];
const dispatch1 = agentHostService.turnActions[0];
const action1 = dispatch1.action as ITurnStartedAction;
// Echo the turnStarted to clear pending write-ahead
agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } });
@@ -397,16 +422,16 @@ suite('AgentHostChatContribution', () => {
() => { }, [], CancellationToken.None,
);
await timeout(10);
const dispatch2 = agentHostService.dispatchedActions[1];
const dispatch2 = agentHostService.turnActions[1];
const action2 = dispatch2.action as ITurnStartedAction;
agentHostService.fireAction({ action: dispatch2.action, serverSeq: 3, origin: { clientId: agentHostService.clientId, clientSeq: dispatch2.clientSeq } });
agentHostService.fireAction({ action: { type: 'session/turnComplete', session: action2.session, turnId: action2.turnId } as ISessionAction, serverSeq: 4, origin: undefined });
await turn2Promise;
assert.strictEqual(agentHostService.dispatchedActions.length, 2);
assert.strictEqual(agentHostService.turnActions.length, 2);
assert.strictEqual(
(agentHostService.dispatchedActions[0].action as ITurnStartedAction).session.toString(),
(agentHostService.dispatchedActions[1].action as ITurnStartedAction).session.toString(),
(agentHostService.turnActions[0].action as ITurnStartedAction).session.toString(),
(agentHostService.turnActions[1].action as ITurnStartedAction).session.toString(),
);
}));
@@ -1265,8 +1290,8 @@ suite('AgentHostChatContribution', () => {
fire({ type: 'session/turnComplete', session, turnId } as ISessionAction);
await turnPromise;
assert.strictEqual(agentHostService.dispatchedActions.length, 1);
const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction;
assert.strictEqual(agentHostService.turnActions.length, 1);
const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction;
assert.deepStrictEqual(turnAction.userMessage.attachments, [
{ type: 'file', path: URI.file('/workspace/test.ts').fsPath, displayName: 'test.ts' },
]);
@@ -1286,8 +1311,8 @@ suite('AgentHostChatContribution', () => {
fire({ type: 'session/turnComplete', session, turnId } as ISessionAction);
await turnPromise;
assert.strictEqual(agentHostService.dispatchedActions.length, 1);
const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction;
assert.strictEqual(agentHostService.turnActions.length, 1);
const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction;
assert.deepStrictEqual(turnAction.userMessage.attachments, [
{ type: 'directory', path: URI.file('/workspace/src').fsPath, displayName: 'src' },
]);
@@ -1307,8 +1332,8 @@ suite('AgentHostChatContribution', () => {
fire({ type: 'session/turnComplete', session, turnId } as ISessionAction);
await turnPromise;
assert.strictEqual(agentHostService.dispatchedActions.length, 1);
const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction;
assert.strictEqual(agentHostService.turnActions.length, 1);
const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction;
assert.deepStrictEqual(turnAction.userMessage.attachments, [
{ type: 'selection', path: URI.file('/workspace/foo.ts').fsPath, displayName: 'selection' },
]);
@@ -1328,8 +1353,8 @@ suite('AgentHostChatContribution', () => {
fire({ type: 'session/turnComplete', session, turnId } as ISessionAction);
await turnPromise;
assert.strictEqual(agentHostService.dispatchedActions.length, 1);
const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction;
assert.strictEqual(agentHostService.turnActions.length, 1);
const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction;
// No attachments because it's not a file:// URI
assert.strictEqual(turnAction.userMessage.attachments, undefined);
}));
@@ -1348,8 +1373,8 @@ suite('AgentHostChatContribution', () => {
fire({ type: 'session/turnComplete', session, turnId } as ISessionAction);
await turnPromise;
assert.strictEqual(agentHostService.dispatchedActions.length, 1);
const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction;
assert.strictEqual(agentHostService.turnActions.length, 1);
const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction;
assert.strictEqual(turnAction.userMessage.attachments, undefined);
}));
@@ -1370,8 +1395,8 @@ suite('AgentHostChatContribution', () => {
fire({ type: 'session/turnComplete', session, turnId } as ISessionAction);
await turnPromise;
assert.strictEqual(agentHostService.dispatchedActions.length, 1);
const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction;
assert.strictEqual(agentHostService.turnActions.length, 1);
const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction;
assert.deepStrictEqual(turnAction.userMessage.attachments, [
{ type: 'file', path: URI.file('/workspace/a.ts').fsPath, displayName: 'a.ts' },
{ type: 'directory', path: URI.file('/workspace/lib').fsPath, displayName: 'lib' },
@@ -1387,8 +1412,8 @@ suite('AgentHostChatContribution', () => {
fire({ type: 'session/turnComplete', session, turnId } as ISessionAction);
await turnPromise;
assert.strictEqual(agentHostService.dispatchedActions.length, 1);
const turnAction = agentHostService.dispatchedActions[0].action as ITurnStartedAction;
assert.strictEqual(agentHostService.turnActions.length, 1);
const turnAction = agentHostService.turnActions[0].action as ITurnStartedAction;
assert.strictEqual(turnAction.userMessage.attachments, undefined);
}));
});
@@ -1560,8 +1585,8 @@ suite('AgentHostChatContribution', () => {
await turnPromise;
// Turn dispatched via connection.dispatchAction
assert.strictEqual(agentHostService.dispatchedActions.length, 1);
assert.strictEqual((agentHostService.dispatchedActions[0].action as ITurnStartedAction).userMessage.text, 'Test message');
assert.strictEqual(agentHostService.turnActions.length, 1);
assert.strictEqual((agentHostService.turnActions[0].action as ITurnStartedAction).userMessage.text, 'Test message');
}));
});
@@ -1857,13 +1882,16 @@ suite('AgentHostChatContribution', () => {
const chatSession = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None);
disposables.add(toDisposable(() => chatSession.dispose()));
// Clear lifecycle actions so only turn dispatches are counted
agentHostService.dispatchedActions.length = 0;
// First, do a normal turn so the backend session is created
const turn1Promise = chatSession.requestHandler!(
makeRequest({ message: 'Hello', sessionResource }),
() => { }, [], CancellationToken.None,
);
await timeout(10);
const dispatch1 = agentHostService.dispatchedActions[0];
const dispatch1 = agentHostService.turnActions[0];
const action1 = dispatch1.action as ITurnStartedAction;
const session = action1.session;
// Echo + complete the first turn
@@ -1904,13 +1932,16 @@ suite('AgentHostChatContribution', () => {
const chatSession = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None);
disposables.add(toDisposable(() => chatSession.dispose()));
// Clear lifecycle actions so only turn dispatches are counted
agentHostService.dispatchedActions.length = 0;
// Normal turn to create backend session
const turn1Promise = chatSession.requestHandler!(
makeRequest({ message: 'Init', sessionResource }),
() => { }, [], CancellationToken.None,
);
await timeout(10);
const dispatch1 = agentHostService.dispatchedActions[0];
const dispatch1 = agentHostService.turnActions[0];
const action1 = dispatch1.action as ITurnStartedAction;
const session = action1.session;
agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } });
@@ -1976,13 +2007,16 @@ suite('AgentHostChatContribution', () => {
const serverRequestEvents: { prompt: string }[] = [];
disposables.add(chatSession.onDidStartServerRequest!(e => serverRequestEvents.push(e)));
// Clear lifecycle actions so only turn dispatches are counted
agentHostService.dispatchedActions.length = 0;
// Normal client turn — should NOT fire onDidStartServerRequest
const turnPromise = chatSession.requestHandler!(
makeRequest({ message: 'Client turn', sessionResource }),
() => { }, [], CancellationToken.None,
);
await timeout(10);
const dispatch = agentHostService.dispatchedActions[0];
const dispatch = agentHostService.turnActions[0];
const action = dispatch.action as ITurnStartedAction;
agentHostService.fireAction({ action: dispatch.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch.clientSeq } });
agentHostService.fireAction({ action: { type: 'session/turnComplete', session: action.session, turnId: action.turnId } as ISessionAction, serverSeq: 2, origin: undefined });
@@ -1998,13 +2032,16 @@ suite('AgentHostChatContribution', () => {
const chatSession = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None);
disposables.add(toDisposable(() => chatSession.dispose()));
// Clear lifecycle actions so only turn dispatches are counted
agentHostService.dispatchedActions.length = 0;
// First, do a normal turn so the backend session is created
const turn1Promise = chatSession.requestHandler!(
makeRequest({ message: 'Init', sessionResource }),
() => { }, [], CancellationToken.None,
);
await timeout(10);
const dispatch1 = agentHostService.dispatchedActions[0];
const dispatch1 = agentHostService.turnActions[0];
const action1 = dispatch1.action as ITurnStartedAction;
const session = action1.session;
agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } });
@@ -2061,13 +2098,16 @@ suite('AgentHostChatContribution', () => {
const chatSession = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None);
disposables.add(toDisposable(() => chatSession.dispose()));
// Clear lifecycle actions so only turn dispatches are counted
agentHostService.dispatchedActions.length = 0;
// First, do a normal turn so the backend session is created
const turn1Promise = chatSession.requestHandler!(
makeRequest({ message: 'Init', sessionResource }),
() => { }, [], CancellationToken.None,
);
await timeout(10);
const dispatch1 = agentHostService.dispatchedActions[0];
const dispatch1 = agentHostService.turnActions[0];
const action1 = dispatch1.action as ITurnStartedAction;
const session = action1.session;
agentHostService.fireAction({ action: dispatch1.action, serverSeq: 1, origin: { clientId: agentHostService.clientId, clientSeq: dispatch1.clientSeq } });