diff --git a/src/__tests__/addTask.test.ts b/src/__tests__/addTask.test.ts index 49036e7..bd05b25 100644 --- a/src/__tests__/addTask.test.ts +++ b/src/__tests__/addTask.test.ts @@ -275,6 +275,7 @@ describe('addTask', () => { // Given: issue reference "#99" const issueText = 'Issue #99: Fix login timeout\n\nThe login page times out after 30 seconds.'; mockResolveIssueTask.mockReturnValue(issueText); + mockDeterminePiece.mockResolvedValue('default'); mockSummarizeTaskName.mockResolvedValue('fix-login-timeout'); mockConfirm.mockResolvedValue(false); @@ -288,6 +289,9 @@ describe('addTask', () => { // Then: resolveIssueTask was called expect(mockResolveIssueTask).toHaveBeenCalledWith('#99'); + // Then: determinePiece was called for piece selection + expect(mockDeterminePiece).toHaveBeenCalledWith(testDir); + // Then: task file created with issue text directly (no AI summarization) const taskFile = path.join(testDir, '.takt', 'tasks', 'fix-login-timeout.yaml'); expect(fs.existsSync(taskFile)).toBe(true); @@ -298,6 +302,7 @@ describe('addTask', () => { it('should proceed to worktree settings after issue fetch', async () => { // Given: issue with worktree enabled mockResolveIssueTask.mockReturnValue('Issue text'); + mockDeterminePiece.mockResolvedValue('default'); mockSummarizeTaskName.mockResolvedValue('issue-task'); mockConfirm.mockResolvedValue(true); mockPromptInput @@ -331,6 +336,7 @@ describe('addTask', () => { // Given: issue reference "#99" const issueText = 'Issue #99: Fix login timeout'; mockResolveIssueTask.mockReturnValue(issueText); + mockDeterminePiece.mockResolvedValue('default'); mockSummarizeTaskName.mockResolvedValue('fix-login-timeout'); mockConfirm.mockResolvedValue(false); @@ -344,6 +350,42 @@ describe('addTask', () => { expect(content).toContain('issue: 99'); }); + it('should include piece selection in task file when issue reference is used', async () => { + // Given: issue reference "#99" with non-default piece selection + const issueText = 'Issue #99: Fix login timeout'; + mockResolveIssueTask.mockReturnValue(issueText); + mockDeterminePiece.mockResolvedValue('review'); + mockSummarizeTaskName.mockResolvedValue('fix-login-timeout'); + mockConfirm.mockResolvedValue(false); + + // When + await addTask(testDir, '#99'); + + // Then: task file contains piece field + const taskFile = path.join(testDir, '.takt', 'tasks', 'fix-login-timeout.yaml'); + expect(fs.existsSync(taskFile)).toBe(true); + const content = fs.readFileSync(taskFile, 'utf-8'); + expect(content).toContain('piece: review'); + }); + + it('should cancel when piece selection returns null for issue reference', async () => { + // Given: issue fetched successfully but user cancels piece selection + const issueText = 'Issue #99: Fix login timeout'; + mockResolveIssueTask.mockReturnValue(issueText); + mockDeterminePiece.mockResolvedValue(null); + + // When + await addTask(testDir, '#99'); + + // Then: no task file created (cancelled at piece selection) + const tasksDir = path.join(testDir, '.takt', 'tasks'); + const files = fs.readdirSync(tasksDir); + expect(files.length).toBe(0); + + // Then: issue was fetched before cancellation + expect(mockResolveIssueTask).toHaveBeenCalledWith('#99'); + }); + describe('create_issue action', () => { it('should call createIssue when create_issue action is selected', async () => { // Given: interactive mode returns create_issue action diff --git a/src/features/tasks/add/index.ts b/src/features/tasks/add/index.ts index d90788d..7dadf17 100644 --- a/src/features/tasks/add/index.ts +++ b/src/features/tasks/add/index.ts @@ -106,18 +106,14 @@ export async function saveTaskFromInteractive( * add command handler * * Flow: - * 1. ピース選択 - * 2. AI対話モードでタスクを詰める - * 3. 会話履歴からAIがタスク要約を生成 - * 4. 要約からファイル名をAIで生成 - * 5. ワークツリー/ブランチ設定 - * 6. YAMLファイル作成 + * A) Issue参照の場合: issue取得 → ピース選択 → ワークツリー設定 → YAML作成 + * B) それ以外: ピース選択 → AI対話モード → ワークツリー設定 → YAML作成 */ export async function addTask(cwd: string, task?: string): Promise { const tasksDir = path.join(cwd, '.takt', 'tasks'); fs.mkdirSync(tasksDir, { recursive: true }); - // 1. ピース選択(Issue参照以外の場合、対話モードの前に実施) + // ピース選択とタスク内容の決定 let taskContent: string; let issueNumber: number | undefined; let piece: string | undefined; @@ -137,6 +133,14 @@ export async function addTask(cwd: string, task?: string): Promise { info(`Failed to fetch issue ${task}: ${msg}`); return; } + + // ピース選択(issue取得成功後) + const pieceId = await determinePiece(cwd); + if (pieceId === null) { + info('Cancelled.'); + return; + } + piece = pieceId; } else { // ピース選択を先に行い、結果を対話モードに渡す const pieceId = await determinePiece(cwd); @@ -165,7 +169,7 @@ export async function addTask(cwd: string, task?: string): Promise { taskContent = result.task; } - // 3. ワークツリー/ブランチ設定 + // ワークツリー/ブランチ設定 let worktree: boolean | string | undefined; let branch: string | undefined; @@ -180,7 +184,7 @@ export async function addTask(cwd: string, task?: string): Promise { } } - // 4. YAMLファイル作成 + // YAMLファイル作成 const filePath = await saveTaskFile(cwd, taskContent, { piece, issue: issueNumber,