takt/src/__tests__/cli-worktree.test.ts
nrslib 7bac0053ff feat: CLIサブコマンド形式への移行と対話式タスク入力モード (#47)
- スラッシュコマンド形式をサブコマンド形式に変更(takt run, takt add 等)
- 引数なし takt で対話的にAIとタスク要件を詰めるinteractiveモードを追加
- セッション永続化により takt 再起動後も会話を継続
- 調査用ツール(Read, Glob, Grep, Bash, WebSearch, WebFetch)を許可
- プランニング専用のシステムプロンプトでコード変更を禁止
- executor の buildSdkOptions を未定義値を含めないよう修正(SDK ハング対策)
- help/refreshBuiltinコマンドを削除、ejectコマンドを簡素化
- ドキュメント(CLAUDE.md, README, workflows.md)をサブコマンド形式に更新
2026-01-31 01:14:36 +09:00

205 lines
5.7 KiB
TypeScript

/**
* Tests for confirmAndCreateWorktree (CLI clone confirmation flow)
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock dependencies before importing the module under test
vi.mock('../prompt/index.js', () => ({
confirm: vi.fn(),
selectOptionWithDefault: vi.fn(),
}));
vi.mock('../task/clone.js', () => ({
createSharedClone: vi.fn(),
removeClone: vi.fn(),
}));
vi.mock('../task/autoCommit.js', () => ({
autoCommitAndPush: vi.fn(),
}));
vi.mock('../task/summarize.js', () => ({
summarizeTaskName: vi.fn(),
}));
vi.mock('../utils/ui.js', () => ({
info: vi.fn(),
error: vi.fn(),
success: vi.fn(),
header: vi.fn(),
status: vi.fn(),
setLogLevel: vi.fn(),
}));
vi.mock('../utils/debug.js', () => ({
createLogger: () => ({
info: vi.fn(),
debug: vi.fn(),
error: vi.fn(),
}),
initDebugLogger: vi.fn(),
setVerboseConsole: vi.fn(),
getDebugLogFile: vi.fn(),
}));
vi.mock('../config/index.js', () => ({
initGlobalDirs: vi.fn(),
initProjectDirs: vi.fn(),
loadGlobalConfig: vi.fn(() => ({ logLevel: 'info' })),
getEffectiveDebugConfig: vi.fn(),
}));
vi.mock('../config/paths.js', () => ({
clearAgentSessions: vi.fn(),
getCurrentWorkflow: vi.fn(() => 'default'),
isVerboseMode: vi.fn(() => false),
}));
vi.mock('../commands/index.js', () => ({
executeTask: vi.fn(),
runAllTasks: vi.fn(),
switchWorkflow: vi.fn(),
switchConfig: vi.fn(),
addTask: vi.fn(),
watchTasks: vi.fn(),
listTasks: vi.fn(),
interactiveMode: vi.fn(() => Promise.resolve({ confirmed: false, task: '' })),
}));
vi.mock('../config/workflowLoader.js', () => ({
listWorkflows: vi.fn(() => []),
}));
vi.mock('../constants.js', () => ({
DEFAULT_WORKFLOW_NAME: 'default',
}));
vi.mock('../github/issue.js', () => ({
isIssueReference: vi.fn((s: string) => /^#\d+$/.test(s)),
resolveIssueTask: vi.fn(),
}));
vi.mock('../utils/updateNotifier.js', () => ({
checkForUpdates: vi.fn(),
}));
import { confirm } from '../prompt/index.js';
import { createSharedClone } from '../task/clone.js';
import { summarizeTaskName } from '../task/summarize.js';
import { info } from '../utils/ui.js';
import { confirmAndCreateWorktree } from '../cli.js';
const mockConfirm = vi.mocked(confirm);
const mockCreateSharedClone = vi.mocked(createSharedClone);
const mockSummarizeTaskName = vi.mocked(summarizeTaskName);
const mockInfo = vi.mocked(info);
beforeEach(() => {
vi.clearAllMocks();
});
describe('confirmAndCreateWorktree', () => {
it('should return original cwd when user declines clone creation', async () => {
// Given: user says "no" to clone creation
mockConfirm.mockResolvedValue(false);
// When
const result = await confirmAndCreateWorktree('/project', 'fix-auth');
// Then
expect(result.execCwd).toBe('/project');
expect(result.isWorktree).toBe(false);
expect(mockCreateSharedClone).not.toHaveBeenCalled();
expect(mockSummarizeTaskName).not.toHaveBeenCalled();
});
it('should create shared clone and return clone path when user confirms', async () => {
// Given: user says "yes" to clone creation
mockConfirm.mockResolvedValue(true);
mockSummarizeTaskName.mockResolvedValue('fix-auth');
mockCreateSharedClone.mockReturnValue({
path: '/project/../20260128T0504-fix-auth',
branch: 'takt/20260128T0504-fix-auth',
});
// When
const result = await confirmAndCreateWorktree('/project', 'fix-auth');
// Then
expect(result.execCwd).toBe('/project/../20260128T0504-fix-auth');
expect(result.isWorktree).toBe(true);
expect(mockSummarizeTaskName).toHaveBeenCalledWith('fix-auth', { cwd: '/project' });
expect(mockCreateSharedClone).toHaveBeenCalledWith('/project', {
worktree: true,
taskSlug: 'fix-auth',
});
});
it('should display clone info when created', async () => {
// Given
mockConfirm.mockResolvedValue(true);
mockSummarizeTaskName.mockResolvedValue('my-task');
mockCreateSharedClone.mockReturnValue({
path: '/project/../20260128T0504-my-task',
branch: 'takt/20260128T0504-my-task',
});
// When
await confirmAndCreateWorktree('/project', 'my-task');
// Then
expect(mockInfo).toHaveBeenCalledWith(
'Clone created: /project/../20260128T0504-my-task (branch: takt/20260128T0504-my-task)'
);
});
it('should call confirm with default=false', async () => {
// Given
mockConfirm.mockResolvedValue(false);
// When
await confirmAndCreateWorktree('/project', 'task');
// Then
expect(mockConfirm).toHaveBeenCalledWith('Create worktree?', false);
});
it('should summarize Japanese task name to English slug', async () => {
// Given: Japanese task name, AI summarizes to English
mockConfirm.mockResolvedValue(true);
mockSummarizeTaskName.mockResolvedValue('add-auth');
mockCreateSharedClone.mockReturnValue({
path: '/project/../20260128T0504-add-auth',
branch: 'takt/20260128T0504-add-auth',
});
// When
await confirmAndCreateWorktree('/project', '認証機能を追加する');
// Then
expect(mockSummarizeTaskName).toHaveBeenCalledWith('認証機能を追加する', { cwd: '/project' });
expect(mockCreateSharedClone).toHaveBeenCalledWith('/project', {
worktree: true,
taskSlug: 'add-auth',
});
});
it('should show generating message when creating clone', async () => {
// Given
mockConfirm.mockResolvedValue(true);
mockSummarizeTaskName.mockResolvedValue('test-task');
mockCreateSharedClone.mockReturnValue({
path: '/project/../20260128T0504-test-task',
branch: 'takt/20260128T0504-test-task',
});
// When
await confirmAndCreateWorktree('/project', 'テストタスク');
// Then
expect(mockInfo).toHaveBeenCalledWith('Generating branch name...');
});
});