Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/test/claudeCodeFolderMru.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 { beforeEach, describe, expect, it, vi } from 'vitest';6import { IGitService } from '../../../../../platform/git/common/gitService';7import { RepositoryAccessDetails } from '../../../../../platform/git/vscode/git';8import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService';9import { mock } from '../../../../../util/common/test/simpleMock';10import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';11import { Event } from '../../../../../util/vs/base/common/event';12import { URI } from '../../../../../util/vs/base/common/uri';13import { IClaudeCodeSessionInfo } from '../sessionParser/claudeSessionSchema';14import { IClaudeCodeSessionService } from '../sessionParser/claudeCodeSessionService';15import { ClaudeCodeFolderMruService } from '../claudeCodeFolderMru';1617// #region Test Helpers1819class TestGitService extends mock<IGitService>() {20declare readonly _serviceBrand: undefined;21override onDidOpenRepository = Event.None;22override onDidCloseRepository = Event.None;23override onDidFinishInitialization = Event.None;24recentRepos: RepositoryAccessDetails[] = [];25override getRecentRepositories(): Iterable<RepositoryAccessDetails> {26return this.recentRepos;27}28}2930class TestClaudeCodeSessionService extends mock<IClaudeCodeSessionService>() {31declare _serviceBrand: undefined;32sessions: IClaudeCodeSessionInfo[] = [];33override getAllSessions = vi.fn(async () => this.sessions);34}3536function makeSession(overrides: Partial<IClaudeCodeSessionInfo> & { id: string }): IClaudeCodeSessionInfo {37return {38label: 'test',39created: 1000,40...overrides,41};42}4344// #endregion4546describe('ClaudeCodeFolderMruService', () => {47let sessionService: TestClaudeCodeSessionService;48let gitService: TestGitService;49let workspaceService: TestWorkspaceService;50let service: ClaudeCodeFolderMruService;5152beforeEach(() => {53sessionService = new TestClaudeCodeSessionService();54gitService = new TestGitService();55workspaceService = new TestWorkspaceService([]);56service = new ClaudeCodeFolderMruService(sessionService, gitService, workspaceService);57});5859// #region Session extraction6061it('returns empty array when no sessions exist', async () => {62const result = await service.getRecentlyUsedFolders(CancellationToken.None);63expect(result).toEqual([]);64});6566it('converts session cwd to folder URI', async () => {67sessionService.sessions = [68makeSession({ id: 's1', cwd: '/Users/test/project', lastRequestEnded: 2000 }),69];7071const result = await service.getRecentlyUsedFolders(CancellationToken.None);7273expect(result).toHaveLength(1);74expect(result[0].folder.toString()).toBe(URI.file('/Users/test/project').toString());75expect(result[0].lastAccessed).toBe(2000);76});7778it('skips sessions without cwd', async () => {79sessionService.sessions = [makeSession({ id: 's1' })];8081const result = await service.getRecentlyUsedFolders(CancellationToken.None);82expect(result).toEqual([]);83});8485it('skips sessions with .claude/worktrees/ cwd', async () => {86sessionService.sessions = [87makeSession({ id: 's1', cwd: '/Users/test/.claude/worktrees/branch-1' }),88];8990const result = await service.getRecentlyUsedFolders(CancellationToken.None);91expect(result).toEqual([]);92});9394it('skips sessions with .worktrees/copilot- cwd', async () => {95sessionService.sessions = [96makeSession({ id: 's1', cwd: '/Users/test/repo/.worktrees/copilot-abc123' }),97];9899const result = await service.getRecentlyUsedFolders(CancellationToken.None);100expect(result).toEqual([]);101});102103// #endregion104105// #region Timestamp fallback106107it('uses lastRequestEnded as primary timestamp', async () => {108sessionService.sessions = [109makeSession({ id: 's1', cwd: '/a', created: 100, lastRequestStarted: 200, lastRequestEnded: 300 }),110];111const result = await service.getRecentlyUsedFolders(CancellationToken.None);112expect(result[0].lastAccessed).toBe(300);113});114115it('falls back to lastRequestStarted', async () => {116sessionService.sessions = [117makeSession({ id: 's1', cwd: '/a', created: 100, lastRequestStarted: 200 }),118];119const result = await service.getRecentlyUsedFolders(CancellationToken.None);120expect(result[0].lastAccessed).toBe(200);121});122123it('falls back to created', async () => {124sessionService.sessions = [125makeSession({ id: 's1', cwd: '/a', created: 100 }),126];127const result = await service.getRecentlyUsedFolders(CancellationToken.None);128expect(result[0].lastAccessed).toBe(100);129});130131// #endregion132133// #region Git repository merging134135it('merges git repo into matching session entry', async () => {136sessionService.sessions = [137makeSession({ id: 's1', cwd: '/Users/test/project', lastRequestEnded: 100 }),138];139const folderUri = URI.file('/Users/test/project');140gitService.recentRepos = [{ rootUri: folderUri, lastAccessTime: 200 }];141142const result = await service.getRecentlyUsedFolders(CancellationToken.None);143144expect(result).toHaveLength(1);145expect(result[0].repository).toEqual(folderUri);146expect(result[0].lastAccessed).toBe(200);147});148149it('adds standalone git repos not in sessions', async () => {150const repoUri = URI.file('/Users/test/other-repo');151gitService.recentRepos = [{ rootUri: repoUri, lastAccessTime: 500 }];152153const result = await service.getRecentlyUsedFolders(CancellationToken.None);154155expect(result).toHaveLength(1);156expect(result[0].folder).toEqual(repoUri);157expect(result[0].repository).toEqual(repoUri);158expect(result[0].lastAccessed).toBe(500);159});160161it('filters git repos with .claude/worktrees/ path', async () => {162gitService.recentRepos = [163{ rootUri: URI.file('/Users/test/.claude/worktrees/branch'), lastAccessTime: 100 },164];165166const result = await service.getRecentlyUsedFolders(CancellationToken.None);167expect(result).toEqual([]);168});169170it('filters git repos with .worktrees/copilot- path', async () => {171gitService.recentRepos = [172{ rootUri: URI.file('/Users/test/repo/.worktrees/copilot-abc123'), lastAccessTime: 100 },173];174175const result = await service.getRecentlyUsedFolders(CancellationToken.None);176expect(result).toEqual([]);177});178179// #endregion180181// #region Workspace folders182183it('adds workspace folders not already present', async () => {184const folder = URI.file('/Users/test/workspace');185workspaceService = new TestWorkspaceService([folder]);186service = new ClaudeCodeFolderMruService(sessionService, gitService, workspaceService);187188const result = await service.getRecentlyUsedFolders(CancellationToken.None);189190expect(result).toHaveLength(1);191expect(result[0].folder).toEqual(folder);192expect(result[0].repository).toBeUndefined();193});194195it('does not duplicate workspace folders already in sessions', async () => {196const folder = URI.file('/Users/test/project');197sessionService.sessions = [198makeSession({ id: 's1', cwd: '/Users/test/project', lastRequestEnded: 100 }),199];200workspaceService = new TestWorkspaceService([folder]);201service = new ClaudeCodeFolderMruService(sessionService, gitService, workspaceService);202203const result = await service.getRecentlyUsedFolders(CancellationToken.None);204expect(result).toHaveLength(1);205});206207// #endregion208209// #region Sorting, caching, deletion210211it('sorts entries by lastAccessed descending', async () => {212sessionService.sessions = [213makeSession({ id: 's1', cwd: '/Users/test/old', lastRequestEnded: 100 }),214makeSession({ id: 's2', cwd: '/Users/test/new', lastRequestEnded: 300 }),215makeSession({ id: 's3', cwd: '/Users/test/mid', lastRequestEnded: 200 }),216];217218const result = await service.getRecentlyUsedFolders(CancellationToken.None);219expect(result.map(e => e.lastAccessed)).toEqual([300, 200, 100]);220});221222it('deleteRecentlyUsedFolder filters the folder from results', async () => {223const folder = URI.file('/Users/test/project');224sessionService.sessions = [225makeSession({ id: 's1', cwd: '/Users/test/project', lastRequestEnded: 100 }),226];227228await service.deleteRecentlyUsedFolder(folder);229const result = await service.getRecentlyUsedFolders(CancellationToken.None);230expect(result).toEqual([]);231});232233it('returns cached entries on subsequent calls', async () => {234sessionService.sessions = [235makeSession({ id: 's1', cwd: '/Users/test/project', lastRequestEnded: 100 }),236];237238const first = await service.getRecentlyUsedFolders(CancellationToken.None);239expect(first).toHaveLength(1);240241// Add another session — second call returns stale cache immediately242sessionService.sessions.push(243makeSession({ id: 's2', cwd: '/Users/test/other', lastRequestEnded: 200 }),244);245const second = await service.getRecentlyUsedFolders(CancellationToken.None);246expect(second).toHaveLength(1);247});248249// #endregion250});251252253