@ronkovic/aad 0.3.9 → 0.4.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 +292 -12
- package/package.json +6 -1
- package/src/__tests__/e2e/pipeline-e2e.test.ts +1 -0
- package/src/__tests__/e2e/resume-e2e.test.ts +2 -0
- package/src/__tests__/integration/pipeline.test.ts +1 -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 +2 -0
- package/src/modules/claude-provider/__tests__/provider-registry.test.ts +1 -0
- package/src/modules/cli/__tests__/cleanup.test.ts +72 -0
- package/src/modules/cli/__tests__/resume.test.ts +1 -0
- package/src/modules/cli/__tests__/run.test.ts +1 -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 +3 -2
- package/src/modules/cli/commands/run.ts +57 -7
- 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 +143 -18
- 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 +1 -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 +1 -0
- package/src/modules/task-execution/__tests__/executor.test.ts +86 -0
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +4 -3
- package/src/modules/task-execution/executor.ts +87 -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 +50 -1
- package/src/shared/__tests__/prerequisites.test.ts +176 -0
- package/src/shared/config.ts +6 -0
- package/src/shared/prerequisites.ts +190 -0
- package/src/shared/types.ts +13 -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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Task, WorkspaceInfo, EffortLevel, PhaseResult } from "@aad/shared/types";
|
|
2
2
|
import type { ClaudeProvider } from "@aad/claude-provider";
|
|
3
|
+
import type { RetryContext } from "../executor";
|
|
3
4
|
|
|
4
5
|
export interface TesterRedOptions {
|
|
5
6
|
effortLevel?: EffortLevel;
|
|
@@ -10,10 +11,10 @@ export interface TesterRedOptions {
|
|
|
10
11
|
/**
|
|
11
12
|
* Build TDD Red phase prompt for tester agent
|
|
12
13
|
*/
|
|
13
|
-
export function buildRedPhasePrompt(task: Task, workspace: WorkspaceInfo): string {
|
|
14
|
+
export function buildRedPhasePrompt(task: Task, workspace: WorkspaceInfo, retryContext?: RetryContext): string {
|
|
14
15
|
const languagePatterns = getLanguageTestPatterns(workspace.language);
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
let prompt = `testerエージェントとして、TDD Red フェーズを実行してください。
|
|
17
18
|
|
|
18
19
|
Task ID: ${task.taskId as string}
|
|
19
20
|
Task Title: ${task.title}
|
|
@@ -28,11 +29,26 @@ Task Description: ${task.description}
|
|
|
28
29
|
|
|
29
30
|
実行内容:
|
|
30
31
|
1. タスクの要件を理解する
|
|
31
|
-
2.
|
|
32
|
+
2. 失敗するテストを作成する(言語に応じた適切なパターンを使用)
|
|
32
33
|
${languagePatterns}
|
|
33
34
|
3. テストを実行して失敗することを確認する
|
|
34
35
|
|
|
35
36
|
注意: このフェーズでは実装コードは書かないでください。テストのみ作成してください。`;
|
|
37
|
+
|
|
38
|
+
if (retryContext?.previousFailure) {
|
|
39
|
+
prompt += `\n\n⚠️ リトライ情報 (${retryContext.retryCount}回目):
|
|
40
|
+
前回のフェーズ「${retryContext.previousFailure.phase}」で失敗しました。
|
|
41
|
+
エラー: ${retryContext.previousFailure.error}`;
|
|
42
|
+
|
|
43
|
+
if (retryContext.previousFailure.testOutput) {
|
|
44
|
+
prompt += `\n\n前回のテスト出力:
|
|
45
|
+
${retryContext.previousFailure.testOutput}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
prompt += `\n\n前回の失敗を踏まえて、より堅牢なテストを作成してください。`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return prompt;
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
/**
|
|
@@ -72,9 +88,10 @@ export async function runTesterRed(
|
|
|
72
88
|
task: Task,
|
|
73
89
|
workspace: WorkspaceInfo,
|
|
74
90
|
provider: ClaudeProvider,
|
|
75
|
-
options: TesterRedOptions = {}
|
|
91
|
+
options: TesterRedOptions = {},
|
|
92
|
+
retryContext?: RetryContext
|
|
76
93
|
): Promise<PhaseResult> {
|
|
77
|
-
const prompt = buildRedPhasePrompt(task, workspace);
|
|
94
|
+
const prompt = buildRedPhasePrompt(task, workspace, retryContext);
|
|
78
95
|
|
|
79
96
|
const response = await provider.call({
|
|
80
97
|
prompt,
|
|
@@ -48,8 +48,12 @@ export function buildTestCommand(workspace: WorkspaceInfo): string[] {
|
|
|
48
48
|
if (packageManager === "yarn") return ["yarn", "test"];
|
|
49
49
|
return ["npx", "mocha"];
|
|
50
50
|
|
|
51
|
-
case "pytest":
|
|
51
|
+
case "pytest": {
|
|
52
|
+
const { packageManager } = workspace;
|
|
53
|
+
if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
|
|
54
|
+
if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
|
|
52
55
|
return ["pytest", "-v"];
|
|
56
|
+
}
|
|
53
57
|
|
|
54
58
|
case "go-test":
|
|
55
59
|
return ["go", "test", "./..."];
|
|
@@ -63,11 +67,23 @@ export function buildTestCommand(workspace: WorkspaceInfo): string[] {
|
|
|
63
67
|
case "gradle":
|
|
64
68
|
return ["./gradlew", "test"];
|
|
65
69
|
|
|
66
|
-
case "
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
case "playwright":
|
|
71
|
+
return ["npx", "playwright", "test"];
|
|
72
|
+
|
|
73
|
+
case "terraform":
|
|
74
|
+
return ["terraform", "validate"];
|
|
75
|
+
|
|
76
|
+
case "unknown": {
|
|
77
|
+
// Fallback: use package manager-based test command
|
|
78
|
+
const { packageManager } = workspace;
|
|
79
|
+
if (packageManager === "bun") return ["bun", "test"];
|
|
80
|
+
if (packageManager === "npm") return ["npm", "test"];
|
|
81
|
+
if (packageManager === "yarn") return ["yarn", "test"];
|
|
82
|
+
if (packageManager === "pnpm") return ["pnpm", "test"];
|
|
83
|
+
if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
|
|
84
|
+
if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
|
|
85
|
+
return ["npm", "test"];
|
|
86
|
+
}
|
|
71
87
|
|
|
72
88
|
default: {
|
|
73
89
|
const exhaustive: never = testFramework;
|
|
@@ -248,6 +248,13 @@ export class Dispatcher {
|
|
|
248
248
|
task.retryCount += 1;
|
|
249
249
|
task.workerId = undefined;
|
|
250
250
|
task.failureReason = error;
|
|
251
|
+
// Store structured failure context for retry
|
|
252
|
+
task.previousFailure = {
|
|
253
|
+
phase: this.extractPhaseFromError(error),
|
|
254
|
+
error: error,
|
|
255
|
+
testOutput: this.extractTestOutput(error),
|
|
256
|
+
retryCount: task.retryCount,
|
|
257
|
+
};
|
|
251
258
|
await this.deps.taskStore.save(task);
|
|
252
259
|
this.taskMap.set(taskId as string, task);
|
|
253
260
|
|
|
@@ -369,10 +376,52 @@ export class Dispatcher {
|
|
|
369
376
|
|
|
370
377
|
this.deps.logger.warn(
|
|
371
378
|
{ taskId: task.taskId, elapsed },
|
|
372
|
-
"Stale task detected"
|
|
379
|
+
"Stale task detected, triggering retry"
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Trigger retry flow via handleTaskFailed
|
|
383
|
+
void this.handleTaskFailed(
|
|
384
|
+
task.taskId,
|
|
385
|
+
`Task stale (elapsed: ${elapsed}ms)`
|
|
373
386
|
);
|
|
374
387
|
}
|
|
375
388
|
}
|
|
376
389
|
}
|
|
377
390
|
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Extract phase name from error message
|
|
394
|
+
*/
|
|
395
|
+
private extractPhaseFromError(error: string): string {
|
|
396
|
+
const lowerError = error.toLowerCase();
|
|
397
|
+
if (lowerError.includes("red phase")) return "red";
|
|
398
|
+
if (lowerError.includes("green phase")) return "green";
|
|
399
|
+
if (lowerError.includes("verify phase")) return "verify";
|
|
400
|
+
if (lowerError.includes("review phase")) return "review";
|
|
401
|
+
if (lowerError.includes("merge phase")) return "merge";
|
|
402
|
+
return "unknown";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Extract test output from error context (truncated to 2000 chars)
|
|
407
|
+
*/
|
|
408
|
+
private extractTestOutput(error: string): string | undefined {
|
|
409
|
+
try {
|
|
410
|
+
// Try to parse JSON context from PhaseError
|
|
411
|
+
const jsonMatch = error.match(/\{.*\}/s);
|
|
412
|
+
if (!jsonMatch) return undefined;
|
|
413
|
+
|
|
414
|
+
const context = JSON.parse(jsonMatch[0]);
|
|
415
|
+
const output = context.output ?? context.error;
|
|
416
|
+
if (!output) return undefined;
|
|
417
|
+
|
|
418
|
+
// Truncate to 2000 chars
|
|
419
|
+
return output.length > 2000
|
|
420
|
+
? output.substring(0, 2000) + "\n... (truncated)"
|
|
421
|
+
: output;
|
|
422
|
+
} catch {
|
|
423
|
+
// If parsing fails, return undefined
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
378
427
|
}
|
|
@@ -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
|
+
});
|
package/src/shared/config.ts
CHANGED
|
@@ -38,6 +38,9 @@ const configSchema = z.object({
|
|
|
38
38
|
port: z.number().int().min(1).max(65535),
|
|
39
39
|
host: z.string(),
|
|
40
40
|
}),
|
|
41
|
+
git: z.object({
|
|
42
|
+
autoPush: z.boolean(),
|
|
43
|
+
}),
|
|
41
44
|
repos: z.array(z.object({
|
|
42
45
|
name: z.string().optional(),
|
|
43
46
|
path: z.string(),
|
|
@@ -111,6 +114,9 @@ export function loadConfig(env: Record<string, string | undefined> = process.env
|
|
|
111
114
|
port: parseIntOrDefault(env.AAD_DASHBOARD_PORT, 7333),
|
|
112
115
|
host: env.AAD_DASHBOARD_HOST ?? "localhost",
|
|
113
116
|
},
|
|
117
|
+
git: {
|
|
118
|
+
autoPush: parseBoolOrDefault(env.AAD_GIT_AUTO_PUSH, true),
|
|
119
|
+
},
|
|
114
120
|
repos: env.AAD_REPOS
|
|
115
121
|
? env.AAD_REPOS.split(",").map((p) => ({ path: p.trim() }))
|
|
116
122
|
: undefined,
|
|
@@ -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
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -83,6 +83,7 @@ export interface Task {
|
|
|
83
83
|
endTime?: string;
|
|
84
84
|
retryCount: number;
|
|
85
85
|
failureReason?: string;
|
|
86
|
+
previousFailure?: PreviousFailure;
|
|
86
87
|
repoName?: RepoName;
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -129,6 +130,8 @@ export type TestFramework =
|
|
|
129
130
|
| "cargo"
|
|
130
131
|
| "maven"
|
|
131
132
|
| "gradle"
|
|
133
|
+
| "playwright"
|
|
134
|
+
| "terraform"
|
|
132
135
|
| "unknown";
|
|
133
136
|
|
|
134
137
|
// Workspace Info (from project detection)
|
|
@@ -150,3 +153,13 @@ export interface PhaseResult {
|
|
|
150
153
|
output: string;
|
|
151
154
|
duration?: number;
|
|
152
155
|
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Previous failure information for retry context
|
|
159
|
+
*/
|
|
160
|
+
export interface PreviousFailure {
|
|
161
|
+
phase: string;
|
|
162
|
+
error: string;
|
|
163
|
+
testOutput?: string;
|
|
164
|
+
retryCount: number;
|
|
165
|
+
}
|