@letta-ai/letta-code 0.30.26 → 0.30.28
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/agent-presets.js +17 -17
- package/dist/agent-presets.js.map +1 -1
- package/dist/mcp-client.js +2 -2
- package/dist/mcp-client.js.map +1 -1
- package/dist/types/agent/turn-recovery-policy.d.ts +33 -0
- package/dist/types/agent/turn-recovery-policy.d.ts.map +1 -1
- package/dist/types/tools/impl/apply-patch.d.ts.map +1 -1
- package/dist/types/tools/secret-substitution.d.ts.map +1 -1
- package/dist/types/types/loop-status-protocol.d.ts +17 -0
- package/dist/types/types/loop-status-protocol.d.ts.map +1 -0
- package/dist/types/types/protocol_v2.d.ts +2 -19
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/dist/types/websocket/listener/inbound-queue.d.ts +5 -0
- package/dist/types/websocket/listener/inbound-queue.d.ts.map +1 -0
- package/dist/types/websocket/listener/protocol-outbound-routing.d.ts +9 -0
- package/dist/types/websocket/listener/protocol-outbound-routing.d.ts.map +1 -0
- package/dist/types/websocket/listener/protocol-outbound.d.ts.map +1 -1
- package/dist/types/websocket/listener/runtime.d.ts.map +1 -1
- package/dist/types/websocket/listener/turn-correlation.d.ts +10 -0
- package/dist/types/websocket/listener/turn-correlation.d.ts.map +1 -0
- package/dist/types/websocket/listener/types.d.ts +4 -0
- package/dist/types/websocket/listener/types.d.ts.map +1 -1
- package/letta.js +545 -120
- package/package.json +1 -1
- package/scripts/claude-watch/agent-watch.ts +622 -0
- package/scripts/claude-watch/docs-snapshot.test.ts +259 -0
- package/scripts/claude-watch/docs-snapshot.ts +672 -0
- package/scripts/claude-watch/fixtures/historical-replays.json +52 -0
- package/scripts/claude-watch/github.ts +137 -0
- package/scripts/claude-watch/release-analysis.test.ts +235 -0
- package/scripts/claude-watch/release-analysis.ts +297 -0
- package/scripts/claude-watch/release-source.test.ts +179 -0
- package/scripts/claude-watch/release-source.ts +369 -0
- package/scripts/claude-watch/runtime-observations.ts +98 -0
- package/scripts/claude-watch/runtime-probe.test.ts +576 -0
- package/scripts/claude-watch/runtime-probe.ts +911 -0
- package/scripts/claude-watch/runtime-sandbox.ts +170 -0
- package/scripts/claude-watch/state-branch.test.ts +211 -0
- package/scripts/claude-watch/state-branch.ts +316 -0
- package/scripts/claude-watch/tracker.test.ts +148 -0
- package/scripts/claude-watch/tracker.ts +325 -0
- package/scripts/claude-watch/types.ts +186 -0
- package/scripts/claude-watch/update-tracker.ts +201 -0
- package/scripts/codex-watch/agent-watch.ts +2 -2
- package/scripts/codex-watch/release-analysis.ts +14 -2
- package/scripts/codex-watch/tracker.ts +1 -3
- package/scripts/run-unit-tests.cjs +2 -0
- package/scripts/source-file-size-baseline.json +6 -5
- package/skills/creating-mods/references/commands.md +1 -1
- package/skills/creating-mods/references/ui.md +1 -1
- package/skills/customizing-commands/SKILL.md +1 -1
- package/skills/initializing-memory/SKILL.md +7 -7
- package/skills/self-configuration/SKILL.md +4 -4
- package/scripts/codex-watch/check-release.ts +0 -128
- package/scripts/codex-watch/render-issue.ts +0 -273
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, readdir, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { evaluateProbe } from "./runtime-observations.ts";
|
|
6
|
+
import {
|
|
7
|
+
CLAUDE_PROBE_CONTRACT_VERSION,
|
|
8
|
+
captureClaudeRuntime,
|
|
9
|
+
createClaudeRuntimeCommandPlan,
|
|
10
|
+
diffClaudeRuntime,
|
|
11
|
+
isClaudeProbeContractCurrent,
|
|
12
|
+
normalizeVolatile,
|
|
13
|
+
parseClaudeStream,
|
|
14
|
+
} from "./runtime-probe.ts";
|
|
15
|
+
import {
|
|
16
|
+
type CommandResult,
|
|
17
|
+
type CommandRunner,
|
|
18
|
+
type CommandSpec,
|
|
19
|
+
runBoundedCommand,
|
|
20
|
+
sandboxClaudeCommand,
|
|
21
|
+
} from "./runtime-sandbox.ts";
|
|
22
|
+
import type { ClaudeRuntimeSnapshot } from "./types.ts";
|
|
23
|
+
|
|
24
|
+
const tempRoots: string[] = [];
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
await Promise.all(
|
|
28
|
+
tempRoots
|
|
29
|
+
.splice(0)
|
|
30
|
+
.map((root) => rm(root, { recursive: true, force: true })),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
async function temporaryRoot(): Promise<string> {
|
|
35
|
+
const root = await mkdtemp(join(tmpdir(), "runtime-probe-test-"));
|
|
36
|
+
tempRoots.push(root);
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function result(
|
|
41
|
+
stdout = "",
|
|
42
|
+
overrides: Partial<CommandResult> = {},
|
|
43
|
+
): CommandResult {
|
|
44
|
+
return {
|
|
45
|
+
exitCode: 0,
|
|
46
|
+
stdout,
|
|
47
|
+
stderr: "",
|
|
48
|
+
timedOut: false,
|
|
49
|
+
truncated: false,
|
|
50
|
+
signal: null,
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function snapshot(
|
|
56
|
+
overrides: Partial<ClaudeRuntimeSnapshot> = {},
|
|
57
|
+
): ClaudeRuntimeSnapshot {
|
|
58
|
+
return {
|
|
59
|
+
probe_contract_version: CLAUDE_PROBE_CONTRACT_VERSION,
|
|
60
|
+
version: "1.2.3",
|
|
61
|
+
version_output: "1.2.3 (Claude Code)",
|
|
62
|
+
help_text: "--permission-mode auto",
|
|
63
|
+
help_hash: "help-a",
|
|
64
|
+
doctor: { exit_code: 0, summary: "ok" },
|
|
65
|
+
auto_mode_defaults: ["auto default"],
|
|
66
|
+
init: {
|
|
67
|
+
tools: ["Read"],
|
|
68
|
+
model: "claude-test",
|
|
69
|
+
capabilities: null,
|
|
70
|
+
stable_fields: {},
|
|
71
|
+
},
|
|
72
|
+
event_inventory: ["assistant", "system/init"],
|
|
73
|
+
probes: [],
|
|
74
|
+
digest: "digest",
|
|
75
|
+
...overrides,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("command planning", () => {
|
|
80
|
+
test("invalidates cached probes when their observation contract changes", () => {
|
|
81
|
+
expect(isClaudeProbeContractCurrent(snapshot())).toBe(true);
|
|
82
|
+
expect(
|
|
83
|
+
isClaudeProbeContractCurrent(
|
|
84
|
+
snapshot({ probe_contract_version: CLAUDE_PROBE_CONTRACT_VERSION - 1 }),
|
|
85
|
+
),
|
|
86
|
+
).toBe(false);
|
|
87
|
+
expect(isClaudeProbeContractCurrent(undefined)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("uses an exact local package install and a constrained inert command", () => {
|
|
91
|
+
const plan = createClaudeRuntimeCommandPlan("1.2.3", "/tmp/supplied/root", {
|
|
92
|
+
env: { PATH: "/bin", ANTHROPIC_API_KEY: "secret" },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(plan.packageSpec).toBe("@anthropic-ai/claude-code@1.2.3");
|
|
96
|
+
expect(plan.install.command).toBe("npm");
|
|
97
|
+
expect(plan.install.args.join(" ")).toContain(plan.packageSpec);
|
|
98
|
+
expect(plan.install.args).not.toContain("--ignore-scripts");
|
|
99
|
+
expect(plan.install.args.join(" ")).not.toMatch(
|
|
100
|
+
/@latest|\bnpx\b|--global|-g\b/,
|
|
101
|
+
);
|
|
102
|
+
expect(plan.binary).toStartWith(plan.installRoot);
|
|
103
|
+
expect(plan.init.env.HOME).toBe(plan.home);
|
|
104
|
+
expect(plan.init.env.CLAUDE_CONFIG_DIR).toBe(plan.config);
|
|
105
|
+
expect(plan.init.env.DISABLE_UPDATES).toBe("1");
|
|
106
|
+
expect(plan.install.env.ANTHROPIC_API_KEY).toBeUndefined();
|
|
107
|
+
expect(plan.init.env.ANTHROPIC_API_KEY).toBe("secret");
|
|
108
|
+
expect(plan.autoModeDefaults.args).toEqual(["auto-mode", "defaults"]);
|
|
109
|
+
expect(plan.init.args).toContain("stream-json");
|
|
110
|
+
expect(plan.init.args).toContain("dontAsk");
|
|
111
|
+
expect(plan.init.args).toContain("--safe-mode");
|
|
112
|
+
expect(plan.init.args).not.toContain("--tools");
|
|
113
|
+
expect(plan.probes).toHaveLength(2);
|
|
114
|
+
for (const probe of plan.probes) {
|
|
115
|
+
expect(probe.command.args).toContain("--allowedTools");
|
|
116
|
+
expect(probe.command.args).toContain("--safe-mode");
|
|
117
|
+
expect(probe.command.args).not.toContain("--max-turns");
|
|
118
|
+
expect(probe.command.args).toContain("--max-budget-usd");
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("rejects ranges and dist tags", () => {
|
|
123
|
+
expect(() => createClaudeRuntimeCommandPlan("latest", "/tmp/x")).toThrow(
|
|
124
|
+
"exact",
|
|
125
|
+
);
|
|
126
|
+
expect(() => createClaudeRuntimeCommandPlan("^1.2.3", "/tmp/x")).toThrow(
|
|
127
|
+
"exact",
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("container sandbox mounts only the disposable root and hides secret values from argv", () => {
|
|
132
|
+
const plan = createClaudeRuntimeCommandPlan("1.2.3", "/tmp/probe-root", {
|
|
133
|
+
env: {
|
|
134
|
+
PATH: "/usr/bin",
|
|
135
|
+
ANTHROPIC_API_KEY: "sk-ant-never-in-argv",
|
|
136
|
+
GH_TOKEN: "must-not-be-in-plan",
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const sandboxed = sandboxClaudeCommand(plan.init, plan.root);
|
|
140
|
+
const serialized = sandboxed.args.join(" ");
|
|
141
|
+
expect(sandboxed.command).toBe("/usr/bin/docker");
|
|
142
|
+
expect(serialized).toContain("/tmp/probe-root:/watch:rw");
|
|
143
|
+
expect(serialized).toContain("node:22.18.0-bookworm@sha256:");
|
|
144
|
+
expect(serialized).toContain("--cap-drop ALL");
|
|
145
|
+
expect(serialized).toContain("--read-only");
|
|
146
|
+
expect(serialized).toContain("--env ANTHROPIC_API_KEY");
|
|
147
|
+
expect(serialized).not.toContain("sk-ant-never-in-argv");
|
|
148
|
+
expect(serialized).not.toContain("GH_TOKEN");
|
|
149
|
+
expect(serialized).not.toContain(process.cwd());
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("stream parsing and normalization", () => {
|
|
154
|
+
test("extracts first init, event inventory, calls, and results", () => {
|
|
155
|
+
const stream = [
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
type: "system",
|
|
158
|
+
subtype: "init",
|
|
159
|
+
session_id: "volatile",
|
|
160
|
+
tools: ["Write", "Read", "Read"],
|
|
161
|
+
model: "claude-test",
|
|
162
|
+
permissionMode: "dontAsk",
|
|
163
|
+
capabilities: { beta: true, timestamp: "gone" },
|
|
164
|
+
}),
|
|
165
|
+
JSON.stringify({
|
|
166
|
+
type: "assistant",
|
|
167
|
+
message: {
|
|
168
|
+
content: [
|
|
169
|
+
{
|
|
170
|
+
type: "tool_use",
|
|
171
|
+
id: "tool-123",
|
|
172
|
+
name: "Read",
|
|
173
|
+
input: { file_path: "/tmp/random/file" },
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
type: "user",
|
|
180
|
+
message: {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "tool_result",
|
|
184
|
+
tool_use_id: "tool-123",
|
|
185
|
+
content: " 9→\tnine",
|
|
186
|
+
is_error: false,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
].join("\n");
|
|
192
|
+
|
|
193
|
+
const parsed = parseClaudeStream(stream);
|
|
194
|
+
expect(parsed.init?.tools).toEqual(["Read", "Write"]);
|
|
195
|
+
expect(parsed.init?.stableFields).toEqual({ permissionMode: "dontAsk" });
|
|
196
|
+
expect(parsed.init?.capabilities).toEqual({ beta: true });
|
|
197
|
+
expect(parsed.eventTypes).toEqual(["assistant", "system/init", "user"]);
|
|
198
|
+
expect(parsed.toolCalls).toEqual([
|
|
199
|
+
{ id: "tool-123", name: "Read", input: { file_path: "<tmp>" } },
|
|
200
|
+
]);
|
|
201
|
+
expect(parsed.toolResults[0]?.content).toBe(" 9→\tnine");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("evaluates exact Read bytes rather than mere tool completion", () => {
|
|
205
|
+
const definition = {
|
|
206
|
+
name: "read-lines-9-10-tab-prefix",
|
|
207
|
+
allowedTools: ["Read"],
|
|
208
|
+
prompt: "",
|
|
209
|
+
};
|
|
210
|
+
const exact = parseClaudeStream(
|
|
211
|
+
[
|
|
212
|
+
JSON.stringify({ type: "system", subtype: "init", tools: ["Read"] }),
|
|
213
|
+
JSON.stringify({
|
|
214
|
+
type: "assistant",
|
|
215
|
+
message: {
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: "tool_use",
|
|
219
|
+
id: "read-1",
|
|
220
|
+
name: "Read",
|
|
221
|
+
input: { file_path: "./read-fixture.txt", offset: 9, limit: 2 },
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
JSON.stringify({
|
|
227
|
+
type: "user",
|
|
228
|
+
message: {
|
|
229
|
+
content: [
|
|
230
|
+
{
|
|
231
|
+
type: "tool_result",
|
|
232
|
+
tool_use_id: "read-1",
|
|
233
|
+
content: "9\tline9\n10\tline10",
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
}),
|
|
238
|
+
].join("\n"),
|
|
239
|
+
);
|
|
240
|
+
expect(evaluateProbe(definition.name, exact)).toEqual({
|
|
241
|
+
complete: true,
|
|
242
|
+
assertions: {
|
|
243
|
+
exact_line_9: true,
|
|
244
|
+
exact_line_10: true,
|
|
245
|
+
no_line_9_padding: true,
|
|
246
|
+
no_arrow_separator: true,
|
|
247
|
+
result_not_error: true,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const arrow = parseClaudeStream(
|
|
252
|
+
[
|
|
253
|
+
JSON.stringify({ type: "system", subtype: "init", tools: ["Read"] }),
|
|
254
|
+
JSON.stringify({
|
|
255
|
+
type: "assistant",
|
|
256
|
+
message: {
|
|
257
|
+
content: [
|
|
258
|
+
{
|
|
259
|
+
type: "tool_use",
|
|
260
|
+
id: "read-2",
|
|
261
|
+
name: "Read",
|
|
262
|
+
input: { offset: 9, limit: 2 },
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
type: "user",
|
|
269
|
+
message: {
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: "tool_result",
|
|
273
|
+
tool_use_id: "read-2",
|
|
274
|
+
content: " 9→line9\n10→line10",
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
].join("\n"),
|
|
280
|
+
);
|
|
281
|
+
expect(evaluateProbe(definition.name, arrow).complete).toBe(true);
|
|
282
|
+
expect(evaluateProbe(definition.name, arrow).assertions).toMatchObject({
|
|
283
|
+
exact_line_9: false,
|
|
284
|
+
exact_line_10: false,
|
|
285
|
+
no_arrow_separator: false,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("evaluates arbitrary task metadata, null deletion, and permanent deletion", () => {
|
|
290
|
+
const evaluation = evaluateProbe("task-metadata-delete-contract", {
|
|
291
|
+
toolCalls: [
|
|
292
|
+
{ id: "create", name: "TaskCreate", input: {} },
|
|
293
|
+
{
|
|
294
|
+
id: "metadata",
|
|
295
|
+
name: "TaskUpdate",
|
|
296
|
+
input: {
|
|
297
|
+
taskId: "1",
|
|
298
|
+
metadata: {
|
|
299
|
+
probe: null,
|
|
300
|
+
count: 3,
|
|
301
|
+
flags: ["ready"],
|
|
302
|
+
details: { source: "claude-watch" },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
{ id: "before", name: "TaskGet", input: { taskId: "1" } },
|
|
307
|
+
{
|
|
308
|
+
id: "delete",
|
|
309
|
+
name: "TaskUpdate",
|
|
310
|
+
input: { taskId: "1", status: "deleted" },
|
|
311
|
+
},
|
|
312
|
+
{ id: "after", name: "TaskGet", input: { taskId: "1" } },
|
|
313
|
+
{ id: "list", name: "TaskList", input: {} },
|
|
314
|
+
],
|
|
315
|
+
toolResults: [
|
|
316
|
+
{ toolUseId: "create", content: "{}", isError: false },
|
|
317
|
+
{ toolUseId: "metadata", content: "{}", isError: false },
|
|
318
|
+
{
|
|
319
|
+
toolUseId: "before",
|
|
320
|
+
content: JSON.stringify({
|
|
321
|
+
metadata: {
|
|
322
|
+
keep: "yes",
|
|
323
|
+
count: 3,
|
|
324
|
+
flags: ["ready"],
|
|
325
|
+
details: { source: "claude-watch" },
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
328
|
+
isError: false,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
toolUseId: "delete",
|
|
332
|
+
content: '{"status":"deleted"}',
|
|
333
|
+
isError: false,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
toolUseId: "after",
|
|
337
|
+
content: "Task not found",
|
|
338
|
+
isError: true,
|
|
339
|
+
},
|
|
340
|
+
{ toolUseId: "list", content: '{"tasks":[]}', isError: false },
|
|
341
|
+
],
|
|
342
|
+
});
|
|
343
|
+
expect(evaluation).toEqual({
|
|
344
|
+
complete: true,
|
|
345
|
+
assertions: {
|
|
346
|
+
metadata_arbitrary_values_accepted: true,
|
|
347
|
+
metadata_null_update_accepted: true,
|
|
348
|
+
deleted_task_get_errors: true,
|
|
349
|
+
deleted_task_absent: true,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("rejects malformed and empty streams without including raw data", () => {
|
|
355
|
+
expect(() => parseClaudeStream('{"type":"system"}\n{secret')).toThrow(
|
|
356
|
+
"line 2",
|
|
357
|
+
);
|
|
358
|
+
expect(() => parseClaudeStream(" \n")).toThrow("no events");
|
|
359
|
+
try {
|
|
360
|
+
parseClaudeStream("not-json-containing-sk-ant-supersecret");
|
|
361
|
+
} catch (error) {
|
|
362
|
+
expect(String(error)).not.toContain("supersecret");
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("removes volatile fields, paths, identifiers, timestamps, and secrets", () => {
|
|
367
|
+
expect(
|
|
368
|
+
normalizeVolatile({
|
|
369
|
+
session_id: "drop",
|
|
370
|
+
timestamp: "drop",
|
|
371
|
+
stable: "at /tmp/random-123/file",
|
|
372
|
+
nested: { request_id: "drop", token: "sk-ant-abcdefghijk" },
|
|
373
|
+
}),
|
|
374
|
+
).toEqual({ nested: {}, stable: "at <tmp>" });
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe("runtime diff", () => {
|
|
379
|
+
test("reports tool/event changes and normalized probe differences", () => {
|
|
380
|
+
const before = snapshot({
|
|
381
|
+
probes: [
|
|
382
|
+
{
|
|
383
|
+
name: "read-lines-9-10-tab-prefix",
|
|
384
|
+
status: "passed",
|
|
385
|
+
attempts: 1,
|
|
386
|
+
assertions: { exact: false },
|
|
387
|
+
tool_calls: [{ name: "Read", input: { offset: 9, limit: 2 } }],
|
|
388
|
+
tool_results: ["old"],
|
|
389
|
+
filesystem_changes: [],
|
|
390
|
+
error: null,
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
});
|
|
394
|
+
const after = snapshot({
|
|
395
|
+
init: {
|
|
396
|
+
tools: ["Read", "TaskCreate"],
|
|
397
|
+
model: "claude-test",
|
|
398
|
+
capabilities: null,
|
|
399
|
+
stable_fields: {},
|
|
400
|
+
},
|
|
401
|
+
event_inventory: ["assistant", "result", "system/init"],
|
|
402
|
+
probes: [
|
|
403
|
+
{
|
|
404
|
+
name: "read-lines-9-10-tab-prefix",
|
|
405
|
+
status: "passed",
|
|
406
|
+
attempts: 1,
|
|
407
|
+
assertions: { exact: true },
|
|
408
|
+
tool_calls: [{ name: "Read", input: { offset: 9, limit: 2 } }],
|
|
409
|
+
tool_results: ["new"],
|
|
410
|
+
filesystem_changes: [],
|
|
411
|
+
error: null,
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(diffClaudeRuntime(before, after)).toEqual({
|
|
417
|
+
tools_added: ["TaskCreate"],
|
|
418
|
+
tools_removed: [],
|
|
419
|
+
help_changed: false,
|
|
420
|
+
help_lines_added: [],
|
|
421
|
+
help_lines_removed: [],
|
|
422
|
+
doctor_changed: false,
|
|
423
|
+
init_changed: true,
|
|
424
|
+
auto_mode_defaults_changed: false,
|
|
425
|
+
event_types_added: ["result"],
|
|
426
|
+
event_types_removed: [],
|
|
427
|
+
changed_probes: ["read-lines-9-10-tab-prefix"],
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe("capture orchestration", () => {
|
|
433
|
+
function successfulRunner(
|
|
434
|
+
version = "1.2.3",
|
|
435
|
+
calls: CommandSpec[] = [],
|
|
436
|
+
): CommandRunner {
|
|
437
|
+
return async (spec) => {
|
|
438
|
+
calls.push(spec);
|
|
439
|
+
switch (spec.label) {
|
|
440
|
+
case "verify-package":
|
|
441
|
+
return result(version);
|
|
442
|
+
case "version":
|
|
443
|
+
return result(`${version} (Claude Code)`);
|
|
444
|
+
case "help":
|
|
445
|
+
return result("--permission-mode auto (default: auto)");
|
|
446
|
+
case "auto-mode-defaults":
|
|
447
|
+
return result('{"allow":["Read"],"soft_deny":[]}');
|
|
448
|
+
case "doctor":
|
|
449
|
+
return result("Doctor OK");
|
|
450
|
+
default:
|
|
451
|
+
return result();
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
test("fails package version mismatch before invoking the binary", async () => {
|
|
457
|
+
const root = await temporaryRoot();
|
|
458
|
+
const calls: CommandSpec[] = [];
|
|
459
|
+
await expect(
|
|
460
|
+
captureClaudeRuntime({
|
|
461
|
+
version: "1.2.3",
|
|
462
|
+
tempDir: root,
|
|
463
|
+
runner: successfulRunner("1.2.4", calls),
|
|
464
|
+
}),
|
|
465
|
+
).rejects.toThrow("package version mismatch");
|
|
466
|
+
expect(calls.some((call) => call.label === "version")).toBe(false);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("fails claude --version mismatch", async () => {
|
|
470
|
+
const root = await temporaryRoot();
|
|
471
|
+
const runner: CommandRunner = async (spec) => {
|
|
472
|
+
if (spec.label === "verify-package") return result("1.2.3");
|
|
473
|
+
if (spec.label === "version") return result("1.2.4 (Claude Code)");
|
|
474
|
+
return result();
|
|
475
|
+
};
|
|
476
|
+
await expect(
|
|
477
|
+
captureClaudeRuntime({ version: "1.2.3", tempDir: root, runner }),
|
|
478
|
+
).rejects.toThrow("--version mismatch");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("missing auth captures public surfaces and skips authenticated probes", async () => {
|
|
482
|
+
const root = await temporaryRoot();
|
|
483
|
+
const calls: CommandSpec[] = [];
|
|
484
|
+
const captured = await captureClaudeRuntime({
|
|
485
|
+
version: "1.2.3",
|
|
486
|
+
tempDir: root,
|
|
487
|
+
env: { PATH: process.env.PATH },
|
|
488
|
+
runner: successfulRunner("1.2.3", calls),
|
|
489
|
+
requireAuth: false,
|
|
490
|
+
});
|
|
491
|
+
expect(captured?.init).toBeNull();
|
|
492
|
+
expect(captured?.probes.map((probe) => probe.status)).toEqual([
|
|
493
|
+
"skipped",
|
|
494
|
+
"skipped",
|
|
495
|
+
]);
|
|
496
|
+
expect(
|
|
497
|
+
calls.some(
|
|
498
|
+
(call) =>
|
|
499
|
+
call.label === "init-stream" || call.label.startsWith("probe:"),
|
|
500
|
+
),
|
|
501
|
+
).toBe(false);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("captures optional diagnostic timeouts without losing runtime evidence", async () => {
|
|
505
|
+
const root = await temporaryRoot();
|
|
506
|
+
const base = successfulRunner();
|
|
507
|
+
const runner: CommandRunner = async (spec) =>
|
|
508
|
+
spec.label === "doctor" || spec.label === "auto-mode-defaults"
|
|
509
|
+
? result("", { exitCode: null, timedOut: true, signal: "SIGTERM" })
|
|
510
|
+
: base(spec);
|
|
511
|
+
const captured = await captureClaudeRuntime({
|
|
512
|
+
version: "1.2.3",
|
|
513
|
+
tempDir: root,
|
|
514
|
+
env: { PATH: process.env.PATH },
|
|
515
|
+
runner,
|
|
516
|
+
requireAuth: false,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
expect(captured?.doctor).toEqual({ exit_code: -1, summary: "timed_out" });
|
|
520
|
+
expect(captured?.auto_mode_defaults).toBeNull();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test("nonzero authenticated stream fails safely", async () => {
|
|
524
|
+
const root = await temporaryRoot();
|
|
525
|
+
const base = successfulRunner();
|
|
526
|
+
const runner: CommandRunner = async (spec) =>
|
|
527
|
+
spec.label === "init-stream"
|
|
528
|
+
? result("", {
|
|
529
|
+
exitCode: 2,
|
|
530
|
+
stderr: "internal failure: sk-ant-do-not-print",
|
|
531
|
+
})
|
|
532
|
+
: base(spec);
|
|
533
|
+
await expect(
|
|
534
|
+
captureClaudeRuntime({
|
|
535
|
+
version: "1.2.3",
|
|
536
|
+
tempDir: root,
|
|
537
|
+
env: { PATH: process.env.PATH, ANTHROPIC_API_KEY: "secret" },
|
|
538
|
+
runner,
|
|
539
|
+
}),
|
|
540
|
+
).rejects.toThrow("exited nonzero");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("dry-run creates nothing and runs no commands", async () => {
|
|
544
|
+
const root = await temporaryRoot();
|
|
545
|
+
let called = false;
|
|
546
|
+
const captured = await captureClaudeRuntime({
|
|
547
|
+
version: "1.2.3",
|
|
548
|
+
tempDir: root,
|
|
549
|
+
dryRun: true,
|
|
550
|
+
runner: async () => {
|
|
551
|
+
called = true;
|
|
552
|
+
return result();
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
expect(captured).toBeNull();
|
|
556
|
+
expect(called).toBe(false);
|
|
557
|
+
expect(await readdir(root)).toEqual([]);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
describe("bounded process runner", () => {
|
|
562
|
+
test("marks a command timed out and terminates it", async () => {
|
|
563
|
+
const root = await temporaryRoot();
|
|
564
|
+
const commandResult = await runBoundedCommand({
|
|
565
|
+
command: process.execPath,
|
|
566
|
+
args: ["-e", "setInterval(() => {}, 1000)"],
|
|
567
|
+
cwd: root,
|
|
568
|
+
env: process.env,
|
|
569
|
+
timeoutMs: 30,
|
|
570
|
+
outputCapBytes: 1024,
|
|
571
|
+
label: "timeout-test",
|
|
572
|
+
});
|
|
573
|
+
expect(commandResult.timedOut).toBe(true);
|
|
574
|
+
expect(commandResult.exitCode).not.toBe(0);
|
|
575
|
+
});
|
|
576
|
+
});
|