@ronkovic/aad 0.5.1 → 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/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/package.json
CHANGED
|
@@ -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
|