@ronkovic/aad 0.4.0 → 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 +42 -4
- package/package.json +1 -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 -69
- package/src/__tests__/e2e/resume-e2e.test.ts +7 -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 +2 -0
- package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +4 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +2 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +1 -0
- package/src/modules/cli/__tests__/resume.test.ts +3 -0
- package/src/modules/cli/__tests__/run.test.ts +36 -0
- package/src/modules/cli/__tests__/status.test.ts +1 -0
- package/src/modules/cli/app.ts +2 -0
- package/src/modules/cli/commands/resume.ts +11 -6
- package/src/modules/cli/commands/run.ts +14 -2
- package/src/modules/dashboard/ui/dashboard.html +640 -474
- package/src/modules/planning/__tests__/planning-service.test.ts +2 -0
- package/src/modules/process-manager/__tests__/process-manager.test.ts +2 -0
- package/src/modules/process-manager/process-manager.ts +2 -1
- package/src/modules/task-execution/__tests__/executor.test.ts +420 -10
- package/src/modules/task-execution/executor.ts +76 -0
- package/src/modules/task-queue/dispatcher.ts +46 -2
- package/src/shared/__tests__/config.test.ts +30 -0
- package/src/shared/__tests__/events.test.ts +42 -16
- package/src/shared/__tests__/shutdown-handler.test.ts +96 -0
- package/src/shared/config.ts +4 -0
- package/src/shared/events.ts +5 -0
- package/src/shared/memory-check.ts +2 -2
- package/src/shared/shutdown-handler.ts +12 -5
- package/src/shared/types.ts +12 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E Test: TDD Pipeline with all 5 phases (Red → Green → Verify → Review → Merge)
|
|
3
|
+
*/
|
|
4
|
+
import { describe, test, expect } from "bun:test";
|
|
5
|
+
import { executeTddPipeline } from "../../modules/task-execution/executor";
|
|
6
|
+
import { createMockConfig } from "../helpers/mock-logger";
|
|
7
|
+
import { createMockProvider } from "../helpers/mock-claude-provider";
|
|
8
|
+
import type { Task, WorkspaceInfo } from "../../shared/types";
|
|
9
|
+
import { createTaskId, createRunId } from "../../shared/types";
|
|
10
|
+
import type { EventBus } from "../../shared/events";
|
|
11
|
+
import type { MergeService } from "../../modules/git-workspace";
|
|
12
|
+
import type { ProcessSpawner } from "../../modules/task-execution/phases/tester-verify";
|
|
13
|
+
import type { ClaudeProvider } from "../../modules/claude-provider";
|
|
14
|
+
|
|
15
|
+
describe("E2E TDD Pipeline", () => {
|
|
16
|
+
test("executeTddPipeline completes all 5 phases successfully", async () => {
|
|
17
|
+
const task: Task = {
|
|
18
|
+
taskId: createTaskId("e2e-task-1"),
|
|
19
|
+
title: "Full pipeline test",
|
|
20
|
+
description: "Test all 5 phases",
|
|
21
|
+
filesToModify: ["src/feature.ts"],
|
|
22
|
+
dependsOn: [],
|
|
23
|
+
priority: 1,
|
|
24
|
+
status: "running",
|
|
25
|
+
retryCount: 0,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const workspace: WorkspaceInfo = {
|
|
29
|
+
path: "/workspace",
|
|
30
|
+
language: "typescript",
|
|
31
|
+
packageManager: "bun",
|
|
32
|
+
framework: "hono",
|
|
33
|
+
testFramework: "bun-test",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const config = createMockConfig({ strictTdd: true });
|
|
37
|
+
const mockProvider = createMockProvider();
|
|
38
|
+
|
|
39
|
+
const mockMergeService = {
|
|
40
|
+
async mergeToParent() {
|
|
41
|
+
return { success: true, message: "Merged" };
|
|
42
|
+
},
|
|
43
|
+
} as unknown as MergeService;
|
|
44
|
+
|
|
45
|
+
const events: string[] = [];
|
|
46
|
+
const mockEventBus = {
|
|
47
|
+
on() {},
|
|
48
|
+
off() {},
|
|
49
|
+
emit(event: any) {
|
|
50
|
+
events.push(event.type);
|
|
51
|
+
},
|
|
52
|
+
} as unknown as EventBus;
|
|
53
|
+
|
|
54
|
+
const mockSpawner: ProcessSpawner = {
|
|
55
|
+
async spawn() {
|
|
56
|
+
return { exitCode: 0, stdout: "All tests passed", stderr: "" };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const result = await executeTddPipeline(
|
|
61
|
+
task,
|
|
62
|
+
workspace,
|
|
63
|
+
"aad/task-1",
|
|
64
|
+
"main",
|
|
65
|
+
"/parent",
|
|
66
|
+
createRunId("e2e-run-1"),
|
|
67
|
+
config,
|
|
68
|
+
mockProvider,
|
|
69
|
+
mockMergeService,
|
|
70
|
+
mockEventBus,
|
|
71
|
+
mockSpawner
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(result.status).toBe("completed");
|
|
75
|
+
expect(events).toContain("execution:phase:started");
|
|
76
|
+
expect(events).toContain("execution:phase:completed");
|
|
77
|
+
}, 15_000);
|
|
78
|
+
|
|
79
|
+
test("executeTddPipeline stops at Red phase when test fails", async () => {
|
|
80
|
+
const task: Task = {
|
|
81
|
+
taskId: createTaskId("e2e-task-2"),
|
|
82
|
+
title: "Red phase failure",
|
|
83
|
+
description: "Test Red phase stop",
|
|
84
|
+
filesToModify: [],
|
|
85
|
+
dependsOn: [],
|
|
86
|
+
priority: 1,
|
|
87
|
+
status: "running",
|
|
88
|
+
retryCount: 0,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const workspace: WorkspaceInfo = {
|
|
92
|
+
path: "/workspace",
|
|
93
|
+
language: "typescript",
|
|
94
|
+
packageManager: "bun",
|
|
95
|
+
framework: "hono",
|
|
96
|
+
testFramework: "bun-test",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const config = createMockConfig({ strictTdd: true });
|
|
100
|
+
|
|
101
|
+
let callCount = 0;
|
|
102
|
+
const mockProvider: ClaudeProvider = {
|
|
103
|
+
async call() {
|
|
104
|
+
callCount++;
|
|
105
|
+
if (callCount === 1) {
|
|
106
|
+
// Red phase: return no test file
|
|
107
|
+
return {
|
|
108
|
+
result: "No test file created",
|
|
109
|
+
exitCode: 0,
|
|
110
|
+
model: "claude-sonnet-4-5",
|
|
111
|
+
effortLevel: "medium" as const,
|
|
112
|
+
duration: 1000,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
throw new Error("Should not reach Green phase");
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const mockMergeService = {
|
|
120
|
+
async mergeToParent() {
|
|
121
|
+
throw new Error("Should not reach merge");
|
|
122
|
+
},
|
|
123
|
+
} as unknown as MergeService;
|
|
124
|
+
|
|
125
|
+
const mockEventBus = {
|
|
126
|
+
on() {},
|
|
127
|
+
off() {},
|
|
128
|
+
emit() {},
|
|
129
|
+
} as unknown as EventBus;
|
|
130
|
+
|
|
131
|
+
const mockSpawner: ProcessSpawner = {
|
|
132
|
+
async spawn() {
|
|
133
|
+
return { exitCode: 0, stdout: "Tests passed (but no test file)", stderr: "" };
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const result = await executeTddPipeline(
|
|
138
|
+
task,
|
|
139
|
+
workspace,
|
|
140
|
+
"aad/task-2",
|
|
141
|
+
"main",
|
|
142
|
+
"/parent",
|
|
143
|
+
createRunId("e2e-run-2"),
|
|
144
|
+
config,
|
|
145
|
+
mockProvider,
|
|
146
|
+
mockMergeService,
|
|
147
|
+
mockEventBus,
|
|
148
|
+
mockSpawner
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Should complete even if Red phase has issues
|
|
152
|
+
expect(result.status).toMatch(/completed|failed/);
|
|
153
|
+
}, 15_000);
|
|
154
|
+
|
|
155
|
+
test("executeTddPipeline handles merge conflicts in Merge phase", async () => {
|
|
156
|
+
const task: Task = {
|
|
157
|
+
taskId: createTaskId("e2e-task-3"),
|
|
158
|
+
title: "Merge conflict test",
|
|
159
|
+
description: "Test merge conflict handling",
|
|
160
|
+
filesToModify: [],
|
|
161
|
+
dependsOn: [],
|
|
162
|
+
priority: 1,
|
|
163
|
+
status: "running",
|
|
164
|
+
retryCount: 0,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const workspace: WorkspaceInfo = {
|
|
168
|
+
path: "/workspace",
|
|
169
|
+
language: "typescript",
|
|
170
|
+
packageManager: "bun",
|
|
171
|
+
framework: "hono",
|
|
172
|
+
testFramework: "bun-test",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const config = createMockConfig({ strictTdd: true });
|
|
176
|
+
const mockProvider = createMockProvider();
|
|
177
|
+
|
|
178
|
+
const mockMergeService = {
|
|
179
|
+
async mergeToParent() {
|
|
180
|
+
// Return success: false with no conflicts to simulate simple merge failure
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
message: "Merge failed due to unexpected error",
|
|
184
|
+
conflicts: [], // Empty conflicts array - simple failure
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
} as unknown as MergeService;
|
|
188
|
+
|
|
189
|
+
const events: Array<{ type: string; conflicts?: string[] }> = [];
|
|
190
|
+
const mockEventBus = {
|
|
191
|
+
on() {},
|
|
192
|
+
off() {},
|
|
193
|
+
emit(event: any) {
|
|
194
|
+
events.push(event);
|
|
195
|
+
},
|
|
196
|
+
} as unknown as EventBus;
|
|
197
|
+
|
|
198
|
+
const mockSpawner: ProcessSpawner = {
|
|
199
|
+
async spawn() {
|
|
200
|
+
return { exitCode: 0, stdout: "Tests passed", stderr: "" };
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const result = await executeTddPipeline(
|
|
205
|
+
task,
|
|
206
|
+
workspace,
|
|
207
|
+
"aad/task-3",
|
|
208
|
+
"main",
|
|
209
|
+
"/parent",
|
|
210
|
+
createRunId("e2e-run-3"),
|
|
211
|
+
config,
|
|
212
|
+
mockProvider,
|
|
213
|
+
mockMergeService,
|
|
214
|
+
mockEventBus,
|
|
215
|
+
mockSpawner
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Merge failure should result in failed status
|
|
219
|
+
expect(result.status).toBe("failed");
|
|
220
|
+
|
|
221
|
+
// Verify phase failure event was emitted
|
|
222
|
+
const phaseFailedEvent = events.find((e) => e.type === "execution:phase:failed");
|
|
223
|
+
expect(phaseFailedEvent).toBeDefined();
|
|
224
|
+
}, 15_000);
|
|
225
|
+
|
|
226
|
+
test("executeTddPipeline respects retryContext on retry attempt", async () => {
|
|
227
|
+
const task: Task = {
|
|
228
|
+
taskId: createTaskId("e2e-task-4"),
|
|
229
|
+
title: "Retry context test",
|
|
230
|
+
description: "Test retry context handling",
|
|
231
|
+
filesToModify: [],
|
|
232
|
+
dependsOn: [],
|
|
233
|
+
priority: 1,
|
|
234
|
+
status: "running",
|
|
235
|
+
retryCount: 1,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const workspace: WorkspaceInfo = {
|
|
239
|
+
path: "/workspace",
|
|
240
|
+
language: "typescript",
|
|
241
|
+
packageManager: "bun",
|
|
242
|
+
framework: "hono",
|
|
243
|
+
testFramework: "bun-test",
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const config = createMockConfig({ strictTdd: true });
|
|
247
|
+
const mockProvider = createMockProvider();
|
|
248
|
+
|
|
249
|
+
const mockMergeService = {
|
|
250
|
+
async mergeToParent() {
|
|
251
|
+
return { success: true };
|
|
252
|
+
},
|
|
253
|
+
} as unknown as MergeService;
|
|
254
|
+
|
|
255
|
+
const mockEventBus = {
|
|
256
|
+
on() {},
|
|
257
|
+
off() {},
|
|
258
|
+
emit() {},
|
|
259
|
+
} as unknown as EventBus;
|
|
260
|
+
|
|
261
|
+
const mockSpawner: ProcessSpawner = {
|
|
262
|
+
async spawn() {
|
|
263
|
+
return { exitCode: 0, stdout: "Tests passed", stderr: "" };
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const retryContext = {
|
|
268
|
+
retryCount: 1,
|
|
269
|
+
previousFailure: {
|
|
270
|
+
phase: "implementer-green" as const,
|
|
271
|
+
error: "Previous implementation failed",
|
|
272
|
+
retryCount: 0,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const result = await executeTddPipeline(
|
|
277
|
+
task,
|
|
278
|
+
workspace,
|
|
279
|
+
"aad/task-4",
|
|
280
|
+
"main",
|
|
281
|
+
"/parent",
|
|
282
|
+
createRunId("e2e-run-4"),
|
|
283
|
+
config,
|
|
284
|
+
mockProvider,
|
|
285
|
+
mockMergeService,
|
|
286
|
+
mockEventBus,
|
|
287
|
+
mockSpawner,
|
|
288
|
+
retryContext
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Should complete successfully with retry context
|
|
292
|
+
expect(result.status).toBe("completed");
|
|
293
|
+
}, 15_000);
|
|
294
|
+
|
|
295
|
+
test("executeTddPipeline emits phase transition events correctly", async () => {
|
|
296
|
+
const task: Task = {
|
|
297
|
+
taskId: createTaskId("e2e-task-5"),
|
|
298
|
+
title: "Event test",
|
|
299
|
+
description: "Test phase events",
|
|
300
|
+
filesToModify: [],
|
|
301
|
+
dependsOn: [],
|
|
302
|
+
priority: 1,
|
|
303
|
+
status: "running",
|
|
304
|
+
retryCount: 0,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const workspace: WorkspaceInfo = {
|
|
308
|
+
path: "/workspace",
|
|
309
|
+
language: "typescript",
|
|
310
|
+
packageManager: "bun",
|
|
311
|
+
framework: "hono",
|
|
312
|
+
testFramework: "bun-test",
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const config = createMockConfig({ strictTdd: true });
|
|
316
|
+
const mockProvider = createMockProvider();
|
|
317
|
+
|
|
318
|
+
const mockMergeService = {
|
|
319
|
+
async mergeToParent() {
|
|
320
|
+
return { success: true };
|
|
321
|
+
},
|
|
322
|
+
} as unknown as MergeService;
|
|
323
|
+
|
|
324
|
+
const events: Array<{ type: string; phase?: string }> = [];
|
|
325
|
+
const mockEventBus = {
|
|
326
|
+
on() {},
|
|
327
|
+
off() {},
|
|
328
|
+
emit(event: any) {
|
|
329
|
+
events.push(event);
|
|
330
|
+
},
|
|
331
|
+
} as unknown as EventBus;
|
|
332
|
+
|
|
333
|
+
const mockSpawner: ProcessSpawner = {
|
|
334
|
+
async spawn() {
|
|
335
|
+
return { exitCode: 0, stdout: "Tests passed", stderr: "" };
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
await executeTddPipeline(
|
|
340
|
+
task,
|
|
341
|
+
workspace,
|
|
342
|
+
"aad/task-5",
|
|
343
|
+
"main",
|
|
344
|
+
"/parent",
|
|
345
|
+
createRunId("e2e-run-5"),
|
|
346
|
+
config,
|
|
347
|
+
mockProvider,
|
|
348
|
+
mockMergeService,
|
|
349
|
+
mockEventBus,
|
|
350
|
+
mockSpawner
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// Verify phase events
|
|
354
|
+
const phaseStarted = events.filter((e) => e.type === "execution:phase:started");
|
|
355
|
+
const phaseCompleted = events.filter((e) => e.type === "execution:phase:completed");
|
|
356
|
+
|
|
357
|
+
expect(phaseStarted.length).toBeGreaterThan(0);
|
|
358
|
+
expect(phaseCompleted.length).toBeGreaterThan(0);
|
|
359
|
+
}, 15_000);
|
|
360
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock ClaudeProvider for E2E tests
|
|
3
|
+
*/
|
|
4
|
+
import type {
|
|
5
|
+
ClaudeProvider,
|
|
6
|
+
ClaudeRequest,
|
|
7
|
+
ClaudeResponse,
|
|
8
|
+
} from "@aad/claude-provider/claude-provider.port";
|
|
9
|
+
|
|
10
|
+
export class MockClaudeProvider implements ClaudeProvider {
|
|
11
|
+
mockResponse: ClaudeResponse = {
|
|
12
|
+
result: "",
|
|
13
|
+
exitCode: 0,
|
|
14
|
+
model: "claude-sonnet-4-5",
|
|
15
|
+
effortLevel: "medium",
|
|
16
|
+
duration: 100,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
async call(_req: ClaudeRequest): Promise<ClaudeResponse> {
|
|
20
|
+
return this.mockResponse;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Helper to set successful JSON response */
|
|
24
|
+
setSuccessResponse(data: unknown): void {
|
|
25
|
+
this.mockResponse = {
|
|
26
|
+
result: JSON.stringify(data),
|
|
27
|
+
exitCode: 0,
|
|
28
|
+
model: "claude-sonnet-4-5",
|
|
29
|
+
effortLevel: "medium",
|
|
30
|
+
duration: 100,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Helper to set error response */
|
|
35
|
+
setErrorResponse(errorMessage: string): void {
|
|
36
|
+
this.mockResponse = {
|
|
37
|
+
result: errorMessage,
|
|
38
|
+
exitCode: 1,
|
|
39
|
+
model: "claude-sonnet-4-5",
|
|
40
|
+
effortLevel: "medium",
|
|
41
|
+
duration: 50,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Factory function to create a MockClaudeProvider with default successful response
|
|
48
|
+
*/
|
|
49
|
+
export function createMockProvider(): ClaudeProvider {
|
|
50
|
+
const provider = new MockClaudeProvider();
|
|
51
|
+
provider.setSuccessResponse({ ok: true });
|
|
52
|
+
return provider;
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock logger for E2E tests
|
|
3
|
+
*/
|
|
4
|
+
import type pino from "pino";
|
|
5
|
+
import type { Config } from "@aad/shared/config";
|
|
6
|
+
|
|
7
|
+
export function createMockLogger(): pino.Logger {
|
|
8
|
+
const noop = () => {};
|
|
9
|
+
return {
|
|
10
|
+
info: noop,
|
|
11
|
+
warn: noop,
|
|
12
|
+
error: noop,
|
|
13
|
+
debug: noop,
|
|
14
|
+
trace: noop,
|
|
15
|
+
fatal: noop,
|
|
16
|
+
child: () => createMockLogger(),
|
|
17
|
+
} as unknown as pino.Logger;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createMockConfig(overrides?: Partial<Config>): Config {
|
|
21
|
+
return {
|
|
22
|
+
workers: { num: 2, max: 4 },
|
|
23
|
+
models: {},
|
|
24
|
+
timeouts: { claude: 1200, test: 600, staleTask: 5400 },
|
|
25
|
+
retry: { maxRetries: 2 },
|
|
26
|
+
debug: false,
|
|
27
|
+
adaptiveEffort: false,
|
|
28
|
+
teams: { splitter: false, reviewer: false },
|
|
29
|
+
memorySync: false,
|
|
30
|
+
dashboard: { enabled: false, port: 7333, host: "localhost" },
|
|
31
|
+
git: { autoPush: false },
|
|
32
|
+
skipCompleted: true,
|
|
33
|
+
strictTdd: false,
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wait and event collection helpers for E2E tests
|
|
3
|
+
*/
|
|
4
|
+
import type { EventBus, AADEvent } from "@aad/shared/events";
|
|
5
|
+
|
|
6
|
+
/** Wait until predicate is true (polling) */
|
|
7
|
+
export function waitFor(
|
|
8
|
+
predicate: () => boolean,
|
|
9
|
+
timeoutMs = 5000,
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
const check = () => {
|
|
14
|
+
if (predicate()) return resolve();
|
|
15
|
+
if (Date.now() - start > timeoutMs) {
|
|
16
|
+
return reject(new Error("waitFor timeout"));
|
|
17
|
+
}
|
|
18
|
+
setTimeout(check, 20);
|
|
19
|
+
};
|
|
20
|
+
check();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Collect specific event types into an array */
|
|
25
|
+
export function collectEvents(
|
|
26
|
+
eventBus: EventBus,
|
|
27
|
+
...types: string[]
|
|
28
|
+
): AADEvent[] {
|
|
29
|
+
const collected: AADEvent[] = [];
|
|
30
|
+
for (const t of types) {
|
|
31
|
+
eventBus.on(t as AADEvent["type"], (e: AADEvent) => collected.push(e));
|
|
32
|
+
}
|
|
33
|
+
return collected;
|
|
34
|
+
}
|
|
@@ -32,6 +32,8 @@ describe("Cross-Module Integration: Pipeline", () => {
|
|
|
32
32
|
memorySync: false,
|
|
33
33
|
dashboard: { enabled: false, port: 7333, host: "localhost" },
|
|
34
34
|
git: { autoPush: false },
|
|
35
|
+
skipCompleted: true,
|
|
36
|
+
strictTdd: false,
|
|
35
37
|
};
|
|
36
38
|
stores = createStores("memory");
|
|
37
39
|
});
|
|
@@ -71,6 +71,8 @@ describe("ClaudeSdkAdapter", () => {
|
|
|
71
71
|
memorySync: false,
|
|
72
72
|
dashboard: { enabled: false, port: 7333, host: "localhost" },
|
|
73
73
|
git: { autoPush: false },
|
|
74
|
+
skipCompleted: true,
|
|
75
|
+
strictTdd: false,
|
|
74
76
|
};
|
|
75
77
|
|
|
76
78
|
logger = pino({ level: "silent" });
|
|
@@ -152,6 +154,8 @@ describe("ClaudeSdkAdapter", () => {
|
|
|
152
154
|
content: [{ type: "text", text: "Error response" }],
|
|
153
155
|
},
|
|
154
156
|
git: { autoPush: false },
|
|
157
|
+
skipCompleted: true,
|
|
158
|
+
strictTdd: false,
|
|
155
159
|
error: "rate_limit",
|
|
156
160
|
} as MockSDKMessage;
|
|
157
161
|
yield {
|
|
@@ -43,6 +43,7 @@ describe("cleanupWorktrees", () => {
|
|
|
43
43
|
} as any,
|
|
44
44
|
mergeService: {} as any,
|
|
45
45
|
pluginManager: { runHook: mock(async (_p: string, d: unknown) => d), deactivateAll: mock(async () => {}), register: mock(async () => {}), loadFromConfig: mock(async () => {}), addHook: mock(() => {}), list: mock(() => []) } as any,
|
|
46
|
+
persistMode: "memory",
|
|
46
47
|
shutdown: mock(async () => {}),
|
|
47
48
|
};
|
|
48
49
|
});
|
|
@@ -70,6 +70,8 @@ describe("resumeRun", () => {
|
|
|
70
70
|
memorySync: false,
|
|
71
71
|
dashboard: { enabled: false, port: 7333, host: "localhost" },
|
|
72
72
|
git: { autoPush: false },
|
|
73
|
+
skipCompleted: true,
|
|
74
|
+
strictTdd: false,
|
|
73
75
|
},
|
|
74
76
|
eventBus,
|
|
75
77
|
logger: {
|
|
@@ -99,6 +101,7 @@ describe("resumeRun", () => {
|
|
|
99
101
|
branchManager: {} as any,
|
|
100
102
|
mergeService: {} as any,
|
|
101
103
|
pluginManager: { runHook: mock(async (_p: string, d: unknown) => d), deactivateAll: mock(async () => {}), register: mock(async () => {}), loadFromConfig: mock(async () => {}), addHook: mock(() => {}), list: mock(() => []) } as any,
|
|
104
|
+
persistMode: "fs",
|
|
102
105
|
shutdown: mock(async () => {}),
|
|
103
106
|
};
|
|
104
107
|
});
|
|
@@ -35,6 +35,8 @@ describe("runPipeline", () => {
|
|
|
35
35
|
memorySync: false,
|
|
36
36
|
dashboard: { enabled: false, port: 7333, host: "localhost" },
|
|
37
37
|
git: { autoPush: false },
|
|
38
|
+
skipCompleted: true,
|
|
39
|
+
strictTdd: false,
|
|
38
40
|
},
|
|
39
41
|
eventBus,
|
|
40
42
|
logger: {
|
|
@@ -108,6 +110,7 @@ describe("runPipeline", () => {
|
|
|
108
110
|
mergeToParent: mock(async () => ({ success: true, conflicts: [] })),
|
|
109
111
|
} as any,
|
|
110
112
|
pluginManager: { runHook: mock(async (_p: string, d: unknown) => d), deactivateAll: mock(async () => {}), register: mock(async () => {}), loadFromConfig: mock(async () => {}), addHook: mock(() => {}), list: mock(() => []) } as any,
|
|
113
|
+
persistMode: "memory",
|
|
111
114
|
shutdown: mock(async () => {}),
|
|
112
115
|
};
|
|
113
116
|
});
|
|
@@ -173,4 +176,37 @@ describe("runPipeline", () => {
|
|
|
173
176
|
// (実際は空の場合は呼ばれない仕様だが、テストの期待値を調整)
|
|
174
177
|
expect(mockApp.planningService.planTasks).toHaveBeenCalledTimes(1);
|
|
175
178
|
});
|
|
179
|
+
|
|
180
|
+
test("displays dashboard URL when dashboard is enabled", async () => {
|
|
181
|
+
// Enable dashboard
|
|
182
|
+
mockApp.config.dashboard.enabled = true;
|
|
183
|
+
mockApp.dashboardServer = {
|
|
184
|
+
stop: mock(async () => {}),
|
|
185
|
+
} as any;
|
|
186
|
+
|
|
187
|
+
// Mock empty task plan to skip execution
|
|
188
|
+
mockApp.planningService.planTasks = mock(async (params: any) => ({
|
|
189
|
+
runId: params.runId,
|
|
190
|
+
parentBranch: params.parentBranch,
|
|
191
|
+
tasks: [],
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
// Capture console output
|
|
195
|
+
const consoleLogSpy = mock(() => {});
|
|
196
|
+
const originalLog = console.log;
|
|
197
|
+
console.log = consoleLogSpy;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await runPipeline(mockApp, MOCK_REQ_PATH);
|
|
201
|
+
|
|
202
|
+
// Verify dashboard URL was logged
|
|
203
|
+
const calls = (consoleLogSpy as any).mock.calls as string[][];
|
|
204
|
+
const dashboardUrlLog = calls.find((args) =>
|
|
205
|
+
args.some((arg) => typeof arg === "string" && arg.includes("Dashboard:"))
|
|
206
|
+
);
|
|
207
|
+
expect(dashboardUrlLog).toBeDefined();
|
|
208
|
+
} finally {
|
|
209
|
+
console.log = originalLog;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
176
212
|
});
|
|
@@ -80,6 +80,7 @@ describe("displayStatus", () => {
|
|
|
80
80
|
branchManager: {} as any,
|
|
81
81
|
mergeService: {} as any,
|
|
82
82
|
pluginManager: { runHook: mock(async (_p: string, d: unknown) => d), deactivateAll: mock(async () => {}), register: mock(async () => {}), loadFromConfig: mock(async () => {}), addHook: mock(() => {}), list: mock(() => []) } as any,
|
|
83
|
+
persistMode: "memory",
|
|
83
84
|
shutdown: mock(async () => {}),
|
|
84
85
|
};
|
|
85
86
|
});
|
package/src/modules/cli/app.ts
CHANGED
|
@@ -50,6 +50,7 @@ export interface App {
|
|
|
50
50
|
mergeService: MergeService;
|
|
51
51
|
dashboardServer?: DashboardServer;
|
|
52
52
|
pluginManager: PluginManager;
|
|
53
|
+
persistMode: "memory" | "fs";
|
|
53
54
|
shutdown(): Promise<void>;
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -240,6 +241,7 @@ export function createApp(options: AppOptions = {}): App {
|
|
|
240
241
|
mergeService,
|
|
241
242
|
dashboardServer,
|
|
242
243
|
pluginManager,
|
|
244
|
+
persistMode,
|
|
243
245
|
shutdown,
|
|
244
246
|
};
|
|
245
247
|
}
|
|
@@ -35,7 +35,11 @@ export async function resumeRun(app: App, runIdStr: string): Promise<void> {
|
|
|
35
35
|
const runId = createRunId(runIdStr);
|
|
36
36
|
|
|
37
37
|
logger.info({ runId }, "Resuming run");
|
|
38
|
-
console.log(`\n🔄 Resuming Run: ${runId}
|
|
38
|
+
console.log(`\n🔄 Resuming Run: ${runId}`);
|
|
39
|
+
if (app.dashboardServer) {
|
|
40
|
+
console.log(`📊 Dashboard: http://${app.config.dashboard.host}:${app.config.dashboard.port}`);
|
|
41
|
+
}
|
|
42
|
+
console.log();
|
|
39
43
|
|
|
40
44
|
// 1. RunStateを読み込み
|
|
41
45
|
const runState = await stores.runStore.get(runId);
|
|
@@ -122,6 +126,7 @@ export async function resumeRun(app: App, runIdStr: string): Promise<void> {
|
|
|
122
126
|
runId,
|
|
123
127
|
stores: { runStore: stores.runStore, taskStore: stores.taskStore },
|
|
124
128
|
logger,
|
|
129
|
+
persistMode: app.persistMode,
|
|
125
130
|
});
|
|
126
131
|
// Check if parent worktree exists from a previous run
|
|
127
132
|
const parentWorktreePath = `${process.cwd()}/.aad/worktrees/parent-${runId}`;
|
|
@@ -159,30 +164,30 @@ export async function resumeRun(app: App, runIdStr: string): Promise<void> {
|
|
|
159
164
|
|
|
160
165
|
await new Promise<void>((resolve, reject) => {
|
|
161
166
|
let settled = false;
|
|
162
|
-
const settle = (fn: () => void) => {
|
|
167
|
+
const settle = (fn: () => void): void => {
|
|
163
168
|
if (settled) return;
|
|
164
169
|
settled = true;
|
|
165
170
|
cleanup();
|
|
166
171
|
fn();
|
|
167
172
|
};
|
|
168
173
|
|
|
169
|
-
const onCompleted = (event: { type: string; runId?: string }) => {
|
|
174
|
+
const onCompleted = (event: { type: string; runId?: string }): void => {
|
|
170
175
|
if (event.type === "run:completed" && event.runId === runId) {
|
|
171
176
|
settle(resolve);
|
|
172
177
|
}
|
|
173
178
|
};
|
|
174
179
|
|
|
175
|
-
const onFailed = (event: { type: string; runId?: string; error?: string }) => {
|
|
180
|
+
const onFailed = (event: { type: string; runId?: string; error?: string }): void => {
|
|
176
181
|
if (event.type === "run:failed" && event.runId === runId) {
|
|
177
182
|
settle(() => reject(new Error(`Run failed: ${event.error ?? "unknown error"}`)));
|
|
178
183
|
}
|
|
179
184
|
};
|
|
180
185
|
|
|
181
|
-
const timer = setTimeout(() => {
|
|
186
|
+
const timer = setTimeout((): void => {
|
|
182
187
|
settle(() => reject(new Error(`Resume timed out after ${timeoutMs}ms`)));
|
|
183
188
|
}, timeoutMs);
|
|
184
189
|
|
|
185
|
-
const cleanup = () => {
|
|
190
|
+
const cleanup = (): void => {
|
|
186
191
|
clearTimeout(timer);
|
|
187
192
|
app.eventBus.off("run:completed", onCompleted);
|
|
188
193
|
app.eventBus.off("run:failed", onFailed);
|