From c2f530f2a01982563f480e0d3370d6a4b06e67e2 Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:46:50 +0900 Subject: [PATCH] =?UTF-8?q?takt:=20task=E3=81=8C=E3=81=A1=E3=82=83?= =?UTF-8?q?=E3=82=93=E3=81=A8=E6=AD=A3=E5=B8=B8=E7=B5=82=E4=BA=86=E3=81=97?= =?UTF-8?q?=E3=81=AA=E3=81=8F=E3=81=A6=E3=82=82completed=E3=81=AB=E9=80=B2?= =?UTF-8?q?=E3=82=93=E3=81=A7=E3=81=97=E3=81=BE=E3=81=86=E3=81=93=E3=81=A8?= =?UTF-8?q?=E3=81=8C=E3=81=82=E3=82=8B=E3=80=82=E4=BF=AE=E6=AD=A3=E3=81=97?= =?UTF-8?q?=E3=81=A6=E3=81=BB=E3=81=97=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 04-ai-review.md | 16 ++++++++ src/__tests__/task.test.ts | 75 +++++++++++++++++++++++++++++++++-- src/commands/taskExecution.ts | 8 ++-- src/task/runner.ts | 41 +++++++++++++++---- 4 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 04-ai-review.md diff --git a/04-ai-review.md b/04-ai-review.md new file mode 100644 index 0000000..1dadaa5 --- /dev/null +++ b/04-ai-review.md @@ -0,0 +1,16 @@ +# AI生成コードレビュー + +## 結果: APPROVE + +## サマリー +失敗タスクを`completed`ではなく`failed`ディレクトリに移動する修正は、既存パターンに適合し、API・呼び出し元の整合性も問題なし。 + +## 検証した項目 +| 観点 | 結果 | 備考 | +|------|------|------| +| 仮定の妥当性 | ✅ | 元の要求(失敗タスクがcompletedに入る問題)を正確に修正 | +| API/ライブラリの実在 | ✅ | Node.js fs API全て正しく使用 | +| コンテキスト適合 | ✅ | 命名・構造・エラーハンドリングが既存コードと一致 | +| スコープ | ✅ | 必要最小限の変更、スコープクリープなし | +| 呼び出し元の配線 | ✅ | completeTask/failTask の全3箇所の呼び出し元が正しく更新済み | +| デッドコード | ✅ | 不要コードの残存なし | diff --git a/src/__tests__/task.test.ts b/src/__tests__/task.test.ts index dc4ec7f..35dc51b 100644 --- a/src/__tests__/task.test.ts +++ b/src/__tests__/task.test.ts @@ -3,7 +3,7 @@ */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdirSync, writeFileSync, existsSync, rmSync, readFileSync } from 'node:fs'; +import { mkdirSync, writeFileSync, existsSync, rmSync, readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { TaskRunner } from '../task/runner.js'; @@ -23,10 +23,11 @@ describe('TaskRunner', () => { }); describe('ensureDirs', () => { - it('should create tasks and completed directories', () => { + it('should create tasks, completed, and failed directories', () => { runner.ensureDirs(); expect(existsSync(join(testDir, '.takt', 'tasks'))).toBe(true); expect(existsSync(join(testDir, '.takt', 'completed'))).toBe(true); + expect(existsSync(join(testDir, '.takt', 'failed'))).toBe(true); }); }); @@ -134,7 +135,7 @@ describe('TaskRunner', () => { expect(logData.success).toBe(true); }); - it('should record failure status', () => { + it('should throw error when called with a failed result', () => { const tasksDir = join(testDir, '.takt', 'tasks'); mkdirSync(tasksDir, { recursive: true }); writeFileSync(join(tasksDir, 'fail-task.md'), 'Will fail'); @@ -149,9 +150,75 @@ describe('TaskRunner', () => { completedAt: '2024-01-01T00:01:00.000Z', }; - const reportFile = runner.completeTask(result); + expect(() => runner.completeTask(result)).toThrow( + 'Cannot complete a failed task. Use failTask() instead.' + ); + }); + }); + + describe('failTask', () => { + it('should move task to failed directory', () => { + const tasksDir = join(testDir, '.takt', 'tasks'); + mkdirSync(tasksDir, { recursive: true }); + const taskFile = join(tasksDir, 'fail-task.md'); + writeFileSync(taskFile, 'Task that will fail'); + + const task = runner.getTask('fail-task')!; + const result = { + task, + success: false, + response: 'Error occurred', + executionLog: ['Started', 'Error'], + startedAt: '2024-01-01T00:00:00.000Z', + completedAt: '2024-01-01T00:01:00.000Z', + }; + + const reportFile = runner.failTask(result); + + // Original task file should be removed from tasks dir + expect(existsSync(taskFile)).toBe(false); + + // Report should be in .takt/failed/ (not .takt/completed/) + expect(reportFile).toContain(join('.takt', 'failed')); + expect(reportFile).not.toContain(join('.takt', 'completed')); + expect(existsSync(reportFile)).toBe(true); + const reportContent = readFileSync(reportFile, 'utf-8'); + expect(reportContent).toContain('# タスク実行レポート'); + expect(reportContent).toContain('fail-task'); expect(reportContent).toContain('失敗'); + + // Log file should be created in failed dir + const logFile = reportFile.replace('report.md', 'log.json'); + expect(existsSync(logFile)).toBe(true); + const logData = JSON.parse(readFileSync(logFile, 'utf-8')); + expect(logData.taskName).toBe('fail-task'); + expect(logData.success).toBe(false); + }); + + it('should not move failed task to completed directory', () => { + const tasksDir = join(testDir, '.takt', 'tasks'); + const completedDir = join(testDir, '.takt', 'completed'); + mkdirSync(tasksDir, { recursive: true }); + const taskFile = join(tasksDir, 'another-fail.md'); + writeFileSync(taskFile, 'Another failing task'); + + const task = runner.getTask('another-fail')!; + const result = { + task, + success: false, + response: 'Something went wrong', + executionLog: [], + startedAt: '2024-01-01T00:00:00.000Z', + completedAt: '2024-01-01T00:01:00.000Z', + }; + + runner.failTask(result); + + // completed directory should be empty (only the dir itself exists) + mkdirSync(completedDir, { recursive: true }); + const completedContents = readdirSync(completedDir); + expect(completedContents).toHaveLength(0); }); }); diff --git a/src/commands/taskExecution.ts b/src/commands/taskExecution.ts index f5c2be0..0cb1dbe 100644 --- a/src/commands/taskExecution.ts +++ b/src/commands/taskExecution.ts @@ -86,18 +86,20 @@ export async function executeAndCompleteTask( } } - taskRunner.completeTask({ + const taskResult = { task, success: taskSuccess, response: taskSuccess ? 'Task completed successfully' : 'Task failed', executionLog, startedAt, completedAt, - }); + }; if (taskSuccess) { + taskRunner.completeTask(taskResult); success(`Task "${task.name}" completed`); } else { + taskRunner.failTask(taskResult); error(`Task "${task.name}" failed`); } @@ -105,7 +107,7 @@ export async function executeAndCompleteTask( } catch (err) { const completedAt = new Date().toISOString(); - taskRunner.completeTask({ + taskRunner.failTask({ task, success: false, response: getErrorMessage(err), diff --git a/src/task/runner.ts b/src/task/runner.ts index 9b0670c..5bd22be 100644 --- a/src/task/runner.ts +++ b/src/task/runner.ts @@ -45,17 +45,20 @@ export class TaskRunner { private projectDir: string; private tasksDir: string; private completedDir: string; + private failedDir: string; constructor(projectDir: string) { this.projectDir = projectDir; this.tasksDir = path.join(projectDir, '.takt', 'tasks'); this.completedDir = path.join(projectDir, '.takt', 'completed'); + this.failedDir = path.join(projectDir, '.takt', 'failed'); } /** ディレクトリ構造を作成 */ ensureDirs(): void { fs.mkdirSync(this.tasksDir, { recursive: true }); fs.mkdirSync(this.completedDir, { recursive: true }); + fs.mkdirSync(this.failedDir, { recursive: true }); } /** タスクディレクトリのパスを取得 */ @@ -126,30 +129,52 @@ export class TaskRunner { * @returns レポートファイルのパス */ completeTask(result: TaskResult): string { + if (!result.success) { + throw new Error('Cannot complete a failed task. Use failTask() instead.'); + } + return this.moveTask(result, this.completedDir); + } + + /** + * タスクを失敗としてマーク + * + * タスクファイルを .takt/failed に移動し、 + * レポートファイルを作成する。 + * + * @returns レポートファイルのパス + */ + failTask(result: TaskResult): string { + return this.moveTask(result, this.failedDir); + } + + /** + * タスクファイルを指定ディレクトリに移動し、レポート・ログを生成する + */ + private moveTask(result: TaskResult, targetDir: string): string { this.ensureDirs(); // タイムスタンプを生成 const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); - // 完了ディレクトリにサブディレクトリを作成 - const taskCompletedDir = path.join( - this.completedDir, + // ターゲットディレクトリにサブディレクトリを作成 + const taskTargetDir = path.join( + targetDir, `${timestamp}_${result.task.name}` ); - fs.mkdirSync(taskCompletedDir, { recursive: true }); + fs.mkdirSync(taskTargetDir, { recursive: true }); // 元のタスクファイルを移動(元の拡張子を保持) const originalExt = path.extname(result.task.filePath); - const completedTaskFile = path.join(taskCompletedDir, `${result.task.name}${originalExt}`); - fs.renameSync(result.task.filePath, completedTaskFile); + const movedTaskFile = path.join(taskTargetDir, `${result.task.name}${originalExt}`); + fs.renameSync(result.task.filePath, movedTaskFile); // レポートを生成 - const reportFile = path.join(taskCompletedDir, 'report.md'); + const reportFile = path.join(taskTargetDir, 'report.md'); const reportContent = this.generateReport(result); fs.writeFileSync(reportFile, reportContent, 'utf-8'); // ログを保存 - const logFile = path.join(taskCompletedDir, 'log.json'); + const logFile = path.join(taskTargetDir, 'log.json'); const logData = { taskName: result.task.name, success: result.success,