@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,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,6 @@
1
+ /**
2
+ * Barrel export for test helpers
3
+ */
4
+ export { MockClaudeProvider } from "./mock-claude-provider";
5
+ export { createMockLogger, createMockConfig } from "./mock-logger";
6
+ export { waitFor, collectEvents } from "./wait-helpers";
@@ -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 {
@@ -346,4 +350,79 @@ describe("ClaudeSdkAdapter", () => {
346
350
  expect(response.exitCode).toBe(0);
347
351
  expect(response.result).toBe("Final result");
348
352
  });
353
+
354
+ test("call() - ANTHROPIC_API_KEY が env に渡される", async () => {
355
+ const originalKey = process.env.ANTHROPIC_API_KEY;
356
+ process.env.ANTHROPIC_API_KEY = "sk-ant-test-key";
357
+ try {
358
+ await adapter.call({ prompt: "Test" });
359
+ const callArgs = mockQuery.mock.calls[0] as unknown as [
360
+ { prompt: string; options: { env?: Record<string, string> } }
361
+ ];
362
+ expect(callArgs[0]!.options.env?.ANTHROPIC_API_KEY).toBe("sk-ant-test-key");
363
+ } finally {
364
+ if (originalKey) process.env.ANTHROPIC_API_KEY = originalKey;
365
+ else delete process.env.ANTHROPIC_API_KEY;
366
+ }
367
+ });
368
+
369
+ test("call() - CLAUDE_CODE_OAUTH_TOKEN が env に渡される", async () => {
370
+ const originalToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
371
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-test-token";
372
+ try {
373
+ await adapter.call({ prompt: "Test" });
374
+ const callArgs = mockQuery.mock.calls[0] as unknown as [
375
+ { prompt: string; options: { env?: Record<string, string> } }
376
+ ];
377
+ expect(callArgs[0]!.options.env?.CLAUDE_CODE_OAUTH_TOKEN).toBe("oauth-test-token");
378
+ } finally {
379
+ if (originalToken) process.env.CLAUDE_CODE_OAUTH_TOKEN = originalToken;
380
+ else delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
381
+ }
382
+ });
383
+
384
+ test("call() - タイムアウトで AbortError が発生する", async () => {
385
+ mockQuery.mockImplementationOnce(
386
+ (params: { prompt: string; options?: unknown }) => {
387
+ const abortController = (params.options as { abortController?: AbortController })?.abortController;
388
+ return (async function* () {
389
+ // AbortControllerのabortを監視
390
+ if (abortController) {
391
+ await new Promise((_, reject) => {
392
+ abortController.signal.addEventListener("abort", () => {
393
+ reject(new Error("The operation was aborted"));
394
+ });
395
+ // 長時間待機
396
+ setTimeout(() => {}, 5000);
397
+ });
398
+ }
399
+ yield { type: "result", subtype: "success", result: "late" } as MockSDKMessage;
400
+ })() as AsyncGenerator<MockSDKMessage, void, unknown>;
401
+ }
402
+ );
403
+
404
+ await expect(adapter.call({ prompt: "Test", timeout: 50 })).rejects.toThrow();
405
+ }, 10_000);
406
+
407
+ test("call() - effortLevel がレスポンスに反映される", async () => {
408
+ const response = await adapter.call({ prompt: "Test", effortLevel: "low" });
409
+ expect(response.effortLevel).toBe("low");
410
+ });
411
+
412
+ test("call() - model override がレスポンスに反映される", async () => {
413
+ mockQuery.mockReturnValueOnce(
414
+ (async function* () {
415
+ yield {
416
+ type: "assistant",
417
+ message: {
418
+ model: "claude-haiku-4-5-20251001",
419
+ content: [{ type: "text", text: "Response" }],
420
+ },
421
+ } as MockSDKMessage;
422
+ yield { type: "result", subtype: "success", result: "Response" } as MockSDKMessage;
423
+ })() as AsyncGenerator<MockSDKMessage, void, unknown>
424
+ );
425
+ const response = await adapter.call({ prompt: "Test", model: "claude-haiku-4-5-20251001" });
426
+ expect(response.model).toContain("haiku");
427
+ });
349
428
  });
@@ -59,6 +59,8 @@ const mockAppConfig: Config = {
59
59
  host: "localhost",
60
60
  },
61
61
  git: { autoPush: false },
62
+ skipCompleted: true,
63
+ strictTdd: false,
62
64
  };
63
65
 
64
66
  describe("ProviderRegistry", () => {
@@ -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
  });