/** * Tests for session selector */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { SessionIndexEntry } from '../infra/claude/session-reader.js'; const mockLoadSessionIndex = vi.fn<(dir: string) => SessionIndexEntry[]>(); const mockExtractLastAssistantResponse = vi.fn<(path: string, maxLen: number) => string | null>(); vi.mock('../infra/claude/session-reader.js', () => ({ loadSessionIndex: (...args: [string]) => mockLoadSessionIndex(...args), extractLastAssistantResponse: (...args: [string, number]) => mockExtractLastAssistantResponse(...args), })); const mockSelectOption = vi.fn<(prompt: string, options: unknown[]) => Promise>(); vi.mock('../shared/prompt/index.js', () => ({ selectOption: (...args: [string, unknown[]]) => mockSelectOption(...args), })); const mockInfo = vi.fn<(message: string) => void>(); vi.mock('../shared/ui/index.js', () => ({ info: (...args: [string]) => mockInfo(...args), })); vi.mock('../shared/i18n/index.js', () => ({ getLabel: (key: string, _lang: string, params?: Record) => { if (key === 'interactive.sessionSelector.newSession') return 'New session'; if (key === 'interactive.sessionSelector.newSessionDescription') return 'Start a new conversation'; if (key === 'interactive.sessionSelector.noSessions') return 'No sessions found'; if (key === 'interactive.sessionSelector.messages') return `${params?.count} messages`; if (key === 'interactive.sessionSelector.lastResponse') return `Last: ${params?.response}`; if (key === 'interactive.sessionSelector.prompt') return 'Select a session'; return key; }, })); import { selectRecentSession } from '../features/interactive/sessionSelector.js'; describe('selectRecentSession', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should return null when no sessions exist', async () => { mockLoadSessionIndex.mockReturnValue([]); const result = await selectRecentSession('/project', 'en'); expect(result).toBeNull(); expect(mockSelectOption).not.toHaveBeenCalled(); expect(mockInfo).toHaveBeenCalledWith('No sessions found'); }); it('should return null when user selects __new__', async () => { mockLoadSessionIndex.mockReturnValue([ createSession('session-1', 'Hello world', '2026-01-28T10:00:00.000Z'), ]); mockExtractLastAssistantResponse.mockReturnValue(null); mockSelectOption.mockResolvedValue('__new__'); const result = await selectRecentSession('/project', 'en'); expect(result).toBeNull(); }); it('should return null when user cancels selection', async () => { mockLoadSessionIndex.mockReturnValue([ createSession('session-1', 'Hello world', '2026-01-28T10:00:00.000Z'), ]); mockExtractLastAssistantResponse.mockReturnValue(null); mockSelectOption.mockResolvedValue(null); const result = await selectRecentSession('/project', 'en'); expect(result).toBeNull(); }); it('should return sessionId when user selects a session', async () => { mockLoadSessionIndex.mockReturnValue([ createSession('session-abc', 'Fix the bug', '2026-01-28T10:00:00.000Z'), ]); mockExtractLastAssistantResponse.mockReturnValue(null); mockSelectOption.mockResolvedValue('session-abc'); const result = await selectRecentSession('/project', 'en'); expect(result).toBe('session-abc'); }); it('should pass correct options to selectOption with new session first', async () => { mockLoadSessionIndex.mockReturnValue([ createSession('s1', 'First prompt', '2026-01-28T10:00:00.000Z', 5), ]); mockExtractLastAssistantResponse.mockReturnValue('Some response'); mockSelectOption.mockResolvedValue('s1'); await selectRecentSession('/project', 'en'); expect(mockSelectOption).toHaveBeenCalledWith( 'Select a session', expect.arrayContaining([ expect.objectContaining({ value: '__new__', label: 'New session' }), expect.objectContaining({ value: 's1' }), ]), ); const options = mockSelectOption.mock.calls[0]![1] as Array<{ value: string }>; expect(options[0]!.value).toBe('__new__'); expect(options[1]!.value).toBe('s1'); }); it('should limit display to MAX_DISPLAY_SESSIONS (10)', async () => { const sessions = Array.from({ length: 15 }, (_, i) => createSession(`s${i}`, `Prompt ${i}`, `2026-01-${String(i + 10).padStart(2, '0')}T10:00:00.000Z`), ); mockLoadSessionIndex.mockReturnValue(sessions); mockExtractLastAssistantResponse.mockReturnValue(null); mockSelectOption.mockResolvedValue(null); await selectRecentSession('/project', 'en'); const options = mockSelectOption.mock.calls[0]![1] as Array<{ value: string }>; // 1 new session + 10 display sessions = 11 total expect(options).toHaveLength(11); }); it('should include last response details when available', async () => { mockLoadSessionIndex.mockReturnValue([ createSession('s1', 'Hello', '2026-01-28T10:00:00.000Z', 3, '/path/to/s1.jsonl'), ]); mockExtractLastAssistantResponse.mockReturnValue('AI response text'); mockSelectOption.mockResolvedValue('s1'); await selectRecentSession('/project', 'en'); expect(mockExtractLastAssistantResponse).toHaveBeenCalledWith('/path/to/s1.jsonl', 200); const options = mockSelectOption.mock.calls[0]![1] as Array<{ value: string; details?: string[] }>; const sessionOption = options[1]!; expect(sessionOption.details).toBeDefined(); expect(sessionOption.details![0]).toContain('AI response text'); }); }); function createSession( sessionId: string, firstPrompt: string, modified: string, messageCount = 5, fullPath = `/path/to/${sessionId}.jsonl`, ): SessionIndexEntry { return { sessionId, firstPrompt, modified, messageCount, gitBranch: 'main', isSidechain: false, fullPath, }; }