@ronkovic/aad 0.4.0 → 0.5.1

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 (39) hide show
  1. package/README.md +42 -4
  2. package/package.json +1 -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 -69
  6. package/src/__tests__/e2e/resume-e2e.test.ts +7 -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 +2 -0
  15. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +79 -0
  16. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +2 -0
  17. package/src/modules/cli/__tests__/cleanup.test.ts +1 -0
  18. package/src/modules/cli/__tests__/resume.test.ts +3 -0
  19. package/src/modules/cli/__tests__/run.test.ts +36 -0
  20. package/src/modules/cli/__tests__/status.test.ts +1 -0
  21. package/src/modules/cli/app.ts +2 -0
  22. package/src/modules/cli/commands/resume.ts +11 -6
  23. package/src/modules/cli/commands/run.ts +14 -2
  24. package/src/modules/dashboard/ui/dashboard.html +640 -474
  25. package/src/modules/planning/__tests__/planning-service.test.ts +2 -0
  26. package/src/modules/process-manager/__tests__/process-manager.test.ts +2 -0
  27. package/src/modules/process-manager/process-manager.ts +2 -1
  28. package/src/modules/task-execution/__tests__/executor.test.ts +420 -10
  29. package/src/modules/task-execution/executor.ts +76 -0
  30. package/src/modules/task-queue/dispatcher.ts +46 -2
  31. package/src/shared/__tests__/config.test.ts +30 -0
  32. package/src/shared/__tests__/events.test.ts +42 -16
  33. package/src/shared/__tests__/shutdown-handler.test.ts +96 -0
  34. package/src/shared/config.ts +4 -0
  35. package/src/shared/events.ts +5 -0
  36. package/src/shared/memory-check.ts +2 -2
  37. package/src/shared/shutdown-handler.ts +12 -5
  38. package/src/shared/types.ts +12 -0
  39. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +0 -127
@@ -0,0 +1,285 @@
1
+ /**
2
+ * E2E Test: Task retry mechanism with retryContext propagation
3
+ */
4
+ import { describe, test, expect, beforeEach } from "bun:test";
5
+ import { EventBus } from "@aad/shared/events";
6
+ import { Dispatcher } from "@aad/task-queue";
7
+ import { createStores, type Stores } from "@aad/persistence";
8
+ import {
9
+ createRunId,
10
+ createTaskId,
11
+ createWorkerId,
12
+ type Task,
13
+ } from "@aad/shared/types";
14
+ import {
15
+ MockClaudeProvider,
16
+ createMockLogger,
17
+ createMockConfig,
18
+ waitFor,
19
+ collectEvents,
20
+ } from "../helpers";
21
+ import { PlanningService, type FileChecker } from "@aad/planning";
22
+
23
+ const mockFileChecker: FileChecker = {
24
+ exists: async () => false,
25
+ readText: async () => "",
26
+ glob: async () => [],
27
+ };
28
+
29
+ describe("E2E Task Retry", () => {
30
+ let eventBus: EventBus;
31
+ let stores: Stores;
32
+ let dispatcher: Dispatcher;
33
+ let mockProvider: MockClaudeProvider;
34
+
35
+ beforeEach(() => {
36
+ eventBus = new EventBus();
37
+ stores = createStores("memory");
38
+ mockProvider = new MockClaudeProvider();
39
+
40
+ dispatcher = new Dispatcher({
41
+ taskStore: stores.taskStore,
42
+ workerStore: stores.workerStore,
43
+ runStore: stores.runStore,
44
+ eventBus,
45
+ config: { maxRetries: 2 },
46
+ logger: createMockLogger(),
47
+ });
48
+ });
49
+
50
+ test("retry increments retryCount and preserves previousFailure", async () => {
51
+ const config = createMockConfig();
52
+ const runId = createRunId("retry-run-1");
53
+
54
+ // Create 1-task plan
55
+ mockProvider.setSuccessResponse({
56
+ run_id: "retry-run-1",
57
+ parent_branch: "main",
58
+ tasks: [
59
+ {
60
+ task_id: "retry-task-1",
61
+ title: "Retry Task",
62
+ description: "Will fail then retry",
63
+ files_to_modify: ["a.ts"],
64
+ depends_on: [],
65
+ priority: 1,
66
+ },
67
+ ],
68
+ });
69
+
70
+ const planningService = new PlanningService(
71
+ mockProvider,
72
+ eventBus,
73
+ config,
74
+ createMockLogger(),
75
+ { fileChecker: mockFileChecker }
76
+ );
77
+
78
+ const taskPlan = await planningService.planTasks({
79
+ runId,
80
+ parentBranch: "main",
81
+ requirementsPath: "/fake/req.md",
82
+ targetDocsDir: "/fake/docs",
83
+ });
84
+
85
+ await dispatcher.initialize(taskPlan);
86
+
87
+ const events = collectEvents(eventBus, "task:retry");
88
+
89
+ // Simulate failure
90
+ await dispatcher.handleTaskFailed(
91
+ createTaskId("retry-task-1"),
92
+ "Test failure"
93
+ );
94
+
95
+ // Verify retry event
96
+ await waitFor(() => events.length > 0, 1000);
97
+ expect(events).toHaveLength(1);
98
+ expect(events[0]?.type).toBe("task:retry");
99
+ if (events[0]?.type === "task:retry") {
100
+ expect(events[0]?.retryCount).toBe(1);
101
+ }
102
+
103
+ // Verify task state
104
+ const task = await stores.taskStore.get(createTaskId("retry-task-1"));
105
+ expect(task?.status).toBe("pending");
106
+ expect(task?.retryCount).toBe(1);
107
+ expect(task?.failureReason).toBe("Test failure");
108
+ expect(task?.previousFailure).toBeDefined();
109
+ expect(task?.previousFailure?.error).toBe("Test failure");
110
+ }, 15_000);
111
+
112
+ test("retry reaches maxRetries and marks task as failed", async () => {
113
+ const config = createMockConfig();
114
+ const runId = createRunId("retry-run-2");
115
+
116
+ mockProvider.setSuccessResponse({
117
+ run_id: "retry-run-2",
118
+ parent_branch: "main",
119
+ tasks: [
120
+ {
121
+ task_id: "fail-task",
122
+ title: "Fail Task",
123
+ description: "Will fail permanently",
124
+ files_to_modify: [],
125
+ depends_on: [],
126
+ priority: 1,
127
+ },
128
+ ],
129
+ });
130
+
131
+ const planningService = new PlanningService(
132
+ mockProvider,
133
+ eventBus,
134
+ config,
135
+ createMockLogger(),
136
+ { fileChecker: mockFileChecker }
137
+ );
138
+
139
+ const taskPlan = await planningService.planTasks({
140
+ runId,
141
+ parentBranch: "main",
142
+ requirementsPath: "/fake/req.md",
143
+ targetDocsDir: "/fake/docs",
144
+ });
145
+
146
+ await dispatcher.initialize(taskPlan);
147
+
148
+ const events = collectEvents(eventBus, "task:failed", "task:retry");
149
+
150
+ // Fail 3 times (maxRetries = 2)
151
+ await dispatcher.handleTaskFailed(createTaskId("fail-task"), "Error 1");
152
+ await dispatcher.handleTaskFailed(createTaskId("fail-task"), "Error 2");
153
+ await dispatcher.handleTaskFailed(createTaskId("fail-task"), "Error 3");
154
+
155
+ // Wait for retry events (should be 2)
156
+ await waitFor(() => events.filter((e) => e.type === "task:retry").length >= 2, 1000);
157
+
158
+ const retryEvents = events.filter((e) => e.type === "task:retry");
159
+
160
+ expect(retryEvents).toHaveLength(2);
161
+
162
+ // Verify task is permanently failed (no task:failed event, check store directly)
163
+ const task = await stores.taskStore.get(createTaskId("fail-task"));
164
+ expect(task?.status).toBe("failed");
165
+ expect(task?.retryCount).toBe(2);
166
+ expect(task?.endTime).toBeDefined();
167
+ }, 15_000);
168
+
169
+ test("retryContext propagates through task execution (mock)", async () => {
170
+ // This test verifies previousFailure structure is saved
171
+ const task: Task = {
172
+ taskId: createTaskId("context-task"),
173
+ title: "Context Task",
174
+ description: "Test context",
175
+ filesToModify: [],
176
+ dependsOn: [],
177
+ priority: 1,
178
+ status: "failed",
179
+ retryCount: 1,
180
+ failureReason: "Phase Red failed",
181
+ previousFailure: {
182
+ phase: "Red",
183
+ error: "Phase Red failed",
184
+ testOutput: "test output here",
185
+ retryCount: 1,
186
+ },
187
+ };
188
+
189
+ await stores.taskStore.save(task);
190
+
191
+ const saved = await stores.taskStore.get(createTaskId("context-task"));
192
+ expect(saved?.previousFailure).toBeDefined();
193
+ expect(saved?.previousFailure?.phase).toBe("Red");
194
+ expect(saved?.previousFailure?.testOutput).toBe("test output here");
195
+ }, 15_000);
196
+
197
+ test("retry resets task status from running to pending", async () => {
198
+ const task: Task = {
199
+ taskId: createTaskId("reset-task"),
200
+ title: "Reset Task",
201
+ description: "Running task",
202
+ filesToModify: [],
203
+ dependsOn: [],
204
+ priority: 1,
205
+ status: "running",
206
+ retryCount: 0,
207
+ workerId: createWorkerId("worker-1"),
208
+ };
209
+
210
+ await stores.taskStore.save(task);
211
+
212
+ const dispatcher = new Dispatcher({
213
+ taskStore: stores.taskStore,
214
+ workerStore: stores.workerStore,
215
+ runStore: stores.runStore,
216
+ eventBus,
217
+ config: { maxRetries: 1 },
218
+ logger: createMockLogger(),
219
+ });
220
+
221
+ // Initialize taskMap (dispatcher needs this)
222
+ const runId = createRunId("reset-run");
223
+ await dispatcher.initialize({
224
+ runId,
225
+ parentBranch: "main",
226
+ tasks: [task],
227
+ });
228
+
229
+ // Fail task → should reset to pending
230
+ await dispatcher.handleTaskFailed(createTaskId("reset-task"), "Test error");
231
+
232
+ const saved = await stores.taskStore.get(createTaskId("reset-task"));
233
+ expect(saved?.status).toBe("pending");
234
+ expect(saved?.retryCount).toBe(1);
235
+ expect(saved?.workerId).toBeUndefined();
236
+ }, 15_000);
237
+
238
+ test("retry emits task:retry event with correct retryCount", async () => {
239
+ const task: Task = {
240
+ taskId: createTaskId("emit-task"),
241
+ title: "Emit Task",
242
+ description: "Event test",
243
+ filesToModify: [],
244
+ dependsOn: [],
245
+ priority: 1,
246
+ status: "running",
247
+ retryCount: 0,
248
+ };
249
+
250
+ await stores.taskStore.save(task);
251
+
252
+ const dispatcher = new Dispatcher({
253
+ taskStore: stores.taskStore,
254
+ workerStore: stores.workerStore,
255
+ runStore: stores.runStore,
256
+ eventBus,
257
+ config: { maxRetries: 3, staleTaskCheckInterval: 60000, staleTaskThreshold: 5400000 },
258
+ logger: createMockLogger(),
259
+ getIdleWorkerIds: () => [],
260
+ });
261
+
262
+ const runId = createRunId("emit-run");
263
+ await dispatcher.initialize({
264
+ runId,
265
+ parentBranch: "main",
266
+ tasks: [task],
267
+ });
268
+
269
+ const events = collectEvents(eventBus, "task:retry");
270
+
271
+ // Fail twice
272
+ await dispatcher.handleTaskFailed(createTaskId("emit-task"), "Error 1");
273
+ await dispatcher.handleTaskFailed(createTaskId("emit-task"), "Error 2");
274
+
275
+ await waitFor(() => events.length >= 2, 1000);
276
+
277
+ expect(events).toHaveLength(2);
278
+ if (events[0]?.type === "task:retry") {
279
+ expect(events[0]?.retryCount).toBe(1);
280
+ }
281
+ if (events[1]?.type === "task:retry") {
282
+ expect(events[1]?.retryCount).toBe(2);
283
+ }
284
+ }, 15_000);
285
+ });
@@ -0,0 +1,227 @@
1
+ /**
2
+ * E2E Test: Status command (displayStatus with stores)
3
+ */
4
+ import { describe, test, expect, beforeEach } from "bun:test";
5
+ import { createStores, type Stores } from "@aad/persistence";
6
+ import { EventBus } from "@aad/shared/events";
7
+ import { displayStatus, type App } from "@aad/cli";
8
+ import {
9
+ createRunId,
10
+ createTaskId,
11
+ type RunState,
12
+ type Task,
13
+ } from "@aad/shared/types";
14
+ import { createMockLogger } from "../helpers";
15
+
16
+ describe("E2E Status Command", () => {
17
+ let stores: Stores;
18
+ let eventBus: EventBus;
19
+
20
+ beforeEach(() => {
21
+ // Use in-memory stores for status tests (FSRunStore has single-file limitation)
22
+ stores = createStores("memory");
23
+ eventBus = new EventBus();
24
+ });
25
+
26
+ function createApp(): App {
27
+ return {
28
+ stores,
29
+ logger: createMockLogger(),
30
+ eventBus,
31
+ } as App;
32
+ }
33
+
34
+ test("displayStatus reads from FS store and displays progress", async () => {
35
+ const runId = createRunId("status-run-1");
36
+
37
+ // Create run state
38
+ const runState: RunState = {
39
+ runId,
40
+ parentBranch: "main",
41
+ totalTasks: 3,
42
+ pending: 1,
43
+ running: 1,
44
+ completed: 1,
45
+ failed: 0,
46
+ startTime: new Date().toISOString(),
47
+ };
48
+ await stores.runStore.save(runState);
49
+
50
+ // Create tasks
51
+ const tasks: Task[] = [
52
+ {
53
+ taskId: createTaskId("task-1"),
54
+ title: "Task 1",
55
+ description: "First task",
56
+ filesToModify: ["a.ts"],
57
+ dependsOn: [],
58
+ priority: 1,
59
+ status: "completed",
60
+ retryCount: 0,
61
+ },
62
+ {
63
+ taskId: createTaskId("task-2"),
64
+ title: "Task 2",
65
+ description: "Second task",
66
+ filesToModify: ["b.ts"],
67
+ dependsOn: [],
68
+ priority: 2,
69
+ status: "running",
70
+ retryCount: 0,
71
+ },
72
+ {
73
+ taskId: createTaskId("task-3"),
74
+ title: "Task 3",
75
+ description: "Third task",
76
+ filesToModify: ["c.ts"],
77
+ dependsOn: [],
78
+ priority: 3,
79
+ status: "pending",
80
+ retryCount: 0,
81
+ },
82
+ ];
83
+ for (const t of tasks) {
84
+ await stores.taskStore.save(t);
85
+ }
86
+
87
+ const app = createApp();
88
+
89
+ // Should not throw
90
+ await expect(displayStatus(app, "status-run-1")).resolves.toBeUndefined();
91
+
92
+ // Verify stores were read
93
+ const savedRun = await stores.runStore.get(runId);
94
+ expect(savedRun).toBeDefined();
95
+ expect(savedRun?.totalTasks).toBe(3);
96
+ expect(savedRun?.completed).toBe(1);
97
+ }, 15_000);
98
+
99
+ test("displayStatus handles multiple runs (latest vs specific runId)", async () => {
100
+ // Create multiple runs with tasks
101
+ await stores.runStore.save({
102
+ runId: createRunId("old-run"),
103
+ parentBranch: "main",
104
+ totalTasks: 1,
105
+ pending: 0,
106
+ running: 0,
107
+ completed: 1,
108
+ failed: 0,
109
+ startTime: new Date(Date.now() - 10000).toISOString(),
110
+ });
111
+
112
+ await stores.taskStore.save({
113
+ taskId: createTaskId("old-task-1"),
114
+ title: "Old Task",
115
+ description: "From old run",
116
+ filesToModify: [],
117
+ dependsOn: [],
118
+ priority: 1,
119
+ status: "completed",
120
+ retryCount: 0,
121
+ });
122
+
123
+ await stores.runStore.save({
124
+ runId: createRunId("latest-run"),
125
+ parentBranch: "main",
126
+ totalTasks: 2,
127
+ pending: 1,
128
+ running: 1,
129
+ completed: 0,
130
+ failed: 0,
131
+ startTime: new Date().toISOString(),
132
+ });
133
+
134
+ await stores.taskStore.save({
135
+ taskId: createTaskId("latest-task-1"),
136
+ title: "Latest Task 1",
137
+ description: "From latest run",
138
+ filesToModify: [],
139
+ dependsOn: [],
140
+ priority: 1,
141
+ status: "running",
142
+ retryCount: 0,
143
+ });
144
+
145
+ await stores.taskStore.save({
146
+ taskId: createTaskId("latest-task-2"),
147
+ title: "Latest Task 2",
148
+ description: "From latest run",
149
+ filesToModify: [],
150
+ dependsOn: [],
151
+ priority: 2,
152
+ status: "pending",
153
+ retryCount: 0,
154
+ });
155
+
156
+ const app = createApp();
157
+
158
+ // Without runId arg, should use latest
159
+ await displayStatus(app);
160
+
161
+ // With specific runId, should use that one
162
+ await displayStatus(app, "old-run");
163
+
164
+ // Verify correct run was retrieved
165
+ const oldRun = await stores.runStore.get(createRunId("old-run"));
166
+ expect(oldRun).toBeDefined();
167
+ expect(oldRun?.totalTasks).toBe(1);
168
+ }, 15_000);
169
+
170
+ test("displayStatus shows per-task details when requested", async () => {
171
+ const runId = createRunId("task-detail-run");
172
+
173
+ await stores.runStore.save({
174
+ runId,
175
+ parentBranch: "main",
176
+ totalTasks: 2,
177
+ pending: 0,
178
+ running: 0,
179
+ completed: 2,
180
+ failed: 0,
181
+ startTime: new Date().toISOString(),
182
+ });
183
+
184
+ const tasks: Task[] = [
185
+ {
186
+ taskId: createTaskId("detail-task-1"),
187
+ title: "Detailed Task 1",
188
+ description: "First detailed",
189
+ filesToModify: ["x.ts"],
190
+ dependsOn: [],
191
+ priority: 1,
192
+ status: "completed",
193
+ retryCount: 0,
194
+ },
195
+ {
196
+ taskId: createTaskId("detail-task-2"),
197
+ title: "Detailed Task 2",
198
+ description: "Second detailed",
199
+ filesToModify: ["y.ts"],
200
+ dependsOn: [createTaskId("detail-task-1")],
201
+ priority: 2,
202
+ status: "completed",
203
+ retryCount: 1,
204
+ },
205
+ ];
206
+ for (const t of tasks) {
207
+ await stores.taskStore.save(t);
208
+ }
209
+
210
+ const app = createApp();
211
+ await expect(displayStatus(app, "task-detail-run")).resolves.toBeUndefined();
212
+
213
+ // Verify tasks can be retrieved
214
+ const allTasks = await stores.taskStore.getAll();
215
+ expect(allTasks).toHaveLength(2);
216
+ expect(allTasks[1]?.retryCount).toBe(1);
217
+ }, 15_000);
218
+
219
+ test("displayStatus returns error when runId not found", async () => {
220
+ const app = createApp();
221
+
222
+ // No runs exist
223
+ await expect(displayStatus(app, "nonexistent-run")).rejects.toThrow(
224
+ "Run not found: nonexistent-run"
225
+ );
226
+ }, 15_000);
227
+ });