diff --git a/src/__tests__/saveTaskFile.test.ts b/src/__tests__/saveTaskFile.test.ts index 5111656..9d69a29 100644 --- a/src/__tests__/saveTaskFile.test.ts +++ b/src/__tests__/saveTaskFile.test.ts @@ -17,6 +17,11 @@ vi.mock('../shared/ui/index.js', () => ({ blankLine: vi.fn(), })); +vi.mock('../shared/prompt/index.js', () => ({ + confirm: vi.fn(), + promptInput: vi.fn(), +})); + vi.mock('../shared/utils/index.js', async (importOriginal) => ({ ...(await importOriginal>()), createLogger: () => ({ @@ -28,11 +33,14 @@ vi.mock('../shared/utils/index.js', async (importOriginal) => ({ import { summarizeTaskName } from '../infra/task/summarize.js'; import { success, info } from '../shared/ui/index.js'; +import { confirm, promptInput } from '../shared/prompt/index.js'; import { saveTaskFile, saveTaskFromInteractive } from '../features/tasks/add/index.js'; const mockSummarizeTaskName = vi.mocked(summarizeTaskName); const mockSuccess = vi.mocked(success); const mockInfo = vi.mocked(info); +const mockConfirm = vi.mocked(confirm); +const mockPromptInput = vi.mocked(promptInput); let testDir: string; @@ -163,16 +171,82 @@ describe('saveTaskFile', () => { }); describe('saveTaskFromInteractive', () => { - it('should save task and display success message', async () => { + it('should save task with worktree settings when user confirms worktree', async () => { + // Given: user confirms worktree, accepts defaults, confirms auto-PR + mockConfirm.mockResolvedValueOnce(true); // Create worktree? → Yes + mockPromptInput.mockResolvedValueOnce(''); // Worktree path → auto + mockPromptInput.mockResolvedValueOnce(''); // Branch name → auto + mockConfirm.mockResolvedValueOnce(true); // Auto-create PR? → Yes + // When await saveTaskFromInteractive(testDir, 'Task content'); // Then expect(mockSuccess).toHaveBeenCalledWith('Task created: test-task.yaml'); expect(mockInfo).toHaveBeenCalledWith(expect.stringContaining('Path:')); + const tasksDir = path.join(testDir, '.takt', 'tasks'); + const files = fs.readdirSync(tasksDir); + const content = fs.readFileSync(path.join(tasksDir, files[0]!), 'utf-8'); + expect(content).toContain('worktree: true'); + expect(content).toContain('auto_pr: true'); + }); + + it('should save task without worktree settings when user declines worktree', async () => { + // Given: user declines worktree + mockConfirm.mockResolvedValueOnce(false); // Create worktree? → No + + // When + await saveTaskFromInteractive(testDir, 'Task content'); + + // Then + expect(mockSuccess).toHaveBeenCalledWith('Task created: test-task.yaml'); + const tasksDir = path.join(testDir, '.takt', 'tasks'); + const files = fs.readdirSync(tasksDir); + const content = fs.readFileSync(path.join(tasksDir, files[0]!), 'utf-8'); + expect(content).not.toContain('worktree:'); + expect(content).not.toContain('branch:'); + expect(content).not.toContain('auto_pr:'); + }); + + it('should save custom worktree path and branch when specified', async () => { + // Given: user specifies custom path and branch + mockConfirm.mockResolvedValueOnce(true); // Create worktree? → Yes + mockPromptInput.mockResolvedValueOnce('/custom/path'); // Worktree path + mockPromptInput.mockResolvedValueOnce('feat/branch'); // Branch name + mockConfirm.mockResolvedValueOnce(false); // Auto-create PR? → No + + // When + await saveTaskFromInteractive(testDir, 'Task content'); + + // Then + const tasksDir = path.join(testDir, '.takt', 'tasks'); + const files = fs.readdirSync(tasksDir); + const content = fs.readFileSync(path.join(tasksDir, files[0]!), 'utf-8'); + expect(content).toContain('worktree: /custom/path'); + expect(content).toContain('branch: feat/branch'); + expect(content).toContain('auto_pr: false'); + }); + + it('should display worktree/branch/auto-PR info when settings are provided', async () => { + // Given + mockConfirm.mockResolvedValueOnce(true); // Create worktree? → Yes + mockPromptInput.mockResolvedValueOnce('/my/path'); // Worktree path + mockPromptInput.mockResolvedValueOnce('my-branch'); // Branch name + mockConfirm.mockResolvedValueOnce(true); // Auto-create PR? → Yes + + // When + await saveTaskFromInteractive(testDir, 'Task content'); + + // Then + expect(mockInfo).toHaveBeenCalledWith(' Worktree: /my/path'); + expect(mockInfo).toHaveBeenCalledWith(' Branch: my-branch'); + expect(mockInfo).toHaveBeenCalledWith(' Auto-PR: yes'); }); it('should display piece info when specified', async () => { + // Given + mockConfirm.mockResolvedValueOnce(false); // Create worktree? → No + // When await saveTaskFromInteractive(testDir, 'Task content', 'review'); @@ -181,6 +255,9 @@ describe('saveTaskFromInteractive', () => { }); it('should include piece in saved YAML', async () => { + // Given + mockConfirm.mockResolvedValueOnce(false); // Create worktree? → No + // When await saveTaskFromInteractive(testDir, 'Task content', 'custom'); @@ -193,6 +270,9 @@ describe('saveTaskFromInteractive', () => { }); it('should not display piece info when not specified', async () => { + // Given + mockConfirm.mockResolvedValueOnce(false); // Create worktree? → No + // When await saveTaskFromInteractive(testDir, 'Task content'); @@ -202,4 +282,18 @@ describe('saveTaskFromInteractive', () => { ); expect(pieceInfoCalls.length).toBe(0); }); + + it('should display auto worktree info when no custom path', async () => { + // Given + mockConfirm.mockResolvedValueOnce(true); // Create worktree? → Yes + mockPromptInput.mockResolvedValueOnce(''); // Worktree path → auto + mockPromptInput.mockResolvedValueOnce(''); // Branch name → auto + mockConfirm.mockResolvedValueOnce(true); // Auto-create PR? → Yes + + // When + await saveTaskFromInteractive(testDir, 'Task content'); + + // Then + expect(mockInfo).toHaveBeenCalledWith(' Worktree: auto'); + }); }); diff --git a/src/features/tasks/add/index.ts b/src/features/tasks/add/index.ts index 6da95e3..6e9e37a 100644 --- a/src/features/tasks/add/index.ts +++ b/src/features/tasks/add/index.ts @@ -87,19 +87,52 @@ export function createIssueFromTask(task: string): void { } } +interface WorktreeSettings { + worktree?: boolean | string; + branch?: string; + autoPr?: boolean; +} + +async function promptWorktreeSettings(): Promise { + const useWorktree = await confirm('Create worktree?', true); + if (!useWorktree) { + return {}; + } + + const customPath = await promptInput('Worktree path (Enter for auto)'); + const worktree: boolean | string = customPath || true; + + const customBranch = await promptInput('Branch name (Enter for auto)'); + const branch = customBranch || undefined; + + const autoPr = await confirm('Auto-create PR?', true); + + return { worktree, branch, autoPr }; +} + /** * Save a task from interactive mode result. - * Does not prompt for worktree/branch settings. + * Prompts for worktree/branch/auto_pr settings before saving. */ export async function saveTaskFromInteractive( cwd: string, task: string, piece?: string, ): Promise { - const filePath = await saveTaskFile(cwd, task, { piece }); + const settings = await promptWorktreeSettings(); + const filePath = await saveTaskFile(cwd, task, { piece, ...settings }); const filename = path.basename(filePath); success(`Task created: ${filename}`); info(` Path: ${filePath}`); + if (settings.worktree) { + info(` Worktree: ${typeof settings.worktree === 'string' ? settings.worktree : 'auto'}`); + } + if (settings.branch) { + info(` Branch: ${settings.branch}`); + } + if (settings.autoPr) { + info(` Auto-PR: yes`); + } if (piece) info(` Piece: ${piece}`); } @@ -173,43 +206,25 @@ export async function addTask(cwd: string, task?: string): Promise { } // 3. ワークツリー/ブランチ/PR設定 - let worktree: boolean | string | undefined; - let branch: string | undefined; - let autoPr: boolean | undefined; - - const useWorktree = await confirm('Create worktree?', true); - if (useWorktree) { - const customPath = await promptInput('Worktree path (Enter for auto)'); - worktree = customPath || true; - - const customBranch = await promptInput('Branch name (Enter for auto)'); - if (customBranch) { - branch = customBranch; - } - - // PR確認(worktreeが有効な場合のみ) - autoPr = await confirm('Auto-create PR?', true); - } + const settings = await promptWorktreeSettings(); // YAMLファイル作成 const filePath = await saveTaskFile(cwd, taskContent, { piece, issue: issueNumber, - worktree, - branch, - autoPr, + ...settings, }); const filename = path.basename(filePath); success(`Task created: ${filename}`); info(` Path: ${filePath}`); - if (worktree) { - info(` Worktree: ${typeof worktree === 'string' ? worktree : 'auto'}`); + if (settings.worktree) { + info(` Worktree: ${typeof settings.worktree === 'string' ? settings.worktree : 'auto'}`); } - if (branch) { - info(` Branch: ${branch}`); + if (settings.branch) { + info(` Branch: ${settings.branch}`); } - if (autoPr) { + if (settings.autoPr) { info(` Auto-PR: yes`); } if (piece) {