Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/protocol/turnExecution.integrationTest.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
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 assert from 'assert';
7
import { SubscribeResult } from '../../../common/state/protocol/commands.js';
8
import type { IResponsePartAction } from '../../../common/state/sessionActions.js';
9
import type { FetchTurnsResult } from '../../../common/state/sessionProtocol.js';
10
import { ResponsePartKind, buildSubagentSessionUri, type MarkdownResponsePart, type SessionState } from '../../../common/state/sessionState.js';
11
import {
12
createAndSubscribeSession,
13
dispatchTurnStarted,
14
getActionEnvelope,
15
IServerHandle,
16
isActionNotification,
17
startServer,
18
TestProtocolClient,
19
} from './testHelpers.js';
20
21
suite('Protocol WebSocket — Turn Execution', function () {
22
23
let server: IServerHandle;
24
let client: TestProtocolClient;
25
26
suiteSetup(async function () {
27
this.timeout(15_000);
28
server = await startServer();
29
});
30
31
suiteTeardown(function () {
32
server.process.kill();
33
});
34
35
setup(async function () {
36
this.timeout(10_000);
37
client = new TestProtocolClient(server.port);
38
await client.connect();
39
});
40
41
teardown(function () {
42
client.close();
43
});
44
45
test('send message and receive responsePart + turnComplete', async function () {
46
this.timeout(10_000);
47
48
const sessionUri = await createAndSubscribeSession(client, 'test-send-message');
49
dispatchTurnStarted(client, sessionUri, 'turn-1', 'hello', 1);
50
51
const responsePart = await client.waitForNotification(n => isActionNotification(n, 'session/responsePart'));
52
const responsePartAction = getActionEnvelope(responsePart).action as IResponsePartAction;
53
assert.strictEqual(responsePartAction.part.kind, ResponsePartKind.Markdown);
54
assert.strictEqual((responsePartAction.part as MarkdownResponsePart).content, 'Hello, world!');
55
56
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
57
});
58
59
test('tool invocation: toolCallStart → toolCallComplete → responsePart → turnComplete', async function () {
60
this.timeout(10_000);
61
62
const sessionUri = await createAndSubscribeSession(client, 'test-tool-invocation');
63
dispatchTurnStarted(client, sessionUri, 'turn-tool', 'use-tool', 1);
64
65
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallStart'));
66
await client.waitForNotification(n => isActionNotification(n, 'session/toolCallReady'));
67
const toolComplete = await client.waitForNotification(n => isActionNotification(n, 'session/toolCallComplete'));
68
const tcAction = getActionEnvelope(toolComplete).action;
69
if (tcAction.type === 'session/toolCallComplete') {
70
assert.strictEqual(tcAction.result.success, true);
71
}
72
await client.waitForNotification(n => isActionNotification(n, 'session/responsePart'));
73
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
74
});
75
76
test('error prompt triggers session/error', async function () {
77
this.timeout(10_000);
78
79
const sessionUri = await createAndSubscribeSession(client, 'test-error');
80
dispatchTurnStarted(client, sessionUri, 'turn-err', 'error', 1);
81
82
const errorNotif = await client.waitForNotification(n => isActionNotification(n, 'session/error'));
83
const errorAction = getActionEnvelope(errorNotif).action;
84
if (errorAction.type === 'session/error') {
85
assert.strictEqual(errorAction.error.message, 'Something went wrong');
86
}
87
});
88
89
test('cancel turn stops in-progress processing', async function () {
90
this.timeout(10_000);
91
92
const sessionUri = await createAndSubscribeSession(client, 'test-cancel');
93
dispatchTurnStarted(client, sessionUri, 'turn-cancel', 'slow', 1);
94
95
client.notify('dispatchAction', {
96
clientSeq: 2,
97
action: { type: 'session/turnCancelled', session: sessionUri, turnId: 'turn-cancel' },
98
});
99
100
await client.waitForNotification(n => isActionNotification(n, 'session/turnCancelled'));
101
102
const snapshot = await client.call<SubscribeResult>('subscribe', { resource: sessionUri });
103
const state = snapshot.snapshot.state as SessionState;
104
assert.ok(state.turns.length >= 1);
105
assert.strictEqual(state.turns[state.turns.length - 1].state, 'cancelled');
106
});
107
108
test('multiple sequential turns accumulate in history', async function () {
109
this.timeout(15_000);
110
111
const sessionUri = await createAndSubscribeSession(client, 'test-multi-turns');
112
113
dispatchTurnStarted(client, sessionUri, 'turn-m1', 'hello', 1);
114
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
115
116
dispatchTurnStarted(client, sessionUri, 'turn-m2', 'hello', 2);
117
await new Promise(resolve => setTimeout(resolve, 200));
118
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
119
120
const snapshot = await client.call<SubscribeResult>('subscribe', { resource: sessionUri });
121
const state = snapshot.snapshot.state as SessionState;
122
assert.ok(state.turns.length >= 2, `expected >= 2 turns but got ${state.turns.length}`);
123
assert.strictEqual(state.turns[0].id, 'turn-m1');
124
assert.strictEqual(state.turns[1].id, 'turn-m2');
125
});
126
127
test('fetchTurns returns completed turn history', async function () {
128
this.timeout(15_000);
129
130
const sessionUri = await createAndSubscribeSession(client, 'test-fetchTurns');
131
132
dispatchTurnStarted(client, sessionUri, 'turn-ft-1', 'hello', 1);
133
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
134
135
dispatchTurnStarted(client, sessionUri, 'turn-ft-2', 'hello', 2);
136
await new Promise(resolve => setTimeout(resolve, 200));
137
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
138
139
const result = await client.call<FetchTurnsResult>('fetchTurns', { session: sessionUri, limit: 10 });
140
assert.ok(result.turns.length >= 2);
141
assert.strictEqual(typeof result.hasMore, 'boolean');
142
});
143
144
test('usage info is captured on completed turn', async function () {
145
this.timeout(10_000);
146
147
const sessionUri = await createAndSubscribeSession(client, 'test-usage');
148
dispatchTurnStarted(client, sessionUri, 'turn-usage', 'with-usage', 1);
149
150
const usageNotif = await client.waitForNotification(n => isActionNotification(n, 'session/usage'));
151
const usageAction = getActionEnvelope(usageNotif).action as { type: string; usage: { inputTokens: number; outputTokens: number } };
152
assert.strictEqual(usageAction.usage.inputTokens, 100);
153
assert.strictEqual(usageAction.usage.outputTokens, 50);
154
155
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
156
157
const snapshot = await client.call<SubscribeResult>('subscribe', { resource: sessionUri });
158
const state = snapshot.snapshot.state as SessionState;
159
assert.ok(state.turns.length >= 1);
160
const turn = state.turns[state.turns.length - 1];
161
assert.ok(turn.usage);
162
assert.strictEqual(turn.usage!.inputTokens, 100);
163
assert.strictEqual(turn.usage!.outputTokens, 50);
164
});
165
166
test('modifiedAt updates on turn completion', async function () {
167
this.timeout(10_000);
168
169
const sessionUri = await createAndSubscribeSession(client, 'test-modifiedAt');
170
171
const initialSnapshot = await client.call<SubscribeResult>('subscribe', { resource: sessionUri });
172
const initialModifiedAt = (initialSnapshot.snapshot.state as SessionState).summary.modifiedAt;
173
174
await new Promise(resolve => setTimeout(resolve, 50));
175
176
dispatchTurnStarted(client, sessionUri, 'turn-mod', 'hello', 1);
177
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
178
179
const updatedSnapshot = await client.call<SubscribeResult>('subscribe', { resource: sessionUri });
180
const updatedModifiedAt = (updatedSnapshot.snapshot.state as SessionState).summary.modifiedAt;
181
assert.ok(updatedModifiedAt >= initialModifiedAt);
182
});
183
184
test('subagent: inner tool calls land in child session, not parent', async function () {
185
this.timeout(15_000);
186
187
const sessionUri = await createAndSubscribeSession(client, 'test-subagent');
188
dispatchTurnStarted(client, sessionUri, 'turn-sa', 'subagent', 1);
189
190
// Wait for the parent turn to complete.
191
await client.waitForNotification(n => isActionNotification(n, 'session/turnComplete'));
192
193
// Subscribe to the child subagent session — its URI is derived from
194
// the parent session URI + parent toolCallId.
195
const childUri = buildSubagentSessionUri(sessionUri, 'tc-task-1');
196
197
const parentSnapshot = await client.call<SubscribeResult>('subscribe', { resource: sessionUri });
198
const parentState = parentSnapshot.snapshot.state as SessionState;
199
const childSnapshot = await client.call<SubscribeResult>('subscribe', { resource: childUri });
200
const childState = childSnapshot.snapshot.state as SessionState;
201
202
// Parent turn should contain the `task` tool call but NOT the inner one.
203
const parentTurn = parentState.turns[parentState.turns.length - 1];
204
const parentToolCalls = parentTurn.responseParts.filter(p => p.kind === ResponsePartKind.ToolCall);
205
const parentToolNames = parentToolCalls.map(p => p.toolCall.toolName);
206
assert.deepStrictEqual(parentToolNames, ['task'], 'parent turn should only contain the `task` tool call (inner tool must route to subagent)');
207
208
// Child session should have one turn with the inner `echo_tool` call.
209
assert.ok(childState.turns.length >= 1, 'child subagent session should have at least one turn');
210
const childTurn = childState.turns[childState.turns.length - 1];
211
const childToolCalls = childTurn.responseParts.filter(p => p.kind === ResponsePartKind.ToolCall);
212
const childToolNames = childToolCalls.map(p => p.toolCall.toolName);
213
assert.deepStrictEqual(childToolNames, ['echo_tool'], 'child subagent session should contain the inner `echo_tool` call');
214
});
215
});
216
217