@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.
- package/README.md +332 -14
- package/package.json +6 -1
- package/src/__tests__/e2e/cleanup-e2e.test.ts +186 -0
- package/src/__tests__/e2e/dashboard-api-e2e.test.ts +87 -0
- package/src/__tests__/e2e/pipeline-e2e.test.ts +10 -68
- package/src/__tests__/e2e/resume-e2e.test.ts +9 -11
- package/src/__tests__/e2e/retry-e2e.test.ts +285 -0
- package/src/__tests__/e2e/status-e2e.test.ts +227 -0
- package/src/__tests__/e2e/tdd-pipeline-e2e.test.ts +360 -0
- package/src/__tests__/helpers/index.ts +6 -0
- package/src/__tests__/helpers/mock-claude-provider.ts +53 -0
- package/src/__tests__/helpers/mock-logger.ts +36 -0
- package/src/__tests__/helpers/wait-helpers.ts +34 -0
- package/src/__tests__/integration/pipeline.test.ts +3 -0
- package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +1 -1
- package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +6 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +3 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +73 -0
- package/src/modules/cli/__tests__/resume.test.ts +4 -0
- package/src/modules/cli/__tests__/run.test.ts +37 -0
- package/src/modules/cli/__tests__/status.test.ts +1 -0
- package/src/modules/cli/app.ts +2 -0
- package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +145 -0
- package/src/modules/cli/commands/cleanup.ts +26 -11
- package/src/modules/cli/commands/resume.ts +14 -8
- package/src/modules/cli/commands/run.ts +70 -8
- package/src/modules/cli/commands/task-dispatch-handler.ts +73 -3
- package/src/modules/dashboard/__tests__/api-graph.test.ts +332 -0
- package/src/modules/dashboard/__tests__/api-timeline.test.ts +461 -0
- package/src/modules/dashboard/routes/sse.ts +3 -2
- package/src/modules/dashboard/server.ts +1 -0
- package/src/modules/dashboard/services/sse-broadcaster.ts +29 -0
- package/src/modules/dashboard/ui/dashboard.html +640 -349
- package/src/modules/git-workspace/__tests__/branch-manager.test.ts +52 -0
- package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +77 -0
- package/src/modules/git-workspace/__tests__/git-exec.test.ts +26 -0
- package/src/modules/git-workspace/__tests__/merge-service.test.ts +19 -0
- package/src/modules/git-workspace/__tests__/pr-manager.test.ts +80 -0
- package/src/modules/git-workspace/__tests__/template-copy.test.ts +189 -0
- package/src/modules/git-workspace/__tests__/worktree-cleanup.test.ts +29 -2
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +64 -4
- package/src/modules/git-workspace/branch-manager.ts +24 -3
- package/src/modules/git-workspace/dependency-installer.ts +113 -0
- package/src/modules/git-workspace/git-exec.ts +3 -2
- package/src/modules/git-workspace/index.ts +10 -1
- package/src/modules/git-workspace/merge-service.ts +36 -2
- package/src/modules/git-workspace/pr-manager.ts +278 -0
- package/src/modules/git-workspace/template-copy.ts +302 -0
- package/src/modules/git-workspace/worktree-manager.ts +37 -11
- package/src/modules/planning/__tests__/planning-service.test.ts +3 -0
- package/src/modules/planning/__tests__/planning.service.test.ts +149 -0
- package/src/modules/planning/__tests__/project-detection.test.ts +7 -1
- package/src/modules/planning/planning.service.ts +16 -2
- package/src/modules/planning/project-detection.ts +4 -1
- package/src/modules/process-manager/__tests__/process-manager.test.ts +3 -0
- package/src/modules/process-manager/process-manager.ts +2 -1
- package/src/modules/task-execution/__tests__/executor.test.ts +496 -0
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
- package/src/modules/task-execution/executor.ts +163 -4
- package/src/modules/task-execution/phases/implementer-green.ts +22 -5
- package/src/modules/task-execution/phases/merge.ts +44 -2
- package/src/modules/task-execution/phases/tester-red.ts +22 -5
- package/src/modules/task-execution/phases/tester-verify.ts +22 -6
- package/src/modules/task-queue/dispatcher.ts +96 -3
- package/src/shared/__tests__/config.test.ts +30 -0
- package/src/shared/__tests__/events.test.ts +42 -16
- package/src/shared/__tests__/prerequisites.test.ts +176 -0
- package/src/shared/__tests__/shutdown-handler.test.ts +96 -0
- package/src/shared/config.ts +10 -0
- package/src/shared/events.ts +5 -0
- package/src/shared/memory-check.ts +2 -2
- package/src/shared/prerequisites.ts +190 -0
- package/src/shared/shutdown-handler.ts +12 -5
- package/src/shared/types.ts +25 -0
- package/templates/CLAUDE.md +122 -0
- package/templates/settings.json +117 -0
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +0 -5
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +0 -13
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +0 -10
- package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +0 -5
- 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
|
-
.
|
|
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
|
-
|
|
33
|
-
|
|
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.
|
|
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}
|
|
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
|
-
|
|
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 "
|
|
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
|
+
});
|