Path: blob/main/src/vs/platform/agentHost/test/node/protocol/sessionConfig.integrationTest.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { mkdtempSync, rmSync } from 'fs';7import { tmpdir } from 'os';8import { URI } from '../../../../../base/common/uri.js';9import type { ResolveSessionConfigResult, SessionConfigCompletionsResult, SubscribeResult } from '../../../common/state/protocol/commands.js';10import { ActionType, type SessionAddedNotification } from '../../../common/state/sessionActions.js';11import { PROTOCOL_VERSION } from '../../../common/state/sessionCapabilities.js';12import type { INotificationBroadcastParams } from '../../../common/state/sessionProtocol.js';13import type { SessionState } from '../../../common/state/sessionState.js';14import {15getActionEnvelope,16isActionNotification,17IServerHandle,18nextSessionUri,19startServer,20TestProtocolClient,21} from './testHelpers.js';2223suite('Protocol WebSocket - Session Config', function () {2425let server: IServerHandle;26let client: TestProtocolClient;2728suiteSetup(async function () {29this.timeout(15_000);30server = await startServer();31});3233suiteTeardown(function () {34server.process.kill();35});3637setup(async function () {38this.timeout(10_000);39client = new TestProtocolClient(server.port);40await client.connect();41await client.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'test-session-config' });42});4344teardown(function () {45client.close();46});4748test('resolveSessionConfig returns schema and re-resolves dependent read-only state', async function () {49this.timeout(10_000);5051const workingDirectory = URI.file('/mock/workspace').toString();52const initial = await client.call<ResolveSessionConfigResult>('resolveSessionConfig', {53provider: 'mock',54workingDirectory,55});5657assert.deepStrictEqual(initial.values, { isolation: 'worktree', branch: 'main' });58assert.deepStrictEqual(Object.keys(initial.schema.properties), ['isolation', 'branch']);59assert.deepStrictEqual(initial.schema.properties.branch.enum, ['main']);60assert.strictEqual(initial.schema.properties.branch.enumDynamic, true);61assert.strictEqual(initial.schema.properties.branch.readOnly, false);6263const folder = await client.call<ResolveSessionConfigResult>('resolveSessionConfig', {64provider: 'mock',65workingDirectory,66config: { isolation: 'folder', branch: 'feature/config' },67});6869assert.deepStrictEqual(folder.values, { isolation: 'folder', branch: 'main' });70assert.strictEqual(folder.schema.properties.branch.enumDynamic, false);71assert.strictEqual(folder.schema.properties.branch.readOnly, true);72});7374test('sessionConfigCompletions returns dynamic branch matches', async function () {75this.timeout(10_000);7677const result = await client.call<SessionConfigCompletionsResult>('sessionConfigCompletions', {78provider: 'mock',79workingDirectory: URI.file('/mock/workspace').toString(),80config: { isolation: 'worktree' },81property: 'branch',82query: 'feat',83});8485assert.deepStrictEqual(result, {86items: [{ value: 'feature/config', label: 'feature/config' }],87});88});8990test('createSession stores config schema and values on session state', async function () {91this.timeout(10_000);9293const config = { isolation: 'worktree', branch: 'feature/config' };94await client.call('createSession', {95session: nextSessionUri(),96provider: 'mock',97workingDirectory: URI.file('/mock/workspace').toString(),98config,99});100101const notif = await client.waitForNotification(n =>102n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded'103);104const notification = (notif.params as INotificationBroadcastParams).notification as SessionAddedNotification;105assert.strictEqual(Object.hasOwn(notification.summary, 'config'), false);106107const snapshot = await client.call<SubscribeResult>('subscribe', { resource: notification.summary.resource });108const state = snapshot.snapshot.state as SessionState;109assert.deepStrictEqual(state.config?.values, config);110assert.deepStrictEqual(Object.keys(state.config?.schema.properties ?? {}), ['isolation', 'branch']);111});112113test('session/configChanged merges config values into session state', async function () {114this.timeout(10_000);115116await client.call('createSession', {117session: nextSessionUri(),118provider: 'mock',119config: { isolation: 'folder', branch: 'main' },120});121122const notif = await client.waitForNotification(n =>123n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded'124);125const session = ((notif.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource;126await client.call<SubscribeResult>('subscribe', { resource: session });127client.clearReceived();128129client.notify('dispatchAction', {130clientSeq: 1,131action: {132type: ActionType.SessionConfigChanged,133session,134config: { branch: 'release' },135},136});137138const configChanged = await client.waitForNotification(n => isActionNotification(n, ActionType.SessionConfigChanged));139assert.strictEqual(getActionEnvelope(configChanged).action.type, ActionType.SessionConfigChanged);140141const snapshot = await client.call<SubscribeResult>('subscribe', { resource: session });142const state = snapshot.snapshot.state as SessionState;143assert.deepStrictEqual(state.config?.values, { isolation: 'folder', branch: 'release' });144});145});146147suite('Protocol WebSocket - Session Config persistence across restarts', function () {148149let userDataDir: string;150151setup(function () {152userDataDir = mkdtempSync(`${tmpdir()}/vscode-agent-host-config-`);153});154155teardown(function () {156try {157rmSync(userDataDir, { recursive: true, force: true });158} catch {159// Best-effort cleanup; the OS will reap the temp dir eventually.160}161});162163test('persisted config values are restored on subscribe after server restart', async function () {164this.timeout(30_000);165166const initialConfig = { isolation: 'worktree', branch: 'main' };167const updatedBranch = 'release';168let sessionUri: string;169170// ---- Phase 1: create session, change config, wait for persistence ----171const server1 = await startServer({ userDataDir });172try {173const client1 = new TestProtocolClient(server1.port);174await client1.connect();175await client1.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'test-config-restore-1' });176177await client1.call('createSession', {178session: nextSessionUri(),179provider: 'mock',180workingDirectory: URI.file('/mock/workspace').toString(),181config: initialConfig,182});183const addedNotif = await client1.waitForNotification(n =>184n.method === 'notification' && (n.params as INotificationBroadcastParams).notification.type === 'notify/sessionAdded'185);186// The mock agent assigns its own URI rather than honoring the187// requested one, so capture the real URI from the notification.188sessionUri = ((addedNotif.params as INotificationBroadcastParams).notification as SessionAddedNotification).summary.resource;189190await client1.call<SubscribeResult>('subscribe', { resource: sessionUri });191192client1.notify('dispatchAction', {193clientSeq: 1,194action: {195type: ActionType.SessionConfigChanged,196session: sessionUri,197config: { branch: updatedBranch },198},199});200const configChanged = await client1.waitForNotification(n => isActionNotification(n, ActionType.SessionConfigChanged));201assert.strictEqual(getActionEnvelope(configChanged).action.type, ActionType.SessionConfigChanged);202203client1.close();204} finally {205// Trigger graceful shutdown by closing stdin rather than sending206// SIGTERM — on Windows, `child.kill()` (SIGTERM) unconditionally207// terminates the process without invoking the shutdown handler,208// so in-flight `setMetadata` writes never reach SQLite. Closing209// stdin fires `process.stdin.on('end', shutdown)` in the server210// on every platform.211server1.process.stdin!.end();212await new Promise<void>(resolve => server1.process.once('exit', () => resolve()));213}214215// ---- Phase 2: restart server, subscribe, verify restored config ----216// The mock agent does not persist its in-memory session list across217// restarts, so seed it via env var so `agent.listSessions()` includes218// our session and `restoreSession` proceeds.219const server2 = await startServer({220userDataDir,221env: { VSCODE_AGENT_HOST_MOCK_SEED_SESSIONS: sessionUri },222});223try {224const client2 = new TestProtocolClient(server2.port);225await client2.connect();226await client2.call('initialize', { protocolVersion: PROTOCOL_VERSION, clientId: 'test-config-restore-2' });227228// Subscribing triggers the restore-on-subscribe path on the server,229// which reads `configValues` from the per-session DB and overlays230// them on the freshly-resolved schema.231const snapshot = await client2.call<SubscribeResult>('subscribe', { resource: sessionUri });232const state = snapshot.snapshot.state as SessionState;233234assert.ok(state.config, 'restored session should have state.config populated');235// Schema is re-resolved by the provider (worktree-mode mock returns236// dynamic branch enum), so just check that our persisted user237// selections survived the round trip.238assert.deepStrictEqual(state.config.values, { isolation: 'worktree', branch: updatedBranch });239240client2.close();241} finally {242server2.process.stdin!.end();243await new Promise<void>(resolve => server2.process.once('exit', () => resolve()));244}245});246});247248249