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.
@@ -1,15 +1,16 @@
1
1
  import { afterEach, describe, expect, it, mock } from "bun:test";
2
- import { Claude, type ClaudeDeferredResult, ClaudeParseError, ClaudeProcessError, type ClaudeResult, type ClaudeRunOptions, isDeferred } from "./claude";
2
+ import { z } from "zod/v4";
3
+ import { Claude, QueryParseError, QueryProcessError, QueryValidationError } from "./claude";
3
4
 
4
- function jsonResult(structuredOutput: unknown, sessionId = "test-session-id"): string {
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
- const TEST_WORKSPACE = "/tmp/macroclaw-test-workspace";
39
- const DUMMY_SCHEMA = '{"type":"object"}';
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, jsonSchema: DUMMY_SCHEMA });
50
+ function makeClaude(config?: { model?: string; systemPrompt?: string }) {
51
+ return new Claude({ workspace: TEST_WORKSPACE, ...config });
43
52
  }
44
53
 
45
- function opts(overrides?: Partial<ClaudeRunOptions>): ClaudeRunOptions {
46
- return {
47
- prompt: "test message",
48
- sessionId: "sid-1",
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
- async function runSync(claude: Claude, options: ClaudeRunOptions): Promise<ClaudeResult> {
54
- const result = await claude.run(options);
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
- it("passes --session-id flag when resume is false/unset", async () => {
61
- mockSpawn({ stdout: jsonResult({ action: "send", message: "Hello" }), exitCode: 0 });
62
- const claude = makeClaude();
63
- const result = await runSync(claude, opts());
64
- expect(result.structuredOutput).toEqual({ action: "send", message: "Hello" });
65
- expect(Bun.spawn).toHaveBeenCalledWith(
66
- expect.arrayContaining(["claude", "-p", "--session-id", "sid-1", "--output-format", "json", "--json-schema"]),
67
- expect.objectContaining({ cwd: TEST_WORKSPACE, stdout: "pipe", stderr: "pipe" }),
68
- );
69
- });
70
-
71
- it("passes --resume flag when resume is true", async () => {
72
- mockSpawn({ stdout: jsonResult({ action: "send" }), exitCode: 0 });
73
- const claude = makeClaude();
74
- await claude.run(opts({ resume: true, sessionId: "sid-2" }));
75
- expect(Bun.spawn).toHaveBeenCalledWith(
76
- expect.arrayContaining(["claude", "-p", "--resume", "sid-2"]),
77
- expect.objectContaining({ cwd: TEST_WORKSPACE }),
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
- it("passes model flag when provided", async () => {
82
- mockSpawn({ stdout: jsonResult({ action: "send" }), exitCode: 0 });
83
- const claude = makeClaude();
84
- await claude.run(opts({ sessionId: "sid-3", model: "haiku", prompt: "msg" }));
85
- expect(Bun.spawn).toHaveBeenCalledWith(
86
- expect.arrayContaining(["--model", "haiku", "msg"]),
87
- expect.objectContaining({ cwd: TEST_WORKSPACE }),
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
- it("passes --append-system-prompt when provided", async () => {
92
- mockSpawn({ stdout: jsonResult({ action: "send" }), exitCode: 0 });
93
- const claude = makeClaude();
94
- await claude.run(opts({ sessionId: "sid-4", systemPrompt: "You are a background agent.", prompt: "msg" }));
95
- expect(Bun.spawn).toHaveBeenCalledWith(
96
- expect.arrayContaining(["--append-system-prompt", "You are a background agent.", "msg"]),
97
- expect.objectContaining({ cwd: TEST_WORKSPACE }),
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
- it("passes --fork-session when forkSession is true", async () => {
102
- mockSpawn({ stdout: jsonResult({ action: "send" }), exitCode: 0 });
103
- const claude = makeClaude();
104
- await claude.run(opts({ resume: true, sessionId: "sid-fork", forkSession: true, prompt: "bg task" }));
105
- expect(Bun.spawn).toHaveBeenCalledWith(
106
- expect.arrayContaining(["--resume", "sid-fork", "--fork-session"]),
107
- expect.objectContaining({ cwd: TEST_WORKSPACE }),
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
- it("omits --fork-session when not specified", async () => {
112
- mockSpawn({ stdout: jsonResult({ action: "send" }), exitCode: 0 });
113
- const claude = makeClaude();
114
- await claude.run(opts({ sessionId: "sid-nofork" }));
115
- const args = (Bun.spawn as any).mock.calls[0][0] as string[];
116
- expect(args).not.toContain("--fork-session");
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
- it("omits --append-system-prompt when undefined", async () => {
120
- mockSpawn({ stdout: jsonResult({ action: "send" }), exitCode: 0 });
121
- const claude = makeClaude();
122
- await claude.run(opts({ sessionId: "sid-5" }));
123
- const args = (Bun.spawn as any).mock.calls[0][0] as string[];
124
- expect(args).not.toContain("--append-system-prompt");
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
- it("omits --json-schema when plainText is true", async () => {
128
- const envelope = JSON.stringify({ type: "result", result: "status update", duration_ms: 100, total_cost_usd: 0.01, session_id: "sid-pt" });
129
- mockSpawn({ stdout: envelope, exitCode: 0 });
130
- const claude = makeClaude();
131
- const result = await runSync(claude, opts({ sessionId: "sid-pt", plainText: true }));
132
- const args = (Bun.spawn as any).mock.calls[0][0] as string[];
133
- expect(args).not.toContain("--json-schema");
134
- expect(args).toContain("--output-format");
135
- expect(result.result).toBe("status update");
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
- it("includes --json-schema when plainText is not set", async () => {
139
- mockSpawn({ stdout: jsonResult({ action: "send" }), exitCode: 0 });
140
- const claude = makeClaude();
141
- await claude.run(opts({ sessionId: "sid-schema" }));
142
- const args = (Bun.spawn as any).mock.calls[0][0] as string[];
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
- it("returns deferred result when process exceeds timeout", async () => {
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
- const completed = await deferred.completion;
191
- expect(completed.structuredOutput).toEqual({ action: "send", message: "done" });
192
- } finally {
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
- it("passes jsonSchema to CLI args", async () => {
258
- const schema = '{"type":"object","properties":{}}';
259
- mockSpawn({ stdout: jsonResult({ action: "send" }), exitCode: 0 });
260
- const claude = new Claude({ workspace: TEST_WORKSPACE, jsonSchema: schema });
261
- await claude.run(opts({ sessionId: "sid-13" }));
262
- const args = (Bun.spawn as any).mock.calls[0][0] as string[];
263
- expect(args).toContain(schema);
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
  });