takt/src/__tests__/sessionSelector.test.ts

165 lines
5.8 KiB
TypeScript

/**
* 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<string | null>>();
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<string, string>) => {
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,
};
}