Files
vscode/src/vs/platform/agentHost/test/node/sessionStateManager.test.ts
2026-03-17 20:34:42 -07:00

251 lines
9.0 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { DisposableStore } from '../../../../base/common/lifecycle.js';
import { URI } from '../../../../base/common/uri.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { NullLogService } from '../../../log/common/log.js';
import type { IActionEnvelope, INotification } from '../../common/state/sessionActions.js';
import { ISessionSummary, ROOT_STATE_URI, SessionLifecycle, SessionStatus, type ISessionState } from '../../common/state/sessionState.js';
import { SessionStateManager } from '../../node/sessionStateManager.js';
suite('SessionStateManager', () => {
let disposables: DisposableStore;
let manager: SessionStateManager;
const sessionUri = URI.from({ scheme: 'copilot', path: '/test-session' }).toString();
function makeSessionSummary(resource?: string): ISessionSummary {
return {
resource: resource ?? sessionUri,
provider: 'copilot',
title: 'Test',
status: SessionStatus.Idle,
createdAt: Date.now(),
modifiedAt: Date.now(),
};
}
setup(() => {
disposables = new DisposableStore();
manager = disposables.add(new SessionStateManager(new NullLogService()));
});
teardown(() => {
disposables.dispose();
});
ensureNoDisposablesAreLeakedInTestSuite();
test('createSession creates initial state with lifecycle Creating', () => {
const state = manager.createSession(makeSessionSummary());
assert.strictEqual(state.lifecycle, SessionLifecycle.Creating);
assert.strictEqual(state.turns.length, 0);
assert.strictEqual(state.activeTurn, undefined);
assert.strictEqual(state.summary.resource.toString(), sessionUri.toString());
});
test('getSnapshot returns undefined for unknown session', () => {
const unknown = URI.from({ scheme: 'copilot', path: '/unknown' }).toString();
const snapshot = manager.getSnapshot(unknown);
assert.strictEqual(snapshot, undefined);
});
test('getSnapshot returns root snapshot', () => {
const snapshot = manager.getSnapshot(ROOT_STATE_URI);
assert.ok(snapshot);
assert.strictEqual(snapshot.resource.toString(), ROOT_STATE_URI.toString());
assert.deepStrictEqual(snapshot.state, { agents: [], activeSessions: 0 });
});
test('getSnapshot returns session snapshot after creation', () => {
manager.createSession(makeSessionSummary());
const snapshot = manager.getSnapshot(sessionUri);
assert.ok(snapshot);
assert.strictEqual(snapshot.resource.toString(), sessionUri.toString());
assert.strictEqual((snapshot.state as ISessionState).lifecycle, SessionLifecycle.Creating);
});
test('dispatchServerAction applies action and emits envelope', () => {
manager.createSession(makeSessionSummary());
const envelopes: IActionEnvelope[] = [];
disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e)));
manager.dispatchServerAction({
type: 'session/ready',
session: sessionUri,
});
const state = manager.getSessionState(sessionUri);
assert.ok(state);
assert.strictEqual(state.lifecycle, SessionLifecycle.Ready);
assert.strictEqual(envelopes.length, 1);
assert.strictEqual(envelopes[0].action.type, 'session/ready');
assert.strictEqual(envelopes[0].serverSeq, 1);
assert.strictEqual(envelopes[0].origin, undefined);
});
test('serverSeq increments monotonically', () => {
manager.createSession(makeSessionSummary());
const envelopes: IActionEnvelope[] = [];
disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e)));
manager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
manager.dispatchServerAction({ type: 'session/titleChanged', session: sessionUri, title: 'Updated' });
assert.strictEqual(envelopes.length, 2);
assert.strictEqual(envelopes[0].serverSeq, 1);
assert.strictEqual(envelopes[1].serverSeq, 2);
assert.ok(envelopes[1].serverSeq > envelopes[0].serverSeq);
});
test('dispatchClientAction includes origin in envelope', () => {
manager.createSession(makeSessionSummary());
const envelopes: IActionEnvelope[] = [];
disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e)));
const origin = { clientId: 'renderer-1', clientSeq: 42 };
manager.dispatchClientAction(
{ type: 'session/ready', session: sessionUri },
origin,
);
assert.strictEqual(envelopes.length, 1);
assert.deepStrictEqual(envelopes[0].origin, origin);
});
test('removeSession clears state and emits notification', () => {
manager.createSession(makeSessionSummary());
const notifications: INotification[] = [];
disposables.add(manager.onDidEmitNotification(n => notifications.push(n)));
manager.removeSession(sessionUri);
assert.strictEqual(manager.getSessionState(sessionUri), undefined);
assert.strictEqual(manager.getSnapshot(sessionUri), undefined);
assert.strictEqual(notifications.length, 1);
assert.strictEqual(notifications[0].type, 'notify/sessionRemoved');
});
test('createSession emits sessionAdded notification', () => {
const notifications: INotification[] = [];
disposables.add(manager.onDidEmitNotification(n => notifications.push(n)));
manager.createSession(makeSessionSummary());
assert.strictEqual(notifications.length, 1);
assert.strictEqual(notifications[0].type, 'notify/sessionAdded');
});
test('getActiveTurnId returns active turn id after turnStarted', () => {
manager.createSession(makeSessionSummary());
manager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
assert.strictEqual(manager.getActiveTurnId(sessionUri), undefined);
manager.dispatchServerAction({
type: 'session/turnStarted',
session: sessionUri,
turnId: 'turn-1',
userMessage: { text: 'hello' },
});
assert.strictEqual(manager.getActiveTurnId(sessionUri), 'turn-1');
});
test('root state starts with activeSessions: 0', () => {
const snapshot = manager.getSnapshot(ROOT_STATE_URI);
assert.ok(snapshot);
assert.deepStrictEqual(snapshot.state, { agents: [], activeSessions: 0 });
});
test('turnStarted dispatches root/activeSessionsChanged with correct count', () => {
manager.createSession(makeSessionSummary());
manager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
const envelopes: IActionEnvelope[] = [];
disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e)));
manager.dispatchServerAction({
type: 'session/turnStarted',
session: sessionUri,
turnId: 'turn-1',
userMessage: { text: 'hello' },
});
const activeChanged = envelopes.filter(e => e.action.type === 'root/activeSessionsChanged');
assert.strictEqual(activeChanged.length, 1);
assert.strictEqual((activeChanged[0].action as { activeSessions: number }).activeSessions, 1);
assert.strictEqual(manager.rootState.activeSessions, 1);
});
test('turnComplete dispatches root/activeSessionsChanged back to 0', () => {
manager.createSession(makeSessionSummary());
manager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
manager.dispatchServerAction({
type: 'session/turnStarted',
session: sessionUri,
turnId: 'turn-1',
userMessage: { text: 'hello' },
});
const envelopes: IActionEnvelope[] = [];
disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e)));
manager.dispatchServerAction({
type: 'session/turnComplete',
session: sessionUri,
turnId: 'turn-1',
});
const activeChanged = envelopes.filter(e => e.action.type === 'root/activeSessionsChanged');
assert.strictEqual(activeChanged.length, 1);
assert.strictEqual((activeChanged[0].action as { activeSessions: number }).activeSessions, 0);
assert.strictEqual(manager.rootState.activeSessions, 0);
});
test('activeSessions reflects concurrent turn count across sessions', () => {
const session2Uri = URI.from({ scheme: 'copilot', path: '/test-session-2' }).toString();
manager.createSession(makeSessionSummary(sessionUri));
manager.createSession(makeSessionSummary(session2Uri));
manager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
manager.dispatchServerAction({ type: 'session/ready', session: session2Uri });
manager.dispatchServerAction({
type: 'session/turnStarted',
session: sessionUri,
turnId: 'turn-1',
userMessage: { text: 'a' },
});
manager.dispatchServerAction({
type: 'session/turnStarted',
session: session2Uri,
turnId: 'turn-2',
userMessage: { text: 'b' },
});
assert.strictEqual(manager.rootState.activeSessions, 2);
manager.dispatchServerAction({
type: 'session/turnComplete',
session: sessionUri,
turnId: 'turn-1',
});
assert.strictEqual(manager.rootState.activeSessions, 1);
manager.dispatchServerAction({
type: 'session/turnComplete',
session: session2Uri,
turnId: 'turn-2',
});
assert.strictEqual(manager.rootState.activeSessions, 0);
});
});