@ronkovic/aad 0.3.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +332 -14
  2. package/package.json +6 -1
  3. package/src/__tests__/e2e/cleanup-e2e.test.ts +186 -0
  4. package/src/__tests__/e2e/dashboard-api-e2e.test.ts +87 -0
  5. package/src/__tests__/e2e/pipeline-e2e.test.ts +10 -68
  6. package/src/__tests__/e2e/resume-e2e.test.ts +9 -11
  7. package/src/__tests__/e2e/retry-e2e.test.ts +285 -0
  8. package/src/__tests__/e2e/status-e2e.test.ts +227 -0
  9. package/src/__tests__/e2e/tdd-pipeline-e2e.test.ts +360 -0
  10. package/src/__tests__/helpers/index.ts +6 -0
  11. package/src/__tests__/helpers/mock-claude-provider.ts +53 -0
  12. package/src/__tests__/helpers/mock-logger.ts +36 -0
  13. package/src/__tests__/helpers/wait-helpers.ts +34 -0
  14. package/src/__tests__/integration/pipeline.test.ts +3 -0
  15. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
  16. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +6 -0
  17. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +3 -0
  18. package/src/modules/cli/__tests__/cleanup.test.ts +73 -0
  19. package/src/modules/cli/__tests__/resume.test.ts +4 -0
  20. package/src/modules/cli/__tests__/run.test.ts +37 -0
  21. package/src/modules/cli/__tests__/status.test.ts +1 -0
  22. package/src/modules/cli/app.ts +2 -0
  23. package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
  24. package/src/modules/cli/commands/cleanup.ts +26 -11
  25. package/src/modules/cli/commands/resume.ts +14 -8
  26. package/src/modules/cli/commands/run.ts +70 -8
  27. package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
  28. package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
  29. package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
  30. package/src/modules/dashboard/routes/sse.ts +3 -2
  31. package/src/modules/dashboard/server.ts +1 -0
  32. package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
  33. package/src/modules/dashboard/ui/dashboard.html +640 -349
  34. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
  35. package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
  36. package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
  37. package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
  38. package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
  39. package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
  40. package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
  41. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
  42. package/src/modules/git-workspace/branch-manager.ts +24 -3
  43. package/src/modules/git-workspace/dependency-installer.ts +113 -0
  44. package/src/modules/git-workspace/git-exec.ts +3 -2
  45. package/src/modules/git-workspace/index.ts +10 -1
  46. package/src/modules/git-workspace/merge-service.ts +36 -2
  47. package/src/modules/git-workspace/pr-manager.ts +278 -0
  48. package/src/modules/git-workspace/template-copy.ts +302 -0
  49. package/src/modules/git-workspace/worktree-manager.ts +37 -11
  50. package/src/modules/planning/__tests__/planning-service.test.ts +3 -0
  51. package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
  52. package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
  53. package/src/modules/planning/planning.service.ts +16 -2
  54. package/src/modules/planning/project-detection.ts +4 -1
  55. package/src/modules/process-manager/__tests__/process-manager.test.ts +3 -0
  56. package/src/modules/process-manager/process-manager.ts +2 -1
  57. package/src/modules/task-execution/__tests__/executor.test.ts +496 -0
  58. package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
  59. package/src/modules/task-execution/executor.ts +163 -4
  60. package/src/modules/task-execution/phases/implementer-green.ts +22 -5
  61. package/src/modules/task-execution/phases/merge.ts +44 -2
  62. package/src/modules/task-execution/phases/tester-red.ts +22 -5
  63. package/src/modules/task-execution/phases/tester-verify.ts +22 -6
  64. package/src/modules/task-queue/dispatcher.ts +96 -3
  65. package/src/shared/__tests__/config.test.ts +30 -0
  66. package/src/shared/__tests__/events.test.ts +42 -16
  67. package/src/shared/__tests__/prerequisites.test.ts +176 -0
  68. package/src/shared/__tests__/shutdown-handler.test.ts +96 -0
  69. package/src/shared/config.ts +10 -0
  70. package/src/shared/events.ts +5 -0
  71. package/src/shared/memory-check.ts +2 -2
  72. package/src/shared/prerequisites.ts +190 -0
  73. package/src/shared/shutdown-handler.ts +12 -5
  74. package/src/shared/types.ts +25 -0
  75. package/templates/CLAUDE.md +122 -0
  76. package/templates/settings.json +117 -0
  77. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
  78. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
  115. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
  116. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
  117. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
  118. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
  119. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
  120. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
  121. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
  122. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
  123. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
  124. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
  125. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
  126. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
  127. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
  128. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
  129. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
  130. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
  131. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
  132. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +0 -5
@@ -8,11 +8,12 @@ import { randomUUID } from "node:crypto";
8
8
  import type { App } from "../app";
9
9
  import { formatProgress, formatTaskTable, createSpinner } from "../output";
10
10
  import { createRunId, createWorkerId } from "../../../shared/types";
11
- import { getCurrentBranch, cleanupOrphanedFromPreviousRuns } from "../../git-workspace";
11
+ import { getCurrentBranch, cleanupOrphanedFromPreviousRuns, copyTemplatesToWorktree, installDependencies } from "../../git-workspace";
12
12
  import { checkMemoryAndWarn } from "../../../shared/memory-check";
13
13
  import { installShutdownHandler } from "../../../shared/shutdown-handler";
14
- import { registerTaskDispatchHandler } from "./task-dispatch-handler";
14
+ import { registerTaskDispatchHandler, detectWorkspace } from "./task-dispatch-handler";
15
15
  import { generateFeatureName } from "../../planning/feature-name-generator";
16
+ import { checkPrerequisites } from "../../../shared/prerequisites";
16
17
 
17
18
  export function createRunCommand(getApp: () => App): Command {
18
19
  const command = new Command("run")
@@ -27,13 +28,31 @@ export function createRunCommand(getApp: () => App): Command {
27
28
  .option("--strategy <type>", "Multi-repo strategy: independent or coordinated", "independent")
28
29
  .option("--plugins <paths>", "Comma-separated plugin paths to load")
29
30
  .option("--keep-worktrees", "Keep worktrees and branches after completion (default: auto-cleanup)", false)
30
- .action(async (requirementsPath: string, cmdOptions: { keepWorktrees?: boolean }) => {
31
+ .option("--dry-run", "Display task plan without executing", false)
32
+ .option("--strict-tdd", "Always run full TDD pipeline (disable pre-check optimization)", false)
33
+ .action(async (requirementsPath: string, cmdOptions: { keepWorktrees?: boolean; dryRun?: boolean; strictTdd?: boolean }) => {
34
+ let app: App | null = null;
31
35
  try {
32
- const app = getApp();
33
- await runPipeline(app, requirementsPath, cmdOptions.keepWorktrees ?? false);
36
+ app = getApp();
37
+
38
+ // Override config with CLI options
39
+ if (cmdOptions.strictTdd !== undefined) {
40
+ app.config.strictTdd = cmdOptions.strictTdd;
41
+ }
42
+
43
+ await runPipeline(app, requirementsPath, cmdOptions.keepWorktrees ?? false, cmdOptions.dryRun ?? false);
44
+ await app.shutdown();
45
+ process.exit(process.exitCode ?? 0);
34
46
  } catch (error) {
35
47
  const message = error instanceof Error ? error.message : String(error);
36
48
  console.error("Run pipeline failed:", message);
49
+ if (app) {
50
+ try {
51
+ await app.shutdown();
52
+ } catch {
53
+ // Ignore shutdown errors
54
+ }
55
+ }
37
56
  process.exit(1);
38
57
  }
39
58
  });
@@ -44,7 +63,7 @@ export function createRunCommand(getApp: () => App): Command {
44
63
  /**
45
64
  * メインパイプライン実行
46
65
  */
47
- export async function runPipeline(app: App, requirementsPath: string, keepWorktrees = false): Promise<void> {
66
+ export async function runPipeline(app: App, requirementsPath: string, keepWorktrees = false, dryRun = false): Promise<void> {
48
67
  const { config, logger, eventBus, planningService, dispatcher, processManager, worktreeManager, stores, branchManager, providerRegistry } = app;
49
68
 
50
69
  // Validate requirements file exists
@@ -55,7 +74,10 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
55
74
 
56
75
  logger.info({ requirementsPath }, "Starting AAD pipeline");
57
76
 
58
- // 0. Memory check
77
+ // 0. Prerequisites check
78
+ await checkPrerequisites(logger);
79
+
80
+ // 0.1. Memory check
59
81
  await checkMemoryAndWarn(logger);
60
82
 
61
83
  // 0.5. Cleanup orphaned worktrees/branches from previous runs
@@ -71,7 +93,11 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
71
93
 
72
94
  logger.info({ runId, parentBranch }, "Initialized run");
73
95
  console.log(`\n🚀 AAD Run: ${runId}`);
74
- console.log(`📍 Parent Branch: ${parentBranch}\n`);
96
+ console.log(`📍 Parent Branch: ${parentBranch}`);
97
+ if (app.dashboardServer) {
98
+ console.log(`📊 Dashboard: http://${config.dashboard.host}:${config.dashboard.port}`);
99
+ }
100
+ console.log();
75
101
 
76
102
  // 2. PlanningService.planTasks() → TaskPlan
77
103
  const planSpinner = createSpinner("Planning tasks...");
@@ -110,6 +136,12 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
110
136
  console.log(formatTaskTable(taskPlan.tasks));
111
137
  console.log();
112
138
 
139
+ // Dry-run: タスク一覧表示後に終了
140
+ if (dryRun) {
141
+ console.log("🔍 Dry-run mode: Exiting without execution\n");
142
+ return;
143
+ }
144
+
113
145
  // 3. Dispatcher.initialize(taskPlan) + ProcessManager.initializePool()
114
146
  await dispatcher.initialize(taskPlan);
115
147
  await processManager.initializePool(config.workers.num);
@@ -143,6 +175,7 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
143
175
  runId,
144
176
  stores: { runStore: stores.runStore, taskStore: stores.taskStore },
145
177
  logger,
178
+ persistMode: app.persistMode,
146
179
  });
147
180
 
148
181
  // 5. Generate feature name and create parent worktree with real branch
@@ -160,6 +193,32 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
160
193
  featureBranchName = result.branch;
161
194
  logger.info({ parentWorktreePath, featureBranchName }, "Parent worktree created for merging");
162
195
  console.log(`🌿 Feature Branch: ${featureBranchName}`);
196
+
197
+ // Copy templates to parent worktree
198
+ try {
199
+ await copyTemplatesToWorktree({
200
+ targetDir: parentWorktreePath,
201
+ projectRoot: process.cwd(),
202
+ logger,
203
+ });
204
+ logger.info({ parentWorktreePath }, "Templates copied to parent worktree");
205
+ } catch (templateError) {
206
+ logger.warn({ templateError }, "Template copy failed (non-critical)");
207
+ }
208
+
209
+ // Install dependencies in parent worktree (non-critical)
210
+ try {
211
+ const workspace = await detectWorkspace(parentWorktreePath, logger);
212
+ const installResult = await installDependencies(workspace, logger);
213
+ if (!installResult.skipped && !installResult.success) {
214
+ logger.warn(
215
+ { parentWorktreePath, output: installResult.output.slice(0, 500) },
216
+ "Parent worktree dependency install failed (non-fatal)"
217
+ );
218
+ }
219
+ } catch (installError) {
220
+ logger.warn({ installError }, "Parent worktree install error (non-fatal)");
221
+ }
163
222
  } catch (error) {
164
223
  logger.warn({ error }, "Failed to create parent worktree, using repo root as fallback");
165
224
  }
@@ -272,6 +331,9 @@ export async function runPipeline(app: App, requirementsPath: string, keepWorktr
272
331
 
273
332
  console.log(` Removed ${removed} worktree(s)`);
274
333
  console.log(` Deleted ${deletedBranches.length} branch(es)`);
334
+ if (featureBranchName) {
335
+ console.log(` 🌿 Parent branch preserved: ${featureBranchName}`);
336
+ }
275
337
  logger.info({ runId, removedWorktrees: removed, deletedBranches: deletedBranches.length }, "Auto-cleanup completed");
276
338
  } catch (error) {
277
339
  logger.error({ runId, error }, "Auto-cleanup failed");
@@ -20,6 +20,7 @@ import {
20
20
  detectArchitecturePattern,
21
21
  createBunFileChecker,
22
22
  } from "../../planning";
23
+ import { copyTemplatesToWorktree, harvestMemory, installDependencies } from "../../git-workspace";
23
24
 
24
25
  export interface TaskDispatchContext {
25
26
  app: App;
@@ -69,18 +70,86 @@ export function registerTaskDispatchHandler(ctx: TaskDispatchContext): void {
69
70
  const worktreePath = await worktreeManager.createTaskWorktree(taskId, branchName, featureBranchName);
70
71
  logger.info({ taskId, worktreePath, branchName }, "Worktree created");
71
72
 
73
+ // Copy templates to task worktree
74
+ try {
75
+ await copyTemplatesToWorktree({
76
+ targetDir: worktreePath,
77
+ projectRoot: process.cwd(),
78
+ logger,
79
+ });
80
+ } catch (templateError) {
81
+ logger.warn({ templateError }, "Template copy failed (non-critical)");
82
+ }
83
+
72
84
  const workspace = await detectWorkspace(worktreePath, logger);
73
85
 
86
+ // Install dependencies in task worktree
87
+ try {
88
+ const installResult = await installDependencies(workspace, logger);
89
+ if (!installResult.skipped && !installResult.success) {
90
+ logger.warn(
91
+ {
92
+ taskId,
93
+ packageManager: workspace.packageManager,
94
+ output: installResult.output.slice(0, 500),
95
+ },
96
+ "Dependency install failed (non-fatal, Claude agent may handle)"
97
+ );
98
+ }
99
+ } catch (installError) {
100
+ logger.warn({ taskId, error: installError }, "Dependency install error (non-fatal)");
101
+ }
102
+
103
+ // Fetch dependency changes if task has dependencies
104
+ if (task.dependsOn.length > 0) {
105
+ try {
106
+ for (const depTaskId of task.dependsOn) {
107
+ const depTask = await stores.taskStore.get(depTaskId);
108
+ if (depTask?.status === "completed") {
109
+ const depBranchName = `${branchPrefix}/${depTaskId}`;
110
+ await app.mergeService.fetchDependencyChanges(depBranchName, worktreePath);
111
+ logger.info({ taskId, depTaskId }, "Fetched dependency changes");
112
+ }
113
+ }
114
+ } catch (depError) {
115
+ logger.warn({ taskId, error: depError }, "Dependency fetch failed (non-critical)");
116
+ }
117
+ }
118
+
74
119
  await app.pluginManager.runHook("before:execution", { taskId, task, worktreePath });
75
120
 
76
121
  const provider = providerRegistry.getProvider("implementer");
122
+ const retryContext = task.retryCount > 0 ? {
123
+ retryCount: task.retryCount,
124
+ previousFailure: task.previousFailure,
125
+ } : undefined;
126
+
77
127
  const result = await executeTddPipeline(
78
128
  task, workspace, branchName, parentBranch, mergeTarget,
79
129
  runId, app.config, provider, app.mergeService, eventBus,
130
+ undefined, // testSpawner
131
+ retryContext,
80
132
  );
81
133
 
82
134
  await app.pluginManager.runHook("after:execution", { taskId, result });
83
- eventBus.emit({ type: "task:completed", taskId, result });
135
+
136
+ if (result.status === "failed") {
137
+ logger.warn({ taskId, error: result.error, output: result.output }, "TDD pipeline returned failure");
138
+ eventBus.emit({ type: "task:failed", taskId, error: result.error ?? "Pipeline failed" });
139
+ } else {
140
+ eventBus.emit({ type: "task:completed", taskId, result });
141
+
142
+ // Harvest agent memory (non-critical)
143
+ try {
144
+ await harvestMemory({
145
+ sourceDir: `${worktreePath}/.claude/agent-memory`,
146
+ targetDir: `${process.cwd()}/.claude/agent-memory`,
147
+ logger,
148
+ });
149
+ } catch (memoryError) {
150
+ logger.warn({ taskId, error: memoryError }, "Memory harvest failed (non-critical)");
151
+ }
152
+ }
84
153
  } catch (error) {
85
154
  logger.error({ taskId, workerId, error }, "TDD pipeline failed");
86
155
  eventBus.emit({ type: "task:failed", taskId, error: String(error) });
@@ -135,7 +204,7 @@ export function registerTaskDispatchHandler(ctx: TaskDispatchContext): void {
135
204
  /**
136
205
  * Detect workspace information from worktree path
137
206
  */
138
- async function detectWorkspace(
207
+ export async function detectWorkspace(
139
208
  worktreePath: string,
140
209
  logger: import("pino").Logger
141
210
  ): Promise<WorkspaceInfo> {
@@ -202,13 +271,14 @@ function mapProjectTypeToLanguage(projectType: import("../../planning").ProjectT
202
271
  function mapTestFramework(tf: import("../../planning").TestFramework): import("../../../shared/types").TestFramework {
203
272
  if (tf === "bun:test") return "bun-test";
204
273
  if (tf === "unittest") return "pytest";
205
- if (tf === "terraform-validate") return "unknown";
274
+ if (tf === "terraform-validate") return "terraform";
206
275
 
207
276
  const mapping: Partial<Record<import("../../planning").TestFramework, import("../../../shared/types").TestFramework>> = {
208
277
  pytest: "pytest",
209
278
  vitest: "vitest",
210
279
  jest: "jest",
211
280
  mocha: "mocha",
281
+ playwright: "playwright",
212
282
  "go-test": "go-test",
213
283
  "cargo-test": "cargo",
214
284
  unknown: "unknown",
@@ -0,0 +1,332 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import type { StateAggregator } from "../services/state-aggregator";
3
+
4
+ /**
5
+ * テストケース: /api/graph エンドポイント
6
+ *
7
+ * DAG (Directed Acyclic Graph) 可視化用のタスク依存関係グラフを返す
8
+ * - nodes: タスクのリスト (id, status)
9
+ * - edges: 依存関係のリスト (from → to)
10
+ */
11
+ describe("GET /api/graph - Task Dependency Graph", () => {
12
+ /**
13
+ * テストケース 1: 空のグラフ (タスクなし)
14
+ *
15
+ * 期待値:
16
+ * - nodes: 空配列
17
+ * - edges: 空配列
18
+ */
19
+ test("returns empty graph when no tasks exist", async () => {
20
+ const mockAggregator: StateAggregator = {
21
+ getGraph: async () => ({
22
+ nodes: [],
23
+ edges: [],
24
+ }),
25
+ } as any;
26
+
27
+ const result = await mockAggregator.getGraph();
28
+
29
+ expect(result.nodes).toEqual([]);
30
+ expect(result.edges).toEqual([]);
31
+ });
32
+
33
+ /**
34
+ * テストケース 2: 単一タスク (依存関係なし)
35
+ *
36
+ * グラフ構造:
37
+ *
38
+ * [task-1]
39
+ *
40
+ * 期待値:
41
+ * - nodes: 1つのノード
42
+ * - edges: 空配列
43
+ */
44
+ test("returns single node with no edges for independent task", async () => {
45
+ const mockAggregator: StateAggregator = {
46
+ getGraph: async () => ({
47
+ nodes: [
48
+ { id: "task-1", status: "pending" },
49
+ ],
50
+ edges: [],
51
+ }),
52
+ } as any;
53
+
54
+ const result = await mockAggregator.getGraph();
55
+
56
+ expect(result.nodes).toHaveLength(1);
57
+ expect(result.nodes[0]).toEqual({ id: "task-1", status: "pending" });
58
+ expect(result.edges).toEqual([]);
59
+ });
60
+
61
+ /**
62
+ * テストケース 3: 線形依存 (task-1 → task-2 → task-3)
63
+ *
64
+ * グラフ構造:
65
+ *
66
+ * [task-1] → [task-2] → [task-3]
67
+ *
68
+ * 依存関係:
69
+ * - task-2 depends_on task-1
70
+ * - task-3 depends_on task-2
71
+ */
72
+ test("returns linear dependency chain", async () => {
73
+ const mockAggregator: StateAggregator = {
74
+ getGraph: async () => ({
75
+ nodes: [
76
+ { id: "task-1", status: "completed" },
77
+ { id: "task-2", status: "running" },
78
+ { id: "task-3", status: "pending" },
79
+ ],
80
+ edges: [
81
+ { from: "task-1", to: "task-2" },
82
+ { from: "task-2", to: "task-3" },
83
+ ],
84
+ }),
85
+ } as any;
86
+
87
+ const result = await mockAggregator.getGraph();
88
+
89
+ expect(result.nodes).toHaveLength(3);
90
+ expect(result.edges).toHaveLength(2);
91
+
92
+ // エッジの正確性を検証
93
+ expect(result.edges).toContainEqual({ from: "task-1", to: "task-2" });
94
+ expect(result.edges).toContainEqual({ from: "task-2", to: "task-3" });
95
+
96
+ // ステータスの検証
97
+ const task1 = result.nodes.find((n) => n.id === "task-1");
98
+ const task2 = result.nodes.find((n) => n.id === "task-2");
99
+ const task3 = result.nodes.find((n) => n.id === "task-3");
100
+
101
+ expect(task1?.status).toBe("completed");
102
+ expect(task2?.status).toBe("running");
103
+ expect(task3?.status).toBe("pending");
104
+ });
105
+
106
+ /**
107
+ * テストケース 4: 並列タスク (task-1, task-2 独立、両方が task-3 に依存される)
108
+ *
109
+ * グラフ構造:
110
+ *
111
+ * [task-1] ─┐
112
+ * ├→ [task-3]
113
+ * [task-2] ─┘
114
+ *
115
+ * 依存関係:
116
+ * - task-3 depends_on [task-1, task-2]
117
+ */
118
+ test("returns parallel tasks converging into single task", async () => {
119
+ const mockAggregator: StateAggregator = {
120
+ getGraph: async () => ({
121
+ nodes: [
122
+ { id: "task-1", status: "completed" },
123
+ { id: "task-2", status: "completed" },
124
+ { id: "task-3", status: "running" },
125
+ ],
126
+ edges: [
127
+ { from: "task-1", to: "task-3" },
128
+ { from: "task-2", to: "task-3" },
129
+ ],
130
+ }),
131
+ } as any;
132
+
133
+ const result = await mockAggregator.getGraph();
134
+
135
+ expect(result.nodes).toHaveLength(3);
136
+ expect(result.edges).toHaveLength(2);
137
+
138
+ // task-3 は task-1 と task-2 両方に依存
139
+ const edgesToTask3 = result.edges.filter((e) => e.to === "task-3");
140
+ expect(edgesToTask3).toHaveLength(2);
141
+
142
+ expect(edgesToTask3).toContainEqual({ from: "task-1", to: "task-3" });
143
+ expect(edgesToTask3).toContainEqual({ from: "task-2", to: "task-3" });
144
+
145
+ // task-1, task-2 は並列実行可能 (互いに依存しない)
146
+ const edgesBetween1And2 = result.edges.filter(
147
+ (e) =>
148
+ (e.from === "task-1" && e.to === "task-2") ||
149
+ (e.from === "task-2" && e.to === "task-1")
150
+ );
151
+ expect(edgesBetween1And2).toHaveLength(0);
152
+ });
153
+
154
+ /**
155
+ * テストケース 5: ダイヤモンド依存 (task-1 → task-2,3 → task-4)
156
+ *
157
+ * グラフ構造:
158
+ *
159
+ * [task-1]
160
+ * ↙ ↘
161
+ * [task-2] [task-3]
162
+ * ↘ ↙
163
+ * [task-4]
164
+ *
165
+ * 依存関係:
166
+ * - task-2 depends_on task-1
167
+ * - task-3 depends_on task-1
168
+ * - task-4 depends_on [task-2, task-3]
169
+ */
170
+ test("returns diamond dependency graph", async () => {
171
+ const mockAggregator: StateAggregator = {
172
+ getGraph: async () => ({
173
+ nodes: [
174
+ { id: "task-1", status: "completed" },
175
+ { id: "task-2", status: "running" },
176
+ { id: "task-3", status: "running" },
177
+ { id: "task-4", status: "pending" },
178
+ ],
179
+ edges: [
180
+ { from: "task-1", to: "task-2" },
181
+ { from: "task-1", to: "task-3" },
182
+ { from: "task-2", to: "task-4" },
183
+ { from: "task-3", to: "task-4" },
184
+ ],
185
+ }),
186
+ } as any;
187
+
188
+ const result = await mockAggregator.getGraph();
189
+
190
+ expect(result.nodes).toHaveLength(4);
191
+ expect(result.edges).toHaveLength(4);
192
+
193
+ // task-1 は task-2, task-3 に分岐
194
+ const edgesFromTask1 = result.edges.filter((e) => e.from === "task-1");
195
+ expect(edgesFromTask1).toHaveLength(2);
196
+ expect(edgesFromTask1).toContainEqual({ from: "task-1", to: "task-2" });
197
+ expect(edgesFromTask1).toContainEqual({ from: "task-1", to: "task-3" });
198
+
199
+ // task-4 は task-2, task-3 からマージ
200
+ const edgesToTask4 = result.edges.filter((e) => e.to === "task-4");
201
+ expect(edgesToTask4).toHaveLength(2);
202
+ expect(edgesToTask4).toContainEqual({ from: "task-2", to: "task-4" });
203
+ expect(edgesToTask4).toContainEqual({ from: "task-3", to: "task-4" });
204
+
205
+ // ステータス検証: task-1完了、task-2/3実行中、task-4保留
206
+ expect(result.nodes.find((n) => n.id === "task-1")?.status).toBe("completed");
207
+ expect(result.nodes.find((n) => n.id === "task-2")?.status).toBe("running");
208
+ expect(result.nodes.find((n) => n.id === "task-3")?.status).toBe("running");
209
+ expect(result.nodes.find((n) => n.id === "task-4")?.status).toBe("pending");
210
+ });
211
+
212
+ /**
213
+ * テストケース 6: 複雑なグラフ (多段階並列依存)
214
+ *
215
+ * グラフ構造:
216
+ *
217
+ * [task-1] → [task-2] ─┐
218
+ * ├→ [task-5]
219
+ * [task-3] → [task-4] ─┘
220
+ *
221
+ * 依存関係:
222
+ * - task-2 depends_on task-1
223
+ * - task-4 depends_on task-3
224
+ * - task-5 depends_on [task-2, task-4]
225
+ */
226
+ test("returns complex multi-level parallel graph", async () => {
227
+ const mockAggregator: StateAggregator = {
228
+ getGraph: async () => ({
229
+ nodes: [
230
+ { id: "task-1", status: "completed" },
231
+ { id: "task-2", status: "completed" },
232
+ { id: "task-3", status: "completed" },
233
+ { id: "task-4", status: "running" },
234
+ { id: "task-5", status: "pending" },
235
+ ],
236
+ edges: [
237
+ { from: "task-1", to: "task-2" },
238
+ { from: "task-3", to: "task-4" },
239
+ { from: "task-2", to: "task-5" },
240
+ { from: "task-4", to: "task-5" },
241
+ ],
242
+ }),
243
+ } as any;
244
+
245
+ const result = await mockAggregator.getGraph();
246
+
247
+ expect(result.nodes).toHaveLength(5);
248
+ expect(result.edges).toHaveLength(4);
249
+
250
+ // task-1, task-3 は並列実行可能な起点
251
+ const rootTasks = result.nodes.filter(
252
+ (node) => !result.edges.some((edge) => edge.to === node.id)
253
+ );
254
+ expect(rootTasks).toHaveLength(2);
255
+ expect(rootTasks.map((t) => t.id).sort()).toEqual(["task-1", "task-3"]);
256
+
257
+ // task-5 は最終的な合流点
258
+ const edgesToTask5 = result.edges.filter((e) => e.to === "task-5");
259
+ expect(edgesToTask5).toHaveLength(2);
260
+ });
261
+
262
+ /**
263
+ * テストケース 7: タスクステータスのバリエーション検証
264
+ *
265
+ * 全ステータス (pending, running, completed, failed) を含むグラフ
266
+ */
267
+ test("returns graph with all task status types", async () => {
268
+ const mockAggregator: StateAggregator = {
269
+ getGraph: async () => ({
270
+ nodes: [
271
+ { id: "task-1", status: "completed" },
272
+ { id: "task-2", status: "running" },
273
+ { id: "task-3", status: "pending" },
274
+ { id: "task-4", status: "failed" },
275
+ ],
276
+ edges: [
277
+ { from: "task-1", to: "task-2" },
278
+ { from: "task-1", to: "task-4" },
279
+ ],
280
+ }),
281
+ } as any;
282
+
283
+ const result = await mockAggregator.getGraph();
284
+
285
+ expect(result.nodes).toHaveLength(4);
286
+
287
+ const statuses = result.nodes.map((n) => n.status).sort();
288
+ expect(statuses).toEqual(["completed", "failed", "pending", "running"]);
289
+
290
+ // 各ステータスのノードが存在することを確認
291
+ expect(result.nodes.some((n) => n.status === "completed")).toBe(true);
292
+ expect(result.nodes.some((n) => n.status === "running")).toBe(true);
293
+ expect(result.nodes.some((n) => n.status === "pending")).toBe(true);
294
+ expect(result.nodes.some((n) => n.status === "failed")).toBe(true);
295
+ });
296
+
297
+ /**
298
+ * テストケース 8: グラフ構造の整合性検証
299
+ *
300
+ * - 全エッジの from/to が nodes に存在することを確認
301
+ * - 自己参照エッジがないことを確認
302
+ */
303
+ test("validates graph structure integrity", async () => {
304
+ const mockAggregator: StateAggregator = {
305
+ getGraph: async () => ({
306
+ nodes: [
307
+ { id: "task-1", status: "completed" },
308
+ { id: "task-2", status: "running" },
309
+ { id: "task-3", status: "pending" },
310
+ ],
311
+ edges: [
312
+ { from: "task-1", to: "task-2" },
313
+ { from: "task-2", to: "task-3" },
314
+ ],
315
+ }),
316
+ } as any;
317
+
318
+ const result = await mockAggregator.getGraph();
319
+
320
+ const nodeIds = new Set(result.nodes.map((n) => n.id));
321
+
322
+ // 全エッジの from/to が nodes に存在
323
+ for (const edge of result.edges) {
324
+ expect(nodeIds.has(edge.from)).toBe(true);
325
+ expect(nodeIds.has(edge.to)).toBe(true);
326
+ }
327
+
328
+ // 自己参照エッジがない
329
+ const selfReferentialEdges = result.edges.filter((e) => e.from === e.to);
330
+ expect(selfReferentialEdges).toHaveLength(0);
331
+ });
332
+ });