Merge remote-tracking branch 'origin/main' into connor4312/ahp-auth

This commit is contained in:
Connor Peet
2026-03-18 14:30:16 -07:00
107 changed files with 3811 additions and 2348 deletions

View File

@@ -31,6 +31,7 @@ import type {
ITurnCompleteAction,
IUsageAction,
} from '../../common/state/sessionActions.js';
import { PermissionKind } from '../../common/state/sessionState.js';
import { mapProgressEventToActions } from '../../node/agentEventMapper.js';
/** Helper: flatten the result of mapProgressEventToActions into an array. */
@@ -188,7 +189,7 @@ suite('AgentEventMapper', () => {
session,
type: 'permission_request',
requestId: 'perm-1',
permissionKind: 'shell',
permissionKind: PermissionKind.Shell,
toolCallId: 'tc-2',
fullCommandText: 'rm -rf /',
intention: 'Delete all files',

View File

@@ -10,7 +10,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c
import { NullLogService } from '../../../log/common/log.js';
import { FileService } from '../../../files/common/fileService.js';
import { AgentSession } from '../../common/agentService.js';
import { IActionEnvelope } from '../../common/state/sessionActions.js';
import { ActionType, IActionEnvelope } from '../../common/state/sessionActions.js';
import { AgentService } from '../../node/agentService.js';
import { MockAgent } from './mockAgent.js';
@@ -51,7 +51,7 @@ suite('AgentService (node dispatcher)', () => {
// Start a turn so there's an active turn to map events to
service.dispatchAction(
{ type: 'session/turnStarted', session: session.toString(), turnId: 'turn-1', userMessage: { text: 'hello' } },
{ type: ActionType.SessionTurnStarted, session: session.toString(), turnId: 'turn-1', userMessage: { text: 'hello' } },
'test-client', 1,
);
@@ -59,7 +59,7 @@ suite('AgentService (node dispatcher)', () => {
disposables.add(service.onDidAction(e => envelopes.push(e)));
copilotAgent.fireProgress({ session, type: 'delta', messageId: 'msg-1', content: 'hello' });
assert.ok(envelopes.some(e => e.action.type === 'session/delta'));
assert.ok(envelopes.some(e => e.action.type === ActionType.SessionDelta));
});
});
@@ -150,7 +150,7 @@ suite('AgentService (node dispatcher)', () => {
// Model fetch is async inside AgentSideEffects — wait for it
await new Promise(r => setTimeout(r, 50));
const agentsChanged = envelopes.find(e => e.action.type === 'root/agentsChanged');
const agentsChanged = envelopes.find(e => e.action.type === ActionType.RootAgentsChanged);
assert.ok(agentsChanged);
});
});

View File

@@ -14,8 +14,8 @@ import { FileService } from '../../../files/common/fileService.js';
import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js';
import { NullLogService } from '../../../log/common/log.js';
import { AgentSession, IAgent } from '../../common/agentService.js';
import { IActionEnvelope, ISessionAction } from '../../common/state/sessionActions.js';
import { SessionStatus } from '../../common/state/sessionState.js';
import { ActionType, IActionEnvelope, ISessionAction } from '../../common/state/sessionActions.js';
import { PermissionKind, SessionStatus } from '../../common/state/sessionState.js';
import { AgentSideEffects } from '../../node/agentSideEffects.js';
import { SessionStateManager } from '../../node/sessionStateManager.js';
import { MockAgent } from './mockAgent.js';
@@ -42,12 +42,12 @@ suite('AgentSideEffects', () => {
createdAt: Date.now(),
modifiedAt: Date.now(),
});
stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri.toString() });
stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri.toString() });
}
function startTurn(turnId: string): void {
stateManager.dispatchClientAction(
{ type: 'session/turnStarted', session: sessionUri.toString(), turnId, userMessage: { text: 'hello' } },
{ type: ActionType.SessionTurnStarted, session: sessionUri.toString(), turnId, userMessage: { text: 'hello' } },
{ clientId: 'test', clientSeq: 1 },
);
}
@@ -84,7 +84,7 @@ suite('AgentSideEffects', () => {
test('calls sendMessage on the agent', async () => {
setupSession();
const action: ISessionAction = {
type: 'session/turnStarted',
type: ActionType.SessionTurnStarted,
session: sessionUri.toString(),
turnId: 'turn-1',
userMessage: { text: 'hello world' },
@@ -109,13 +109,13 @@ suite('AgentSideEffects', () => {
disposables.add(stateManager.onDidEmitEnvelope(e => envelopes.push(e)));
noAgentSideEffects.handleAction({
type: 'session/turnStarted',
type: ActionType.SessionTurnStarted,
session: sessionUri.toString(),
turnId: 'turn-1',
userMessage: { text: 'hello' },
});
const errorAction = envelopes.find(e => e.action.type === 'session/error');
const errorAction = envelopes.find(e => e.action.type === ActionType.SessionError);
assert.ok(errorAction, 'should dispatch session/error');
});
});
@@ -127,7 +127,7 @@ suite('AgentSideEffects', () => {
test('calls abortSession on the agent', async () => {
setupSession();
sideEffects.handleAction({
type: 'session/turnCancelled',
type: ActionType.SessionTurnCancelled,
session: sessionUri.toString(),
turnId: 'turn-1',
});
@@ -152,14 +152,14 @@ suite('AgentSideEffects', () => {
session: sessionUri,
type: 'permission_request',
requestId: 'perm-1',
permissionKind: 'write',
permissionKind: PermissionKind.Write,
path: 'file.ts',
rawRequest: '{}',
});
// Now resolve it
sideEffects.handleAction({
type: 'session/permissionResolved',
type: ActionType.SessionPermissionResolved,
session: sessionUri.toString(),
turnId: 'turn-1',
requestId: 'perm-1',
@@ -177,7 +177,7 @@ suite('AgentSideEffects', () => {
test('calls changeModel on the agent', async () => {
setupSession();
sideEffects.handleAction({
type: 'session/modelChanged',
type: ActionType.SessionModelChanged,
session: sessionUri.toString(),
model: 'gpt-5',
});
@@ -202,7 +202,7 @@ suite('AgentSideEffects', () => {
agent.fireProgress({ session: sessionUri, type: 'delta', messageId: 'msg-1', content: 'hi' });
assert.ok(envelopes.some(e => e.action.type === 'session/delta'));
assert.ok(envelopes.some(e => e.action.type === ActionType.SessionDelta));
});
test('returns a disposable that stops listening', () => {
@@ -214,11 +214,11 @@ suite('AgentSideEffects', () => {
const listener = sideEffects.registerProgressListener(agent);
agent.fireProgress({ session: sessionUri, type: 'delta', messageId: 'msg-1', content: 'before' });
assert.strictEqual(envelopes.filter(e => e.action.type === 'session/delta').length, 1);
assert.strictEqual(envelopes.filter(e => e.action.type === ActionType.SessionDelta).length, 1);
listener.dispose();
agent.fireProgress({ session: sessionUri, type: 'delta', messageId: 'msg-2', content: 'after' });
assert.strictEqual(envelopes.filter(e => e.action.type === 'session/delta').length, 1);
assert.strictEqual(envelopes.filter(e => e.action.type === ActionType.SessionDelta).length, 1);
});
});
@@ -232,7 +232,7 @@ suite('AgentSideEffects', () => {
await sideEffects.handleCreateSession({ session: sessionUri.toString(), provider: 'mock' });
const ready = envelopes.find(e => e.action.type === 'session/ready');
const ready = envelopes.find(e => e.action.type === ActionType.SessionReady);
assert.ok(ready, 'should dispatch session/ready');
});
@@ -318,7 +318,7 @@ suite('AgentSideEffects', () => {
// Model fetch is async — wait for it
await new Promise(r => setTimeout(r, 50));
const action = envelopes.find(e => e.action.type === 'root/agentsChanged');
const action = envelopes.find(e => e.action.type === ActionType.RootAgentsChanged);
assert.ok(action, 'should dispatch root/agentsChanged');
});
});

View File

@@ -7,6 +7,7 @@ import { Emitter } from '../../../../base/common/event.js';
import { URI } from '../../../../base/common/uri.js';
import type { IAuthorizationProtectedResourceMetadata } from '../../../../base/common/oauth.js';
import { AgentSession, type AgentProvider, type IAgent, type IAgentAttachment, type IAgentCreateSessionConfig, type IAgentDescriptor, type IAgentMessageEvent, type IAgentModelInfo, type IAgentProgressEvent, type IAgentSessionMetadata, type IAgentToolCompleteEvent, type IAgentToolStartEvent } from '../../common/agentService.js';
import { PermissionKind } from '../../common/state/sessionState.js';
/**
* General-purpose mock agent for unit tests. Tracks all method calls
@@ -163,10 +164,10 @@ export class ScriptedMockAgent implements IAgent {
type: 'permission_request',
session,
requestId: 'perm-1',
permissionKind: 'shell',
permissionKind: PermissionKind.Shell,
fullCommandText: 'echo test',
intention: 'Run a test command',
rawRequest: JSON.stringify({ permissionKind: 'shell', fullCommandText: 'echo test', intention: 'Run a test command' }),
rawRequest: JSON.stringify({ permissionKind: PermissionKind.Shell, fullCommandText: 'echo test', intention: 'Run a test command' }),
};
setTimeout(() => this._onDidSessionProgress.fire(permEvent), 10);
this._pendingPermissions.set('perm-1', (approved) => {

View File

@@ -9,7 +9,7 @@ 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 { ISessionAction } from '../../common/state/sessionActions.js';
import { ActionType, type ISessionAction } from '../../common/state/sessionActions.js';
import { isJsonRpcNotification, isJsonRpcResponse, JSON_RPC_INTERNAL_ERROR, ProtocolError, type ICreateSessionParams, type IInitializeResult, type IProtocolMessage, type IAhpNotification, type IReconnectResult, type IStateSnapshot } from '../../common/state/sessionProtocol.js';
import { SessionStatus, type ISessionSummary } from '../../common/state/sessionState.js';
import { PROTOCOL_VERSION } from '../../common/state/sessionCapabilities.js';
@@ -207,7 +207,7 @@ suite('ProtocolServerHandler', () => {
test('client action is dispatched and echoed', () => {
stateManager.createSession(makeSessionSummary());
stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri });
const transport = connectClient('client-1', [sessionUri]);
transport.sent.length = 0;
@@ -215,7 +215,7 @@ suite('ProtocolServerHandler', () => {
transport.simulateMessage(notification('dispatchAction', {
clientSeq: 1,
action: {
type: 'session/turnStarted',
type: ActionType.SessionTurnStarted,
session: sessionUri,
turnId: 'turn-1',
userMessage: { text: 'hello' },
@@ -225,7 +225,7 @@ suite('ProtocolServerHandler', () => {
const actionMsgs = findNotifications(transport.sent, 'action');
const turnStarted = actionMsgs.find(m => {
const envelope = m.params as unknown as { action: { type: string } };
return envelope.action.type === 'session/turnStarted';
return envelope.action.type === ActionType.SessionTurnStarted;
});
assert.ok(turnStarted, 'should have echoed turnStarted');
const envelope = turnStarted!.params as unknown as { origin: { clientId: string; clientSeq: number } };
@@ -235,7 +235,7 @@ suite('ProtocolServerHandler', () => {
test('actions are scoped to subscribed sessions', () => {
stateManager.createSession(makeSessionSummary());
stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri });
const transportA = connectClient('client-a', [sessionUri]);
const transportB = connectClient('client-b');
@@ -244,7 +244,7 @@ suite('ProtocolServerHandler', () => {
transportB.sent.length = 0;
stateManager.dispatchServerAction({
type: 'session/titleChanged',
type: ActionType.SessionTitleChanged,
session: sessionUri,
title: 'New Title',
});
@@ -268,15 +268,15 @@ suite('ProtocolServerHandler', () => {
test('reconnect replays missed actions', () => {
stateManager.createSession(makeSessionSummary());
stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri });
const transport1 = connectClient('client-r', [sessionUri]);
const resp = findResponse(transport1.sent, 1);
const initSeq = (resp as { result: IInitializeResult }).result.serverSeq;
transport1.simulateClose();
stateManager.dispatchServerAction({ type: 'session/titleChanged', session: sessionUri, title: 'Title A' });
stateManager.dispatchServerAction({ type: 'session/titleChanged', session: sessionUri, title: 'Title B' });
stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Title A' });
stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Title B' });
const transport2 = new MockProtocolTransport();
server.simulateConnection(transport2);
@@ -297,13 +297,13 @@ suite('ProtocolServerHandler', () => {
test('reconnect sends fresh snapshots when gap too large', () => {
stateManager.createSession(makeSessionSummary());
stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri });
const transport1 = connectClient('client-g', [sessionUri]);
transport1.simulateClose();
for (let i = 0; i < 1100; i++) {
stateManager.dispatchServerAction({ type: 'session/titleChanged', session: sessionUri, title: `Title ${i}` });
stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: `Title ${i}` });
}
const transport2 = new MockProtocolTransport();
@@ -325,14 +325,14 @@ suite('ProtocolServerHandler', () => {
test('client disconnect cleans up', () => {
stateManager.createSession(makeSessionSummary());
stateManager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri });
const transport = connectClient('client-d', [sessionUri]);
transport.sent.length = 0;
transport.simulateClose();
stateManager.dispatchServerAction({ type: 'session/titleChanged', session: sessionUri, title: 'After Disconnect' });
stateManager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'After Disconnect' });
assert.strictEqual(transport.sent.length, 0);
});

View File

@@ -8,7 +8,7 @@ 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 { ActionType, NotificationType, type IActionEnvelope, type 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';
@@ -76,7 +76,7 @@ suite('SessionStateManager', () => {
disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e)));
manager.dispatchServerAction({
type: 'session/ready',
type: ActionType.SessionReady,
session: sessionUri,
});
@@ -85,7 +85,7 @@ suite('SessionStateManager', () => {
assert.strictEqual(state.lifecycle, SessionLifecycle.Ready);
assert.strictEqual(envelopes.length, 1);
assert.strictEqual(envelopes[0].action.type, 'session/ready');
assert.strictEqual(envelopes[0].action.type, ActionType.SessionReady);
assert.strictEqual(envelopes[0].serverSeq, 1);
assert.strictEqual(envelopes[0].origin, undefined);
});
@@ -96,8 +96,8 @@ suite('SessionStateManager', () => {
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' });
manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri });
manager.dispatchServerAction({ type: ActionType.SessionTitleChanged, session: sessionUri, title: 'Updated' });
assert.strictEqual(envelopes.length, 2);
assert.strictEqual(envelopes[0].serverSeq, 1);
@@ -113,7 +113,7 @@ suite('SessionStateManager', () => {
const origin = { clientId: 'renderer-1', clientSeq: 42 };
manager.dispatchClientAction(
{ type: 'session/ready', session: sessionUri },
{ type: ActionType.SessionReady, session: sessionUri },
origin,
);
@@ -132,7 +132,7 @@ suite('SessionStateManager', () => {
assert.strictEqual(manager.getSessionState(sessionUri), undefined);
assert.strictEqual(manager.getSnapshot(sessionUri), undefined);
assert.strictEqual(notifications.length, 1);
assert.strictEqual(notifications[0].type, 'notify/sessionRemoved');
assert.strictEqual(notifications[0].type, NotificationType.SessionRemoved);
});
test('createSession emits sessionAdded notification', () => {
@@ -142,17 +142,17 @@ suite('SessionStateManager', () => {
manager.createSession(makeSessionSummary());
assert.strictEqual(notifications.length, 1);
assert.strictEqual(notifications[0].type, 'notify/sessionAdded');
assert.strictEqual(notifications[0].type, NotificationType.SessionAdded);
});
test('getActiveTurnId returns active turn id after turnStarted', () => {
manager.createSession(makeSessionSummary());
manager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri });
assert.strictEqual(manager.getActiveTurnId(sessionUri), undefined);
manager.dispatchServerAction({
type: 'session/turnStarted',
type: ActionType.SessionTurnStarted,
session: sessionUri,
turnId: 'turn-1',
userMessage: { text: 'hello' },
@@ -169,19 +169,19 @@ suite('SessionStateManager', () => {
test('turnStarted dispatches root/activeSessionsChanged with correct count', () => {
manager.createSession(makeSessionSummary());
manager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri });
const envelopes: IActionEnvelope[] = [];
disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e)));
manager.dispatchServerAction({
type: 'session/turnStarted',
type: ActionType.SessionTurnStarted,
session: sessionUri,
turnId: 'turn-1',
userMessage: { text: 'hello' },
});
const activeChanged = envelopes.filter(e => e.action.type === 'root/activeSessionsChanged');
const activeChanged = envelopes.filter(e => e.action.type === ActionType.RootActiveSessionsChanged);
assert.strictEqual(activeChanged.length, 1);
assert.strictEqual((activeChanged[0].action as { activeSessions: number }).activeSessions, 1);
assert.strictEqual(manager.rootState.activeSessions, 1);
@@ -189,9 +189,9 @@ suite('SessionStateManager', () => {
test('turnComplete dispatches root/activeSessionsChanged back to 0', () => {
manager.createSession(makeSessionSummary());
manager.dispatchServerAction({ type: 'session/ready', session: sessionUri });
manager.dispatchServerAction({ type: ActionType.SessionReady, session: sessionUri });
manager.dispatchServerAction({
type: 'session/turnStarted',
type: ActionType.SessionTurnStarted,
session: sessionUri,
turnId: 'turn-1',
userMessage: { text: 'hello' },
@@ -201,12 +201,12 @@ suite('SessionStateManager', () => {
disposables.add(manager.onDidEmitEnvelope(e => envelopes.push(e)));
manager.dispatchServerAction({
type: 'session/turnComplete',
type: ActionType.SessionTurnComplete,
session: sessionUri,
turnId: 'turn-1',
});
const activeChanged = envelopes.filter(e => e.action.type === 'root/activeSessionsChanged');
const activeChanged = envelopes.filter(e => e.action.type === ActionType.RootActiveSessionsChanged);
assert.strictEqual(activeChanged.length, 1);
assert.strictEqual((activeChanged[0].action as { activeSessions: number }).activeSessions, 0);
assert.strictEqual(manager.rootState.activeSessions, 0);
@@ -216,17 +216,17 @@ suite('SessionStateManager', () => {
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: ActionType.SessionReady, session: sessionUri });
manager.dispatchServerAction({ type: ActionType.SessionReady, session: session2Uri });
manager.dispatchServerAction({
type: 'session/turnStarted',
type: ActionType.SessionTurnStarted,
session: sessionUri,
turnId: 'turn-1',
userMessage: { text: 'a' },
});
manager.dispatchServerAction({
type: 'session/turnStarted',
type: ActionType.SessionTurnStarted,
session: session2Uri,
turnId: 'turn-2',
userMessage: { text: 'b' },
@@ -234,14 +234,14 @@ suite('SessionStateManager', () => {
assert.strictEqual(manager.rootState.activeSessions, 2);
manager.dispatchServerAction({
type: 'session/turnComplete',
type: ActionType.SessionTurnComplete,
session: sessionUri,
turnId: 'turn-1',
});
assert.strictEqual(manager.rootState.activeSessions, 1);
manager.dispatchServerAction({
type: 'session/turnComplete',
type: ActionType.SessionTurnComplete,
session: session2Uri,
turnId: 'turn-2',
});