@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
@@ -53,6 +53,7 @@ export class Dispatcher {
53
53
  private taskMap: Map<string, Task> = new Map();
54
54
  private initialized = false;
55
55
  private runId?: import("@aad/shared/types").RunId;
56
+ private skippedCount = 0;
56
57
 
57
58
  constructor(deps: DispatcherDeps) {
58
59
  this.deps = deps;
@@ -112,6 +113,10 @@ export class Dispatcher {
112
113
  // Listen to task:completed events
113
114
  this.deps.eventBus.on("task:completed", (event) => {
114
115
  if (event.type === "task:completed") {
116
+ // Track skipped tasks
117
+ if (event.result.skipped) {
118
+ this.skippedCount++;
119
+ }
115
120
  void this.handleTaskCompleted(event.taskId);
116
121
  }
117
122
  });
@@ -337,14 +342,23 @@ export class Dispatcher {
337
342
  // Emit run:completed if all tasks are done (FIX #1)
338
343
  if (progress.pending === 0 && progress.running === 0) {
339
344
  if (this.runId) {
345
+ const metrics = this.getRunMetrics();
346
+
340
347
  this.deps.eventBus.emit({
341
348
  type: "run:completed",
342
349
  runId: this.runId,
343
350
  });
344
351
 
345
352
  this.deps.logger.info(
346
- { runId: this.runId, completed: progress.completed, failed: progress.failed },
347
- "Run completed"
353
+ {
354
+ runId: this.runId,
355
+ completed: progress.completed,
356
+ failed: progress.failed,
357
+ skipped: metrics.skippedTasks,
358
+ totalDuration: metrics.totalDuration,
359
+ averageTaskDuration: Math.round(metrics.averageTaskDuration),
360
+ },
361
+ "Run completed with metrics"
348
362
  );
349
363
  }
350
364
  }
@@ -424,4 +438,34 @@ export class Dispatcher {
424
438
  return undefined;
425
439
  }
426
440
  }
441
+
442
+ /**
443
+ * Calculate run metrics including skipped tasks
444
+ */
445
+ getRunMetrics(): import("@aad/shared/types").RunMetrics {
446
+ const tasks = Array.from(this.taskMap.values());
447
+ const completedTasks = tasks.filter((t) => t.status === "completed");
448
+ const failedTasks = tasks.filter((t) => t.status === "failed");
449
+
450
+ const totalDuration = completedTasks.reduce((sum, task) => {
451
+ if (task.startTime && task.endTime) {
452
+ const start = new Date(task.startTime).getTime();
453
+ const end = new Date(task.endTime).getTime();
454
+ return sum + (end - start);
455
+ }
456
+ return sum;
457
+ }, 0);
458
+
459
+ const averageTaskDuration =
460
+ completedTasks.length > 0 ? totalDuration / completedTasks.length : 0;
461
+
462
+ return {
463
+ totalTasks: tasks.length,
464
+ completedTasks: completedTasks.length,
465
+ failedTasks: failedTasks.length,
466
+ skippedTasks: this.skippedCount,
467
+ totalDuration,
468
+ averageTaskDuration,
469
+ };
470
+ }
427
471
  }
@@ -201,4 +201,34 @@ describe("loadConfig", () => {
201
201
  const config = loadConfig({});
202
202
  expect(config.plugins).toBeUndefined();
203
203
  });
204
+
205
+ test("loads skipCompleted from env (default true)", () => {
206
+ const config = loadConfig({});
207
+ expect(config.skipCompleted).toBe(true);
208
+ });
209
+
210
+ test("loads skipCompleted from env (explicit false)", () => {
211
+ const config = loadConfig({ AAD_SKIP_COMPLETED: "0" });
212
+ expect(config.skipCompleted).toBe(false);
213
+ });
214
+
215
+ test("loads skipCompleted from env (explicit true)", () => {
216
+ const config = loadConfig({ AAD_SKIP_COMPLETED: "1" });
217
+ expect(config.skipCompleted).toBe(true);
218
+ });
219
+
220
+ test("loads strictTdd from env (default false)", () => {
221
+ const config = loadConfig({});
222
+ expect(config.strictTdd).toBe(false);
223
+ });
224
+
225
+ test("loads strictTdd from env (explicit true)", () => {
226
+ const config = loadConfig({ AAD_STRICT_TDD: "1" });
227
+ expect(config.strictTdd).toBe(true);
228
+ });
229
+
230
+ test("loads strictTdd from env (explicit false)", () => {
231
+ const config = loadConfig({ AAD_STRICT_TDD: "0" });
232
+ expect(config.strictTdd).toBe(false);
233
+ });
204
234
  });
@@ -186,22 +186,31 @@ describe("EventBus", () => {
186
186
  test("listener exception does not affect other listeners", () => {
187
187
  let received: AADEvent | undefined;
188
188
 
189
- eventBus.on("worker:idle", () => {
190
- throw new Error("Listener error");
191
- });
192
-
193
- eventBus.on("worker:idle", (event) => {
194
- received = event;
195
- });
196
-
197
- const event: AADEvent = {
198
- type: "worker:idle",
199
- workerId: createWorkerId("w1"),
200
- };
201
-
202
- eventBus.emit(event);
203
-
204
- expect(received).toEqual(event);
189
+ // Mock console.error to suppress error output
190
+ const originalConsoleError = console.error;
191
+ console.error = () => {};
192
+
193
+ try {
194
+ eventBus.on("worker:idle", () => {
195
+ throw new Error("Listener error");
196
+ });
197
+
198
+ eventBus.on("worker:idle", (event) => {
199
+ received = event;
200
+ });
201
+
202
+ const event: AADEvent = {
203
+ type: "worker:idle",
204
+ workerId: createWorkerId("w1"),
205
+ };
206
+
207
+ eventBus.emit(event);
208
+
209
+ expect(received).toEqual(event);
210
+ } finally {
211
+ // Restore console.error
212
+ console.error = originalConsoleError;
213
+ }
205
214
  });
206
215
 
207
216
  test("emits wildcard * for all events", () => {
@@ -493,4 +502,21 @@ describe("EventBus", () => {
493
502
 
494
503
  expect(received).toEqual(event);
495
504
  });
505
+
506
+ test("emits and receives execution:skipped event", () => {
507
+ let received: AADEvent | undefined;
508
+ eventBus.on("execution:skipped", (event) => {
509
+ received = event;
510
+ });
511
+
512
+ const event: AADEvent = {
513
+ type: "execution:skipped",
514
+ taskId: createTaskId("t1"),
515
+ reason: "Tests already pass",
516
+ };
517
+
518
+ eventBus.emit(event);
519
+
520
+ expect(received).toEqual(event);
521
+ });
496
522
  });
@@ -135,4 +135,100 @@ describe("shutdown-handler", () => {
135
135
  // exitFn called only once due to debounce
136
136
  expect(exitFn).toHaveBeenCalledTimes(1);
137
137
  });
138
+
139
+ test("displays resume command example when using fs persistence", async () => {
140
+ const tasks = [makeTask("t1", "running")];
141
+ const runState: RunState = {
142
+ runId,
143
+ parentBranch: "main",
144
+ totalTasks: 1,
145
+ pending: 0,
146
+ running: 1,
147
+ completed: 0,
148
+ failed: 0,
149
+ startTime: new Date().toISOString(),
150
+ };
151
+
152
+ const exitFn = mock(() => {});
153
+ const stores = createMockStores(tasks, runState);
154
+ const logger = createMockLogger();
155
+
156
+ // Capture console output
157
+ const consoleLogSpy = mock(() => {});
158
+ const originalLog = console.log;
159
+ console.log = consoleLogSpy;
160
+
161
+ try {
162
+ installShutdownHandler({
163
+ runId,
164
+ stores: { runStore: stores.runStore, taskStore: stores.taskStore },
165
+ logger,
166
+ exitFn,
167
+ persistMode: "fs",
168
+ });
169
+
170
+ // Simulate SIGINT
171
+ process.emit("SIGINT");
172
+
173
+ // Wait for async handler
174
+ await new Promise((r) => setTimeout(r, 50));
175
+
176
+ // Verify resume command was logged
177
+ const calls = (consoleLogSpy as any).mock.calls as string[][];
178
+ const resumeLog = calls.find((args) =>
179
+ args.some((arg) => typeof arg === "string" && arg.includes("aad resume"))
180
+ );
181
+ expect(resumeLog).toBeDefined();
182
+ } finally {
183
+ console.log = originalLog;
184
+ }
185
+ });
186
+
187
+ test("does not display resume command when using memory persistence", async () => {
188
+ const tasks = [makeTask("t1", "running")];
189
+ const runState: RunState = {
190
+ runId,
191
+ parentBranch: "main",
192
+ totalTasks: 1,
193
+ pending: 0,
194
+ running: 1,
195
+ completed: 0,
196
+ failed: 0,
197
+ startTime: new Date().toISOString(),
198
+ };
199
+
200
+ const exitFn = mock(() => {});
201
+ const stores = createMockStores(tasks, runState);
202
+ const logger = createMockLogger();
203
+
204
+ // Capture console output
205
+ const consoleLogSpy = mock(() => {});
206
+ const originalLog = console.log;
207
+ console.log = consoleLogSpy;
208
+
209
+ try {
210
+ installShutdownHandler({
211
+ runId,
212
+ stores: { runStore: stores.runStore, taskStore: stores.taskStore },
213
+ logger,
214
+ exitFn,
215
+ persistMode: "memory",
216
+ });
217
+
218
+ // Simulate SIGINT
219
+ process.emit("SIGINT");
220
+
221
+ // Wait for async handler
222
+ await new Promise((r) => setTimeout(r, 50));
223
+
224
+ // Verify resume command was NOT logged
225
+ const calls = (consoleLogSpy as any).mock.calls as string[][];
226
+ const resumeLog = calls.find((args) =>
227
+ args.some((arg) => typeof arg === "string" && arg.includes("aad resume"))
228
+ );
229
+ expect(resumeLog).toBeUndefined();
230
+ } finally {
231
+ console.log = originalLog;
232
+ }
233
+ });
138
234
  });
@@ -25,6 +25,8 @@ const configSchema = z.object({
25
25
  }),
26
26
  debug: z.boolean(),
27
27
  adaptiveEffort: z.boolean(),
28
+ skipCompleted: z.boolean(),
29
+ strictTdd: z.boolean(),
28
30
  teams: z.object({
29
31
  splitter: z.boolean(),
30
32
  reviewer: z.boolean(),
@@ -101,6 +103,8 @@ export function loadConfig(env: Record<string, string | undefined> = process.env
101
103
  },
102
104
  debug: parseBoolOrDefault(env.DEBUG, false),
103
105
  adaptiveEffort: parseBoolOrDefault(env.AAD_ADAPTIVE_EFFORT, false),
106
+ skipCompleted: parseBoolOrDefault(env.AAD_SKIP_COMPLETED, true),
107
+ strictTdd: parseBoolOrDefault(env.AAD_STRICT_TDD, false),
104
108
  teams: {
105
109
  splitter: parseBoolOrDefault(env.AAD_USE_TEAMS_FOR_SPLITTER, false),
106
110
  reviewer: parseBoolOrDefault(env.AAD_USE_TEAMS_FOR_REVIEW, false),
@@ -122,6 +122,11 @@ export type AADEvent =
122
122
  type: "execution:merge:conflict";
123
123
  taskId: TaskId;
124
124
  conflictedFiles: string[];
125
+ }
126
+ | {
127
+ type: "execution:skipped";
128
+ taskId: TaskId;
129
+ reason: string;
125
130
  };
126
131
 
127
132
  export type EventType = AADEvent["type"] | "*";
@@ -64,8 +64,8 @@ export async function getMemoryStatus(): Promise<MemoryStatus> {
64
64
  const purgablePages = parseInt(purgableMatch?.[1] ?? "0", 10);
65
65
 
66
66
  // Calculate memory in GB
67
- const bytesToGB = (bytes: number) => bytes / (1024 * 1024 * 1024);
68
- const pagesToGB = (pages: number) => bytesToGB(pages * pageSize);
67
+ const bytesToGB = (bytes: number): number => bytes / (1024 * 1024 * 1024);
68
+ const pagesToGB = (pages: number): number => bytesToGB(pages * pageSize);
69
69
 
70
70
  // Available = free + inactive + speculative + purgeable
71
71
  // macOS reclaims inactive/purgeable pages on demand — they are effectively free
@@ -15,6 +15,7 @@ export interface ShutdownHandlerOptions {
15
15
  };
16
16
  logger: pino.Logger;
17
17
  exitFn?: (code: number) => void;
18
+ persistMode?: "memory" | "fs";
18
19
  }
19
20
 
20
21
  let _shuttingDown = false;
@@ -56,7 +57,7 @@ export function installShutdownHandler(options: ShutdownHandlerOptions): void {
56
57
  const { runId, stores, logger, exitFn = (code: number) => process.exit(code) } = options;
57
58
  let saving = false;
58
59
 
59
- const handleShutdown = async (reason: string) => {
60
+ const handleShutdown = async (reason: string): Promise<void> => {
60
61
  const now = Date.now();
61
62
  // Debounce: ignore signals within 1s
62
63
  if (now - _lastSignalTime < 1000) return;
@@ -95,7 +96,13 @@ export function installShutdownHandler(options: ShutdownHandlerOptions): void {
95
96
  });
96
97
  }
97
98
 
98
- logger.info({ runId, pending, completed, failed }, "State saved. Use --resume to continue.");
99
+ logger.info({ runId, pending, completed, failed }, "State saved.");
100
+
101
+ // Display resume command example when using fs persistence
102
+ if (options.persistMode === "fs") {
103
+ console.log(`\n⚠️ Run interrupted. To resume:\n`);
104
+ console.log(` aad resume ${runId}\n`);
105
+ }
99
106
  } catch (err) {
100
107
  logger.error({ err }, "Failed to save state during shutdown");
101
108
  }
@@ -103,9 +110,9 @@ export function installShutdownHandler(options: ShutdownHandlerOptions): void {
103
110
  exitFn(1);
104
111
  };
105
112
 
106
- const onSigterm = () => { void handleShutdown("SIGTERM"); };
107
- const onSigint = () => { void handleShutdown("SIGINT"); };
108
- const onUncaught = (err: Error) => { void handleShutdown(`uncaughtException: ${err.message}`); };
113
+ const onSigterm = (): void => { void handleShutdown("SIGTERM"); };
114
+ const onSigint = (): void => { void handleShutdown("SIGINT"); };
115
+ const onUncaught = (err: Error): void => { void handleShutdown(`uncaughtException: ${err.message}`); };
109
116
 
110
117
  process.on("SIGTERM", onSigterm);
111
118
  process.on("SIGINT", onSigint);
@@ -54,6 +54,8 @@ export interface TaskExecutionResult {
54
54
  duration: number;
55
55
  output?: string;
56
56
  error?: string;
57
+ skipped?: boolean;
58
+ phasesExecuted?: string[];
57
59
  }
58
60
 
59
61
  // RepoName branded type
@@ -110,6 +112,16 @@ export interface RunState {
110
112
  endTime?: string;
111
113
  }
112
114
 
115
+ // Run Metrics
116
+ export interface RunMetrics {
117
+ totalTasks: number;
118
+ completedTasks: number;
119
+ failedTasks: number;
120
+ skippedTasks: number;
121
+ totalDuration: number;
122
+ averageTaskDuration: number;
123
+ }
124
+
113
125
  // Task Plan
114
126
  export interface TaskPlan {
115
127
  runId: RunId;
@@ -1,127 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { ClaudeSdkAdapter } from "../claude-sdk.adapter";
3
- import { loadConfig } from "../../../shared/config";
4
- import pino from "pino";
5
- import { ClaudeProviderError } from "../../../shared/errors";
6
-
7
- /**
8
- * 実環境バリデーションテスト
9
- * ANTHROPIC_API_KEY または CLAUDE_CODE_OAUTH_TOKEN が設定されている場合のみ実行
10
- */
11
-
12
- const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
13
- const hasOAuth = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
14
- const hasAuth = hasApiKey || hasOAuth;
15
-
16
- const logger = pino({ level: "silent" });
17
-
18
- function createAdapter() {
19
- const config = loadConfig();
20
- return new ClaudeSdkAdapter(config, logger);
21
- }
22
-
23
- describe.skipIf(!hasAuth)("ClaudeSdkAdapter - Real Environment", () => {
24
- test("simple query returns valid response", async () => {
25
- const adapter = createAdapter();
26
- const response = await adapter.call({
27
- prompt: "Reply with exactly: PING",
28
- });
29
-
30
- expect(response.result).toBeDefined();
31
- expect(response.result.length).toBeGreaterThan(0);
32
- expect(response.exitCode).toBe(0);
33
- expect(response.model).toBeDefined();
34
- expect(response.duration).toBeGreaterThan(0);
35
- }, 30_000);
36
-
37
- test("detects authentication method", () => {
38
- if (hasApiKey) {
39
- expect(process.env.ANTHROPIC_API_KEY).toBeDefined();
40
- console.log("Auth: ANTHROPIC_API_KEY");
41
- }
42
- if (hasOAuth) {
43
- expect(process.env.CLAUDE_CODE_OAUTH_TOKEN).toBeDefined();
44
- console.log("Auth: CLAUDE_CODE_OAUTH_TOKEN");
45
- }
46
- });
47
-
48
- test("throws ClaudeProviderError on invalid auth", async () => {
49
- const originalKey = process.env.ANTHROPIC_API_KEY;
50
- const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
51
-
52
- try {
53
- // 無効な認証情報に差し替え
54
- process.env.ANTHROPIC_API_KEY = "sk-ant-invalid-key-for-testing";
55
- delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
56
-
57
- const adapter = createAdapter();
58
- await expect(
59
- adapter.call({ prompt: "test" })
60
- ).rejects.toThrow(ClaudeProviderError);
61
- } finally {
62
- // 復元
63
- if (originalKey) process.env.ANTHROPIC_API_KEY = originalKey;
64
- else delete process.env.ANTHROPIC_API_KEY;
65
- if (originalOAuth) process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
66
- else delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
67
- }
68
- }, 15_000);
69
-
70
- test("respects timeout/abort", async () => {
71
- const adapter = createAdapter();
72
-
73
- await expect(
74
- adapter.call({
75
- prompt: "Write a very long essay about the history of computing.",
76
- timeout: 100, // 100ms — 即タイムアウト
77
- })
78
- ).rejects.toThrow();
79
- }, 10_000);
80
-
81
- test("passes effort level correctly", async () => {
82
- const adapter = createAdapter();
83
- const response = await adapter.call({
84
- prompt: "Reply with exactly: OK",
85
- effortLevel: "low",
86
- });
87
-
88
- expect(response.result).toBeDefined();
89
- expect(response.exitCode).toBe(0);
90
- expect(response.effortLevel).toBe("low");
91
- }, 30_000);
92
-
93
- test("respects model override", async () => {
94
- const adapter = createAdapter();
95
- const response = await adapter.call({
96
- prompt: "Reply with exactly: HI",
97
- model: "claude-haiku-4-5-20251001",
98
- });
99
-
100
- expect(response.result).toBeDefined();
101
- expect(response.exitCode).toBe(0);
102
- // model名はSDKが返す値に依存するが、haiku系であることを確認
103
- expect(response.model).toContain("haiku");
104
- }, 30_000);
105
- });
106
-
107
- describe.skipIf(!hasOAuth)("ClaudeSdkAdapter - OAuth Authentication", () => {
108
- test("authenticates with CLAUDE_CODE_OAUTH_TOKEN", async () => {
109
- const originalKey = process.env.ANTHROPIC_API_KEY;
110
-
111
- try {
112
- // API Keyを一時的に除外してOAuthのみで認証
113
- delete process.env.ANTHROPIC_API_KEY;
114
-
115
- const adapter = createAdapter();
116
- const response = await adapter.call({
117
- prompt: "Reply with exactly: OAUTH_OK",
118
- });
119
-
120
- expect(response.result).toBeDefined();
121
- expect(response.exitCode).toBe(0);
122
- } finally {
123
- if (originalKey) process.env.ANTHROPIC_API_KEY = originalKey;
124
- else delete process.env.ANTHROPIC_API_KEY;
125
- }
126
- }, 30_000);
127
- });