271 lines
7.6 KiB
TypeScript
271 lines
7.6 KiB
TypeScript
/**
|
|
* Tests for summarizeTaskName
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
vi.mock('../infra/providers/index.js', () => ({
|
|
getProvider: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
|
loadGlobalConfig: vi.fn(),
|
|
getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true),
|
|
}));
|
|
|
|
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
|
...(await importOriginal<Record<string, unknown>>()),
|
|
createLogger: () => ({
|
|
info: vi.fn(),
|
|
debug: vi.fn(),
|
|
error: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
import { getProvider } from '../infra/providers/index.js';
|
|
import { loadGlobalConfig } from '../infra/config/global/globalConfig.js';
|
|
import { summarizeTaskName } from '../infra/task/summarize.js';
|
|
|
|
const mockGetProvider = vi.mocked(getProvider);
|
|
const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);
|
|
|
|
const mockProviderCall = vi.fn();
|
|
const mockProvider = {
|
|
call: mockProviderCall,
|
|
callCustom: vi.fn(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockGetProvider.mockReturnValue(mockProvider);
|
|
mockLoadGlobalConfig.mockReturnValue({
|
|
language: 'ja',
|
|
defaultPiece: 'default',
|
|
logLevel: 'info',
|
|
provider: 'claude',
|
|
model: undefined,
|
|
});
|
|
});
|
|
|
|
describe('summarizeTaskName', () => {
|
|
it('should return AI-generated slug for task name', async () => {
|
|
// Given: AI returns a slug for input
|
|
mockProviderCall.mockResolvedValue({
|
|
agent: 'summarizer',
|
|
status: 'done',
|
|
content: 'add-auth',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
// When
|
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
|
|
|
// Then
|
|
expect(result).toBe('add-auth');
|
|
expect(mockGetProvider).toHaveBeenCalledWith('claude');
|
|
expect(mockProviderCall).toHaveBeenCalledWith(
|
|
'summarizer',
|
|
'long task name for testing',
|
|
expect.objectContaining({
|
|
cwd: '/project',
|
|
allowedTools: [],
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should return AI-generated slug for English task name', async () => {
|
|
// Given
|
|
mockProviderCall.mockResolvedValue({
|
|
agent: 'summarizer',
|
|
status: 'done',
|
|
content: 'fix-login-bug',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
// When
|
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
|
|
|
// Then
|
|
expect(result).toBe('fix-login-bug');
|
|
});
|
|
|
|
it('should clean up AI response with extra characters', async () => {
|
|
// Given: AI response has extra whitespace or formatting
|
|
mockProviderCall.mockResolvedValue({
|
|
agent: 'summarizer',
|
|
status: 'done',
|
|
content: ' Add-User-Auth! \n',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
// When
|
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
|
|
|
// Then
|
|
expect(result).toBe('add-user-auth');
|
|
});
|
|
|
|
it('should truncate long slugs to 30 characters without trailing hyphen', async () => {
|
|
// Given: AI returns a long slug
|
|
mockProviderCall.mockResolvedValue({
|
|
agent: 'summarizer',
|
|
status: 'done',
|
|
content: 'this-is-a-very-long-slug-that-exceeds-thirty-characters',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
// When
|
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
|
|
|
// Then
|
|
expect(result.length).toBeLessThanOrEqual(30);
|
|
expect(result).toBe('this-is-a-very-long-slug-that');
|
|
expect(result).not.toMatch(/-$/); // No trailing hyphen
|
|
});
|
|
|
|
it('should return "task" as fallback for empty AI response', async () => {
|
|
// Given: AI returns empty string
|
|
mockProviderCall.mockResolvedValue({
|
|
agent: 'summarizer',
|
|
status: 'done',
|
|
content: '',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
// When
|
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
|
|
|
// Then
|
|
expect(result).toBe('task');
|
|
});
|
|
|
|
it('should use custom model if specified in options', async () => {
|
|
// Given
|
|
mockProviderCall.mockResolvedValue({
|
|
agent: 'summarizer',
|
|
status: 'done',
|
|
content: 'custom-task',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
// When
|
|
await summarizeTaskName('test', { cwd: '/project', model: 'sonnet' });
|
|
|
|
// Then
|
|
expect(mockProviderCall).toHaveBeenCalledWith(
|
|
'summarizer',
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
model: 'sonnet',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should use provider from config.yaml', async () => {
|
|
// Given: config has codex provider
|
|
mockLoadGlobalConfig.mockReturnValue({
|
|
language: 'ja',
|
|
defaultPiece: 'default',
|
|
logLevel: 'info',
|
|
provider: 'codex',
|
|
model: 'gpt-4',
|
|
});
|
|
mockProviderCall.mockResolvedValue({
|
|
agent: 'summarizer',
|
|
status: 'done',
|
|
content: 'codex-task',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
// When
|
|
await summarizeTaskName('test', { cwd: '/project' });
|
|
|
|
// Then
|
|
expect(mockGetProvider).toHaveBeenCalledWith('codex');
|
|
expect(mockProviderCall).toHaveBeenCalledWith(
|
|
'summarizer',
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
model: 'gpt-4',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should remove consecutive hyphens', async () => {
|
|
// Given: AI response has consecutive hyphens
|
|
mockProviderCall.mockResolvedValue({
|
|
agent: 'summarizer',
|
|
status: 'done',
|
|
content: 'fix---multiple---hyphens',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
// When
|
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
|
|
|
// Then
|
|
expect(result).toBe('fix-multiple-hyphens');
|
|
});
|
|
|
|
it('should remove leading and trailing hyphens', async () => {
|
|
// Given: AI response has leading/trailing hyphens
|
|
mockProviderCall.mockResolvedValue({
|
|
agent: 'summarizer',
|
|
status: 'done',
|
|
content: '-leading-trailing-',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
// When
|
|
const result = await summarizeTaskName('long task name for testing', { cwd: '/project' });
|
|
|
|
// Then
|
|
expect(result).toBe('leading-trailing');
|
|
});
|
|
|
|
it('should throw error when config load fails', async () => {
|
|
// Given: config loading throws error
|
|
mockLoadGlobalConfig.mockImplementation(() => {
|
|
throw new Error('Config not found');
|
|
});
|
|
|
|
// When/Then
|
|
await expect(summarizeTaskName('test', { cwd: '/project' })).rejects.toThrow('Config not found');
|
|
});
|
|
|
|
it('should use romanization when useLLM is false', async () => {
|
|
// When: useLLM is explicitly false
|
|
const result = await summarizeTaskName('romanization test', { cwd: '/project', useLLM: false });
|
|
|
|
// Then: should not call provider, should return romaji
|
|
expect(mockProviderCall).not.toHaveBeenCalled();
|
|
expect(result).toMatch(/^[a-z0-9-]+$/);
|
|
expect(result.length).toBeLessThanOrEqual(30);
|
|
});
|
|
|
|
it('should handle mixed Japanese/English with romanization', async () => {
|
|
// When
|
|
const result = await summarizeTaskName('Add romanization', { cwd: '/project', useLLM: false });
|
|
|
|
// Then
|
|
expect(result).toMatch(/^[a-z0-9-]+$/);
|
|
expect(result).not.toMatch(/^-|-$/); // No leading/trailing hyphens
|
|
});
|
|
|
|
it('should use LLM by default', async () => {
|
|
// Given
|
|
mockProviderCall.mockResolvedValue({
|
|
agent: 'summarizer',
|
|
status: 'done',
|
|
content: 'add-auth',
|
|
timestamp: new Date(),
|
|
});
|
|
|
|
// When: useLLM not specified (defaults to true)
|
|
await summarizeTaskName('test', { cwd: '/project' });
|
|
|
|
// Then: should call provider
|
|
expect(mockProviderCall).toHaveBeenCalled();
|
|
});
|
|
});
|