Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeSessionStateService.spec.ts
13406 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 sinon from 'sinon';6import { afterEach, assert, beforeEach, describe, it } from 'vitest';7import type { ClaudeFolderInfo } from '../../common/claudeFolderInfo';8import { parseClaudeModelId } from '../claudeModelId';9import type { SessionStateChangeEvent } from '../../common/claudeSessionStateService';10import { ClaudeSessionStateService } from '../claudeSessionStateService';1112const OPUS_4 = parseClaudeModelId('claude-opus-4-20250514');13const HAIKU_3_5 = parseClaudeModelId('claude-haiku-3-5-20250514');1415describe('ClaudeSessionStateService', () => {16let service: ClaudeSessionStateService;1718beforeEach(() => {19service = new ClaudeSessionStateService();20});2122afterEach(() => {23service.dispose();24sinon.restore();25});2627describe('getModelIdForSession', () => {28it('should return undefined when no model is set for a session', () => {29const modelId = service.getModelIdForSession('session-1');30assert.strictEqual(modelId, undefined);31});3233it('should return the set model when one has been set for a session', () => {34service.setModelIdForSession('session-1', OPUS_4);35const modelId = service.getModelIdForSession('session-1');36assert.strictEqual(modelId, OPUS_4);37});3839it('should return different models for different sessions', () => {40service.setModelIdForSession('session-1', OPUS_4);41service.setModelIdForSession('session-2', HAIKU_3_5);4243const modelId1 = service.getModelIdForSession('session-1');44const modelId2 = service.getModelIdForSession('session-2');4546assert.strictEqual(modelId1, OPUS_4);47assert.strictEqual(modelId2, HAIKU_3_5);48});4950it('should return undefined when model is explicitly set to undefined', () => {51service.setModelIdForSession('session-1', OPUS_4);52service.setModelIdForSession('session-1', undefined);5354const modelId = service.getModelIdForSession('session-1');55assert.strictEqual(modelId, undefined);56});57});5859describe('setModelIdForSession', () => {60it('should fire onDidChangeSessionState event when model is set', () => {61const events: SessionStateChangeEvent[] = [];62service.onDidChangeSessionState(e => events.push(e));6364service.setModelIdForSession('session-1', OPUS_4);6566assert.strictEqual(events.length, 1);67assert.strictEqual(events[0].sessionId, 'session-1');68assert.strictEqual(events[0].modelId, OPUS_4);69assert.strictEqual(events[0].permissionMode, undefined);70});7172it('should preserve permission mode when setting model', () => {73service.setPermissionModeForSession('session-1', 'bypassPermissions');74service.setModelIdForSession('session-1', OPUS_4);7576const permissionMode = service.getPermissionModeForSession('session-1');77assert.strictEqual(permissionMode, 'bypassPermissions');78});79});8081describe('getPermissionModeForSession', () => {82it('should return default acceptEdits when no mode is set', () => {83const mode = service.getPermissionModeForSession('session-1');84assert.strictEqual(mode, 'acceptEdits');85});8687it('should return the set permission mode', () => {88service.setPermissionModeForSession('session-1', 'bypassPermissions');89const mode = service.getPermissionModeForSession('session-1');90assert.strictEqual(mode, 'bypassPermissions');91});9293it('should return different modes for different sessions', () => {94service.setPermissionModeForSession('session-1', 'bypassPermissions');95service.setPermissionModeForSession('session-2', 'default');9697const mode1 = service.getPermissionModeForSession('session-1');98const mode2 = service.getPermissionModeForSession('session-2');99100assert.strictEqual(mode1, 'bypassPermissions');101assert.strictEqual(mode2, 'default');102});103});104105describe('setPermissionModeForSession', () => {106it('should fire onDidChangeSessionState event when permission mode is set', () => {107const events: SessionStateChangeEvent[] = [];108service.onDidChangeSessionState(e => events.push(e));109110service.setPermissionModeForSession('session-1', 'bypassPermissions');111112assert.strictEqual(events.length, 1);113assert.strictEqual(events[0].sessionId, 'session-1');114assert.strictEqual(events[0].permissionMode, 'bypassPermissions');115assert.strictEqual(events[0].modelId, undefined);116});117118it('should preserve model id when setting permission mode', () => {119service.setModelIdForSession('session-1', OPUS_4);120service.setPermissionModeForSession('session-1', 'bypassPermissions');121122const modelId = service.getModelIdForSession('session-1');123assert.strictEqual(modelId, OPUS_4);124});125});126127describe('getFolderInfoForSession', () => {128it('should return undefined when no folder info is set', () => {129const folderInfo = service.getFolderInfoForSession('session-1');130assert.strictEqual(folderInfo, undefined);131});132133it('should return the set folder info', () => {134const info: ClaudeFolderInfo = { cwd: '/home/user', additionalDirectories: ['/tmp'] };135service.setFolderInfoForSession('session-1', info);136const folderInfo = service.getFolderInfoForSession('session-1');137assert.deepStrictEqual(folderInfo, info);138});139});140141describe('setFolderInfoForSession', () => {142it('should fire onDidChangeSessionState event when folder info is set', () => {143const events: SessionStateChangeEvent[] = [];144service.onDidChangeSessionState(e => events.push(e));145146const info: ClaudeFolderInfo = { cwd: '/home/user', additionalDirectories: [] };147service.setFolderInfoForSession('session-1', info);148149assert.strictEqual(events.length, 1);150assert.strictEqual(events[0].sessionId, 'session-1');151assert.deepStrictEqual(events[0].folderInfo, info);152assert.strictEqual(events[0].modelId, undefined);153assert.strictEqual(events[0].permissionMode, undefined);154});155156it('should not fire event when folder info is unchanged', () => {157const info: ClaudeFolderInfo = { cwd: '/home/user', additionalDirectories: ['/tmp'] };158service.setFolderInfoForSession('session-1', info);159160const events: SessionStateChangeEvent[] = [];161service.onDidChangeSessionState(e => events.push(e));162163service.setFolderInfoForSession('session-1', { cwd: '/home/user', additionalDirectories: ['/tmp'] });164assert.strictEqual(events.length, 0);165});166167it('should fire event when cwd changes', () => {168service.setFolderInfoForSession('session-1', { cwd: '/home/user', additionalDirectories: [] });169170const events: SessionStateChangeEvent[] = [];171service.onDidChangeSessionState(e => events.push(e));172173service.setFolderInfoForSession('session-1', { cwd: '/home/other', additionalDirectories: [] });174assert.strictEqual(events.length, 1);175});176177it('should fire event when additionalDirectories change', () => {178service.setFolderInfoForSession('session-1', { cwd: '/home/user', additionalDirectories: ['/tmp'] });179180const events: SessionStateChangeEvent[] = [];181service.onDidChangeSessionState(e => events.push(e));182183service.setFolderInfoForSession('session-1', { cwd: '/home/user', additionalDirectories: ['/tmp', '/var'] });184assert.strictEqual(events.length, 1);185});186187it('should preserve other state when setting folder info', () => {188service.setModelIdForSession('session-1', OPUS_4);189service.setPermissionModeForSession('session-1', 'bypassPermissions');190service.setFolderInfoForSession('session-1', { cwd: '/home/user', additionalDirectories: [] });191192const modelId = service.getModelIdForSession('session-1');193assert.strictEqual(modelId, OPUS_4);194const permissionMode = service.getPermissionModeForSession('session-1');195assert.strictEqual(permissionMode, 'bypassPermissions');196});197});198199describe('getReasoningEffortForSession', () => {200it('should return undefined when no reasoning effort is set', () => {201const effort = service.getReasoningEffortForSession('session-1');202assert.strictEqual(effort, undefined);203});204205it('should return the set reasoning effort', () => {206service.setReasoningEffortForSession('session-1', 'high');207const effort = service.getReasoningEffortForSession('session-1');208assert.strictEqual(effort, 'high');209});210211it('should return different efforts for different sessions', () => {212service.setReasoningEffortForSession('session-1', 'high');213service.setReasoningEffortForSession('session-2', 'low');214215assert.strictEqual(service.getReasoningEffortForSession('session-1'), 'high');216assert.strictEqual(service.getReasoningEffortForSession('session-2'), 'low');217});218});219220describe('setReasoningEffortForSession', () => {221it('should allow setting a reasoning effort', () => {222service.setReasoningEffortForSession('session-1', 'medium');223assert.strictEqual(service.getReasoningEffortForSession('session-1'), 'medium');224});225226it('should allow clearing a reasoning effort', () => {227service.setReasoningEffortForSession('session-1', 'high');228service.setReasoningEffortForSession('session-1', undefined);229assert.strictEqual(service.getReasoningEffortForSession('session-1'), undefined);230});231232it('should not update state when effort is unchanged', () => {233service.setReasoningEffortForSession('session-1', 'high');234const stateBefore = service.getReasoningEffortForSession('session-1');235service.setReasoningEffortForSession('session-1', 'high');236assert.strictEqual(service.getReasoningEffortForSession('session-1'), stateBefore);237});238239it('should preserve other state when setting reasoning effort', () => {240service.setModelIdForSession('session-1', OPUS_4);241service.setPermissionModeForSession('session-1', 'bypassPermissions');242243service.setReasoningEffortForSession('session-1', 'high');244245assert.strictEqual(service.getModelIdForSession('session-1'), OPUS_4);246assert.strictEqual(service.getPermissionModeForSession('session-1'), 'bypassPermissions');247});248249it('should not fire onDidChangeSessionState event', () => {250const events: SessionStateChangeEvent[] = [];251service.onDidChangeSessionState(e => events.push(e));252253service.setReasoningEffortForSession('session-1', 'high');254255assert.strictEqual(events.length, 0);256});257258it('should initialize defaults when session has no prior state', () => {259service.setReasoningEffortForSession('new-session', 'medium');260261assert.strictEqual(service.getModelIdForSession('new-session'), undefined);262assert.strictEqual(service.getPermissionModeForSession('new-session'), 'acceptEdits');263assert.strictEqual(service.getCapturingTokenForSession('new-session'), undefined);264assert.strictEqual(service.getFolderInfoForSession('new-session'), undefined);265assert.strictEqual(service.getUsageHandlerForSession('new-session'), undefined);266assert.strictEqual(service.getReasoningEffortForSession('new-session'), 'medium');267});268});269270describe('dispose', () => {271it('should clear session state on dispose', () => {272service.setModelIdForSession('session-1', OPUS_4);273service.setPermissionModeForSession('session-1', 'bypassPermissions');274275service.dispose();276277// After dispose, getting state should return defaults (though event subscriptions won't work)278// We can't really test this fully without internal access, but we can verify it doesn't throw279const newService = new ClaudeSessionStateService();280const modelId = newService.getModelIdForSession('session-1');281assert.strictEqual(modelId, undefined);282newService.dispose();283});284});285286describe('getUsageHandlerForSession', () => {287it('should return undefined when no usage handler is set', () => {288const handler = service.getUsageHandlerForSession('session-1');289assert.strictEqual(handler, undefined);290});291292it('should return the set usage handler', () => {293const mockHandler = sinon.stub();294service.setUsageHandlerForSession('session-1', mockHandler);295const handler = service.getUsageHandlerForSession('session-1');296assert.strictEqual(handler, mockHandler);297});298299it('should return different handlers for different sessions', () => {300const handler1 = sinon.stub();301const handler2 = sinon.stub();302service.setUsageHandlerForSession('session-1', handler1);303service.setUsageHandlerForSession('session-2', handler2);304305const retrieved1 = service.getUsageHandlerForSession('session-1');306const retrieved2 = service.getUsageHandlerForSession('session-2');307308assert.strictEqual(retrieved1, handler1);309assert.strictEqual(retrieved2, handler2);310});311});312313describe('setUsageHandlerForSession', () => {314it('should allow setting a usage handler', () => {315const mockHandler = sinon.stub();316service.setUsageHandlerForSession('session-1', mockHandler);317318const handler = service.getUsageHandlerForSession('session-1');319assert.strictEqual(handler, mockHandler);320});321322it('should allow clearing a usage handler', () => {323const mockHandler = sinon.stub();324service.setUsageHandlerForSession('session-1', mockHandler);325service.setUsageHandlerForSession('session-1', undefined);326327const handler = service.getUsageHandlerForSession('session-1');328assert.strictEqual(handler, undefined);329});330331it('should preserve other state when setting usage handler', () => {332service.setModelIdForSession('session-1', OPUS_4);333service.setPermissionModeForSession('session-1', 'bypassPermissions');334335const mockHandler = sinon.stub();336service.setUsageHandlerForSession('session-1', mockHandler);337338const modelId = service.getModelIdForSession('session-1');339assert.strictEqual(modelId, OPUS_4);340const permissionMode = service.getPermissionModeForSession('session-1');341assert.strictEqual(permissionMode, 'bypassPermissions');342});343344it('should allow usage handler to be called after setting', () => {345const mockHandler = sinon.stub();346service.setUsageHandlerForSession('session-1', mockHandler);347348const handler = service.getUsageHandlerForSession('session-1');349handler?.({ promptTokens: 100, completionTokens: 50 });350351assert.strictEqual(mockHandler.callCount, 1);352assert.deepStrictEqual(mockHandler.firstCall.args[0], { promptTokens: 100, completionTokens: 50 });353});354355it('should not fire onDidChangeSessionState event', () => {356const events: SessionStateChangeEvent[] = [];357service.onDidChangeSessionState(e => events.push(e));358359const mockHandler = sinon.stub();360service.setUsageHandlerForSession('session-1', mockHandler);361362assert.strictEqual(events.length, 0);363});364365it('should initialize defaults when session has no prior state', () => {366const mockHandler = sinon.stub();367service.setUsageHandlerForSession('new-session', mockHandler);368369assert.strictEqual(service.getModelIdForSession('new-session'), undefined);370assert.strictEqual(service.getPermissionModeForSession('new-session'), 'acceptEdits');371assert.strictEqual(service.getCapturingTokenForSession('new-session'), undefined);372assert.strictEqual(service.getFolderInfoForSession('new-session'), undefined);373assert.strictEqual(service.getUsageHandlerForSession('new-session'), mockHandler);374});375});376});377378379