@towles/tool 0.0.65 → 0.0.67
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/commands/auto-claude/retry.test.ts +8 -11
- package/src/commands/auto-claude/retry.ts +4 -2
- package/src/lib/auto-claude/claude-cli.ts +35 -16
- package/src/lib/auto-claude/labels.test.ts +10 -14
- package/src/lib/auto-claude/labels.ts +22 -23
- package/src/lib/auto-claude/pipeline-execution.test.ts +33 -39
- package/src/lib/auto-claude/pipeline.ts +42 -20
- package/src/lib/auto-claude/run-claude.test.ts +40 -28
- package/src/lib/auto-claude/spawn-claude.ts +2 -0
- package/src/lib/auto-claude/steps/create-pr.ts +58 -55
- package/src/lib/auto-claude/steps/implement.ts +3 -1
- package/src/lib/auto-claude/steps/simple-steps.ts +7 -3
- package/src/lib/auto-claude/steps/steps.test.ts +58 -31
- package/src/lib/auto-claude/templates.test.ts +28 -29
- package/src/lib/auto-claude/templates.ts +21 -4
- package/src/lib/auto-claude/utils.ts +4 -1
- package/src/lib/graph/parser.test.ts +21 -25
- package/src/lib/graph/parser.ts +14 -5
- package/src/utils/git/gh-cli-wrapper.test.ts +16 -15
- package/src/utils/git/gh-cli-wrapper.ts +18 -5
|
@@ -2,15 +2,14 @@ import consola from "consola";
|
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { initConfig } from "./config";
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
5
|
+
import { createMockClaudeProcess, createTestRepo } from "./test-helpers";
|
|
6
|
+
import type { TestRepo } from "./test-helpers";
|
|
7
|
+
import type { SpawnClaudeFn } from "./spawn-claude";
|
|
8
|
+
import type { ClaudeLogger } from "./claude-cli";
|
|
7
9
|
|
|
8
10
|
consola.level = -999;
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
vi.mock("./spawn-claude", () => createSpawnClaudeMock(() => mockSpawnImpl));
|
|
12
|
-
|
|
13
|
-
describe("runClaude (mocked spawn-claude)", () => {
|
|
12
|
+
describe("runClaude (injected spawnFn)", () => {
|
|
14
13
|
let originalCwd: string;
|
|
15
14
|
let repo: TestRepo;
|
|
16
15
|
|
|
@@ -26,19 +25,20 @@ describe("runClaude (mocked spawn-claude)", () => {
|
|
|
26
25
|
});
|
|
27
26
|
|
|
28
27
|
beforeEach(() => {
|
|
29
|
-
mockSpawnImpl = null;
|
|
30
28
|
vi.clearAllMocks();
|
|
31
29
|
});
|
|
32
30
|
|
|
33
31
|
it("parses stream-json result event", async () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
const mockSpawnClaude: SpawnClaudeFn = vi.fn((args: string[]) => {
|
|
33
|
+
return createMockClaudeProcess(
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
result: "All done",
|
|
36
|
+
is_error: false,
|
|
37
|
+
total_cost_usd: 0.05,
|
|
38
|
+
num_turns: 3,
|
|
39
|
+
}),
|
|
40
|
+
0,
|
|
41
|
+
);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
await initConfig({ repo: "test/repo", mainBranch: "main" });
|
|
@@ -48,14 +48,14 @@ describe("runClaude (mocked spawn-claude)", () => {
|
|
|
48
48
|
const result = await runClaude({
|
|
49
49
|
promptFile: "test-prompt.md",
|
|
50
50
|
maxTurns: 10,
|
|
51
|
+
spawnFn: mockSpawnClaude,
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
expect(result.result).toBe("All done");
|
|
54
55
|
expect(result.is_error).toBe(false);
|
|
55
56
|
expect(result.num_turns).toBe(3);
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
expect(spawnClaude).toHaveBeenCalledWith(
|
|
58
|
+
expect(mockSpawnClaude).toHaveBeenCalledWith(
|
|
59
59
|
expect.arrayContaining([
|
|
60
60
|
"-p",
|
|
61
61
|
"--output-format",
|
|
@@ -71,7 +71,13 @@ describe("runClaude (mocked spawn-claude)", () => {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
it("logs tool names from stream_event and shows thinking indicator", async () => {
|
|
74
|
-
const
|
|
74
|
+
const mockLogger: ClaudeLogger = {
|
|
75
|
+
info: vi.fn(),
|
|
76
|
+
warn: vi.fn(),
|
|
77
|
+
error: vi.fn(),
|
|
78
|
+
success: vi.fn(),
|
|
79
|
+
log: vi.fn(),
|
|
80
|
+
};
|
|
75
81
|
|
|
76
82
|
const thinkingEvent = {
|
|
77
83
|
type: "stream_event",
|
|
@@ -94,30 +100,35 @@ describe("runClaude (mocked spawn-claude)", () => {
|
|
|
94
100
|
num_turns: 1,
|
|
95
101
|
};
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
const mockSpawnClaude: SpawnClaudeFn = vi.fn(() => {
|
|
104
|
+
return createMockClaudeProcess(
|
|
105
|
+
[thinkingEvent, toolEvent, resultEvent].map((e) => JSON.stringify(e)).join("\n"),
|
|
106
|
+
0,
|
|
107
|
+
);
|
|
100
108
|
});
|
|
101
109
|
|
|
102
110
|
await initConfig({ repo: "test/repo", mainBranch: "main" });
|
|
103
111
|
|
|
104
112
|
const { runClaude } = await import("./claude-cli");
|
|
105
|
-
const result = await runClaude({
|
|
113
|
+
const result = await runClaude({
|
|
114
|
+
promptFile: "test.md",
|
|
115
|
+
spawnFn: mockSpawnClaude,
|
|
116
|
+
logger: mockLogger,
|
|
117
|
+
});
|
|
106
118
|
|
|
107
119
|
expect(result.result).toBe("Done");
|
|
108
120
|
expect(result.num_turns).toBe(1);
|
|
109
121
|
|
|
110
|
-
const infoCalls =
|
|
122
|
+
const infoCalls = (mockLogger.info as ReturnType<typeof vi.fn>).mock.calls.map((c) =>
|
|
123
|
+
String(c[0]),
|
|
124
|
+
);
|
|
111
125
|
expect(infoCalls.some((msg) => msg.includes("thinking"))).toBe(true);
|
|
112
126
|
expect(infoCalls.some((msg) => msg.includes("Edit"))).toBe(true);
|
|
113
|
-
|
|
114
|
-
infoSpy.mockRestore();
|
|
115
127
|
});
|
|
116
128
|
|
|
117
129
|
it("returns fallback when no result event in stream", async () => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
exitCode: 0,
|
|
130
|
+
const mockSpawnClaude: SpawnClaudeFn = vi.fn(() => {
|
|
131
|
+
return createMockClaudeProcess('{"type":"system","message":"starting"}', 0);
|
|
121
132
|
});
|
|
122
133
|
|
|
123
134
|
await initConfig({ repo: "test/repo", mainBranch: "main" });
|
|
@@ -126,6 +137,7 @@ describe("runClaude (mocked spawn-claude)", () => {
|
|
|
126
137
|
|
|
127
138
|
const result = await runClaude({
|
|
128
139
|
promptFile: "test.md",
|
|
140
|
+
spawnFn: mockSpawnClaude,
|
|
129
141
|
});
|
|
130
142
|
|
|
131
143
|
expect(result.result).toBe("");
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import type { ChildProcess } from "node:child_process";
|
|
3
3
|
|
|
4
|
+
export type SpawnClaudeFn = (args: string[]) => ChildProcess;
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Spawns the Claude CLI as a child process.
|
|
6
8
|
* Extracted into its own module so tests can mock it cleanly
|
|
@@ -9,11 +9,12 @@ import { getConfig } from "../config.js";
|
|
|
9
9
|
import { ARTIFACTS } from "../prompt-templates/index.js";
|
|
10
10
|
import { log } from "../utils.js";
|
|
11
11
|
import type { IssueContext } from "../utils.js";
|
|
12
|
+
import type { ExecSafeFn } from "../labels.js";
|
|
12
13
|
|
|
13
|
-
export async function createPr(ctx: IssueContext): Promise<string> {
|
|
14
|
+
export async function createPr(ctx: IssueContext, exec?: ExecSafeFn): Promise<string> {
|
|
14
15
|
const cfg = getConfig();
|
|
15
16
|
|
|
16
|
-
const existingUrl = await getExistingPrUrl(ctx.branch);
|
|
17
|
+
const existingUrl = await getExistingPrUrl(ctx.branch, exec);
|
|
17
18
|
if (existingUrl) {
|
|
18
19
|
log(`PR already exists: ${existingUrl}`);
|
|
19
20
|
return existingUrl;
|
|
@@ -48,18 +49,21 @@ export async function createPr(ctx: IssueContext): Promise<string> {
|
|
|
48
49
|
"Generated by auto-claude pipeline",
|
|
49
50
|
].join("\n");
|
|
50
51
|
|
|
51
|
-
const prUrl = await ghRaw(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
const prUrl = await ghRaw(
|
|
53
|
+
[
|
|
54
|
+
"pr",
|
|
55
|
+
"create",
|
|
56
|
+
"--repo",
|
|
57
|
+
cfg.repo,
|
|
58
|
+
"--head",
|
|
59
|
+
ctx.branch,
|
|
60
|
+
"--title",
|
|
61
|
+
ctx.title,
|
|
62
|
+
"--body",
|
|
63
|
+
body,
|
|
64
|
+
],
|
|
65
|
+
exec,
|
|
66
|
+
);
|
|
63
67
|
|
|
64
68
|
if (!prUrl) {
|
|
65
69
|
throw new Error("Failed to create PR — gh returned empty output");
|
|
@@ -69,7 +73,7 @@ export async function createPr(ctx: IssueContext): Promise<string> {
|
|
|
69
73
|
log(`PR created: ${prUrl}`);
|
|
70
74
|
|
|
71
75
|
try {
|
|
72
|
-
await attachArtifacts(ctx, prUrl);
|
|
76
|
+
await attachArtifacts(ctx, prUrl, exec);
|
|
73
77
|
} catch (e) {
|
|
74
78
|
consola.warn(`Artifact upload failed (non-blocking): ${e}`);
|
|
75
79
|
}
|
|
@@ -77,7 +81,7 @@ export async function createPr(ctx: IssueContext): Promise<string> {
|
|
|
77
81
|
return prUrl;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
async function attachArtifacts(ctx: IssueContext, prUrl: string): Promise<void> {
|
|
84
|
+
async function attachArtifacts(ctx: IssueContext, prUrl: string, exec?: ExecSafeFn): Promise<void> {
|
|
81
85
|
const archivePath = join(ctx.issueDir, "artifacts.tar.gz");
|
|
82
86
|
const tag = `ac-issue-${ctx.number}`;
|
|
83
87
|
const cfg = getConfig();
|
|
@@ -94,31 +98,27 @@ async function attachArtifacts(ctx: IssueContext, prUrl: string): Promise<void>
|
|
|
94
98
|
]);
|
|
95
99
|
|
|
96
100
|
log("Uploading artifacts to GitHub release…");
|
|
97
|
-
await ghRaw(["release", "delete", tag, "--yes", "--repo", cfg.repo]);
|
|
98
|
-
|
|
99
|
-
await ghRaw(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"--json",
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
".assets[0].url",
|
|
119
|
-
"--repo",
|
|
120
|
-
cfg.repo,
|
|
121
|
-
]);
|
|
101
|
+
await ghRaw(["release", "delete", tag, "--yes", "--repo", cfg.repo], exec);
|
|
102
|
+
|
|
103
|
+
await ghRaw(
|
|
104
|
+
[
|
|
105
|
+
"release",
|
|
106
|
+
"create",
|
|
107
|
+
tag,
|
|
108
|
+
"--prerelease",
|
|
109
|
+
"--title",
|
|
110
|
+
`Artifacts: #${ctx.number}`,
|
|
111
|
+
"--repo",
|
|
112
|
+
cfg.repo,
|
|
113
|
+
archivePath,
|
|
114
|
+
],
|
|
115
|
+
exec,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const assetUrl = await ghRaw(
|
|
119
|
+
["release", "view", tag, "--json", "assets", "--jq", ".assets[0].url", "--repo", cfg.repo],
|
|
120
|
+
exec,
|
|
121
|
+
);
|
|
122
122
|
|
|
123
123
|
if (!assetUrl) {
|
|
124
124
|
consola.warn("Could not get artifact download URL — skipping PR comment");
|
|
@@ -133,22 +133,25 @@ async function attachArtifacts(ctx: IssueContext, prUrl: string): Promise<void>
|
|
|
133
133
|
"Contains: plan.md, completed-summary.md, simplify-summary.md, review.md",
|
|
134
134
|
].join("\n");
|
|
135
135
|
|
|
136
|
-
await ghRaw(["pr", "comment", prUrl, "--body", comment]);
|
|
136
|
+
await ghRaw(["pr", "comment", prUrl, "--body", comment], exec);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
async function getExistingPrUrl(branch: string): Promise<string | null> {
|
|
140
|
-
const out = await ghRaw(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
139
|
+
async function getExistingPrUrl(branch: string, exec?: ExecSafeFn): Promise<string | null> {
|
|
140
|
+
const out = await ghRaw(
|
|
141
|
+
[
|
|
142
|
+
"pr",
|
|
143
|
+
"list",
|
|
144
|
+
"--repo",
|
|
145
|
+
getConfig().repo,
|
|
146
|
+
"--head",
|
|
147
|
+
branch,
|
|
148
|
+
"--state",
|
|
149
|
+
"open",
|
|
150
|
+
"--json",
|
|
151
|
+
"url",
|
|
152
|
+
],
|
|
153
|
+
exec,
|
|
154
|
+
);
|
|
152
155
|
try {
|
|
153
156
|
const prs = JSON.parse(out) as Array<{ url: string }>;
|
|
154
157
|
return prs.length > 0 ? prs[0].url : null;
|
|
@@ -10,8 +10,9 @@ import { runClaude } from "../claude-cli.js";
|
|
|
10
10
|
import { resolveTemplate } from "../templates.js";
|
|
11
11
|
import { buildTokens, log, logStep } from "../utils.js";
|
|
12
12
|
import type { IssueContext } from "../utils.js";
|
|
13
|
+
import type { SpawnClaudeFn } from "../spawn-claude.js";
|
|
13
14
|
|
|
14
|
-
export async function stepImplement(ctx: IssueContext): Promise<boolean> {
|
|
15
|
+
export async function stepImplement(ctx: IssueContext, spawnFn?: SpawnClaudeFn): Promise<boolean> {
|
|
15
16
|
const completedPath = join(ctx.issueDir, ARTIFACTS.completedSummary);
|
|
16
17
|
const maxIterations = getConfig().maxImplementIterations;
|
|
17
18
|
|
|
@@ -36,6 +37,7 @@ export async function stepImplement(ctx: IssueContext): Promise<boolean> {
|
|
|
36
37
|
const result = await runClaude({
|
|
37
38
|
promptFile,
|
|
38
39
|
maxTurns: getConfig().maxTurns,
|
|
40
|
+
spawnFn,
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
if (result.is_error) {
|
|
@@ -3,8 +3,9 @@ import { join } from "node:path";
|
|
|
3
3
|
import { ARTIFACTS, STEP_LABELS, TEMPLATES } from "../prompt-templates/index.js";
|
|
4
4
|
import { ensureBranch, runStepWithArtifact } from "../utils.js";
|
|
5
5
|
import type { IssueContext } from "../utils.js";
|
|
6
|
+
import type { SpawnClaudeFn } from "../spawn-claude.js";
|
|
6
7
|
|
|
7
|
-
export async function stepPlan(ctx: IssueContext): Promise<boolean> {
|
|
8
|
+
export async function stepPlan(ctx: IssueContext, spawnFn?: SpawnClaudeFn): Promise<boolean> {
|
|
8
9
|
await ensureBranch(ctx.branch);
|
|
9
10
|
|
|
10
11
|
return runStepWithArtifact({
|
|
@@ -12,23 +13,26 @@ export async function stepPlan(ctx: IssueContext): Promise<boolean> {
|
|
|
12
13
|
ctx,
|
|
13
14
|
artifactPath: join(ctx.issueDir, ARTIFACTS.plan),
|
|
14
15
|
templateName: TEMPLATES.plan,
|
|
16
|
+
spawnFn,
|
|
15
17
|
});
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
export async function stepSimplify(ctx: IssueContext): Promise<boolean> {
|
|
20
|
+
export async function stepSimplify(ctx: IssueContext, spawnFn?: SpawnClaudeFn): Promise<boolean> {
|
|
19
21
|
return runStepWithArtifact({
|
|
20
22
|
stepName: STEP_LABELS.simplify,
|
|
21
23
|
ctx,
|
|
22
24
|
artifactPath: join(ctx.issueDir, ARTIFACTS.simplifySummary),
|
|
23
25
|
templateName: TEMPLATES.simplify,
|
|
26
|
+
spawnFn,
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
export async function stepReview(ctx: IssueContext): Promise<boolean> {
|
|
30
|
+
export async function stepReview(ctx: IssueContext, spawnFn?: SpawnClaudeFn): Promise<boolean> {
|
|
28
31
|
return runStepWithArtifact({
|
|
29
32
|
stepName: STEP_LABELS.review,
|
|
30
33
|
ctx,
|
|
31
34
|
artifactPath: join(ctx.issueDir, ARTIFACTS.review),
|
|
32
35
|
templateName: TEMPLATES.review,
|
|
36
|
+
spawnFn,
|
|
33
37
|
});
|
|
34
38
|
}
|
|
@@ -9,29 +9,44 @@ import { initConfig } from "../config";
|
|
|
9
9
|
import { ARTIFACTS } from "../prompt-templates/index";
|
|
10
10
|
import {
|
|
11
11
|
buildTestContext,
|
|
12
|
-
|
|
12
|
+
createMockClaudeProcess,
|
|
13
13
|
createTestRepoWithRemote,
|
|
14
14
|
errorClaudeJson,
|
|
15
15
|
successClaudeJson,
|
|
16
16
|
} from "../test-helpers";
|
|
17
17
|
import type { MockClaudeImpl, TestRepo } from "../test-helpers";
|
|
18
18
|
import type { IssueContext } from "../utils";
|
|
19
|
+
import type { SpawnClaudeFn } from "../spawn-claude";
|
|
19
20
|
|
|
20
21
|
consola.level = -999;
|
|
21
22
|
|
|
22
|
-
let mockClaudeImpl: MockClaudeImpl = null;
|
|
23
|
-
vi.mock("../spawn-claude", () => createSpawnClaudeMock(() => mockClaudeImpl));
|
|
24
|
-
|
|
25
23
|
// ── Shared setup/teardown for all step tests ──
|
|
26
24
|
|
|
27
|
-
function setupStepTest(): {
|
|
25
|
+
function setupStepTest(): {
|
|
26
|
+
originalCwd: string;
|
|
27
|
+
repo: TestRepo;
|
|
28
|
+
ctx: IssueContext;
|
|
29
|
+
spawnFn: SpawnClaudeFn;
|
|
30
|
+
mockClaudeImpl: { current: MockClaudeImpl };
|
|
31
|
+
} {
|
|
28
32
|
const originalCwd = process.cwd();
|
|
29
33
|
const repo = createTestRepoWithRemote();
|
|
30
34
|
process.chdir(repo.dir);
|
|
31
35
|
const ctx = buildTestContext(repo.dir);
|
|
32
36
|
mkdirSync(ctx.issueDir, { recursive: true });
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
|
|
38
|
+
const mockClaudeImpl = { current: null as MockClaudeImpl };
|
|
39
|
+
|
|
40
|
+
const spawnFn = vi.fn((args: string[]) => {
|
|
41
|
+
const impl = mockClaudeImpl.current;
|
|
42
|
+
if (impl) {
|
|
43
|
+
const { stdout, exitCode } = impl(args);
|
|
44
|
+
return createMockClaudeProcess(stdout, exitCode);
|
|
45
|
+
}
|
|
46
|
+
throw new Error("Unexpected spawnClaude call -- set mockClaudeImpl.current");
|
|
47
|
+
}) as SpawnClaudeFn;
|
|
48
|
+
|
|
49
|
+
return { originalCwd, repo, ctx, spawnFn, mockClaudeImpl };
|
|
35
50
|
}
|
|
36
51
|
|
|
37
52
|
function teardownStepTest(originalCwd: string, repo: TestRepo): void {
|
|
@@ -45,9 +60,11 @@ describe("runStepWithArtifact", () => {
|
|
|
45
60
|
let originalCwd: string;
|
|
46
61
|
let repo: TestRepo;
|
|
47
62
|
let ctx: IssueContext;
|
|
63
|
+
let spawnFn: SpawnClaudeFn;
|
|
64
|
+
let mockClaudeImpl: { current: MockClaudeImpl };
|
|
48
65
|
|
|
49
66
|
beforeEach(async () => {
|
|
50
|
-
({ originalCwd, repo, ctx } = setupStepTest());
|
|
67
|
+
({ originalCwd, repo, ctx, spawnFn, mockClaudeImpl } = setupStepTest());
|
|
51
68
|
await initConfig({ repo: "test/repo", mainBranch: "main" });
|
|
52
69
|
});
|
|
53
70
|
|
|
@@ -63,13 +80,14 @@ describe("runStepWithArtifact", () => {
|
|
|
63
80
|
ctx,
|
|
64
81
|
artifactPath,
|
|
65
82
|
templateName: "01_plan.prompt.md",
|
|
83
|
+
spawnFn,
|
|
66
84
|
});
|
|
67
85
|
|
|
68
86
|
expect(result).toBe(true);
|
|
69
87
|
});
|
|
70
88
|
|
|
71
89
|
it("returns false when Claude returns is_error", async () => {
|
|
72
|
-
mockClaudeImpl = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
|
|
90
|
+
mockClaudeImpl.current = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
|
|
73
91
|
|
|
74
92
|
const { runStepWithArtifact } = await import("../utils");
|
|
75
93
|
const artifactPath = join(ctx.issueDir, "missing-artifact.md");
|
|
@@ -79,13 +97,14 @@ describe("runStepWithArtifact", () => {
|
|
|
79
97
|
ctx,
|
|
80
98
|
artifactPath,
|
|
81
99
|
templateName: "01_plan.prompt.md",
|
|
100
|
+
spawnFn,
|
|
82
101
|
});
|
|
83
102
|
|
|
84
103
|
expect(result).toBe(false);
|
|
85
104
|
});
|
|
86
105
|
|
|
87
106
|
it("returns false when artifact not produced after Claude run", async () => {
|
|
88
|
-
mockClaudeImpl = () => ({ stdout: successClaudeJson(), exitCode: 0 });
|
|
107
|
+
mockClaudeImpl.current = () => ({ stdout: successClaudeJson(), exitCode: 0 });
|
|
89
108
|
|
|
90
109
|
const { runStepWithArtifact } = await import("../utils");
|
|
91
110
|
const artifactPath = join(ctx.issueDir, "never-created.md");
|
|
@@ -95,6 +114,7 @@ describe("runStepWithArtifact", () => {
|
|
|
95
114
|
ctx,
|
|
96
115
|
artifactPath,
|
|
97
116
|
templateName: "01_plan.prompt.md",
|
|
117
|
+
spawnFn,
|
|
98
118
|
});
|
|
99
119
|
|
|
100
120
|
expect(result).toBe(false);
|
|
@@ -104,7 +124,7 @@ describe("runStepWithArtifact", () => {
|
|
|
104
124
|
const { runStepWithArtifact } = await import("../utils");
|
|
105
125
|
const artifactPath = join(ctx.issueDir, "plan.md");
|
|
106
126
|
|
|
107
|
-
mockClaudeImpl = () => {
|
|
127
|
+
mockClaudeImpl.current = () => {
|
|
108
128
|
writeFileSync(artifactPath, "# Plan\n\nDetailed plan content here.");
|
|
109
129
|
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
110
130
|
};
|
|
@@ -114,6 +134,7 @@ describe("runStepWithArtifact", () => {
|
|
|
114
134
|
ctx,
|
|
115
135
|
artifactPath,
|
|
116
136
|
templateName: "01_plan.prompt.md",
|
|
137
|
+
spawnFn,
|
|
117
138
|
});
|
|
118
139
|
|
|
119
140
|
expect(result).toBe(true);
|
|
@@ -126,9 +147,11 @@ describe("stepPlan", () => {
|
|
|
126
147
|
let originalCwd: string;
|
|
127
148
|
let repo: TestRepo;
|
|
128
149
|
let ctx: IssueContext;
|
|
150
|
+
let spawnFn: SpawnClaudeFn;
|
|
151
|
+
let mockClaudeImpl: { current: MockClaudeImpl };
|
|
129
152
|
|
|
130
153
|
beforeEach(async () => {
|
|
131
|
-
({ originalCwd, repo, ctx } = setupStepTest());
|
|
154
|
+
({ originalCwd, repo, ctx, spawnFn, mockClaudeImpl } = setupStepTest());
|
|
132
155
|
await initConfig({ repo: "test/repo", mainBranch: "main" });
|
|
133
156
|
});
|
|
134
157
|
|
|
@@ -139,7 +162,7 @@ describe("stepPlan", () => {
|
|
|
139
162
|
|
|
140
163
|
writeFileSync(join(ctx.issueDir, ARTIFACTS.plan), "# Existing plan");
|
|
141
164
|
|
|
142
|
-
const result = await stepPlan(ctx);
|
|
165
|
+
const result = await stepPlan(ctx, spawnFn);
|
|
143
166
|
expect(result).toBe(true);
|
|
144
167
|
});
|
|
145
168
|
|
|
@@ -147,12 +170,12 @@ describe("stepPlan", () => {
|
|
|
147
170
|
const { stepPlan } = await import("./simple-steps");
|
|
148
171
|
const planPath = join(ctx.issueDir, ARTIFACTS.plan);
|
|
149
172
|
|
|
150
|
-
mockClaudeImpl = () => {
|
|
173
|
+
mockClaudeImpl.current = () => {
|
|
151
174
|
writeFileSync(planPath, "# Plan\n\nDetailed plan.");
|
|
152
175
|
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
153
176
|
};
|
|
154
177
|
|
|
155
|
-
const result = await stepPlan(ctx);
|
|
178
|
+
const result = await stepPlan(ctx, spawnFn);
|
|
156
179
|
expect(result).toBe(true);
|
|
157
180
|
|
|
158
181
|
// Verify we ended up on the branch (ensureBranch was called)
|
|
@@ -165,9 +188,9 @@ describe("stepPlan", () => {
|
|
|
165
188
|
it("returns false when Claude fails", async () => {
|
|
166
189
|
const { stepPlan } = await import("./simple-steps");
|
|
167
190
|
|
|
168
|
-
mockClaudeImpl = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
|
|
191
|
+
mockClaudeImpl.current = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
|
|
169
192
|
|
|
170
|
-
const result = await stepPlan(ctx);
|
|
193
|
+
const result = await stepPlan(ctx, spawnFn);
|
|
171
194
|
expect(result).toBe(false);
|
|
172
195
|
});
|
|
173
196
|
});
|
|
@@ -178,9 +201,11 @@ describe("stepSimplify", () => {
|
|
|
178
201
|
let originalCwd: string;
|
|
179
202
|
let repo: TestRepo;
|
|
180
203
|
let ctx: IssueContext;
|
|
204
|
+
let spawnFn: SpawnClaudeFn;
|
|
205
|
+
let mockClaudeImpl: { current: MockClaudeImpl };
|
|
181
206
|
|
|
182
207
|
beforeEach(async () => {
|
|
183
|
-
({ originalCwd, repo, ctx } = setupStepTest());
|
|
208
|
+
({ originalCwd, repo, ctx, spawnFn, mockClaudeImpl } = setupStepTest());
|
|
184
209
|
await initConfig({ repo: "test/repo", mainBranch: "main" });
|
|
185
210
|
|
|
186
211
|
// stepSimplify doesn't switch branches, but we need to be on one
|
|
@@ -194,7 +219,7 @@ describe("stepSimplify", () => {
|
|
|
194
219
|
|
|
195
220
|
writeFileSync(join(ctx.issueDir, ARTIFACTS.simplifySummary), "# Simplified");
|
|
196
221
|
|
|
197
|
-
const result = await stepSimplify(ctx);
|
|
222
|
+
const result = await stepSimplify(ctx, spawnFn);
|
|
198
223
|
expect(result).toBe(true);
|
|
199
224
|
});
|
|
200
225
|
|
|
@@ -202,21 +227,21 @@ describe("stepSimplify", () => {
|
|
|
202
227
|
const { stepSimplify } = await import("./simple-steps");
|
|
203
228
|
const artifactPath = join(ctx.issueDir, ARTIFACTS.simplifySummary);
|
|
204
229
|
|
|
205
|
-
mockClaudeImpl = () => {
|
|
230
|
+
mockClaudeImpl.current = () => {
|
|
206
231
|
writeFileSync(artifactPath, "# Simplify Summary\n\nCode simplified.");
|
|
207
232
|
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
208
233
|
};
|
|
209
234
|
|
|
210
|
-
const result = await stepSimplify(ctx);
|
|
235
|
+
const result = await stepSimplify(ctx, spawnFn);
|
|
211
236
|
expect(result).toBe(true);
|
|
212
237
|
});
|
|
213
238
|
|
|
214
239
|
it("returns false when Claude fails", async () => {
|
|
215
240
|
const { stepSimplify } = await import("./simple-steps");
|
|
216
241
|
|
|
217
|
-
mockClaudeImpl = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
|
|
242
|
+
mockClaudeImpl.current = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
|
|
218
243
|
|
|
219
|
-
const result = await stepSimplify(ctx);
|
|
244
|
+
const result = await stepSimplify(ctx, spawnFn);
|
|
220
245
|
expect(result).toBe(false);
|
|
221
246
|
});
|
|
222
247
|
});
|
|
@@ -227,9 +252,11 @@ describe("stepImplement", () => {
|
|
|
227
252
|
let originalCwd: string;
|
|
228
253
|
let repo: TestRepo;
|
|
229
254
|
let ctx: IssueContext;
|
|
255
|
+
let spawnFn: SpawnClaudeFn;
|
|
256
|
+
let mockClaudeImpl: { current: MockClaudeImpl };
|
|
230
257
|
|
|
231
258
|
beforeEach(async () => {
|
|
232
|
-
({ originalCwd, repo, ctx } = setupStepTest());
|
|
259
|
+
({ originalCwd, repo, ctx, spawnFn, mockClaudeImpl } = setupStepTest());
|
|
233
260
|
await initConfig({
|
|
234
261
|
repo: "test/repo",
|
|
235
262
|
mainBranch: "main",
|
|
@@ -247,7 +274,7 @@ describe("stepImplement", () => {
|
|
|
247
274
|
|
|
248
275
|
writeFileSync(join(ctx.issueDir, ARTIFACTS.completedSummary), "# Done");
|
|
249
276
|
|
|
250
|
-
const result = await stepImplement(ctx);
|
|
277
|
+
const result = await stepImplement(ctx, spawnFn);
|
|
251
278
|
expect(result).toBe(true);
|
|
252
279
|
});
|
|
253
280
|
|
|
@@ -255,12 +282,12 @@ describe("stepImplement", () => {
|
|
|
255
282
|
const { stepImplement } = await import("./implement");
|
|
256
283
|
|
|
257
284
|
let callCount = 0;
|
|
258
|
-
mockClaudeImpl = () => {
|
|
285
|
+
mockClaudeImpl.current = () => {
|
|
259
286
|
callCount++;
|
|
260
287
|
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
261
288
|
};
|
|
262
289
|
|
|
263
|
-
const result = await stepImplement(ctx);
|
|
290
|
+
const result = await stepImplement(ctx, spawnFn);
|
|
264
291
|
expect(result).toBe(false);
|
|
265
292
|
expect(callCount).toBe(3);
|
|
266
293
|
});
|
|
@@ -272,12 +299,12 @@ describe("stepImplement", () => {
|
|
|
272
299
|
writeFileSync(join(ctx.issueDir, ARTIFACTS.review), "# Review\n\nFix the tests.");
|
|
273
300
|
|
|
274
301
|
const completedPath = join(ctx.issueDir, ARTIFACTS.completedSummary);
|
|
275
|
-
mockClaudeImpl = () => {
|
|
302
|
+
mockClaudeImpl.current = () => {
|
|
276
303
|
writeFileSync(completedPath, "# Done");
|
|
277
304
|
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
278
305
|
};
|
|
279
306
|
|
|
280
|
-
const result = await stepImplement(ctx);
|
|
307
|
+
const result = await stepImplement(ctx, spawnFn);
|
|
281
308
|
expect(result).toBe(true);
|
|
282
309
|
});
|
|
283
310
|
|
|
@@ -287,7 +314,7 @@ describe("stepImplement", () => {
|
|
|
287
314
|
let callCount = 0;
|
|
288
315
|
const completedPath = join(ctx.issueDir, ARTIFACTS.completedSummary);
|
|
289
316
|
|
|
290
|
-
mockClaudeImpl = () => {
|
|
317
|
+
mockClaudeImpl.current = () => {
|
|
291
318
|
callCount++;
|
|
292
319
|
if (callCount === 2) {
|
|
293
320
|
writeFileSync(completedPath, "# Implementation Complete\n\nAll tasks done.");
|
|
@@ -295,7 +322,7 @@ describe("stepImplement", () => {
|
|
|
295
322
|
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
296
323
|
};
|
|
297
324
|
|
|
298
|
-
const result = await stepImplement(ctx);
|
|
325
|
+
const result = await stepImplement(ctx, spawnFn);
|
|
299
326
|
expect(result).toBe(true);
|
|
300
327
|
expect(callCount).toBe(2);
|
|
301
328
|
});
|