1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
//! Tests for the headless Claude Code worker (#6887).
//!
//! Why: every assertion here is about a decision made BEFORE the child runs —
//! the argv, the stdin envelope, the scrubbed environment, and how a reply or a
//! failure is read back. None of them spends a token, so the suite is safe to
//! run on every build.
//! What: the `#[cfg(test)]` module `divert_worker.rs` includes.
use super::*;
/// Why (#6887): the file bytes must travel on stdin, not argv — argv is visible
/// to every process on the machine via `ps` and is length-bounded. A refactor
/// that "simplified" the payload into a `--append-system-prompt` would leak the
/// source of every diverted file into the process table.
/// What: asserts the model reaches argv, the headless/JSON flags are present,
/// every capability the worker does not need is switched off, and no argument
/// carries file content.
#[test]
fn worker_argv_carries_the_model_and_no_file_content() {
let argv = worker_argv("claude-haiku-4-5");
assert!(
argv.contains(&"-p".to_string()),
"must run headless: {argv:?}"
);
let model_at = argv
.iter()
.position(|a| a == "--model")
.expect("--model must be present");
assert_eq!(argv[model_at + 1], "claude-haiku-4-5");
let fmt_at = argv
.iter()
.position(|a| a == "--output-format")
.expect("--output-format must be present");
assert_eq!(
argv[fmt_at + 1],
"json",
"usage and cost only come back structured"
);
// The worker needs no tools, no MCP servers, no skills, and nobody is there
// to answer a permission prompt.
for flag in [
"--restricted",
"--strict-mcp-config",
"--disable-slash-commands",
"--permission-prompts",
] {
assert!(argv.contains(&flag.to_string()), "{flag} missing: {argv:?}");
}
assert!(!argv.contains(&"--mcp-config".to_string()));
// The one thing that must never be in argv.
let secret = "SENTINEL_FILE_BODY_9f2c";
let payload = wrap_files(&[("a.rs".into(), secret.into())], "what is this?");
assert!(payload.contains(secret), "the payload carries the content");
assert!(
!worker_argv("claude-haiku-4-5")
.iter()
.any(|a| a.contains(secret)),
"file content must never reach argv"
);
}
/// Why (#6887): shunt's envelope is what the #6882 measurements were taken
/// against, so the wire shape carries over only if it is byte-compatible.
/// What: one `<file path="…">` element per file, then the question.
#[test]
fn wrap_files_uses_the_shunt_envelope() {
let payload = wrap_files(
&[
("src/a.rs".into(), "fn a() {}".into()),
("src/b.rs".into(), "fn b() {}".into()),
],
"which functions exist?",
);
assert!(payload.contains("<file path=\"src/a.rs\">\nfn a() {}\n</file>"));
assert!(payload.contains("<file path=\"src/b.rs\">\nfn b() {}\n</file>"));
assert!(
payload
.trim_end()
.ends_with("Question: which functions exist?")
);
}
/// Why (#6887, the nesting risk): `claude -p` DOES start inside a running
/// Claude Code session, so the scrub is not a workaround for a refusal — it is
/// there because those variables bind the child to the parent's session id, IPC
/// socket, output cap and effort level. A child that kept them would talk on
/// the parent's messaging socket and claim the parent's session id.
/// What: asserts every parent-session binding observed in a live hook
/// subprocess is on the scrub list.
#[test]
fn scrubbed_env_drops_every_nested_session_binding() {
for name in [
"CLAUDECODE",
"CLAUDE_CODE_SESSION_ID",
"CLAUDE_CODE_MESSAGING_SOCKET",
"CLAUDE_CODE_MESSAGING_TOKEN",
"CLAUDE_CODE_CHILD_SESSION",
"CLAUDE_CODE_ENTRYPOINT",
"CLAUDE_CODE_EXECPATH",
"CLAUDE_CODE_MAX_OUTPUT_TOKENS",
"CLAUDE_EFFORT",
"CLAUDE_PID",
] {
assert!(
NESTED_SESSION_ENV.contains(&name),
"{name} binds the child to the parent session and must be scrubbed"
);
}
}
/// Why (owner ruling 2026-09-07): the worker runs under the developer's
/// EXISTING Claude Code login, which is exactly what `CLAUDE_CONFIG_DIR`
/// selects. Scrubbing it would send the child to a config dir with no OAuth
/// session, and every diversion would fail with `Not logged in`.
/// What: asserts the config dir is not on the scrub list.
#[test]
fn scrubbed_env_keeps_the_config_dir() {
assert!(
!NESTED_SESSION_ENV.contains(&"CLAUDE_CONFIG_DIR"),
"CLAUDE_CONFIG_DIR is the login the child inherits; scrubbing it breaks auth"
);
}
/// Why (#6887 acceptance criterion 6): the ledger line reports what the
/// diversion cost, so the numbers have to come off the child's own JSON rather
/// than be estimated.
/// What: a representative `--output-format json` object parses into the answer
/// text, the billed model, and all four token counters plus the cost.
#[test]
fn parse_worker_json_extracts_text_and_usage() {
let raw = r#"{
"result": " It defines two functions. ",
"is_error": false,
"total_cost_usd": 0.019422,
"usage": {
"input_tokens": 10,
"output_tokens": 412,
"cache_read_input_tokens": 9094,
"cache_creation_input_tokens": 21922
},
"modelUsage": { "claude-haiku-4-5": { "costUSD": 0.019422 } }
}"#;
let reply = parse_worker_json(raw).expect("a well-formed reply parses");
assert_eq!(reply.text, "It defines two functions.");
assert_eq!(reply.model, "claude-haiku-4-5");
assert_eq!(reply.input_tokens, 10);
assert_eq!(reply.output_tokens, 412);
assert_eq!(reply.cache_read_tokens, 9094);
assert_eq!(reply.cache_creation_tokens, 21922);
assert!((reply.cost_usd - 0.019422).abs() < 1e-9);
}
/// Why (#6887 acceptance criterion 4): a logged-out child exits ZERO and
/// reports the failure inside the JSON (`is_error: true`, `result: "Not logged
/// in · Please run /login"`), verified live on this workstation with `--bare`.
/// A parser that trusted the exit code would print that sentence as the file
/// summary — a silent wrong answer, which is worse than a fall-through.
/// What: `is_error: true` and an empty `result` both become `Err`.
#[test]
fn parse_worker_json_rejects_an_error_result() {
let logged_out = r#"{"is_error": true, "result": "Not logged in · Please run /login",
"total_cost_usd": 0, "usage": {}}"#;
let err = parse_worker_json(logged_out).expect_err("an error result must not answer");
assert!(
err.contains("Not logged in"),
"the reason must survive: {err}"
);
let empty = r#"{"is_error": false, "result": "", "usage": {}}"#;
assert!(
parse_worker_json(empty).is_err(),
"an empty answer is a failure, not a summary"
);
assert!(parse_worker_json("not json at all").is_err());
}
/// Why (#6887 acceptance criterion 4): "the `claude` binary is missing" is one
/// of the three named worker failures, and it must come back as the same `Err`
/// shape as the other two so the caller has one recovery path.
/// What: a program path that cannot be executed reports the failure by name
/// instead of panicking. The path is injected rather than arranged by emptying
/// `PATH`, because this bin target forbids process-global env mutation (#5544)
/// and a `PATH` rewrite corrupts every other test in the binary.
#[tokio::test]
async fn spawn_worker_at_reports_an_unstartable_binary() {
let tmp = tempfile::tempdir().unwrap();
let missing = tmp.path().join("no-such-claude");
let err = spawn_worker_at(&missing, "m", "payload", std::time::Duration::from_secs(5))
.await
.expect_err("an unstartable binary must not answer");
assert!(
err.contains(WORKER_BINARY),
"the error must name the worker: {err}"
);
}
/// Why (#6887 acceptance criterion 4): a hung worker is the third named
/// failure. Without the timeout the agent waits forever on a command it cannot
/// cancel.
/// What: a stand-in that never exits is killed and reported as a timeout — not
/// as a generic spawn failure, which would send the operator looking for the
/// wrong cause.
#[cfg(unix)]
#[tokio::test]
async fn spawn_worker_at_times_out() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
let fake = tmp.path().join("slow-claude");
// An absolute `sleep`: the child inherits this process's PATH, which the
// test must not touch.
std::fs::write(&fake, "#!/bin/sh\nexec /bin/sleep 30\n").unwrap();
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
let err = spawn_worker_at(&fake, "m", "payload", std::time::Duration::from_millis(300))
.await
.expect_err("a hung worker must not answer");
assert!(
err.contains("timed out"),
"a hang must report as a timeout, not a generic failure: {err}"
);
}
/// Why (#6887 acceptance criterion 4): the hook's fail-open branch turns on
/// this predicate, so it must actually reflect whether a binary is resolvable
/// rather than being a constant `true`.
/// What: agrees with `resolve_binary` for the worker binary.
#[test]
fn worker_available_reports_the_resolver() {
assert_eq!(
worker_available(),
trusty_common::bin_resolve::resolve_binary(WORKER_BINARY).is_some()
);
}