From e5902b87ad34725c71c22a1131d0c1af72be8f2d Mon Sep 17 00:00:00 2001 From: nrs <38722970+nrslib@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:05:13 +0900 Subject: [PATCH] =?UTF-8?q?takt:=20Execute=20=E3=82=A2=E3=82=AF=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=81=A7=20tasks.yaml=20=E3=81=B8=E3=81=AE?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=82=92=E3=82=B9=E3=82=AD=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=81=99=E3=82=8B=20skipTaskList=20=E3=82=AA=E3=83=97=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=20(#334)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SelectAndExecuteOptions に skipTaskList フラグを追加 - routing.ts の Execute アクションで skipTaskList: true を設定 - taskRecord の null チェックで条件分岐を統一 - テストを現在の taskResultHandler API に合わせて修正 --- .../selectAndExecute-skipTaskList.test.ts | 165 ++++++++++++++++++ src/app/cli/routing.ts | 1 + .../tasks/execute/selectAndExecute.ts | 53 +++--- src/features/tasks/execute/types.ts | 2 + 4 files changed, 198 insertions(+), 23 deletions(-) create mode 100644 src/__tests__/selectAndExecute-skipTaskList.test.ts diff --git a/src/__tests__/selectAndExecute-skipTaskList.test.ts b/src/__tests__/selectAndExecute-skipTaskList.test.ts new file mode 100644 index 0000000..44d6913 --- /dev/null +++ b/src/__tests__/selectAndExecute-skipTaskList.test.ts @@ -0,0 +1,165 @@ +/** + * Tests for skipTaskList option in selectAndExecuteTask + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const { + mockAddTask, + mockExecuteTask, + mockPersistTaskResult, + mockPersistTaskError, + mockBuildBooleanTaskResult, +} = vi.hoisted(() => ({ + mockAddTask: vi.fn(() => ({ + name: 'test-task', + content: 'test task', + filePath: '/project/.takt/tasks.yaml', + createdAt: '2026-02-14T00:00:00.000Z', + status: 'pending', + data: { task: 'test task' }, + })), + mockExecuteTask: vi.fn(), + mockPersistTaskResult: vi.fn(), + mockPersistTaskError: vi.fn(), + mockBuildBooleanTaskResult: vi.fn(() => ({ task: 'mock-result' })), +})); + +vi.mock('../shared/prompt/index.js', () => ({ + confirm: vi.fn(), +})); + +vi.mock('../infra/config/index.js', () => ({ + resolvePieceConfigValue: vi.fn(), + loadPieceByIdentifier: vi.fn(() => ({ name: 'default' })), + listPieces: vi.fn(() => ['default']), + listPieceEntries: vi.fn(() => []), + isPiecePath: vi.fn(() => false), +})); + +vi.mock('../infra/task/index.js', () => ({ + createSharedClone: vi.fn(), + autoCommitAndPush: vi.fn(), + summarizeTaskName: vi.fn(), + resolveBaseBranch: vi.fn(() => ({ branch: 'main' })), + TaskRunner: vi.fn(() => ({ + addTask: (...args: unknown[]) => mockAddTask(...args), + })), +})); + +vi.mock('../shared/ui/index.js', () => ({ + info: vi.fn(), + error: vi.fn(), + success: vi.fn(), + withProgress: async ( + _startMessage: string, + _completionMessage: string | ((result: T) => string), + operation: () => Promise, + ): Promise => operation(), +})); + +vi.mock('../shared/utils/index.js', async (importOriginal) => ({ + ...(await importOriginal>()), + createLogger: () => ({ + info: vi.fn(), + debug: vi.fn(), + error: vi.fn(), + }), +})); + +vi.mock('../infra/github/index.js', () => ({ + createPullRequest: vi.fn(), + buildPrBody: vi.fn(), + pushBranch: vi.fn(), + findExistingPr: vi.fn(), + commentOnPr: vi.fn(), +})); + +vi.mock('../features/tasks/execute/taskExecution.js', () => ({ + executeTask: (...args: unknown[]) => mockExecuteTask(...args), +})); + +vi.mock('../features/tasks/execute/taskResultHandler.js', () => ({ + buildBooleanTaskResult: (...args: unknown[]) => mockBuildBooleanTaskResult(...args), + persistTaskResult: (...args: unknown[]) => mockPersistTaskResult(...args), + persistTaskError: (...args: unknown[]) => mockPersistTaskError(...args), +})); + +vi.mock('../features/pieceSelection/index.js', () => ({ + selectPiece: vi.fn(), +})); + +import { confirm } from '../shared/prompt/index.js'; +import { selectAndExecuteTask } from '../features/tasks/execute/selectAndExecute.js'; + +const mockConfirm = vi.mocked(confirm); + +beforeEach(() => { + vi.clearAllMocks(); + mockExecuteTask.mockResolvedValue(true); + // worktree を使わない(confirm で false) + mockConfirm.mockResolvedValue(false); +}); + +describe('skipTaskList option in selectAndExecuteTask', () => { + it('skipTaskList: true の場合はタスクリストに追加しない', async () => { + await selectAndExecuteTask('/project', 'test task', { + piece: 'default', + skipTaskList: true, + }); + + expect(mockAddTask).not.toHaveBeenCalled(); + expect(mockPersistTaskResult).not.toHaveBeenCalled(); + expect(mockExecuteTask).toHaveBeenCalled(); + }); + + it('skipTaskList: false の場合はタスクリストに追加する', async () => { + await selectAndExecuteTask('/project', 'test task', { + piece: 'default', + skipTaskList: false, + }); + + expect(mockAddTask).toHaveBeenCalled(); + expect(mockBuildBooleanTaskResult).toHaveBeenCalled(); + expect(mockPersistTaskResult).toHaveBeenCalled(); + expect(mockExecuteTask).toHaveBeenCalled(); + }); + + it('skipTaskList 未指定の場合はタスクリストに追加する', async () => { + await selectAndExecuteTask('/project', 'test task', { + piece: 'default', + }); + + expect(mockAddTask).toHaveBeenCalled(); + expect(mockPersistTaskResult).toHaveBeenCalled(); + expect(mockExecuteTask).toHaveBeenCalled(); + }); + + it('skipTaskList: true でエラー時は persistTaskError を呼ばない', async () => { + mockExecuteTask.mockRejectedValue(new Error('Task execution failed')); + + await expect( + selectAndExecuteTask('/project', 'test task', { + piece: 'default', + skipTaskList: true, + }), + ).rejects.toThrow('Task execution failed'); + + expect(mockAddTask).not.toHaveBeenCalled(); + expect(mockPersistTaskError).not.toHaveBeenCalled(); + }); + + it('skipTaskList: false でエラー時は persistTaskError を呼ぶ', async () => { + mockExecuteTask.mockRejectedValue(new Error('Task execution failed')); + + await expect( + selectAndExecuteTask('/project', 'test task', { + piece: 'default', + skipTaskList: false, + }), + ).rejects.toThrow('Task execution failed'); + + expect(mockAddTask).toHaveBeenCalled(); + expect(mockPersistTaskError).toHaveBeenCalled(); + }); +}); diff --git a/src/app/cli/routing.ts b/src/app/cli/routing.ts index fb47a8c..9eaeb1d 100644 --- a/src/app/cli/routing.ts +++ b/src/app/cli/routing.ts @@ -214,6 +214,7 @@ export async function executeDefaultAction(task?: string): Promise { selectOptions.interactiveUserInput = true; selectOptions.piece = pieceId; selectOptions.interactiveMetadata = { confirmed: true, task: confirmedTask }; + selectOptions.skipTaskList = true; await selectAndExecuteTask(resolvedCwd, confirmedTask, selectOptions, agentOverrides); }, create_issue: async ({ task: confirmedTask }) => { diff --git a/src/features/tasks/execute/selectAndExecute.ts b/src/features/tasks/execute/selectAndExecute.ts index f5308da..4a30f3c 100644 --- a/src/features/tasks/execute/selectAndExecute.ts +++ b/src/features/tasks/execute/selectAndExecute.ts @@ -108,15 +108,18 @@ export async function selectAndExecuteTask( log.info('Starting task execution', { piece: pieceIdentifier, worktree: isWorktree, autoPr: shouldCreatePr, draftPr: shouldDraftPr }); const taskRunner = new TaskRunner(cwd); - const taskRecord = taskRunner.addTask(task, { - piece: pieceIdentifier, - ...(isWorktree ? { worktree: true } : {}), - ...(branch ? { branch } : {}), - ...(isWorktree ? { worktree_path: execCwd } : {}), - auto_pr: shouldCreatePr, - draft_pr: shouldDraftPr, - ...(taskSlug ? { slug: taskSlug } : {}), - }); + let taskRecord: Awaited> | null = null; + if (options?.skipTaskList !== true) { + taskRecord = taskRunner.addTask(task, { + piece: pieceIdentifier, + ...(isWorktree ? { worktree: true } : {}), + ...(branch ? { branch } : {}), + ...(isWorktree ? { worktree_path: execCwd } : {}), + auto_pr: shouldCreatePr, + draft_pr: shouldDraftPr, + ...(taskSlug ? { slug: taskSlug } : {}), + }); + } const startedAt = new Date().toISOString(); let taskSuccess: boolean; @@ -132,9 +135,11 @@ export async function selectAndExecuteTask( }); } catch (err) { const completedAt = new Date().toISOString(); - persistTaskError(taskRunner, taskRecord, startedAt, completedAt, err, { - responsePrefix: 'Task failed: ', - }); + if (taskRecord) { + persistTaskError(taskRunner, taskRecord, startedAt, completedAt, err, { + responsePrefix: 'Task failed: ', + }); + } throw err; } @@ -160,17 +165,19 @@ export async function selectAndExecuteTask( } const effectiveSuccess = taskSuccess && !prFailed; - const taskResult = buildBooleanTaskResult({ - task: taskRecord, - taskSuccess: effectiveSuccess, - successResponse: 'Task completed successfully', - failureResponse: prFailed ? `PR creation failed: ${prError}` : 'Task failed', - startedAt, - completedAt, - branch, - ...(isWorktree ? { worktreePath: execCwd } : {}), - }); - persistTaskResult(taskRunner, taskResult); + if (taskRecord) { + const taskResult = buildBooleanTaskResult({ + task: taskRecord, + taskSuccess: effectiveSuccess, + successResponse: 'Task completed successfully', + failureResponse: prFailed ? `PR creation failed: ${prError}` : 'Task failed', + startedAt, + completedAt, + branch, + ...(isWorktree ? { worktreePath: execCwd } : {}), + }); + persistTaskResult(taskRunner, taskResult); + } if (!effectiveSuccess) { process.exit(1); diff --git a/src/features/tasks/execute/types.ts b/src/features/tasks/execute/types.ts index 751ca30..fe6282a 100644 --- a/src/features/tasks/execute/types.ts +++ b/src/features/tasks/execute/types.ts @@ -145,4 +145,6 @@ export interface SelectAndExecuteOptions { interactiveMetadata?: InteractiveMetadata; /** GitHub Issues to associate with the PR (adds "Closes #N" for each issue) */ issues?: GitHubIssue[]; + /** Skip adding task to tasks.yaml */ + skipTaskList?: boolean; }