@ronkovic/aad 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- 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 +41 -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 +9 -2
- 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 +85 -38
- package/src/modules/task-execution/index.ts +1 -1
- package/src/modules/task-execution/phases/tester-verify.ts +31 -14
- package/src/shared/__tests__/utils.test.ts +21 -1
- package/src/shared/utils.ts +11 -0
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
NPM template
|
|
@@ -75,3 +75,44 @@ 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
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("installDependencies integration", () => {
|
|
99
|
+
test("integrates with resolveAvailableCommand", async () => {
|
|
100
|
+
// This test verifies that installDependencies calls resolveAvailableCommand
|
|
101
|
+
// Actual fallback behavior is tested in resolveAvailableCommand tests above
|
|
102
|
+
const { buildInstallCommand } = await import("../dependency-installer");
|
|
103
|
+
|
|
104
|
+
const workspace: WorkspaceInfo = {
|
|
105
|
+
path: "/tmp/test",
|
|
106
|
+
language: "javascript",
|
|
107
|
+
packageManager: "yarn",
|
|
108
|
+
framework: "react",
|
|
109
|
+
testFramework: "jest",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const command = buildInstallCommand(workspace);
|
|
113
|
+
expect(command).toEqual(["yarn", "install", "--frozen-lockfile"]);
|
|
114
|
+
|
|
115
|
+
// The actual fallback is applied at runtime in installDependencies
|
|
116
|
+
// when yarn is not available in PATH, resolveAvailableCommand returns "npx"
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -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;
|
|
@@ -72,11 +76,14 @@ export async function installDependencies(
|
|
|
72
76
|
return { success: true, output: "", duration: 0, skipped: true };
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
const [
|
|
79
|
+
const [rawCmd, ...args] = command;
|
|
80
|
+
|
|
81
|
+
// Resolve command with fallback (e.g., yarn → npx if yarn not found)
|
|
82
|
+
const cmd = await resolveAvailableCommand(rawCmd!);
|
|
76
83
|
logger.info({ cmd, args, cwd: workspace.path }, "Installing dependencies");
|
|
77
84
|
|
|
78
85
|
// Bun.spawn で実行(default-spawner.ts と同パターン)
|
|
79
|
-
const proc = Bun.spawn([cmd
|
|
86
|
+
const proc = Bun.spawn([cmd, ...args], {
|
|
80
87
|
cwd: workspace.path,
|
|
81
88
|
stdout: "pipe",
|
|
82
89
|
stderr: "pipe",
|
|
@@ -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(
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
buildTestCommandWithFallback,
|
|
4
4
|
runTests,
|
|
5
5
|
type ProcessSpawner,
|
|
6
6
|
} from "../phases/tester-verify";
|
|
7
7
|
import type { WorkspaceInfo } from "@aad/shared/types";
|
|
8
8
|
|
|
9
|
-
describe("
|
|
10
|
-
test("builds bun test command", () => {
|
|
9
|
+
describe("buildTestCommandWithFallback", () => {
|
|
10
|
+
test("builds bun test command", async () => {
|
|
11
11
|
const workspace: WorkspaceInfo = {
|
|
12
12
|
path: "/path/to/workspace",
|
|
13
13
|
language: "typescript",
|
|
@@ -16,10 +16,10 @@ describe("buildTestCommand", () => {
|
|
|
16
16
|
testFramework: "bun-test",
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
expect(
|
|
19
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["bun", "test"]);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
test("builds vitest command", () => {
|
|
22
|
+
test("builds vitest command", async () => {
|
|
23
23
|
const workspace: WorkspaceInfo = {
|
|
24
24
|
path: "/path/to/workspace",
|
|
25
25
|
language: "typescript",
|
|
@@ -28,10 +28,10 @@ describe("buildTestCommand", () => {
|
|
|
28
28
|
testFramework: "vitest",
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
expect(
|
|
31
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["npm", "run", "test"]);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
test("builds jest command", () => {
|
|
34
|
+
test("builds jest command", async () => {
|
|
35
35
|
const workspace: WorkspaceInfo = {
|
|
36
36
|
path: "/path/to/workspace",
|
|
37
37
|
language: "javascript",
|
|
@@ -40,10 +40,10 @@ describe("buildTestCommand", () => {
|
|
|
40
40
|
testFramework: "jest",
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
expect(
|
|
43
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["yarn", "test"]);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
test("builds pytest command", () => {
|
|
46
|
+
test("builds pytest command", async () => {
|
|
47
47
|
const workspace: WorkspaceInfo = {
|
|
48
48
|
path: "/path/to/workspace",
|
|
49
49
|
language: "python",
|
|
@@ -52,10 +52,10 @@ describe("buildTestCommand", () => {
|
|
|
52
52
|
testFramework: "pytest",
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
expect(
|
|
55
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["pytest", "-v"]);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
test("builds go test command", () => {
|
|
58
|
+
test("builds go test command", async () => {
|
|
59
59
|
const workspace: WorkspaceInfo = {
|
|
60
60
|
path: "/path/to/workspace",
|
|
61
61
|
language: "go",
|
|
@@ -64,10 +64,10 @@ describe("buildTestCommand", () => {
|
|
|
64
64
|
testFramework: "go-test",
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
expect(
|
|
67
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["go", "test", "./..."]);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
test("builds cargo test command", () => {
|
|
70
|
+
test("builds cargo test command", async () => {
|
|
71
71
|
const workspace: WorkspaceInfo = {
|
|
72
72
|
path: "/path/to/workspace",
|
|
73
73
|
language: "rust",
|
|
@@ -76,10 +76,10 @@ describe("buildTestCommand", () => {
|
|
|
76
76
|
testFramework: "cargo",
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
expect(
|
|
79
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["cargo", "test"]);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
test("builds vitest command with yarn", () => {
|
|
82
|
+
test("builds vitest command with yarn", async () => {
|
|
83
83
|
const workspace: WorkspaceInfo = {
|
|
84
84
|
path: "/path",
|
|
85
85
|
language: "typescript",
|
|
@@ -87,10 +87,10 @@ describe("buildTestCommand", () => {
|
|
|
87
87
|
framework: "vite",
|
|
88
88
|
testFramework: "vitest",
|
|
89
89
|
};
|
|
90
|
-
expect(
|
|
90
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["yarn", "test"]);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
test("builds vitest command with pnpm", () => {
|
|
93
|
+
test("builds vitest command with pnpm", async () => {
|
|
94
94
|
const workspace: WorkspaceInfo = {
|
|
95
95
|
path: "/path",
|
|
96
96
|
language: "typescript",
|
|
@@ -98,10 +98,10 @@ describe("buildTestCommand", () => {
|
|
|
98
98
|
framework: "vite",
|
|
99
99
|
testFramework: "vitest",
|
|
100
100
|
};
|
|
101
|
-
expect(
|
|
101
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["pnpm", "test"]);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
test("builds vitest command with default (npx)", () => {
|
|
104
|
+
test("builds vitest command with default (npx)", async () => {
|
|
105
105
|
const workspace: WorkspaceInfo = {
|
|
106
106
|
path: "/path",
|
|
107
107
|
language: "typescript",
|
|
@@ -109,10 +109,10 @@ describe("buildTestCommand", () => {
|
|
|
109
109
|
framework: "vite",
|
|
110
110
|
testFramework: "vitest",
|
|
111
111
|
};
|
|
112
|
-
expect(
|
|
112
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["npx", "vitest", "run"]);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
test("builds jest command with npm", () => {
|
|
115
|
+
test("builds jest command with npm", async () => {
|
|
116
116
|
const workspace: WorkspaceInfo = {
|
|
117
117
|
path: "/path",
|
|
118
118
|
language: "javascript",
|
|
@@ -120,10 +120,10 @@ describe("buildTestCommand", () => {
|
|
|
120
120
|
framework: "react",
|
|
121
121
|
testFramework: "jest",
|
|
122
122
|
};
|
|
123
|
-
expect(
|
|
123
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["npm", "test"]);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
test("builds jest command with pnpm", () => {
|
|
126
|
+
test("builds jest command with pnpm", async () => {
|
|
127
127
|
const workspace: WorkspaceInfo = {
|
|
128
128
|
path: "/path",
|
|
129
129
|
language: "javascript",
|
|
@@ -131,10 +131,10 @@ describe("buildTestCommand", () => {
|
|
|
131
131
|
framework: "react",
|
|
132
132
|
testFramework: "jest",
|
|
133
133
|
};
|
|
134
|
-
expect(
|
|
134
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["pnpm", "test"]);
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
test("builds jest command with default (npx)", () => {
|
|
137
|
+
test("builds jest command with default (npx)", async () => {
|
|
138
138
|
const workspace: WorkspaceInfo = {
|
|
139
139
|
path: "/path",
|
|
140
140
|
language: "javascript",
|
|
@@ -142,10 +142,10 @@ describe("buildTestCommand", () => {
|
|
|
142
142
|
framework: "react",
|
|
143
143
|
testFramework: "jest",
|
|
144
144
|
};
|
|
145
|
-
expect(
|
|
145
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["npx", "jest"]);
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
test("builds mocha command with npm", () => {
|
|
148
|
+
test("builds mocha command with npm", async () => {
|
|
149
149
|
const workspace: WorkspaceInfo = {
|
|
150
150
|
path: "/path",
|
|
151
151
|
language: "javascript",
|
|
@@ -153,10 +153,10 @@ describe("buildTestCommand", () => {
|
|
|
153
153
|
framework: "express",
|
|
154
154
|
testFramework: "mocha",
|
|
155
155
|
};
|
|
156
|
-
expect(
|
|
156
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["npm", "test"]);
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
-
test("builds mocha command with yarn", () => {
|
|
159
|
+
test("builds mocha command with yarn", async () => {
|
|
160
160
|
const workspace: WorkspaceInfo = {
|
|
161
161
|
path: "/path",
|
|
162
162
|
language: "javascript",
|
|
@@ -164,10 +164,10 @@ describe("buildTestCommand", () => {
|
|
|
164
164
|
framework: "express",
|
|
165
165
|
testFramework: "mocha",
|
|
166
166
|
};
|
|
167
|
-
expect(
|
|
167
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["yarn", "test"]);
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
-
test("builds mocha command with default (npx)", () => {
|
|
170
|
+
test("builds mocha command with default (npx)", async () => {
|
|
171
171
|
const workspace: WorkspaceInfo = {
|
|
172
172
|
path: "/path",
|
|
173
173
|
language: "javascript",
|
|
@@ -175,10 +175,10 @@ describe("buildTestCommand", () => {
|
|
|
175
175
|
framework: "express",
|
|
176
176
|
testFramework: "mocha",
|
|
177
177
|
};
|
|
178
|
-
expect(
|
|
178
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["npx", "mocha"]);
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
-
test("builds maven test command", () => {
|
|
181
|
+
test("builds maven test command", async () => {
|
|
182
182
|
const workspace: WorkspaceInfo = {
|
|
183
183
|
path: "/path",
|
|
184
184
|
language: "java",
|
|
@@ -186,10 +186,10 @@ describe("buildTestCommand", () => {
|
|
|
186
186
|
framework: "spring",
|
|
187
187
|
testFramework: "maven",
|
|
188
188
|
};
|
|
189
|
-
expect(
|
|
189
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["mvn", "test"]);
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
-
test("builds gradle test command", () => {
|
|
192
|
+
test("builds gradle test command", async () => {
|
|
193
193
|
const workspace: WorkspaceInfo = {
|
|
194
194
|
path: "/path",
|
|
195
195
|
language: "java",
|
|
@@ -197,10 +197,10 @@ describe("buildTestCommand", () => {
|
|
|
197
197
|
framework: "spring",
|
|
198
198
|
testFramework: "gradle",
|
|
199
199
|
};
|
|
200
|
-
expect(
|
|
200
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["./gradlew", "test"]);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
-
test("returns fallback for unknown test framework", () => {
|
|
203
|
+
test("returns fallback for unknown test framework", async () => {
|
|
204
204
|
const workspace: WorkspaceInfo = {
|
|
205
205
|
path: "/path/to/workspace",
|
|
206
206
|
language: "unknown",
|
|
@@ -210,7 +210,54 @@ describe("buildTestCommand", () => {
|
|
|
210
210
|
};
|
|
211
211
|
|
|
212
212
|
// After fallback implementation, unknown should return npm test
|
|
213
|
-
expect(
|
|
213
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["npm", "test"]);
|
|
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 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 buildTestCommandWithFallback(workspace);
|
|
257
|
+
expect(result).toEqual(["npx", "pnpm", "test"]);
|
|
258
|
+
} finally {
|
|
259
|
+
Bun.which = originalWhich;
|
|
260
|
+
}
|
|
214
261
|
});
|
|
215
262
|
});
|
|
216
263
|
|
|
@@ -14,7 +14,7 @@ export {
|
|
|
14
14
|
} from "./phases/implementer-green";
|
|
15
15
|
export type { ImplementerGreenOptions } from "./phases/implementer-green";
|
|
16
16
|
|
|
17
|
-
export { runTests,
|
|
17
|
+
export { runTests, buildTestCommandWithFallback } from "./phases/tester-verify";
|
|
18
18
|
export type {
|
|
19
19
|
TestResult,
|
|
20
20
|
ProcessSpawner,
|
|
@@ -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;
|
|
@@ -22,34 +23,51 @@ export interface TestResult {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Build test command
|
|
26
|
+
* Build test command with fallback support for unavailable package managers
|
|
26
27
|
*/
|
|
27
|
-
export function
|
|
28
|
+
export async function buildTestCommandWithFallback(workspace: WorkspaceInfo): Promise<string[]> {
|
|
28
29
|
const { testFramework, packageManager } = workspace;
|
|
29
30
|
|
|
30
31
|
switch (testFramework) {
|
|
31
32
|
case "bun-test":
|
|
32
33
|
return ["bun", "test"];
|
|
33
34
|
|
|
34
|
-
case "vitest":
|
|
35
|
+
case "vitest": {
|
|
35
36
|
if (packageManager === "npm") return ["npm", "run", "test"];
|
|
36
|
-
if (packageManager === "yarn")
|
|
37
|
-
|
|
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
|
+
}
|
|
38
45
|
return ["npx", "vitest", "run"];
|
|
46
|
+
}
|
|
39
47
|
|
|
40
|
-
case "jest":
|
|
48
|
+
case "jest": {
|
|
41
49
|
if (packageManager === "npm") return ["npm", "test"];
|
|
42
|
-
if (packageManager === "yarn")
|
|
43
|
-
|
|
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
|
+
}
|
|
44
58
|
return ["npx", "jest"];
|
|
59
|
+
}
|
|
45
60
|
|
|
46
|
-
case "mocha":
|
|
61
|
+
case "mocha": {
|
|
47
62
|
if (packageManager === "npm") return ["npm", "test"];
|
|
48
|
-
if (packageManager === "yarn")
|
|
63
|
+
if (packageManager === "yarn") {
|
|
64
|
+
const cmd = await resolveAvailableCommand("yarn");
|
|
65
|
+
return cmd === "npx" ? ["npx", "yarn", "test"] : ["yarn", "test"];
|
|
66
|
+
}
|
|
49
67
|
return ["npx", "mocha"];
|
|
68
|
+
}
|
|
50
69
|
|
|
51
70
|
case "pytest": {
|
|
52
|
-
const { packageManager } = workspace;
|
|
53
71
|
if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
|
|
54
72
|
if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
|
|
55
73
|
return ["pytest", "-v"];
|
|
@@ -74,8 +92,6 @@ export function buildTestCommand(workspace: WorkspaceInfo): string[] {
|
|
|
74
92
|
return ["terraform", "validate"];
|
|
75
93
|
|
|
76
94
|
case "unknown": {
|
|
77
|
-
// Fallback: use package manager-based test command
|
|
78
|
-
const { packageManager } = workspace;
|
|
79
95
|
if (packageManager === "bun") return ["bun", "test"];
|
|
80
96
|
if (packageManager === "npm") return ["npm", "test"];
|
|
81
97
|
if (packageManager === "yarn") return ["yarn", "test"];
|
|
@@ -95,6 +111,7 @@ export function buildTestCommand(workspace: WorkspaceInfo): string[] {
|
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
113
|
|
|
114
|
+
|
|
98
115
|
/**
|
|
99
116
|
* Run tests in workspace and return result
|
|
100
117
|
* @param workspace - Workspace information with test framework
|
|
@@ -106,7 +123,7 @@ export async function runTests(
|
|
|
106
123
|
spawner?: ProcessSpawner,
|
|
107
124
|
timeout = 300000
|
|
108
125
|
): Promise<TestResult> {
|
|
109
|
-
const command =
|
|
126
|
+
const command = await buildTestCommandWithFallback(workspace);
|
|
110
127
|
const [cmd, ...args] = command;
|
|
111
128
|
|
|
112
129
|
if (!cmd) {
|
|
@@ -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
|