@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
|
@@ -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
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prerequisites Tests
|
|
3
|
+
* Verifies prerequisite checks for AAD pipeline
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect, beforeEach } from "bun:test";
|
|
7
|
+
import { checkPrerequisites } from "../prerequisites";
|
|
8
|
+
import { pino } from "pino";
|
|
9
|
+
|
|
10
|
+
describe("checkPrerequisites", () => {
|
|
11
|
+
let logger: import("pino").Logger;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
logger = pino({ level: "silent" });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("runs all prerequisite checks", async () => {
|
|
18
|
+
const result = await checkPrerequisites(logger);
|
|
19
|
+
|
|
20
|
+
expect(result).toBeDefined();
|
|
21
|
+
expect(result.passed).toBeDefined();
|
|
22
|
+
expect(Array.isArray(result.failures)).toBe(true);
|
|
23
|
+
expect(Array.isArray(result.warnings)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("passes if all required checks pass with ANTHROPIC_API_KEY", async () => {
|
|
27
|
+
// In a git repo with ANTHROPIC_API_KEY set, all required checks should pass
|
|
28
|
+
// This test assumes we're running in a valid git repo
|
|
29
|
+
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
30
|
+
const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
31
|
+
process.env.ANTHROPIC_API_KEY = "test-key";
|
|
32
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await checkPrerequisites(logger);
|
|
36
|
+
|
|
37
|
+
// Git, git-version, git-repo, and claude-auth should all pass
|
|
38
|
+
// If we're in a git repo, failures should be 0
|
|
39
|
+
// If not (e.g., running in a worktree without git), we skip this assertion
|
|
40
|
+
if (result.passed) {
|
|
41
|
+
expect(result.failures.length).toBe(0);
|
|
42
|
+
expect(result.passed).toBe(true);
|
|
43
|
+
} else {
|
|
44
|
+
// We're likely in a non-git environment or worktree - just verify structure
|
|
45
|
+
expect(result.failures).toBeDefined();
|
|
46
|
+
expect(result.warnings).toBeDefined();
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
if (originalKey) {
|
|
50
|
+
process.env.ANTHROPIC_API_KEY = originalKey;
|
|
51
|
+
} else {
|
|
52
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
53
|
+
}
|
|
54
|
+
if (originalOAuth) {
|
|
55
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
|
|
56
|
+
} else {
|
|
57
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("passes if CLAUDE_CODE_OAUTH_TOKEN is set", async () => {
|
|
63
|
+
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
64
|
+
const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
65
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
66
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-oauth-token";
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const result = await checkPrerequisites(logger);
|
|
70
|
+
|
|
71
|
+
// Should pass auth check with OAuth token
|
|
72
|
+
const authFailure = result.failures.find((f) => f.name === "claude-auth");
|
|
73
|
+
expect(authFailure).toBeUndefined();
|
|
74
|
+
} finally {
|
|
75
|
+
if (originalKey) {
|
|
76
|
+
process.env.ANTHROPIC_API_KEY = originalKey;
|
|
77
|
+
} else {
|
|
78
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
79
|
+
}
|
|
80
|
+
if (originalOAuth) {
|
|
81
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
|
|
82
|
+
} else {
|
|
83
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("passes if both ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN are set", async () => {
|
|
89
|
+
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
90
|
+
const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
91
|
+
process.env.ANTHROPIC_API_KEY = "test-key";
|
|
92
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-oauth-token";
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const result = await checkPrerequisites(logger);
|
|
96
|
+
|
|
97
|
+
// Should pass auth check with both set
|
|
98
|
+
const authFailure = result.failures.find((f) => f.name === "claude-auth");
|
|
99
|
+
expect(authFailure).toBeUndefined();
|
|
100
|
+
} finally {
|
|
101
|
+
if (originalKey) {
|
|
102
|
+
process.env.ANTHROPIC_API_KEY = originalKey;
|
|
103
|
+
} else {
|
|
104
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
105
|
+
}
|
|
106
|
+
if (originalOAuth) {
|
|
107
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
|
|
108
|
+
} else {
|
|
109
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("fails if neither ANTHROPIC_API_KEY nor CLAUDE_CODE_OAUTH_TOKEN is set", async () => {
|
|
115
|
+
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
116
|
+
const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
117
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
118
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await checkPrerequisites(logger);
|
|
122
|
+
|
|
123
|
+
expect(result.passed).toBe(false);
|
|
124
|
+
expect(result.failures.some((f) => f.name === "claude-auth")).toBe(true);
|
|
125
|
+
} finally {
|
|
126
|
+
if (originalKey) {
|
|
127
|
+
process.env.ANTHROPIC_API_KEY = originalKey;
|
|
128
|
+
}
|
|
129
|
+
if (originalOAuth) {
|
|
130
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("includes warnings for non-required checks", async () => {
|
|
136
|
+
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
137
|
+
process.env.ANTHROPIC_API_KEY = "test-key";
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const result = await checkPrerequisites(logger);
|
|
141
|
+
|
|
142
|
+
// Clean working directory is optional, may appear in warnings
|
|
143
|
+
// (depends on current git state)
|
|
144
|
+
expect(result.warnings).toBeDefined();
|
|
145
|
+
} finally {
|
|
146
|
+
if (originalKey) {
|
|
147
|
+
process.env.ANTHROPIC_API_KEY = originalKey;
|
|
148
|
+
} else {
|
|
149
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("formats error messages properly", async () => {
|
|
155
|
+
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
156
|
+
const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
157
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
158
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = await checkPrerequisites(logger);
|
|
162
|
+
|
|
163
|
+
const authFailure = result.failures.find((f) => f.name === "claude-auth");
|
|
164
|
+
expect(authFailure).toBeDefined();
|
|
165
|
+
expect(authFailure?.message).toContain("ANTHROPIC_API_KEY");
|
|
166
|
+
expect(authFailure?.message).toContain("CLAUDE_CODE_OAUTH_TOKEN");
|
|
167
|
+
} finally {
|
|
168
|
+
if (originalKey) {
|
|
169
|
+
process.env.ANTHROPIC_API_KEY = originalKey;
|
|
170
|
+
}
|
|
171
|
+
if (originalOAuth) {
|
|
172
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -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
|
});
|
package/src/shared/config.ts
CHANGED
|
@@ -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(),
|
|
@@ -38,6 +40,9 @@ const configSchema = z.object({
|
|
|
38
40
|
port: z.number().int().min(1).max(65535),
|
|
39
41
|
host: z.string(),
|
|
40
42
|
}),
|
|
43
|
+
git: z.object({
|
|
44
|
+
autoPush: z.boolean(),
|
|
45
|
+
}),
|
|
41
46
|
repos: z.array(z.object({
|
|
42
47
|
name: z.string().optional(),
|
|
43
48
|
path: z.string(),
|
|
@@ -98,6 +103,8 @@ export function loadConfig(env: Record<string, string | undefined> = process.env
|
|
|
98
103
|
},
|
|
99
104
|
debug: parseBoolOrDefault(env.DEBUG, false),
|
|
100
105
|
adaptiveEffort: parseBoolOrDefault(env.AAD_ADAPTIVE_EFFORT, false),
|
|
106
|
+
skipCompleted: parseBoolOrDefault(env.AAD_SKIP_COMPLETED, true),
|
|
107
|
+
strictTdd: parseBoolOrDefault(env.AAD_STRICT_TDD, false),
|
|
101
108
|
teams: {
|
|
102
109
|
splitter: parseBoolOrDefault(env.AAD_USE_TEAMS_FOR_SPLITTER, false),
|
|
103
110
|
reviewer: parseBoolOrDefault(env.AAD_USE_TEAMS_FOR_REVIEW, false),
|
|
@@ -111,6 +118,9 @@ export function loadConfig(env: Record<string, string | undefined> = process.env
|
|
|
111
118
|
port: parseIntOrDefault(env.AAD_DASHBOARD_PORT, 7333),
|
|
112
119
|
host: env.AAD_DASHBOARD_HOST ?? "localhost",
|
|
113
120
|
},
|
|
121
|
+
git: {
|
|
122
|
+
autoPush: parseBoolOrDefault(env.AAD_GIT_AUTO_PUSH, true),
|
|
123
|
+
},
|
|
114
124
|
repos: env.AAD_REPOS
|
|
115
125
|
? env.AAD_REPOS.split(",").map((p) => ({ path: p.trim() }))
|
|
116
126
|
: undefined,
|
package/src/shared/events.ts
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prerequisites Checker
|
|
3
|
+
* Verifies required tools and environment before running AAD pipeline
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Logger } from "pino";
|
|
7
|
+
|
|
8
|
+
export interface PrerequisiteCheck {
|
|
9
|
+
name: string;
|
|
10
|
+
required: boolean;
|
|
11
|
+
check: () => Promise<boolean>;
|
|
12
|
+
errorMessage: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PrerequisiteResult {
|
|
16
|
+
passed: boolean;
|
|
17
|
+
failures: Array<{ name: string; message: string }>;
|
|
18
|
+
warnings: Array<{ name: string; message: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a command exists in PATH
|
|
23
|
+
*/
|
|
24
|
+
async function commandExists(command: string): Promise<boolean> {
|
|
25
|
+
try {
|
|
26
|
+
const proc = Bun.spawn(["which", command], {
|
|
27
|
+
stdout: "pipe",
|
|
28
|
+
stderr: "pipe",
|
|
29
|
+
});
|
|
30
|
+
await proc.exited;
|
|
31
|
+
return proc.exitCode === 0;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check git version (requires >= 2.25 for worktree improvements)
|
|
39
|
+
*/
|
|
40
|
+
async function checkGitVersion(): Promise<boolean> {
|
|
41
|
+
try {
|
|
42
|
+
const proc = Bun.spawn(["git", "--version"], {
|
|
43
|
+
stdout: "pipe",
|
|
44
|
+
stderr: "pipe",
|
|
45
|
+
});
|
|
46
|
+
const output = await new Response(proc.stdout).text();
|
|
47
|
+
await proc.exited;
|
|
48
|
+
|
|
49
|
+
if (proc.exitCode !== 0) return false;
|
|
50
|
+
|
|
51
|
+
// Parse version from "git version 2.39.1"
|
|
52
|
+
const match = output.match(/git version (\d+)\.(\d+)/);
|
|
53
|
+
if (!match?.[1] || !match[2]) return false;
|
|
54
|
+
|
|
55
|
+
const major = parseInt(match[1], 10);
|
|
56
|
+
const minor = parseInt(match[2], 10);
|
|
57
|
+
|
|
58
|
+
return major > 2 || (major === 2 && minor >= 25);
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if git repository is initialized
|
|
66
|
+
*/
|
|
67
|
+
async function checkGitRepo(): Promise<boolean> {
|
|
68
|
+
try {
|
|
69
|
+
const proc = Bun.spawn(["git", "rev-parse", "--is-inside-work-tree"], {
|
|
70
|
+
stdout: "pipe",
|
|
71
|
+
stderr: "pipe",
|
|
72
|
+
});
|
|
73
|
+
await proc.exited;
|
|
74
|
+
return proc.exitCode === 0;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if Claude authentication is configured
|
|
82
|
+
* Accepts either ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN
|
|
83
|
+
*/
|
|
84
|
+
function checkClaudeAuth(): Promise<boolean> {
|
|
85
|
+
return Promise.resolve(
|
|
86
|
+
!!process.env.ANTHROPIC_API_KEY || !!process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if working directory is clean (no uncommitted changes)
|
|
92
|
+
*/
|
|
93
|
+
async function checkCleanWorkingDirectory(): Promise<boolean> {
|
|
94
|
+
try {
|
|
95
|
+
const proc = Bun.spawn(["git", "status", "--porcelain"], {
|
|
96
|
+
stdout: "pipe",
|
|
97
|
+
stderr: "pipe",
|
|
98
|
+
});
|
|
99
|
+
const output = await new Response(proc.stdout).text();
|
|
100
|
+
await proc.exited;
|
|
101
|
+
|
|
102
|
+
if (proc.exitCode !== 0) return false;
|
|
103
|
+
|
|
104
|
+
// Empty output means clean working directory
|
|
105
|
+
return output.trim().length === 0;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Define all prerequisite checks
|
|
113
|
+
*/
|
|
114
|
+
function defineChecks(): PrerequisiteCheck[] {
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
name: "git",
|
|
118
|
+
required: true,
|
|
119
|
+
check: () => commandExists("git"),
|
|
120
|
+
errorMessage: "git is not installed. Please install git to use AAD.",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "git-version",
|
|
124
|
+
required: true,
|
|
125
|
+
check: checkGitVersion,
|
|
126
|
+
errorMessage: "git version must be >= 2.25 for worktree support.",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "git-repo",
|
|
130
|
+
required: true,
|
|
131
|
+
check: checkGitRepo,
|
|
132
|
+
errorMessage: "Current directory is not a git repository. Run 'git init' first.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "claude-auth",
|
|
136
|
+
required: true,
|
|
137
|
+
check: checkClaudeAuth,
|
|
138
|
+
errorMessage: "No authentication configured. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "clean-working-directory",
|
|
142
|
+
required: false,
|
|
143
|
+
check: checkCleanWorkingDirectory,
|
|
144
|
+
errorMessage: "Working directory has uncommitted changes. Commit or stash them before running AAD.",
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Run all prerequisite checks
|
|
151
|
+
*/
|
|
152
|
+
export async function checkPrerequisites(logger?: Logger): Promise<PrerequisiteResult> {
|
|
153
|
+
logger?.debug("Running prerequisite checks");
|
|
154
|
+
|
|
155
|
+
const checks = defineChecks();
|
|
156
|
+
const failures: Array<{ name: string; message: string }> = [];
|
|
157
|
+
const warnings: Array<{ name: string; message: string }> = [];
|
|
158
|
+
|
|
159
|
+
for (const check of checks) {
|
|
160
|
+
try {
|
|
161
|
+
const passed = await check.check();
|
|
162
|
+
|
|
163
|
+
if (!passed) {
|
|
164
|
+
if (check.required) {
|
|
165
|
+
failures.push({ name: check.name, message: check.errorMessage });
|
|
166
|
+
logger?.error({ check: check.name }, check.errorMessage);
|
|
167
|
+
} else {
|
|
168
|
+
warnings.push({ name: check.name, message: check.errorMessage });
|
|
169
|
+
logger?.warn({ check: check.name }, check.errorMessage);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
logger?.debug({ check: check.name }, "Prerequisite check passed");
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const errorMessage = `Failed to check ${check.name}: ${error}`;
|
|
176
|
+
if (check.required) {
|
|
177
|
+
failures.push({ name: check.name, message: errorMessage });
|
|
178
|
+
logger?.error({ check: check.name, error }, errorMessage);
|
|
179
|
+
} else {
|
|
180
|
+
warnings.push({ name: check.name, message: errorMessage });
|
|
181
|
+
logger?.warn({ check: check.name, error }, errorMessage);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const passed = failures.length === 0;
|
|
187
|
+
logger?.info({ passed, failures: failures.length, warnings: warnings.length }, "Prerequisites check completed");
|
|
188
|
+
|
|
189
|
+
return { passed, failures, warnings };
|
|
190
|
+
}
|
|
@@ -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.
|
|
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);
|
package/src/shared/types.ts
CHANGED
|
@@ -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
|
|
@@ -83,6 +85,7 @@ export interface Task {
|
|
|
83
85
|
endTime?: string;
|
|
84
86
|
retryCount: number;
|
|
85
87
|
failureReason?: string;
|
|
88
|
+
previousFailure?: PreviousFailure;
|
|
86
89
|
repoName?: RepoName;
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -109,6 +112,16 @@ export interface RunState {
|
|
|
109
112
|
endTime?: string;
|
|
110
113
|
}
|
|
111
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
|
+
|
|
112
125
|
// Task Plan
|
|
113
126
|
export interface TaskPlan {
|
|
114
127
|
runId: RunId;
|
|
@@ -129,6 +142,8 @@ export type TestFramework =
|
|
|
129
142
|
| "cargo"
|
|
130
143
|
| "maven"
|
|
131
144
|
| "gradle"
|
|
145
|
+
| "playwright"
|
|
146
|
+
| "terraform"
|
|
132
147
|
| "unknown";
|
|
133
148
|
|
|
134
149
|
// Workspace Info (from project detection)
|
|
@@ -150,3 +165,13 @@ export interface PhaseResult {
|
|
|
150
165
|
output: string;
|
|
151
166
|
duration?: number;
|
|
152
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Previous failure information for retry context
|
|
171
|
+
*/
|
|
172
|
+
export interface PreviousFailure {
|
|
173
|
+
phase: string;
|
|
174
|
+
error: string;
|
|
175
|
+
testOutput?: string;
|
|
176
|
+
retryCount: number;
|
|
177
|
+
}
|