@ronkovic/aad 0.5.0 → 0.6.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/package.json +1 -1
- package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +75 -0
- package/src/modules/git-workspace/__tests__/.tmp-template-copy-test/npm-install-test/node_modules/aad/templates/CLAUDE.md +1 -0
- package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +19 -0
- package/src/modules/git-workspace/__tests__/template-copy.test.ts +25 -0
- package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +83 -0
- package/src/modules/git-workspace/dependency-installer.ts +4 -0
- package/src/modules/git-workspace/template-copy.ts +12 -3
- package/src/modules/git-workspace/worktree-manager.ts +3 -0
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +47 -0
- package/src/modules/task-execution/phases/tester-verify.ts +90 -0
- package/src/shared/__tests__/utils.test.ts +21 -1
- package/src/shared/utils.ts +11 -0
- package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +0 -127
package/package.json
CHANGED
|
@@ -350,4 +350,79 @@ describe("ClaudeSdkAdapter", () => {
|
|
|
350
350
|
expect(response.exitCode).toBe(0);
|
|
351
351
|
expect(response.result).toBe("Final result");
|
|
352
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
|
+
});
|
|
353
428
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
NPM template
|
|
@@ -75,3 +75,22 @@ describe("buildInstallCommand", () => {
|
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
});
|
|
78
|
+
|
|
79
|
+
describe("resolveAvailableCommand", () => {
|
|
80
|
+
test("returns command if binary exists in PATH", async () => {
|
|
81
|
+
// Import the function
|
|
82
|
+
const { resolveAvailableCommand } = await import("../dependency-installer");
|
|
83
|
+
|
|
84
|
+
// bun should exist in PATH during tests
|
|
85
|
+
const result = await resolveAvailableCommand("bun");
|
|
86
|
+
expect(result).toBe("bun");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("falls back to npx if binary does not exist in PATH", async () => {
|
|
90
|
+
const { resolveAvailableCommand } = await import("../dependency-installer");
|
|
91
|
+
|
|
92
|
+
// Use a non-existent binary name
|
|
93
|
+
const result = await resolveAvailableCommand("nonexistent-binary-xyz123");
|
|
94
|
+
expect(result).toBe("npx");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -186,4 +186,29 @@ describe("resolveTemplateDir", () => {
|
|
|
186
186
|
expect(result).toContain("templates");
|
|
187
187
|
expect(result).not.toContain("sandbox/templates");
|
|
188
188
|
});
|
|
189
|
+
|
|
190
|
+
test("checks multiple fallback paths for npm distribution", async () => {
|
|
191
|
+
// Create test directory structure mimicking npm installation
|
|
192
|
+
const npmTestDir = join(testDir, "npm-install-test");
|
|
193
|
+
const fakeProjectRoot = join(npmTestDir, "user-project");
|
|
194
|
+
const packageRoot = join(npmTestDir, "node_modules", "aad");
|
|
195
|
+
const npmTemplateDir = join(packageRoot, "templates");
|
|
196
|
+
|
|
197
|
+
await mkdir(fakeProjectRoot, { recursive: true });
|
|
198
|
+
await mkdir(npmTemplateDir, { recursive: true });
|
|
199
|
+
await writeFile(join(npmTemplateDir, "CLAUDE.md"), "NPM template");
|
|
200
|
+
|
|
201
|
+
// Verify the actual project has templates/ at the root for npm distribution
|
|
202
|
+
// src/modules/git-workspace/__tests__/ -> ../../../../templates
|
|
203
|
+
const { existsSync } = await import("node:fs");
|
|
204
|
+
const actualProjectRoot = join(import.meta.dir, "..", "..", "..", "..");
|
|
205
|
+
const actualNpmTemplate = join(actualProjectRoot, "templates");
|
|
206
|
+
|
|
207
|
+
expect(existsSync(actualNpmTemplate)).toBe(true);
|
|
208
|
+
|
|
209
|
+
// Verify resolveTemplateDir returns templates/ when .aad/templates doesn't exist
|
|
210
|
+
const result = resolveTemplateDir(fakeProjectRoot);
|
|
211
|
+
// Should fallback to package templates/ since no local override exists
|
|
212
|
+
expect(result).toContain("templates");
|
|
213
|
+
});
|
|
189
214
|
});
|
|
@@ -109,6 +109,89 @@ describe("WorktreeManager", () => {
|
|
|
109
109
|
worktreeManager.createParentWorktree(runId, parentBranch)
|
|
110
110
|
).rejects.toThrow(GitWorkspaceError);
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
test("cleans up stale parent worktree before creating new one", async () => {
|
|
114
|
+
const runId = createRunId("run-003");
|
|
115
|
+
const parentBranch = "main";
|
|
116
|
+
const featureBranch = "feat/run-003/parent";
|
|
117
|
+
const calls: string[][] = [];
|
|
118
|
+
|
|
119
|
+
mockGitOps.gitExec = mock(async (args: string[]) => {
|
|
120
|
+
calls.push(args);
|
|
121
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await worktreeManager.createParentWorktree(runId, parentBranch, featureBranch);
|
|
125
|
+
|
|
126
|
+
// Should call worktree remove before worktree add
|
|
127
|
+
const removeIdx = calls.findIndex(c => c[0] === "worktree" && c[1] === "remove");
|
|
128
|
+
const addIdx = calls.findIndex(c => c[0] === "worktree" && c[1] === "add");
|
|
129
|
+
expect(removeIdx).toBeGreaterThanOrEqual(0);
|
|
130
|
+
expect(addIdx).toBeGreaterThan(removeIdx);
|
|
131
|
+
expect(calls[removeIdx]).toEqual(["worktree", "remove", "/test/worktrees/parent-run-003", "--force"]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("deletes stale parent branch before creating new one", async () => {
|
|
135
|
+
const runId = createRunId("run-004");
|
|
136
|
+
const parentBranch = "main";
|
|
137
|
+
const featureBranch = "feat/run-004/parent";
|
|
138
|
+
const calls: string[][] = [];
|
|
139
|
+
|
|
140
|
+
mockGitOps.gitExec = mock(async (args: string[]) => {
|
|
141
|
+
calls.push(args);
|
|
142
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await worktreeManager.createParentWorktree(runId, parentBranch, featureBranch);
|
|
146
|
+
|
|
147
|
+
const branchDeleteCall = calls.find(c => c[0] === "branch" && c[1] === "-D");
|
|
148
|
+
expect(branchDeleteCall).toBeDefined();
|
|
149
|
+
expect(branchDeleteCall![2]).toBe(featureBranch);
|
|
150
|
+
|
|
151
|
+
// Branch delete should happen before worktree add
|
|
152
|
+
const branchIdx = calls.indexOf(branchDeleteCall!);
|
|
153
|
+
const addIdx = calls.findIndex(c => c[0] === "worktree" && c[1] === "add");
|
|
154
|
+
expect(branchIdx).toBeLessThan(addIdx);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("handles gracefully when no stale parent worktree exists", async () => {
|
|
158
|
+
const runId = createRunId("run-005");
|
|
159
|
+
const parentBranch = "main";
|
|
160
|
+
const featureBranch = "feat/run-005/parent";
|
|
161
|
+
|
|
162
|
+
// Simulate: worktree remove fails, rm succeeds, prune succeeds, branch -D fails, add succeeds
|
|
163
|
+
mockGitOps.gitExec = mock(async (args: string[]) => {
|
|
164
|
+
if (args[1] === "remove") throw new Error("not a worktree");
|
|
165
|
+
if (args[0] === "branch" && args[1] === "-D") throw new Error("branch not found");
|
|
166
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Should not throw — cleanup errors are swallowed
|
|
170
|
+
const result = await worktreeManager.createParentWorktree(runId, parentBranch, featureBranch);
|
|
171
|
+
expect(result.worktreePath).toBe("/test/worktrees/parent-run-005");
|
|
172
|
+
expect(result.branch).toBe(featureBranch);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("calls git worktree prune during parent cleanup", async () => {
|
|
176
|
+
const runId = createRunId("run-006");
|
|
177
|
+
const parentBranch = "main";
|
|
178
|
+
const calls: string[][] = [];
|
|
179
|
+
|
|
180
|
+
mockGitOps.gitExec = mock(async (args: string[]) => {
|
|
181
|
+
calls.push(args);
|
|
182
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await worktreeManager.createParentWorktree(runId, parentBranch);
|
|
186
|
+
|
|
187
|
+
const pruneCall = calls.find(c => c[0] === "worktree" && c[1] === "prune");
|
|
188
|
+
expect(pruneCall).toBeDefined();
|
|
189
|
+
|
|
190
|
+
// Prune should happen before worktree add
|
|
191
|
+
const pruneIdx = calls.indexOf(pruneCall!);
|
|
192
|
+
const addIdx = calls.findIndex(c => c[0] === "worktree" && c[1] === "add");
|
|
193
|
+
expect(pruneIdx).toBeLessThan(addIdx);
|
|
194
|
+
});
|
|
112
195
|
});
|
|
113
196
|
|
|
114
197
|
describe("removeWorktree", () => {
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
import type { WorkspaceInfo } from "../../shared/types";
|
|
7
7
|
import type { Logger } from "pino";
|
|
8
|
+
import { resolveAvailableCommand } from "../../shared/utils";
|
|
9
|
+
|
|
10
|
+
// Re-export for external use
|
|
11
|
+
export { resolveAvailableCommand };
|
|
8
12
|
|
|
9
13
|
export interface InstallResult {
|
|
10
14
|
success: boolean;
|
|
@@ -16,7 +16,8 @@ const PROJECT_CONTEXT_HEADER = "# プロジェクト固有コンテキスト\n\n
|
|
|
16
16
|
/**
|
|
17
17
|
* Resolve template directory path:
|
|
18
18
|
* 1. Project local override: {projectRoot}/.aad/templates/ (if exists)
|
|
19
|
-
* 2. Package bundled: {packageRoot}/templates/
|
|
19
|
+
* 2. Package bundled (dev): {packageRoot}/.aad/templates/ (if exists)
|
|
20
|
+
* 3. Package bundled (npm): {packageRoot}/templates/ (for npm distribution)
|
|
20
21
|
*/
|
|
21
22
|
export function resolveTemplateDir(projectRoot: string): string {
|
|
22
23
|
const localTemplateDir = join(projectRoot, ".aad", "templates");
|
|
@@ -27,9 +28,17 @@ export function resolveTemplateDir(projectRoot: string): string {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// Fallback to package bundled templates
|
|
30
|
-
// src/modules/git-workspace/ ->
|
|
31
|
+
// src/modules/git-workspace/ -> ../../../
|
|
31
32
|
const packageRoot = join(import.meta.dir, "..", "..", "..");
|
|
32
|
-
|
|
33
|
+
|
|
34
|
+
// Try .aad/templates first (dev environment)
|
|
35
|
+
const devTemplateDir = join(packageRoot, ".aad", "templates");
|
|
36
|
+
if (existsSync(devTemplateDir)) {
|
|
37
|
+
return devTemplateDir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fallback to templates/ (npm distribution)
|
|
41
|
+
return join(packageRoot, "templates");
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
/**
|
|
@@ -127,6 +127,9 @@ export class WorktreeManager {
|
|
|
127
127
|
try {
|
|
128
128
|
await this.fsOps.mkdir(this.worktreeBase, { recursive: true });
|
|
129
129
|
|
|
130
|
+
// Clean up stale worktree/branch from previous runs
|
|
131
|
+
await this.cleanupStaleWorktree(worktreePath, branch);
|
|
132
|
+
|
|
130
133
|
// Create worktree with a new branch based on parentBranch
|
|
131
134
|
// e.g. git worktree add -b feat/auth-feature/parent <path> main
|
|
132
135
|
await this.gitOps.gitExec(
|
|
@@ -212,6 +212,53 @@ describe("buildTestCommand", () => {
|
|
|
212
212
|
// After fallback implementation, unknown should return npm test
|
|
213
213
|
expect(buildTestCommand(workspace)).toEqual(["npm", "test"]);
|
|
214
214
|
});
|
|
215
|
+
|
|
216
|
+
test("builds vitest with fallback when yarn is unavailable", async () => {
|
|
217
|
+
const workspace: WorkspaceInfo = {
|
|
218
|
+
path: "/path",
|
|
219
|
+
language: "typescript",
|
|
220
|
+
packageManager: "yarn",
|
|
221
|
+
framework: "vite",
|
|
222
|
+
testFramework: "vitest",
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Mock Bun.which to simulate yarn not available
|
|
226
|
+
const originalWhich = Bun.which;
|
|
227
|
+
Bun.which = (cmd: string) => {
|
|
228
|
+
if (cmd === "yarn") return null;
|
|
229
|
+
return originalWhich(cmd);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const result = await (await import("../phases/tester-verify")).buildTestCommandWithFallback(workspace);
|
|
234
|
+
expect(result).toEqual(["npx", "yarn", "test"]);
|
|
235
|
+
} finally {
|
|
236
|
+
Bun.which = originalWhich;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("builds jest with fallback when pnpm is unavailable", async () => {
|
|
241
|
+
const workspace: WorkspaceInfo = {
|
|
242
|
+
path: "/path",
|
|
243
|
+
language: "javascript",
|
|
244
|
+
packageManager: "pnpm",
|
|
245
|
+
framework: "react",
|
|
246
|
+
testFramework: "jest",
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const originalWhich = Bun.which;
|
|
250
|
+
Bun.which = (cmd: string) => {
|
|
251
|
+
if (cmd === "pnpm") return null;
|
|
252
|
+
return originalWhich(cmd);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const result = await (await import("../phases/tester-verify")).buildTestCommandWithFallback(workspace);
|
|
257
|
+
expect(result).toEqual(["npx", "pnpm", "test"]);
|
|
258
|
+
} finally {
|
|
259
|
+
Bun.which = originalWhich;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
215
262
|
});
|
|
216
263
|
|
|
217
264
|
describe("runTests", () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WorkspaceInfo } from "@aad/shared/types";
|
|
2
2
|
import { TestRunnerError } from "@aad/shared/errors";
|
|
3
|
+
import { resolveAvailableCommand } from "@aad/shared/utils";
|
|
3
4
|
|
|
4
5
|
export interface ProcessResult {
|
|
5
6
|
exitCode: number;
|
|
@@ -21,6 +22,95 @@ export interface TestResult {
|
|
|
21
22
|
error?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Build test command with fallback support for unavailable package managers
|
|
27
|
+
*/
|
|
28
|
+
export async function buildTestCommandWithFallback(workspace: WorkspaceInfo): Promise<string[]> {
|
|
29
|
+
const { testFramework, packageManager } = workspace;
|
|
30
|
+
|
|
31
|
+
switch (testFramework) {
|
|
32
|
+
case "bun-test":
|
|
33
|
+
return ["bun", "test"];
|
|
34
|
+
|
|
35
|
+
case "vitest": {
|
|
36
|
+
if (packageManager === "npm") return ["npm", "run", "test"];
|
|
37
|
+
if (packageManager === "yarn") {
|
|
38
|
+
const cmd = await resolveAvailableCommand("yarn");
|
|
39
|
+
return cmd === "npx" ? ["npx", "yarn", "test"] : ["yarn", "test"];
|
|
40
|
+
}
|
|
41
|
+
if (packageManager === "pnpm") {
|
|
42
|
+
const cmd = await resolveAvailableCommand("pnpm");
|
|
43
|
+
return cmd === "npx" ? ["npx", "pnpm", "test"] : ["pnpm", "test"];
|
|
44
|
+
}
|
|
45
|
+
return ["npx", "vitest", "run"];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case "jest": {
|
|
49
|
+
if (packageManager === "npm") return ["npm", "test"];
|
|
50
|
+
if (packageManager === "yarn") {
|
|
51
|
+
const cmd = await resolveAvailableCommand("yarn");
|
|
52
|
+
return cmd === "npx" ? ["npx", "yarn", "test"] : ["yarn", "test"];
|
|
53
|
+
}
|
|
54
|
+
if (packageManager === "pnpm") {
|
|
55
|
+
const cmd = await resolveAvailableCommand("pnpm");
|
|
56
|
+
return cmd === "npx" ? ["npx", "pnpm", "test"] : ["pnpm", "test"];
|
|
57
|
+
}
|
|
58
|
+
return ["npx", "jest"];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case "mocha": {
|
|
62
|
+
if (packageManager === "npm") return ["npm", "test"];
|
|
63
|
+
if (packageManager === "yarn") {
|
|
64
|
+
const cmd = await resolveAvailableCommand("yarn");
|
|
65
|
+
return cmd === "npx" ? ["npx", "yarn", "test"] : ["yarn", "test"];
|
|
66
|
+
}
|
|
67
|
+
return ["npx", "mocha"];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case "pytest": {
|
|
71
|
+
if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
|
|
72
|
+
if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
|
|
73
|
+
return ["pytest", "-v"];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case "go-test":
|
|
77
|
+
return ["go", "test", "./..."];
|
|
78
|
+
|
|
79
|
+
case "cargo":
|
|
80
|
+
return ["cargo", "test"];
|
|
81
|
+
|
|
82
|
+
case "maven":
|
|
83
|
+
return ["mvn", "test"];
|
|
84
|
+
|
|
85
|
+
case "gradle":
|
|
86
|
+
return ["./gradlew", "test"];
|
|
87
|
+
|
|
88
|
+
case "playwright":
|
|
89
|
+
return ["npx", "playwright", "test"];
|
|
90
|
+
|
|
91
|
+
case "terraform":
|
|
92
|
+
return ["terraform", "validate"];
|
|
93
|
+
|
|
94
|
+
case "unknown": {
|
|
95
|
+
if (packageManager === "bun") return ["bun", "test"];
|
|
96
|
+
if (packageManager === "npm") return ["npm", "test"];
|
|
97
|
+
if (packageManager === "yarn") return ["yarn", "test"];
|
|
98
|
+
if (packageManager === "pnpm") return ["pnpm", "test"];
|
|
99
|
+
if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
|
|
100
|
+
if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
|
|
101
|
+
return ["npm", "test"];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
default: {
|
|
105
|
+
const exhaustive: never = testFramework;
|
|
106
|
+
throw new TestRunnerError(
|
|
107
|
+
`Unsupported test framework: ${exhaustive}`,
|
|
108
|
+
{ testFramework }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
24
114
|
/**
|
|
25
115
|
* Build test command based on detected test framework
|
|
26
116
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { capitalize, toLowerCase, trim, reverse, toUpperCase } from "../utils";
|
|
2
|
+
import { capitalize, toLowerCase, trim, reverse, toUpperCase, resolveAvailableCommand } from "../utils";
|
|
3
3
|
|
|
4
4
|
describe("capitalize", () => {
|
|
5
5
|
test.each([
|
|
@@ -292,3 +292,23 @@ describe("toUpperCase", () => {
|
|
|
292
292
|
expect(toUpperCase(input)).toBe(expected);
|
|
293
293
|
});
|
|
294
294
|
});
|
|
295
|
+
|
|
296
|
+
describe("resolveAvailableCommand", () => {
|
|
297
|
+
test("returns command if binary exists in PATH", async () => {
|
|
298
|
+
// bun should exist in PATH during tests
|
|
299
|
+
const result = await resolveAvailableCommand("bun");
|
|
300
|
+
expect(result).toBe("bun");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("falls back to npx if binary does not exist in PATH", async () => {
|
|
304
|
+
// Use a non-existent binary name
|
|
305
|
+
const result = await resolveAvailableCommand("nonexistent-binary-xyz123");
|
|
306
|
+
expect(result).toBe("npx");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("handles common package managers", async () => {
|
|
310
|
+
// npm should always be available if bun is available
|
|
311
|
+
const npmResult = await resolveAvailableCommand("npm");
|
|
312
|
+
expect(npmResult).toBe("npm");
|
|
313
|
+
});
|
|
314
|
+
});
|
package/src/shared/utils.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
// Utility functions for shared functionality
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* コマンドのバイナリが $PATH に存在するか確認し、
|
|
5
|
+
* 存在しない場合は npx にフォールバックする
|
|
6
|
+
* @param command - The command to check
|
|
7
|
+
* @returns The command if available, otherwise "npx"
|
|
8
|
+
*/
|
|
9
|
+
export async function resolveAvailableCommand(command: string): Promise<string> {
|
|
10
|
+
const available = await Bun.which(command);
|
|
11
|
+
return available ? command : "npx";
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
/**
|
|
4
15
|
* Capitalizes the first character of a string.
|
|
5
16
|
* @param str - The string to capitalize
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { ClaudeSdkAdapter } from "../claude-sdk.adapter";
|
|
3
|
-
import { loadConfig } from "../../../shared/config";
|
|
4
|
-
import pino from "pino";
|
|
5
|
-
import { ClaudeProviderError } from "../../../shared/errors";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 実環境バリデーションテスト
|
|
9
|
-
* ANTHROPIC_API_KEY または CLAUDE_CODE_OAUTH_TOKEN が設定されている場合のみ実行
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
13
|
-
const hasOAuth = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
14
|
-
const hasAuth = hasApiKey || hasOAuth;
|
|
15
|
-
|
|
16
|
-
const logger = pino({ level: "silent" });
|
|
17
|
-
|
|
18
|
-
function createAdapter() {
|
|
19
|
-
const config = loadConfig();
|
|
20
|
-
return new ClaudeSdkAdapter(config, logger);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe.skipIf(!hasAuth)("ClaudeSdkAdapter - Real Environment", () => {
|
|
24
|
-
test("simple query returns valid response", async () => {
|
|
25
|
-
const adapter = createAdapter();
|
|
26
|
-
const response = await adapter.call({
|
|
27
|
-
prompt: "Reply with exactly: PING",
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
expect(response.result).toBeDefined();
|
|
31
|
-
expect(response.result.length).toBeGreaterThan(0);
|
|
32
|
-
expect(response.exitCode).toBe(0);
|
|
33
|
-
expect(response.model).toBeDefined();
|
|
34
|
-
expect(response.duration).toBeGreaterThan(0);
|
|
35
|
-
}, 30_000);
|
|
36
|
-
|
|
37
|
-
test("detects authentication method", () => {
|
|
38
|
-
if (hasApiKey) {
|
|
39
|
-
expect(process.env.ANTHROPIC_API_KEY).toBeDefined();
|
|
40
|
-
console.log("Auth: ANTHROPIC_API_KEY");
|
|
41
|
-
}
|
|
42
|
-
if (hasOAuth) {
|
|
43
|
-
expect(process.env.CLAUDE_CODE_OAUTH_TOKEN).toBeDefined();
|
|
44
|
-
console.log("Auth: CLAUDE_CODE_OAUTH_TOKEN");
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("throws ClaudeProviderError on invalid auth", async () => {
|
|
49
|
-
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
50
|
-
const originalOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
// 無効な認証情報に差し替え
|
|
54
|
-
process.env.ANTHROPIC_API_KEY = "sk-ant-invalid-key-for-testing";
|
|
55
|
-
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
56
|
-
|
|
57
|
-
const adapter = createAdapter();
|
|
58
|
-
await expect(
|
|
59
|
-
adapter.call({ prompt: "test" })
|
|
60
|
-
).rejects.toThrow(ClaudeProviderError);
|
|
61
|
-
} finally {
|
|
62
|
-
// 復元
|
|
63
|
-
if (originalKey) process.env.ANTHROPIC_API_KEY = originalKey;
|
|
64
|
-
else delete process.env.ANTHROPIC_API_KEY;
|
|
65
|
-
if (originalOAuth) process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuth;
|
|
66
|
-
else delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
67
|
-
}
|
|
68
|
-
}, 15_000);
|
|
69
|
-
|
|
70
|
-
test("respects timeout/abort", async () => {
|
|
71
|
-
const adapter = createAdapter();
|
|
72
|
-
|
|
73
|
-
await expect(
|
|
74
|
-
adapter.call({
|
|
75
|
-
prompt: "Write a very long essay about the history of computing.",
|
|
76
|
-
timeout: 100, // 100ms — 即タイムアウト
|
|
77
|
-
})
|
|
78
|
-
).rejects.toThrow();
|
|
79
|
-
}, 10_000);
|
|
80
|
-
|
|
81
|
-
test("passes effort level correctly", async () => {
|
|
82
|
-
const adapter = createAdapter();
|
|
83
|
-
const response = await adapter.call({
|
|
84
|
-
prompt: "Reply with exactly: OK",
|
|
85
|
-
effortLevel: "low",
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
expect(response.result).toBeDefined();
|
|
89
|
-
expect(response.exitCode).toBe(0);
|
|
90
|
-
expect(response.effortLevel).toBe("low");
|
|
91
|
-
}, 30_000);
|
|
92
|
-
|
|
93
|
-
test("respects model override", async () => {
|
|
94
|
-
const adapter = createAdapter();
|
|
95
|
-
const response = await adapter.call({
|
|
96
|
-
prompt: "Reply with exactly: HI",
|
|
97
|
-
model: "claude-haiku-4-5-20251001",
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
expect(response.result).toBeDefined();
|
|
101
|
-
expect(response.exitCode).toBe(0);
|
|
102
|
-
// model名はSDKが返す値に依存するが、haiku系であることを確認
|
|
103
|
-
expect(response.model).toContain("haiku");
|
|
104
|
-
}, 30_000);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe.skipIf(!hasOAuth)("ClaudeSdkAdapter - OAuth Authentication", () => {
|
|
108
|
-
test("authenticates with CLAUDE_CODE_OAUTH_TOKEN", async () => {
|
|
109
|
-
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
// API Keyを一時的に除外してOAuthのみで認証
|
|
113
|
-
delete process.env.ANTHROPIC_API_KEY;
|
|
114
|
-
|
|
115
|
-
const adapter = createAdapter();
|
|
116
|
-
const response = await adapter.call({
|
|
117
|
-
prompt: "Reply with exactly: OAUTH_OK",
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
expect(response.result).toBeDefined();
|
|
121
|
-
expect(response.exitCode).toBe(0);
|
|
122
|
-
} finally {
|
|
123
|
-
if (originalKey) process.env.ANTHROPIC_API_KEY = originalKey;
|
|
124
|
-
else delete process.env.ANTHROPIC_API_KEY;
|
|
125
|
-
}
|
|
126
|
-
}, 30_000);
|
|
127
|
-
});
|