Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/protocol/sessionConfig.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 { mkdtempSync, rmSync } from 'fs';
8
import { tmpdir } from 'os';
9
import { URI } from '../../../../../base/common/uri.js';
10
import type { ResolveSessionConfigResult, SessionConfigCompletionsResult, SubscribeResult } from '../../../common/state/protocol/commands.js';
11
import { ActionType, type SessionAddedNotification } from '../../../common/state/sessionActions.js';
12
import { PROTOCOL_VERSION } from '../../../common/state/sessionCapabilities.js';
13
import type { INotificationBroadcastParams } from '../../../common/state/sessionProtocol.js';
14
import type { SessionState } from '../../../common/state/sessionState.js';
15
import {
16
getActionEnvelope,
17
isActionNotification,
18
IServerHandle,
19
nextSessionUri,
20
startServer,
21
TestProtocolClient,
22
} from './testHelpers.js';
23
24
suite('Protocol WebSocket - Session Config', 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
await client.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'test-session-config' });
43
});
44
45
teardown(function () {
46
client.close();
47
});
48
49
test('resolveSessionConfig returns schema and re-resolves dependent read-only state', async function () {
50
this.timeout(10_000);
51
52
const workingDirectory = URI.file('/mock/workspace').toString();
53
const initial = await client.call<ResolveSessionConfigResult>('resolveSessionConfig', {
54
provider: 'mock',
55
workingDirectory,
56
});
57
58
assert.deepStrictEqual(initial.values, { isolation: 'worktree', branch: 'main' });
59
assert.deepStrictEqual(Object.keys(initial.schema.properties), ['isolation', 'branch']);
60
assert.deepStrictEqual(initial.schema.properties.branch.enum, ['main']);
61
assert.strictEqual(initial.schema.properties.branch.enumDynamic, true);
62
assert.strictEqual(initial.schema.properties.branch.readOnly, false);
63
64
const folder = await client.call<ResolveSessionConfigResult>('resolveSessionConfig', {
65
provider: 'mock',
66
workingDirectory,
67
config: { isolation: 'folder', branch: 'feature/config' },
68
});
69
70
assert.deepStrictEqual(folder.values, { isolation: 'folder', branch: 'main' });
71
assert.strictEqual(folder.schema.properties.branch.enumDynamic, false);
72
assert.strictEqual(folder.schema.properties.branch.readOnly, true);
73
});
74
75
test('sessionConfigCompletions returns dynamic branch matches', async function () {
76
this.timeout(10_000);
77
78
const result = await client.call<SessionConfigCompletionsResult>('sessionConfigCompletions', {
79
provider: 'mock',
80
workingDirectory: URI.file('/mock/workspace').toString(),
81
config: { isolation: 'worktree' },
82
property: 'branch',
83
query: 'feat',
84
});
85
86
assert.deepStrictEqual(result, {
87
items: [{ value: 'feature/config', label: 'feature/config' }],
88
});
89
});
90
91
test('createSession stores config schema and values on session state', async function () {
92
this.timeout(10_000);
93
94
const config = { isolation: 'worktree', branch: 'feature/config' };
95
await client.call('createSession', {
96
session: nextSessionUri(),
97
provider: 'mock',
98
workingDirectory: URI.file('/mock/workspace').toString(),
99
config,
100
});
101
102
const notif = await client.waitForNotification(n =>
103
n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded'
104
);
105
const notification = (notif.params as INotificationBroadcastParams).notification as SessionAddedNotification;
106
assert.strictEqual(Object.hasOwn(notification.summary, 'config'), false);
107
108
const snapshot = await client.call<SubscribeResult>('subscribe', { resource: notification.summary.resource });
109
const state = snapshot.snapshot.state as SessionState;
110
assert.deepStrictEqual(state.config?.values, config);
111
assert.deepStrictEqual(Object.keys(state.config?.schema.properties ?? {}), ['isolation', 'branch']);
112
});
113
114
test('session/configChanged merges config values into session state', async function () {
115
this.timeout(10_000);
116
117
await client.call('createSession', {
118
session: nextSessionUri(),
119
provider: 'mock',
120
config: { isolation: 'folder', branch: 'main' },
121
});
122
123
const notif = await client.waitForNotification(n =>
124
n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded'
125
);
126
const session = ((notif.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource;
127
await client.call<SubscribeResult>('subscribe', { resource: session });
128
client.clearReceived();
129
130
client.notify('dispatchAction', {
131
clientSeq: 1,
132
action: {
133
type: ActionType.SessionConfigChanged,
134
session,
135
config: { branch: 'release' },
136
},
137
});
138
139
const configChanged = await client.waitForNotification(n => isActionNotification(n, ActionType.SessionConfigChanged));
140
assert.strictEqual(getActionEnvelope(configChanged).action.type, ActionType.SessionConfigChanged);
141
142
const snapshot = await client.call<SubscribeResult>('subscribe', { resource: session });
143
const state = snapshot.snapshot.state as SessionState;
144
assert.deepStrictEqual(state.config?.values, { isolation: 'folder', branch: 'release' });
145
});
146
});
147
148
suite('Protocol WebSocket - Session Config persistence across restarts', function () {
149
150
let userDataDir: string;
151
152
setup(function () {
153
userDataDir = mkdtempSync(`${tmpdir()}/vscode-agent-host-config-`);
154
});
155
156
teardown(function () {
157
try {
158
rmSync(userDataDir, { recursive: true, force: true });
159
} catch {
160
// Best-effort cleanup; the OS will reap the temp dir eventually.
161
}
162
});
163
164
test('persisted config values are restored on subscribe after server restart', async function () {
165
this.timeout(30_000);
166
167
const initialConfig = { isolation: 'worktree', branch: 'main' };
168
const updatedBranch = 'release';
169
let sessionUri: string;
170
171
// ---- Phase 1: create session, change config, wait for persistence ----
172
const server1 = await startServer({ userDataDir });
173
try {
174
const client1 = new TestProtocolClient(server1.port);
175
await client1.connect();
176
await client1.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'test-config-restore-1' });
177
178
await client1.call('createSession', {
179
session: nextSessionUri(),
180
provider: 'mock',
181
workingDirectory: URI.file('/mock/workspace').toString(),
182
config: initialConfig,
183
});
184
const addedNotif = await client1.waitForNotification(n =>
185
n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded'
186
);
187
// The mock agent assigns its own URI rather than honoring the
188
// requested one, so capture the real URI from the notification.
189
sessionUri = ((addedNotif.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource;
190
191
await client1.call<SubscribeResult>('subscribe', { resource: sessionUri });
192
193
client1.notify('dispatchAction', {
194
clientSeq: 1,
195
action: {
196
type: ActionType.SessionConfigChanged,
197
session: sessionUri,
198
config: { branch: updatedBranch },
199
},
200
});
201
const configChanged = await client1.waitForNotification(n => isActionNotification(n, ActionType.SessionConfigChanged));
202
assert.strictEqual(getActionEnvelope(configChanged).action.type, ActionType.SessionConfigChanged);
203
204
client1.close();
205
} finally {
206
// Trigger graceful shutdown by closing stdin rather than sending
207
// SIGTERM — on Windows, `child.kill()` (SIGTERM) unconditionally
208
// terminates the process without invoking the shutdown handler,
209
// so in-flight `setMetadata` writes never reach SQLite. Closing
210
// stdin fires `process.stdin.on('end', shutdown)` in the server
211
// on every platform.
212
server1.process.stdin!.end();
213
await new Promise<void>(resolve => server1.process.once('exit', () => resolve()));
214
}
215
216
// ---- Phase 2: restart server, subscribe, verify restored config ----
217
// The mock agent does not persist its in-memory session list across
218
// restarts, so seed it via env var so `agent.listSessions()` includes
219
// our session and `restoreSession` proceeds.
220
const server2 = await startServer({
221
userDataDir,
222
env: { VSCODE_AGENT_HOST_MOCK_SEED_SESSIONS: sessionUri },
223
});
224
try {
225
const client2 = new TestProtocolClient(server2.port);
226
await client2.connect();
227
await client2.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'test-config-restore-2' });
228
229
// Subscribing triggers the restore-on-subscribe path on the server,
230
// which reads `configValues` from the per-session DB and overlays
231
// them on the freshly-resolved schema.
232
const snapshot = await client2.call<SubscribeResult>('subscribe', { resource: sessionUri });
233
const state = snapshot.snapshot.state as SessionState;
234
235
assert.ok(state.config, 'restored session should have state.config populated');
236
// Schema is re-resolved by the provider (worktree-mode mock returns
237
// dynamic branch enum), so just check that our persisted user
238
// selections survived the round trip.
239
assert.deepStrictEqual(state.config.values, { isolation: 'worktree', branch: updatedBranch });
240
241
client2.close();
242
} finally {
243
server2.process.stdin!.end();
244
await new Promise<void>(resolve => server2.process.once('exit', () => resolve()));
245
}
246
});
247
});
248
249