Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/protocol/sessionLifecycle.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 { timeout } from '../../../../../base/common/async.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { SubscribeResult } from '../../../common/state/protocol/commands.js';
10
import type { SessionAddedNotification, SessionRemovedNotification } from '../../../common/state/sessionActions.js';
11
import { PROTOCOL_VERSION } from '../../../common/state/sessionCapabilities.js';
12
import type { ListSessionsResult, INotificationBroadcastParams } from '../../../common/state/sessionProtocol.js';
13
import { ResponsePartKind, SessionStatus, type MarkdownResponsePart, type SessionState, type ToolCallResponsePart } from '../../../common/state/sessionState.js';
14
import { PRE_EXISTING_SESSION_URI } from '../mockAgent.js';
15
import {
16
createAndSubscribeSession,
17
isActionNotification,
18
IServerHandle,
19
nextSessionUri,
20
startServer,
21
TestProtocolClient
22
} from './testHelpers.js';
23
24
suite('Protocol WebSocket — Session Lifecycle', function () {
25
26
let server: IServerHandle;
27
let client: TestProtocolClient;
28
29
suiteSetup(async function () {
30
this.timeout(15_000);
31
server = await startServer();
32
});
33
34
suiteTeardown(function () {
35
server.process.kill();
36
});
37
38
setup(async function () {
39
this.timeout(10_000);
40
client = new TestProtocolClient(server.port);
41
await client.connect();
42
});
43
44
teardown(function () {
45
client.close();
46
});
47
48
test('create session triggers sessionAdded notification', async function () {
49
this.timeout(10_000);
50
51
await client.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'test-create-session' });
52
53
await client.call('createSession', { session: nextSessionUri(), provider: 'mock' });
54
55
const notif = await client.waitForNotification(n =>
56
n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded'
57
);
58
const notification = (notif.params as INotificationBroadcastParams).notification as SessionAddedNotification;
59
assert.strictEqual(URI.parse(notification.summary.resource).scheme, 'mock');
60
assert.strictEqual(notification.summary.provider, 'mock');
61
});
62
63
test('listSessions returns sessions', async function () {
64
this.timeout(10_000);
65
66
await client.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'test-list-sessions' });
67
68
await client.call('createSession', { session: nextSessionUri(), provider: 'mock' });
69
await client.waitForNotification(n =>
70
n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded'
71
);
72
73
const result = await client.call<ListSessionsResult>('listSessions');
74
assert.ok(Array.isArray(result.items));
75
assert.ok(result.items.length >= 1, 'should have at least one session');
76
});
77
78
test('dispose session sends sessionRemoved notification', async function () {
79
this.timeout(10_000);
80
81
const sessionUri = await createAndSubscribeSession(client, 'test-dispose');
82
await client.call('disposeSession', { session: sessionUri });
83
84
const notif = await client.waitForNotification(n =>
85
n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionRemoved'
86
);
87
const removed = (notif.params as INotificationBroadcastParams).notification as SessionRemovedNotification;
88
assert.strictEqual(removed.session.toString(), sessionUri.toString());
89
});
90
91
test('subscribe to a pre-existing session restores turns from agent history', async function () {
92
this.timeout(10_000);
93
94
await client.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'test-restore' });
95
96
// The mock agent seeds a pre-existing session that was never created
97
// through the server's handleCreateSession -- simulating a session
98
// from a previous server lifetime.
99
const preExistingUri = PRE_EXISTING_SESSION_URI.toString();
100
const list = await client.call<ListSessionsResult>('listSessions');
101
const preExisting = list.items.find(s => s.resource === preExistingUri);
102
assert.ok(preExisting, 'listSessions should include the pre-existing session');
103
104
// Clear notifications so we can verify no duplicate sessionAdded fires.
105
client.clearReceived();
106
107
// Subscribing to this session should trigger the restore path: the
108
// server fetches message history from the agent and reconstructs turns.
109
const result = await client.call<SubscribeResult>('subscribe', { resource: preExistingUri });
110
const state = result.snapshot.state as SessionState;
111
112
assert.strictEqual(state.lifecycle, 'ready', 'restored session should be in ready state');
113
assert.ok(state.turns.length >= 1, `expected at least 1 restored turn but got ${state.turns.length}`);
114
115
const turn = state.turns[0];
116
assert.strictEqual(turn.userMessage.text, 'What files are here?');
117
assert.strictEqual(turn.state, 'complete');
118
const toolCallParts = turn.responseParts.filter((p): p is ToolCallResponsePart => p.kind === ResponsePartKind.ToolCall);
119
assert.ok(toolCallParts.length >= 1, 'turn should have tool call response parts');
120
assert.strictEqual(toolCallParts[0].toolCall.toolName, 'list_files');
121
const mdParts = turn.responseParts.filter((p): p is MarkdownResponsePart => p.kind === ResponsePartKind.Markdown);
122
assert.ok(mdParts.some(p => p.content.includes('file1.ts')), 'turn should have markdown part mentioning file1.ts');
123
124
// Restoring should NOT emit a duplicate sessionAdded notification
125
// (the session is already known to clients via listSessions).
126
await new Promise(resolve => setTimeout(resolve, 200));
127
const sessionAddedNotifs = client.receivedNotifications(n =>
128
n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded'
129
);
130
assert.strictEqual(sessionAddedNotifs.length, 0, 'restore should not emit sessionAdded');
131
});
132
133
test('isRead and isArchived flags survive in listSessions after dispatch', async function () {
134
this.timeout(15_000);
135
136
const sessionUri = await createAndSubscribeSession(client, 'test-read-archived-flags');
137
138
// Dispatch isArchived=true
139
client.notify('dispatchAction', {
140
clientSeq: 1,
141
action: {
142
type: 'session/isArchivedChanged',
143
session: sessionUri,
144
isArchived: true,
145
},
146
});
147
148
await client.waitForNotification(n => isActionNotification(n, 'session/isArchivedChanged'));
149
150
// Dispatch isRead=true
151
client.notify('dispatchAction', {
152
clientSeq: 2,
153
action: {
154
type: 'session/isReadChanged',
155
session: sessionUri,
156
isRead: true,
157
},
158
});
159
160
await client.waitForNotification(n => isActionNotification(n, 'session/isReadChanged'));
161
162
// Verify the flags are reflected in the subscribed session state
163
const snapshot = await client.call<SubscribeResult>('subscribe', { resource: sessionUri });
164
const state = snapshot.snapshot.state as SessionState;
165
assert.ok(state.summary.status & SessionStatus.IsArchived, 'IsArchived flag should be set in snapshot');
166
assert.ok(state.summary.status & SessionStatus.IsRead, 'IsRead flag should be set in snapshot');
167
168
// Poll listSessions until the persisted flags appear (async DB write)
169
client.close();
170
const client2 = new TestProtocolClient(server.port);
171
await client2.connect();
172
await client2.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'test-read-archived-flags-2' });
173
174
let session: ListSessionsResult['items'][0] | undefined;
175
for (let i = 0; i < 20; i++) {
176
const result = await client2.call<ListSessionsResult>('listSessions');
177
session = result.items.find(s => s.resource === sessionUri);
178
if (session && (session.status & SessionStatus.IsArchived) && (session.status & SessionStatus.IsRead)) {
179
break;
180
}
181
await timeout(100);
182
}
183
assert.ok(session, 'session should appear in listSessions');
184
assert.ok(session.status & SessionStatus.IsArchived, 'IsArchived should be persisted in listSessions');
185
assert.ok(session.status & SessionStatus.IsRead, 'IsRead should be persisted in listSessions');
186
187
client2.close();
188
});
189
190
test('dispatching isRead=false explicitly persists as false', async function () {
191
this.timeout(15_000);
192
193
const sessionUri = await createAndSubscribeSession(client, 'test-isread-false');
194
195
// On a fresh session, isRead is undefined in the DB. Dispatching
196
// isRead=false should persist the value so that listSessions
197
// returns an explicit `false` rather than omitting the field.
198
client.notify('dispatchAction', {
199
clientSeq: 1,
200
action: {
201
type: 'session/isReadChanged',
202
session: sessionUri,
203
isRead: false,
204
},
205
});
206
207
await client.waitForNotification(n => isActionNotification(n, 'session/isReadChanged'));
208
209
client.close();
210
const client2 = new TestProtocolClient(server.port);
211
await client2.connect();
212
await client2.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'test-isread-false-2' });
213
214
let session: ListSessionsResult['items'][0] | undefined;
215
for (let i = 0; i < 20; i++) {
216
const result = await client2.call<ListSessionsResult>('listSessions');
217
session = result.items.find(s => s.resource === sessionUri);
218
if (session && !(session.status & SessionStatus.IsRead)) {
219
break;
220
}
221
await timeout(100);
222
}
223
assert.ok(session, 'session should appear in listSessions');
224
assert.strictEqual(session.status & SessionStatus.IsRead, 0, 'IsRead flag should not be set');
225
226
client2.close();
227
});
228
});
229
230