@demon-utils/playwright 0.1.3 → 0.1.6
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/dist/bin/demon-demo-review.js +785 -0
- package/dist/bin/demon-demo-review.js.map +13 -0
- package/dist/index.js +1115 -8
- package/dist/index.js.map +9 -4
- package/package.json +4 -1
- package/src/bin/demon-demo-review.ts +138 -0
- package/src/commentary.test.ts +96 -0
- package/src/commentary.ts +31 -8
- package/src/git-context.test.ts +90 -0
- package/src/git-context.ts +62 -0
- package/src/html-generator.e2e.test.ts +349 -0
- package/src/html-generator.test.ts +561 -0
- package/src/html-generator.ts +461 -0
- package/src/index.ts +13 -0
- package/src/recorder.test.ts +161 -0
- package/src/recorder.ts +74 -0
- package/src/review-types.ts +27 -0
- package/src/review.test.ts +365 -0
- package/src/review.ts +260 -0
package/src/recorder.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
import type { ShowCommentaryOptions } from "./commentary.ts";
|
|
4
|
+
import { showCommentary as defaultShowCommentary } from "./commentary.ts";
|
|
5
|
+
|
|
6
|
+
export interface DemoStep {
|
|
7
|
+
text: string;
|
|
8
|
+
timestampSeconds: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type ShowCommentaryFn = (
|
|
12
|
+
page: Page,
|
|
13
|
+
options: ShowCommentaryOptions,
|
|
14
|
+
) => Promise<void>;
|
|
15
|
+
|
|
16
|
+
type TestStepFn = (
|
|
17
|
+
title: string,
|
|
18
|
+
body: () => Promise<void>,
|
|
19
|
+
) => Promise<void>;
|
|
20
|
+
|
|
21
|
+
export interface DemoRecorderOptions {
|
|
22
|
+
showCommentary?: ShowCommentaryFn;
|
|
23
|
+
testStep?: TestStepFn;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class DemoRecorder {
|
|
27
|
+
private steps: DemoStep[] = [];
|
|
28
|
+
private startTime: number;
|
|
29
|
+
private showCommentaryFn: ShowCommentaryFn;
|
|
30
|
+
private testStepFn: TestStepFn | undefined;
|
|
31
|
+
|
|
32
|
+
constructor(options?: DemoRecorderOptions) {
|
|
33
|
+
this.startTime = Date.now();
|
|
34
|
+
this.showCommentaryFn = options?.showCommentary ?? defaultShowCommentary;
|
|
35
|
+
this.testStepFn = options?.testStep;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async step(
|
|
39
|
+
page: Page,
|
|
40
|
+
text: string,
|
|
41
|
+
options: { selector: string },
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const body = async () => {
|
|
44
|
+
await this.showCommentaryFn(page, {
|
|
45
|
+
selector: options.selector,
|
|
46
|
+
text,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const timestampSeconds =
|
|
50
|
+
Math.round((Date.now() - this.startTime) / 100) / 10;
|
|
51
|
+
|
|
52
|
+
this.steps.push({ text, timestampSeconds });
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (this.testStepFn) {
|
|
56
|
+
await this.testStepFn(text, body);
|
|
57
|
+
} else {
|
|
58
|
+
await body();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getSteps(): DemoStep[] {
|
|
63
|
+
return [...this.steps];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async save(outputDir: string): Promise<void> {
|
|
67
|
+
const { join } = await import("node:path");
|
|
68
|
+
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
69
|
+
|
|
70
|
+
mkdirSync(outputDir, { recursive: true });
|
|
71
|
+
const filePath = join(outputDir, "demo-steps.json");
|
|
72
|
+
writeFileSync(filePath, JSON.stringify(this.steps, null, 2) + "\n");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface DemoMetadata {
|
|
2
|
+
file: string;
|
|
3
|
+
summary: string;
|
|
4
|
+
steps: Array<{ timestampSeconds: number; text: string }>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type IssueSeverity = "major" | "minor" | "nit";
|
|
8
|
+
|
|
9
|
+
export interface ReviewIssue {
|
|
10
|
+
severity: IssueSeverity;
|
|
11
|
+
description: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ReviewVerdict = "approve" | "request_changes";
|
|
15
|
+
|
|
16
|
+
export interface CodeReview {
|
|
17
|
+
summary: string;
|
|
18
|
+
highlights: string[];
|
|
19
|
+
verdict: ReviewVerdict;
|
|
20
|
+
verdictReason: string;
|
|
21
|
+
issues: ReviewIssue[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ReviewMetadata {
|
|
25
|
+
demos: DemoMetadata[];
|
|
26
|
+
review?: CodeReview;
|
|
27
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { SpawnFn } from "./review.ts";
|
|
4
|
+
import {
|
|
5
|
+
buildReviewPrompt,
|
|
6
|
+
extractJson,
|
|
7
|
+
invokeClaude,
|
|
8
|
+
parseLlmResponse,
|
|
9
|
+
} from "./review.ts";
|
|
10
|
+
|
|
11
|
+
function makeValidResponse(overrides?: Record<string, unknown>) {
|
|
12
|
+
return {
|
|
13
|
+
demos: [{ file: "login.webm", summary: "Shows a login flow" }],
|
|
14
|
+
review: {
|
|
15
|
+
summary: "Good changes overall",
|
|
16
|
+
highlights: ["Clean implementation"],
|
|
17
|
+
verdict: "approve",
|
|
18
|
+
verdictReason: "No major issues found",
|
|
19
|
+
issues: [],
|
|
20
|
+
},
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("buildReviewPrompt", () => {
|
|
26
|
+
test("includes all filenames in the prompt", () => {
|
|
27
|
+
const stepsMap = {
|
|
28
|
+
"login-flow.webm": [],
|
|
29
|
+
"signup.webm": [],
|
|
30
|
+
};
|
|
31
|
+
const prompt = buildReviewPrompt({ filenames: ["login-flow.webm", "signup.webm"], stepsMap });
|
|
32
|
+
expect(prompt).toContain("login-flow.webm");
|
|
33
|
+
expect(prompt).toContain("signup.webm");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("includes schema instructions", () => {
|
|
37
|
+
const stepsMap = { "demo.webm": [] };
|
|
38
|
+
const prompt = buildReviewPrompt({ filenames: ["demo.webm"], stepsMap });
|
|
39
|
+
expect(prompt).toContain('"demos"');
|
|
40
|
+
expect(prompt).toContain('"file"');
|
|
41
|
+
expect(prompt).toContain('"summary"');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("does not include annotations in schema", () => {
|
|
45
|
+
const stepsMap = { "demo.webm": [] };
|
|
46
|
+
const prompt = buildReviewPrompt({ filenames: ["demo.webm"], stepsMap });
|
|
47
|
+
expect(prompt).not.toContain('"annotations"');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("includes step data in prompt", () => {
|
|
51
|
+
const stepsMap = {
|
|
52
|
+
"login-flow.webm": [
|
|
53
|
+
{ text: "Navigate to login", timestampSeconds: 0.5 },
|
|
54
|
+
{ text: "Enter credentials", timestampSeconds: 3.2 },
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
const prompt = buildReviewPrompt({ filenames: ["login-flow.webm"], stepsMap });
|
|
58
|
+
expect(prompt).toContain("[0.5s] Navigate to login");
|
|
59
|
+
expect(prompt).toContain("[3.2s] Enter credentials");
|
|
60
|
+
expect(prompt).toContain("Recorded steps:");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("shows (no steps recorded) when steps array is empty", () => {
|
|
64
|
+
const stepsMap = { "demo.webm": [] };
|
|
65
|
+
const prompt = buildReviewPrompt({ filenames: ["demo.webm"], stepsMap });
|
|
66
|
+
expect(prompt).toContain("(no steps recorded)");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("shows (no steps recorded) when video not in stepsMap", () => {
|
|
70
|
+
const stepsMap = { "other.webm": [] };
|
|
71
|
+
const prompt = buildReviewPrompt({ filenames: ["demo.webm"], stepsMap });
|
|
72
|
+
expect(prompt).toContain("(no steps recorded)");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("includes git diff when provided", () => {
|
|
76
|
+
const prompt = buildReviewPrompt({
|
|
77
|
+
filenames: ["demo.webm"],
|
|
78
|
+
stepsMap: { "demo.webm": [] },
|
|
79
|
+
gitDiff: "diff --git a/file.ts\n+added line",
|
|
80
|
+
});
|
|
81
|
+
expect(prompt).toContain("## Git Diff");
|
|
82
|
+
expect(prompt).toContain("+added line");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("does not include git diff section when not provided", () => {
|
|
86
|
+
const prompt = buildReviewPrompt({
|
|
87
|
+
filenames: ["demo.webm"],
|
|
88
|
+
stepsMap: { "demo.webm": [] },
|
|
89
|
+
});
|
|
90
|
+
expect(prompt).not.toContain("## Git Diff");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("includes guidelines when provided", () => {
|
|
94
|
+
const prompt = buildReviewPrompt({
|
|
95
|
+
filenames: ["demo.webm"],
|
|
96
|
+
stepsMap: { "demo.webm": [] },
|
|
97
|
+
guidelines: ["# CLAUDE.md\nUse dependency injection"],
|
|
98
|
+
});
|
|
99
|
+
expect(prompt).toContain("## Coding Guidelines");
|
|
100
|
+
expect(prompt).toContain("Use dependency injection");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("does not include guidelines section when empty", () => {
|
|
104
|
+
const prompt = buildReviewPrompt({
|
|
105
|
+
filenames: ["demo.webm"],
|
|
106
|
+
stepsMap: { "demo.webm": [] },
|
|
107
|
+
guidelines: [],
|
|
108
|
+
});
|
|
109
|
+
expect(prompt).not.toContain("## Coding Guidelines");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("truncates git diff at 50k characters", () => {
|
|
113
|
+
const longDiff = "x".repeat(60_000);
|
|
114
|
+
const prompt = buildReviewPrompt({
|
|
115
|
+
filenames: ["demo.webm"],
|
|
116
|
+
stepsMap: { "demo.webm": [] },
|
|
117
|
+
gitDiff: longDiff,
|
|
118
|
+
});
|
|
119
|
+
expect(prompt).toContain("diff truncated at 50k characters");
|
|
120
|
+
expect(prompt).not.toContain("x".repeat(60_000));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("includes review schema in prompt", () => {
|
|
124
|
+
const prompt = buildReviewPrompt({
|
|
125
|
+
filenames: ["demo.webm"],
|
|
126
|
+
stepsMap: { "demo.webm": [] },
|
|
127
|
+
});
|
|
128
|
+
expect(prompt).toContain('"review"');
|
|
129
|
+
expect(prompt).toContain('"verdict"');
|
|
130
|
+
expect(prompt).toContain('"issues"');
|
|
131
|
+
expect(prompt).toContain('"highlights"');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("extractJson", () => {
|
|
136
|
+
test("returns raw string when it is valid JSON", () => {
|
|
137
|
+
const json = '{"key": "value"}';
|
|
138
|
+
expect(extractJson(json)).toBe(json);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("strips preamble text before JSON object", () => {
|
|
142
|
+
const raw = 'Here is the review.\n\n{"demos": []}';
|
|
143
|
+
expect(extractJson(raw)).toBe('{"demos": []}');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("strips trailing text after JSON object", () => {
|
|
147
|
+
const raw = '{"demos": []}\n\nHope this helps!';
|
|
148
|
+
expect(extractJson(raw)).toBe('{"demos": []}');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("strips both preamble and trailing text", () => {
|
|
152
|
+
const raw = 'Let me analyze this.\n{"demos": []}\nDone.';
|
|
153
|
+
expect(extractJson(raw)).toBe('{"demos": []}');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("handles nested braces correctly", () => {
|
|
157
|
+
const json = '{"a": {"b": "c"}}';
|
|
158
|
+
const raw = `Some preamble\n${json}\nsome trailing`;
|
|
159
|
+
expect(extractJson(raw)).toBe(json);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("throws when no JSON object found", () => {
|
|
163
|
+
expect(() => extractJson("no json here at all")).toThrow("No JSON object found");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("throws when only opening brace", () => {
|
|
167
|
+
expect(() => extractJson("just { nothing")).toThrow("No JSON object found");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("parseLlmResponse", () => {
|
|
172
|
+
test("parses valid input correctly", () => {
|
|
173
|
+
const result = parseLlmResponse(JSON.stringify(makeValidResponse()));
|
|
174
|
+
expect(result.demos).toHaveLength(1);
|
|
175
|
+
expect(result.demos[0]!.file).toBe("login.webm");
|
|
176
|
+
expect(result.demos[0]!.summary).toBe("Shows a login flow");
|
|
177
|
+
expect(result.review.verdict).toBe("approve");
|
|
178
|
+
expect(result.review.summary).toBe("Good changes overall");
|
|
179
|
+
expect(result.review.highlights).toEqual(["Clean implementation"]);
|
|
180
|
+
expect(result.review.issues).toEqual([]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("parses response with issues", () => {
|
|
184
|
+
const input = makeValidResponse({
|
|
185
|
+
review: {
|
|
186
|
+
summary: "Needs work",
|
|
187
|
+
highlights: ["Good tests"],
|
|
188
|
+
verdict: "request_changes",
|
|
189
|
+
verdictReason: "Major bug found",
|
|
190
|
+
issues: [
|
|
191
|
+
{ severity: "major", description: "Null pointer dereference" },
|
|
192
|
+
{ severity: "minor", description: "Missing error message" },
|
|
193
|
+
{ severity: "nit", description: "Inconsistent naming" },
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
const result = parseLlmResponse(JSON.stringify(input));
|
|
198
|
+
expect(result.review.issues).toHaveLength(3);
|
|
199
|
+
expect(result.review.issues[0]!.severity).toBe("major");
|
|
200
|
+
expect(result.review.verdict).toBe("request_changes");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("parses response with preamble text before JSON", () => {
|
|
204
|
+
const json = JSON.stringify(makeValidResponse());
|
|
205
|
+
const raw = `Let me review the changes.\n\n${json}`;
|
|
206
|
+
const result = parseLlmResponse(raw);
|
|
207
|
+
expect(result.demos).toHaveLength(1);
|
|
208
|
+
expect(result.review.verdict).toBe("approve");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("parses response with trailing text after JSON", () => {
|
|
212
|
+
const json = JSON.stringify(makeValidResponse());
|
|
213
|
+
const raw = `${json}\n\nHope this helps!`;
|
|
214
|
+
const result = parseLlmResponse(raw);
|
|
215
|
+
expect(result.demos).toHaveLength(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("throws when no JSON object found", () => {
|
|
219
|
+
expect(() => parseLlmResponse("not json at all")).toThrow("No JSON object found");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("throws when demos array is missing", () => {
|
|
223
|
+
expect(() => parseLlmResponse("{}")).toThrow("Missing 'demos' array");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("throws when demos is not an array", () => {
|
|
227
|
+
expect(() => parseLlmResponse('{"demos": "nope"}')).toThrow(
|
|
228
|
+
"'demos' must be an array",
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("throws when demo is missing file", () => {
|
|
233
|
+
expect(() =>
|
|
234
|
+
parseLlmResponse(
|
|
235
|
+
JSON.stringify({ demos: [{ summary: "x" }], review: makeValidResponse().review }),
|
|
236
|
+
),
|
|
237
|
+
).toThrow("'file' string");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("throws when demo is missing summary", () => {
|
|
241
|
+
expect(() =>
|
|
242
|
+
parseLlmResponse(
|
|
243
|
+
JSON.stringify({ demos: [{ file: "x" }], review: makeValidResponse().review }),
|
|
244
|
+
),
|
|
245
|
+
).toThrow("'summary' string");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("throws when review object is missing", () => {
|
|
249
|
+
expect(() =>
|
|
250
|
+
parseLlmResponse(
|
|
251
|
+
JSON.stringify({ demos: [{ file: "x", summary: "s" }] }),
|
|
252
|
+
),
|
|
253
|
+
).toThrow("Missing 'review' object");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("throws when review.verdict is invalid", () => {
|
|
257
|
+
const input = makeValidResponse({
|
|
258
|
+
review: { ...makeValidResponse().review, verdict: "maybe" },
|
|
259
|
+
});
|
|
260
|
+
expect(() => parseLlmResponse(JSON.stringify(input))).toThrow(
|
|
261
|
+
"review.verdict must be 'approve' or 'request_changes'",
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("throws when review.highlights is empty", () => {
|
|
266
|
+
const input = makeValidResponse({
|
|
267
|
+
review: { ...makeValidResponse().review, highlights: [] },
|
|
268
|
+
});
|
|
269
|
+
expect(() => parseLlmResponse(JSON.stringify(input))).toThrow(
|
|
270
|
+
"review.highlights must not be empty",
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("throws when review.summary is missing", () => {
|
|
275
|
+
const input = makeValidResponse({
|
|
276
|
+
review: { ...makeValidResponse().review, summary: 42 },
|
|
277
|
+
});
|
|
278
|
+
expect(() => parseLlmResponse(JSON.stringify(input))).toThrow(
|
|
279
|
+
"review.summary must be a string",
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("throws when review.verdictReason is missing", () => {
|
|
284
|
+
const input = makeValidResponse({
|
|
285
|
+
review: { ...makeValidResponse().review, verdictReason: null },
|
|
286
|
+
});
|
|
287
|
+
expect(() => parseLlmResponse(JSON.stringify(input))).toThrow(
|
|
288
|
+
"review.verdictReason must be a string",
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("throws when issue has invalid severity", () => {
|
|
293
|
+
const input = makeValidResponse({
|
|
294
|
+
review: {
|
|
295
|
+
...makeValidResponse().review,
|
|
296
|
+
issues: [{ severity: "critical", description: "bad" }],
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
expect(() => parseLlmResponse(JSON.stringify(input))).toThrow(
|
|
300
|
+
"Each issue severity must be 'major', 'minor', or 'nit'",
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("throws when issue is missing description", () => {
|
|
305
|
+
const input = makeValidResponse({
|
|
306
|
+
review: {
|
|
307
|
+
...makeValidResponse().review,
|
|
308
|
+
issues: [{ severity: "major" }],
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
expect(() => parseLlmResponse(JSON.stringify(input))).toThrow(
|
|
312
|
+
"Each issue must have a 'description' string",
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("invokeClaude", () => {
|
|
318
|
+
function mockSpawn(
|
|
319
|
+
stdout: string,
|
|
320
|
+
exitCode: number,
|
|
321
|
+
): SpawnFn {
|
|
322
|
+
return (_cmd: string[]) => ({
|
|
323
|
+
exitCode: Promise.resolve(exitCode),
|
|
324
|
+
stdout: new ReadableStream({
|
|
325
|
+
start(controller) {
|
|
326
|
+
controller.enqueue(new TextEncoder().encode(stdout));
|
|
327
|
+
controller.close();
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
test("returns stdout on success", async () => {
|
|
334
|
+
const result = await invokeClaude("test prompt", { spawn: mockSpawn("hello", 0) });
|
|
335
|
+
expect(result).toBe("hello");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("trims whitespace from output", async () => {
|
|
339
|
+
const result = await invokeClaude(
|
|
340
|
+
"test prompt",
|
|
341
|
+
{ spawn: mockSpawn(" hello \n", 0) },
|
|
342
|
+
);
|
|
343
|
+
expect(result).toBe("hello");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("throws on non-zero exit code", async () => {
|
|
347
|
+
try {
|
|
348
|
+
await invokeClaude("test prompt", { spawn: mockSpawn("error msg", 1) });
|
|
349
|
+
expect(true).toBe(false); // should not reach here
|
|
350
|
+
} catch (e) {
|
|
351
|
+
expect((e as Error).message).toContain("exited with code 1");
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("passes agent option to spawn command", async () => {
|
|
356
|
+
let capturedCmd: string[] = [];
|
|
357
|
+
const spySpawn: SpawnFn = (cmd) => {
|
|
358
|
+
capturedCmd = cmd;
|
|
359
|
+
return mockSpawn("ok", 0)(cmd);
|
|
360
|
+
};
|
|
361
|
+
await invokeClaude("test prompt", { agent: "my-agent", spawn: spySpawn });
|
|
362
|
+
expect(capturedCmd[0]).toBe("my-agent");
|
|
363
|
+
expect(capturedCmd[1]).toBe("-p");
|
|
364
|
+
});
|
|
365
|
+
});
|