macroclaw 0.8.0 → 0.10.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 +3 -3
- package/src/app.test.ts +126 -97
- package/src/claude.integration-test.ts +47 -79
- package/src/claude.test.ts +263 -213
- package/src/claude.ts +129 -97
- package/src/cli.test.ts +23 -0
- package/src/cli.ts +33 -1
- package/src/history.test.ts +10 -10
- package/src/history.ts +2 -2
- package/src/main.ts +3 -0
- package/src/orchestrator.test.ts +320 -197
- package/src/orchestrator.ts +172 -237
- package/src/service.test.ts +112 -0
- package/src/service.ts +50 -0
package/src/claude.test.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, mock } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { Claude, QueryParseError, QueryProcessError, QueryValidationError } from "./claude";
|
|
3
4
|
|
|
4
|
-
function
|
|
5
|
+
function envelope(opts: { structuredOutput?: unknown; result?: string; sessionId?: string; durationMs?: number; costUsd?: number }): string {
|
|
5
6
|
return JSON.stringify({
|
|
6
7
|
type: "result",
|
|
7
8
|
subtype: "success",
|
|
8
|
-
duration_ms: 1234,
|
|
9
|
-
total_cost_usd: 0.05,
|
|
10
|
-
result: "",
|
|
11
|
-
session_id: sessionId,
|
|
12
|
-
structured_output: structuredOutput,
|
|
9
|
+
duration_ms: opts.durationMs ?? 1234,
|
|
10
|
+
total_cost_usd: opts.costUsd ?? 0.05,
|
|
11
|
+
result: opts.result ?? "",
|
|
12
|
+
session_id: opts.sessionId ?? "server-sid",
|
|
13
|
+
structured_output: opts.structuredOutput,
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -19,247 +20,296 @@ afterEach(() => {
|
|
|
19
20
|
Bun.spawn = originalSpawn;
|
|
20
21
|
});
|
|
21
22
|
|
|
22
|
-
function mockSpawn(opts: {
|
|
23
|
-
stdout?: string;
|
|
24
|
-
stderr?: string;
|
|
25
|
-
exitCode?: number;
|
|
26
|
-
}) {
|
|
23
|
+
function mockSpawn(opts: { stdout?: string; stderr?: string; exitCode?: number }) {
|
|
27
24
|
const proc = {
|
|
28
25
|
stdout: new Response(opts.stdout ?? "").body,
|
|
29
26
|
stderr: new Response(opts.stderr ?? "").body,
|
|
30
27
|
exited: Promise.resolve(opts.exitCode ?? 0),
|
|
31
28
|
kill: mock(() => {}),
|
|
32
29
|
};
|
|
33
|
-
|
|
34
30
|
Bun.spawn = mock((() => proc) as any);
|
|
35
31
|
return proc;
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
function mockHangingSpawn(stdout: string) {
|
|
35
|
+
let resolveExited!: (code: number) => void;
|
|
36
|
+
const proc = {
|
|
37
|
+
stdout: new Response(stdout).body,
|
|
38
|
+
stderr: new Response("").body,
|
|
39
|
+
exited: new Promise<number>((resolve) => {
|
|
40
|
+
resolveExited = resolve;
|
|
41
|
+
}),
|
|
42
|
+
kill: mock(() => {}),
|
|
43
|
+
};
|
|
44
|
+
Bun.spawn = mock((() => proc) as any);
|
|
45
|
+
return { proc, resolveExited };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const TEST_WORKSPACE = "/tmp/claude2-test";
|
|
40
49
|
|
|
41
|
-
function makeClaude() {
|
|
42
|
-
return new Claude({ workspace: TEST_WORKSPACE,
|
|
50
|
+
function makeClaude(config?: { model?: string; systemPrompt?: string }) {
|
|
51
|
+
return new Claude({ workspace: TEST_WORKSPACE, ...config });
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
...overrides,
|
|
50
|
-
};
|
|
54
|
+
const textResult = { type: "text" } as const;
|
|
55
|
+
|
|
56
|
+
function objectResult(schema?: z.ZodType) {
|
|
57
|
+
return { type: "object" as const, schema: schema ?? z.any() };
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (isDeferred(result)) throw new Error("Expected sync result, got deferred");
|
|
56
|
-
return result;
|
|
60
|
+
function spawnArgs(): string[] {
|
|
61
|
+
return (Bun.spawn as any).mock.calls[0][0] as string[];
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
describe("Claude", () => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
expect.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
describe("newSession", () => {
|
|
66
|
+
it("uses --session-id with a generated UUID", async () => {
|
|
67
|
+
mockSpawn({ stdout: envelope({ result: "hi" }), exitCode: 0 });
|
|
68
|
+
const claude = makeClaude();
|
|
69
|
+
const query = claude.newSession("hello", textResult);
|
|
70
|
+
|
|
71
|
+
expect(query.sessionId).toMatch(/^[0-9a-f-]{36}$/);
|
|
72
|
+
const args = spawnArgs();
|
|
73
|
+
expect(args).toContain("--session-id");
|
|
74
|
+
expect(args).toContain(query.sessionId);
|
|
75
|
+
expect(args).not.toContain("--resume");
|
|
76
|
+
expect(args).not.toContain("--fork-session");
|
|
77
|
+
|
|
78
|
+
await query.result;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns text value for text resultType", async () => {
|
|
82
|
+
mockSpawn({ stdout: envelope({ result: "hello world" }), exitCode: 0 });
|
|
83
|
+
const claude = makeClaude();
|
|
84
|
+
const { value } = await claude.newSession("hi", textResult).result;
|
|
85
|
+
expect(value).toBe("hello world");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns parsed value for object resultType", async () => {
|
|
89
|
+
mockSpawn({ stdout: envelope({ structuredOutput: { action: "send", message: "hi" } }), exitCode: 0 });
|
|
90
|
+
const claude = makeClaude();
|
|
91
|
+
const schema = objectResult();
|
|
92
|
+
const { value } = await claude.newSession("hi", schema).result;
|
|
93
|
+
expect(value).toEqual({ action: "send", message: "hi" });
|
|
94
|
+
});
|
|
79
95
|
});
|
|
80
96
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
expect.
|
|
88
|
-
|
|
97
|
+
describe("resumeSession", () => {
|
|
98
|
+
it("uses --resume with the provided sessionId", async () => {
|
|
99
|
+
mockSpawn({ stdout: envelope({ result: "resumed" }), exitCode: 0 });
|
|
100
|
+
const claude = makeClaude();
|
|
101
|
+
const query = claude.resumeSession("existing-sid", "continue", textResult);
|
|
102
|
+
|
|
103
|
+
expect(query.sessionId).toBe("existing-sid");
|
|
104
|
+
const args = spawnArgs();
|
|
105
|
+
expect(args).toContain("--resume");
|
|
106
|
+
expect(args).toContain("existing-sid");
|
|
107
|
+
expect(args).not.toContain("--session-id");
|
|
108
|
+
expect(args).not.toContain("--fork-session");
|
|
109
|
+
|
|
110
|
+
await query.result;
|
|
111
|
+
});
|
|
89
112
|
});
|
|
90
113
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
expect.
|
|
98
|
-
|
|
114
|
+
describe("forkSession", () => {
|
|
115
|
+
it("generates a new session ID and passes parent via --resume", async () => {
|
|
116
|
+
mockSpawn({ stdout: envelope({ result: "forked" }), exitCode: 0 });
|
|
117
|
+
const claude = makeClaude();
|
|
118
|
+
const query = claude.forkSession("parent-sid", "bg task", textResult);
|
|
119
|
+
|
|
120
|
+
expect(query.sessionId).toMatch(/^[0-9a-f-]{36}$/);
|
|
121
|
+
expect(query.sessionId).not.toBe("parent-sid");
|
|
122
|
+
const args = spawnArgs();
|
|
123
|
+
expect(args).toContain("--resume");
|
|
124
|
+
expect(args).toContain("parent-sid");
|
|
125
|
+
expect(args).toContain("--fork-session");
|
|
126
|
+
expect(args).toContain("--session-id");
|
|
127
|
+
expect(args).toContain(query.sessionId);
|
|
128
|
+
|
|
129
|
+
await query.result;
|
|
130
|
+
});
|
|
99
131
|
});
|
|
100
132
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
expect.
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
describe("options", () => {
|
|
134
|
+
it("uses constructor model as default", async () => {
|
|
135
|
+
mockSpawn({ stdout: envelope({ result: "ok" }), exitCode: 0 });
|
|
136
|
+
const claude = makeClaude({ model: "sonnet" });
|
|
137
|
+
await claude.newSession("hi", textResult).result;
|
|
138
|
+
expect(spawnArgs()).toContain("sonnet");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("per-call model overrides constructor model", async () => {
|
|
142
|
+
mockSpawn({ stdout: envelope({ result: "ok" }), exitCode: 0 });
|
|
143
|
+
const claude = makeClaude({ model: "sonnet" });
|
|
144
|
+
await claude.newSession("hi", textResult, { model: "haiku" }).result;
|
|
145
|
+
const args = spawnArgs();
|
|
146
|
+
expect(args).toContain("haiku");
|
|
147
|
+
expect(args).not.toContain("sonnet");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("uses constructor systemPrompt as default", async () => {
|
|
151
|
+
mockSpawn({ stdout: envelope({ result: "ok" }), exitCode: 0 });
|
|
152
|
+
const claude = makeClaude({ systemPrompt: "Be helpful." });
|
|
153
|
+
await claude.newSession("hi", textResult).result;
|
|
154
|
+
const args = spawnArgs();
|
|
155
|
+
expect(args).toContain("--append-system-prompt");
|
|
156
|
+
expect(args).toContain("Be helpful.");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("per-call systemPrompt overrides constructor systemPrompt", async () => {
|
|
160
|
+
mockSpawn({ stdout: envelope({ result: "ok" }), exitCode: 0 });
|
|
161
|
+
const claude = makeClaude({ systemPrompt: "Be helpful." });
|
|
162
|
+
await claude.newSession("hi", textResult, { systemPrompt: "Be brief." }).result;
|
|
163
|
+
const args = spawnArgs();
|
|
164
|
+
expect(args).toContain("Be brief.");
|
|
165
|
+
expect(args).not.toContain("Be helpful.");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("omits --model when none specified", async () => {
|
|
169
|
+
mockSpawn({ stdout: envelope({ result: "ok" }), exitCode: 0 });
|
|
170
|
+
const claude = makeClaude();
|
|
171
|
+
await claude.newSession("hi", textResult).result;
|
|
172
|
+
expect(spawnArgs()).not.toContain("--model");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("omits --append-system-prompt when none specified", async () => {
|
|
176
|
+
mockSpawn({ stdout: envelope({ result: "ok" }), exitCode: 0 });
|
|
177
|
+
const claude = makeClaude();
|
|
178
|
+
await claude.newSession("hi", textResult).result;
|
|
179
|
+
expect(spawnArgs()).not.toContain("--append-system-prompt");
|
|
180
|
+
});
|
|
109
181
|
});
|
|
110
182
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
183
|
+
describe("resultType", () => {
|
|
184
|
+
it("omits --json-schema for text resultType", async () => {
|
|
185
|
+
mockSpawn({ stdout: envelope({ result: "plain" }), exitCode: 0 });
|
|
186
|
+
const claude = makeClaude();
|
|
187
|
+
await claude.newSession("hi", textResult).result;
|
|
188
|
+
expect(spawnArgs()).not.toContain("--json-schema");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("passes --json-schema for object resultType", async () => {
|
|
192
|
+
mockSpawn({ stdout: envelope({ structuredOutput: { ok: true } }), exitCode: 0 });
|
|
193
|
+
const claude = makeClaude();
|
|
194
|
+
const schema = objectResult();
|
|
195
|
+
await claude.newSession("hi", schema).result;
|
|
196
|
+
const args = spawnArgs();
|
|
197
|
+
expect(args).toContain("--json-schema");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("validates structured output through schema.parse", async () => {
|
|
201
|
+
mockSpawn({ stdout: envelope({ structuredOutput: { count: 5 } }), exitCode: 0 });
|
|
202
|
+
const claude = makeClaude();
|
|
203
|
+
const schema = objectResult(z.object({ count: z.number() }));
|
|
204
|
+
const { value } = await claude.newSession("hi", schema).result;
|
|
205
|
+
expect(value).toEqual({ count: 5 });
|
|
206
|
+
});
|
|
117
207
|
});
|
|
118
208
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
209
|
+
describe("metadata", () => {
|
|
210
|
+
it("returns duration and cost", async () => {
|
|
211
|
+
mockSpawn({ stdout: envelope({ result: "ok", durationMs: 2500, costUsd: 0.123 }), exitCode: 0 });
|
|
212
|
+
const claude = makeClaude();
|
|
213
|
+
const { duration, cost } = await claude.newSession("hi", textResult).result;
|
|
214
|
+
expect(duration).toBe("2.5s");
|
|
215
|
+
expect(cost).toBe("$0.1230");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("returns sessionId from server envelope", async () => {
|
|
219
|
+
mockSpawn({ stdout: envelope({ result: "ok", sessionId: "server-returned-sid" }), exitCode: 0 });
|
|
220
|
+
const claude = makeClaude();
|
|
221
|
+
const { sessionId } = await claude.newSession("hi", textResult).result;
|
|
222
|
+
expect(sessionId).toBe("server-returned-sid");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("sets startedAt on the running query", () => {
|
|
226
|
+
mockSpawn({ stdout: envelope({ result: "ok" }), exitCode: 0 });
|
|
227
|
+
const claude = makeClaude();
|
|
228
|
+
const before = new Date();
|
|
229
|
+
const query = claude.newSession("hi", textResult);
|
|
230
|
+
const after = new Date();
|
|
231
|
+
expect(query.startedAt.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
232
|
+
expect(query.startedAt.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
233
|
+
});
|
|
125
234
|
});
|
|
126
235
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
236
|
+
describe("errors", () => {
|
|
237
|
+
it("rejects with QueryProcessError on non-zero exit", async () => {
|
|
238
|
+
mockSpawn({ stderr: "boom", exitCode: 1 });
|
|
239
|
+
const claude = makeClaude();
|
|
240
|
+
try {
|
|
241
|
+
await claude.newSession("hi", textResult).result;
|
|
242
|
+
expect.unreachable("should have thrown");
|
|
243
|
+
} catch (err) {
|
|
244
|
+
expect(err).toBeInstanceOf(QueryProcessError);
|
|
245
|
+
expect((err as QueryProcessError).exitCode).toBe(1);
|
|
246
|
+
expect((err as QueryProcessError).stderr).toBe("boom");
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("rejects with QueryParseError on invalid JSON", async () => {
|
|
251
|
+
mockSpawn({ stdout: "not json", exitCode: 0 });
|
|
252
|
+
const claude = makeClaude();
|
|
253
|
+
try {
|
|
254
|
+
await claude.newSession("hi", textResult).result;
|
|
255
|
+
expect.unreachable("should have thrown");
|
|
256
|
+
} catch (err) {
|
|
257
|
+
expect(err).toBeInstanceOf(QueryParseError);
|
|
258
|
+
expect((err as QueryParseError).raw).toBe("not json");
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("rejects with QueryValidationError when schema.parse throws", async () => {
|
|
263
|
+
mockSpawn({ stdout: envelope({ structuredOutput: { bad: true } }), exitCode: 0 });
|
|
264
|
+
const claude = makeClaude();
|
|
265
|
+
const schema = objectResult(z.object({ count: z.number() }).strict());
|
|
266
|
+
try {
|
|
267
|
+
await claude.newSession("hi", schema).result;
|
|
268
|
+
expect.unreachable("should have thrown");
|
|
269
|
+
} catch (err) {
|
|
270
|
+
expect(err).toBeInstanceOf(QueryValidationError);
|
|
271
|
+
expect((err as QueryValidationError).raw).toEqual({ bad: true });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
136
274
|
});
|
|
137
275
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
expect(args).toContain("--json-schema");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("throws ClaudeProcessError on non-zero exit", async () => {
|
|
147
|
-
mockSpawn({ stderr: "something went wrong", exitCode: 1 });
|
|
148
|
-
const claude = makeClaude();
|
|
149
|
-
try {
|
|
150
|
-
await claude.run(opts({ sessionId: "sid-6" }));
|
|
151
|
-
expect.unreachable("should have thrown");
|
|
152
|
-
} catch (err) {
|
|
153
|
-
expect(err).toBeInstanceOf(ClaudeProcessError);
|
|
154
|
-
expect((err as ClaudeProcessError).exitCode).toBe(1);
|
|
155
|
-
expect((err as ClaudeProcessError).stderr).toBe("something went wrong");
|
|
156
|
-
}
|
|
157
|
-
});
|
|
276
|
+
describe("kill", () => {
|
|
277
|
+
it("kills the process and waits for exit", async () => {
|
|
278
|
+
const { proc, resolveExited } = mockHangingSpawn(envelope({ result: "done" }));
|
|
279
|
+
const claude = makeClaude();
|
|
280
|
+
const query = claude.newSession("hi", textResult);
|
|
158
281
|
|
|
159
|
-
|
|
160
|
-
let resolveExited!: (code: number) => void;
|
|
161
|
-
const stdoutData = jsonResult({ action: "send", message: "done" });
|
|
162
|
-
const proc = {
|
|
163
|
-
stdout: new Response(stdoutData).body,
|
|
164
|
-
stderr: new Response("").body,
|
|
165
|
-
exited: new Promise<number>((resolve) => {
|
|
166
|
-
resolveExited = resolve;
|
|
167
|
-
}),
|
|
168
|
-
kill: mock(() => {}),
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const origSetTimeout = globalThis.setTimeout;
|
|
172
|
-
globalThis.setTimeout = ((fn: Function) => {
|
|
173
|
-
fn();
|
|
174
|
-
return 0 as any;
|
|
175
|
-
}) as any;
|
|
176
|
-
|
|
177
|
-
Bun.spawn = mock((() => proc) as any);
|
|
178
|
-
const claude = makeClaude();
|
|
179
|
-
try {
|
|
180
|
-
const result = await claude.run(opts({ sessionId: "sid-7", timeoutMs: 60_000 }));
|
|
181
|
-
expect("deferred" in result && result.deferred).toBe(true);
|
|
182
|
-
const deferred = result as ClaudeDeferredResult;
|
|
183
|
-
expect(deferred.sessionId).toBe("sid-7");
|
|
184
|
-
|
|
185
|
-
// Process hasn't been killed
|
|
186
|
-
expect(proc.kill).not.toHaveBeenCalled();
|
|
187
|
-
|
|
188
|
-
// Resolve the process — completion should resolve
|
|
282
|
+
const killPromise = query.kill();
|
|
189
283
|
resolveExited(0);
|
|
190
|
-
|
|
191
|
-
expect(
|
|
192
|
-
}
|
|
193
|
-
globalThis.setTimeout = origSetTimeout;
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("returns structured_output from successful response", async () => {
|
|
198
|
-
mockSpawn({ stdout: jsonResult({ action: "silent", actionReason: "no new results" }), exitCode: 0 });
|
|
199
|
-
const claude = makeClaude();
|
|
200
|
-
const result = await runSync(claude, opts({ resume: true, sessionId: "sid-8" }));
|
|
201
|
-
expect(result.structuredOutput).toEqual({ action: "silent", actionReason: "no new results" });
|
|
202
|
-
expect(result.sessionId).toBe("test-session-id");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("returns null structuredOutput and result text when structured_output is missing", async () => {
|
|
206
|
-
const envelope = JSON.stringify({ type: "result", result: "plain text", duration_ms: 100, total_cost_usd: 0.01, session_id: "sid-abc" });
|
|
207
|
-
mockSpawn({ stdout: envelope, exitCode: 0 });
|
|
208
|
-
const claude = makeClaude();
|
|
209
|
-
const result = await runSync(claude, opts({ resume: true, sessionId: "sid-9" }));
|
|
210
|
-
expect(result.structuredOutput).toBeNull();
|
|
211
|
-
expect(result.result).toBe("plain text");
|
|
212
|
-
expect(result.sessionId).toBe("sid-abc");
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("returns empty sessionId when session_id is missing from envelope", async () => {
|
|
216
|
-
const envelope = JSON.stringify({ type: "result", result: "text", duration_ms: 100, total_cost_usd: 0.01 });
|
|
217
|
-
mockSpawn({ stdout: envelope, exitCode: 0 });
|
|
218
|
-
const claude = makeClaude();
|
|
219
|
-
const result = await runSync(claude, opts({ resume: true, sessionId: "sid-9c" }));
|
|
220
|
-
expect(result.sessionId).toBe("");
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("returns result from envelope", async () => {
|
|
224
|
-
mockSpawn({ stdout: jsonResult({ action: "send" }), exitCode: 0 });
|
|
225
|
-
const claude = makeClaude();
|
|
226
|
-
const result = await runSync(claude, opts({ resume: true, sessionId: "sid-9b" }));
|
|
227
|
-
expect(result.result).toBe("");
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("throws ClaudeParseError when stdout is not valid JSON", async () => {
|
|
231
|
-
mockSpawn({ stdout: "not json at all", exitCode: 0 });
|
|
232
|
-
const claude = makeClaude();
|
|
233
|
-
try {
|
|
234
|
-
await runSync(claude, opts({ resume: true, sessionId: "sid-10" }));
|
|
235
|
-
expect.unreachable("should have thrown");
|
|
236
|
-
} catch (err) {
|
|
237
|
-
expect(err).toBeInstanceOf(ClaudeParseError);
|
|
238
|
-
expect((err as ClaudeParseError).raw).toBe("not json at all");
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("returns duration and cost from envelope", async () => {
|
|
243
|
-
mockSpawn({ stdout: jsonResult({ action: "send" }), exitCode: 0 });
|
|
244
|
-
const claude = makeClaude();
|
|
245
|
-
const result = await runSync(claude, opts({ resume: true, sessionId: "sid-11" }));
|
|
246
|
-
expect(result.duration).toBe("1.2s");
|
|
247
|
-
expect(result.cost).toBe("$0.0500");
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("does not set timeout when timeoutMs is not provided", async () => {
|
|
251
|
-
mockSpawn({ stdout: jsonResult({ action: "send" }), exitCode: 0 });
|
|
252
|
-
const claude = makeClaude();
|
|
253
|
-
const result = await runSync(claude, opts({ resume: true, sessionId: "sid-12" }));
|
|
254
|
-
expect(result.structuredOutput).toEqual({ action: "send" });
|
|
284
|
+
await killPromise;
|
|
285
|
+
expect(proc.kill).toHaveBeenCalled();
|
|
286
|
+
});
|
|
255
287
|
});
|
|
256
288
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
289
|
+
describe("environment", () => {
|
|
290
|
+
it("strips CLAUDECODE env var", async () => {
|
|
291
|
+
mockSpawn({ stdout: envelope({ result: "ok" }), exitCode: 0 });
|
|
292
|
+
const claude = makeClaude();
|
|
293
|
+
await claude.newSession("hi", textResult).result;
|
|
294
|
+
const spawnOpts = (Bun.spawn as any).mock.calls[0][1];
|
|
295
|
+
expect(spawnOpts.env).not.toHaveProperty("CLAUDECODE");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("spawns in the configured workspace directory", async () => {
|
|
299
|
+
mockSpawn({ stdout: envelope({ result: "ok" }), exitCode: 0 });
|
|
300
|
+
const claude = makeClaude();
|
|
301
|
+
await claude.newSession("hi", textResult).result;
|
|
302
|
+
const spawnOpts = (Bun.spawn as any).mock.calls[0][1];
|
|
303
|
+
expect(spawnOpts.cwd).toBe(TEST_WORKSPACE);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("includes disallowedTools in CLI args", async () => {
|
|
307
|
+
mockSpawn({ stdout: envelope({ result: "ok" }), exitCode: 0 });
|
|
308
|
+
const claude = makeClaude();
|
|
309
|
+
await claude.newSession("hi", textResult).result;
|
|
310
|
+
const args = spawnArgs();
|
|
311
|
+
expect(args).toContain("--disallowedTools");
|
|
312
|
+
expect(args).toContain("CronList,CronDelete,CronCreate,AskUserQuestion");
|
|
313
|
+
});
|
|
264
314
|
});
|
|
265
315
|
});
|