use super::*;
const PATH: &str = "probe::builder";
#[test]
fn every_production_launch_line_preserves_transcript_saving() {
let check = check_transcript_saving();
assert_eq!(
check.status,
CheckStatus::Ok,
"every production launch line must preserve transcript saving; got: {}",
check.message
);
assert_eq!(check.name, CHECK_NAME);
}
#[test]
fn launch_lines_covers_every_builder() {
let lines = super::launch_lines();
let labels: Vec<&str> = lines.iter().map(|line| line.label).collect();
for expected in [
"runtime::managed_launch::managed_env_unset",
"core::model_inject::build_claude_command",
"core::model_inject::build_inplace_session_command",
"core::model_inject::build_client_session_command",
"runtime::cli_launch::isolated_spec",
"runtime::cli_launch::inplace_spec",
"runtime::cli_launch::client_spec",
"daemon::spawn_command::relaunch_command",
"daemon::spawn_command::gui_spawn_command",
"core::standalone::run::build_launch_command",
"control::backend::stream_json::build_claude_command",
"control::backend::tmux::pane_claude_line",
] {
assert!(
labels.iter().any(|l| l.contains(expected)),
"the check must cover {expected}; covered: {labels:?}"
);
}
for LaunchLine { label, unset, .. } in &lines {
assert!(
unset.iter().any(|n| n == "CLAUDE_CODE_CHILD_SESSION"),
"{label} must unset CLAUDE_CODE_CHILD_SESSION; unsets: {unset:?}"
);
assert!(
unset.iter().any(|n| n == "TRUSTY_MPM_SESSION_PROFILE"),
"{label} must unset TRUSTY_MPM_SESSION_PROFILE; unsets: {unset:?}"
);
assert!(
!unset.iter().any(|n| n == "CLAUDE_CONFIG_DIR"),
"{label} must NOT unset CLAUDE_CONFIG_DIR (#4455); unsets: {unset:?}"
);
assert!(
!unset.iter().any(|n| n == "CLAUDE_CODE_OAUTH_TOKEN"),
"{label} must NOT unset CLAUDE_CODE_OAUTH_TOKEN (#2246); unsets: {unset:?}"
);
}
}
#[test]
fn pm_launch_builders_carry_the_prompt_file() {
let lines = super::launch_lines();
let pm: Vec<(&str, &str)> = lines
.iter()
.filter_map(|l| l.pm_line.as_deref().map(|line| (l.label, line)))
.collect();
for expected in [
"core::model_inject::build_claude_command",
"core::model_inject::build_inplace_session_command_with_prompt",
"core::model_inject::build_client_session_command",
"runtime::cli_launch::isolated_spec",
"runtime::cli_launch::inplace_spec",
"runtime::cli_launch::client_spec",
"daemon::spawn_command::gui_spawn_command",
"core::standalone::run::build_launch_command",
] {
assert!(
pm.iter().any(|(label, _)| label.contains(expected)),
"{expected} must be a PM entry of launch_lines(); PM entries: {pm:?}"
);
}
for (label, line) in &pm {
assert!(
line.contains("--append-system-prompt-file") && line.contains(super::PROBE_PROMPT),
"{label} must hand the PM prompt file to claude: {line}"
);
}
}
const ALLOWLISTED_CLAUDE_SITES: &[(&str, usize, &str)] = &[
(
"core/model_inject.rs",
2,
"build_client_session_command (in launch_lines()) plus the `head()` test helper \
that reconstructs the shared prefix. #6495 dropped this from 3 to 2 WITHOUT \
removing a launch line: `client_session_command_appends_the_prompt_file` had \
its own copy of that prefix, which had to be rebuilt when the \
alternate-screen operand landed, so it now calls `head()` instead of \
restating it — one literal fewer, one duplicate fewer. \
#4181 dropped this from 4 to 3 WITHOUT removing a launch line: \
build_claude_command gained optional CLAUDE_CONFIG_DIR / \
CLAUDE_CODE_OAUTH_TOKEN assignments between the scrub flags and the \
program word, so its `format!(\"env{} claude\")` became a push_str \
sequence and the literal it emits is ` claude`, which this scanner's \
patterns do not match. The builder is still registered in launch_lines() \
— see `claude_command_relocates_the_config_dir`, which pins the whole \
emitted line",
),
(
"core/standalone/run.rs",
2,
"build_launch_command (tm run, in launch_lines()) and build_login_command \
(`claude auth login` — an auth subcommand, not an interactive session, so it \
creates no transcript to suppress)",
),
(
"daemon/spawn_command.rs",
1,
"relaunch_command; in launch_lines(). #8286's gui_spawn_command, also in \
launch_lines(), emits ` claude --`, which no pattern here matches",
),
(
"control/backend/stream_json.rs",
1,
"build_claude_command (headless); in launch_lines()",
),
(
"core/claude_env_scrub_tests.rs",
2,
"test fixtures for scrub_command — not launch lines",
),
(
"core/alt_screen.rs",
5,
"test fixtures for apply_default_when_unset and apply_configured_to_command (#6495, #7160, #8405) — not launch lines. \
The module emits no command of its own: it supplies the alt-screen and \
mouse-capture defaults the real builders carry, as a shell operand or a \
Command mutation",
),
(
"core/spawn_disclaim/pane.rs",
3,
"test fixtures for the disclaim wrapper — not launch lines",
),
(
"daemon/discovery.rs",
1,
"a test fixture pane-line string parsed by discovery — not a launch line",
),
];
#[test]
fn every_claude_launch_site_in_the_crate_is_allowlisted() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut found: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
let mut stack = vec![src.clone()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("src must be readable") {
let path = entry.expect("dir entry").path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
if path.file_name().and_then(|n| n.to_str())
== Some("doctor_transcript_saving_tests.rs")
{
continue;
}
let text = std::fs::read_to_string(&path).expect("source must be readable");
let hits = text
.lines()
.filter(|line| {
let t = line.trim_start();
!(t.starts_with("//") || t.starts_with("*"))
})
.filter(|line| {
line.contains(r#"Command::new("claude")"#)
|| line.contains(r#""claude ""#)
|| line.contains(r#""claude {"#)
|| line.contains(r#""claude --"#)
|| line.contains(r#"} claude""#)
})
.count();
if hits > 0 {
let rel = path
.strip_prefix(&src)
.expect("under src")
.to_string_lossy()
.replace('\\', "/");
found.insert(rel, hits);
}
}
}
let expected: std::collections::BTreeMap<String, usize> = ALLOWLISTED_CLAUDE_SITES
.iter()
.map(|(f, n, _)| ((*f).to_owned(), *n))
.collect();
assert_eq!(
found, expected,
"\nA `claude` launch site changed.\n\
If you ADDED a launch line: route it through `core::claude_env_scrub` (never a second \
mechanism), register it in `daemon::doctor_transcript_saving::launch_lines()` so the \
`transcript_saving` check covers it, and then update ALLOWLISTED_CLAUDE_SITES.\n\
If it is not a launch line, add it with a reason saying why.\n\
found: {found:#?}\n\
expected: {expected:#?}"
);
}
#[test]
fn ok_message_names_every_binary_crate_gap() {
let check = check_transcript_saving();
assert_eq!(check.status, CheckStatus::Ok, "{}", check.message);
for gap in BINARY_CRATE_BUILDERS {
assert!(
check.message.contains(gap),
"the Ok message must name the binary-crate gap {gap}: {}",
check.message
);
}
assert!(
check.message.contains(&format!(
"{} launch line(s) in the `tm` BINARY",
BINARY_CRATE_BUILDERS.len()
)),
"the message must COUNT the gaps rather than implying one: {}",
check.message
);
}
#[test]
fn probe_supplies_an_oauth_token_so_over_scrub_is_visible() {
let config_dir = std::path::PathBuf::from(PROBE_CONFIG_DIR);
let prefix = crate::core::model_inject::build_claude_command_with_configured(
None,
None,
Some(&config_dir),
Some(PROBE_TOKEN),
&[],
None,
false,
);
assert!(
prefix.contains("CLAUDE_CODE_OAUTH_TOKEN="),
"the probe line must carry the token assignment: {prefix}"
);
assert!(
prefix.contains("CLAUDE_CONFIG_DIR="),
"the probe line must carry the config-dir assignment: {prefix}"
);
let managed = crate::runtime::managed_env_unset(&[]);
for deliberate in DELIBERATE_SPAWN_ENV {
assert!(
!managed.iter().any(|n| n == deliberate),
"the daemon launch must never unset {deliberate}: {managed:?}"
);
}
}
#[test]
fn ok_when_the_marker_is_scrubbed() {
let check = verdict(
PATH,
&["ANTHROPIC_API_KEY", TRANSCRIPT_SUPPRESSING_MARKER],
&[],
);
assert_eq!(check.status, CheckStatus::Ok, "{}", check.message);
}
#[test]
fn fails_when_the_suppressing_marker_is_not_scrubbed() {
let check = verdict(PATH, &["ANTHROPIC_API_KEY"], &[]);
assert_eq!(
check.status,
CheckStatus::Fail,
"omitting the suppressing marker must FAIL: {}",
check.message
);
assert!(
check.message.contains(TRANSCRIPT_SUPPRESSING_MARKER),
"the message must name the marker: {}",
check.message
);
assert!(
check.message.contains("4467"),
"the message must cite the issue: {}",
check.message
);
}
#[test]
fn failure_names_the_leaking_path() {
let check = verdict("core::some::builder", &["ANTHROPIC_API_KEY"], &[]);
assert_eq!(check.status, CheckStatus::Fail);
assert!(
check.message.contains("core::some::builder"),
"the message must name the offending builder: {}",
check.message
);
}
#[test]
fn fails_when_the_scrub_would_take_the_config_dir() {
let check = verdict(
PATH,
&[
"ANTHROPIC_API_KEY",
TRANSCRIPT_SUPPRESSING_MARKER,
"CLAUDE_CONFIG_DIR",
],
&[],
);
assert_eq!(
check.status,
CheckStatus::Fail,
"unsetting CLAUDE_CONFIG_DIR must FAIL: {}",
check.message
);
assert!(
check.message.contains("CLAUDE_CONFIG_DIR"),
"the message must name the variable: {}",
check.message
);
assert!(
check.message.contains("4451"),
"the message must cite the roster regression it prevents: {}",
check.message
);
}
#[test]
fn over_scrub_of_the_oauth_token_is_detected() {
let check = verdict(
PATH,
&[TRANSCRIPT_SUPPRESSING_MARKER, "CLAUDE_CODE_OAUTH_TOKEN"],
&[],
);
assert_eq!(
check.status,
CheckStatus::Fail,
"unsetting the OAuth token must FAIL: {}",
check.message
);
assert!(
check.message.contains("CLAUDE_CODE_OAUTH_TOKEN"),
"the message must name the token: {}",
check.message
);
assert!(
check.message.contains("2246"),
"the message must cite the login-loop regression: {}",
check.message
);
}
#[test]
fn config_dir_over_scrub_takes_precedence_in_the_message() {
let check = verdict(
PATH,
&[TRANSCRIPT_SUPPRESSING_MARKER, "CLAUDE_CONFIG_DIR"],
&[],
);
assert_eq!(check.status, CheckStatus::Fail);
assert!(
check.message.contains("4455"),
"the deliberate-env branch must be the one reported: {}",
check.message
);
}
#[test]
fn failure_is_a_hard_fail_not_a_warn() {
for unset in [
vec!["ANTHROPIC_API_KEY"],
vec![TRANSCRIPT_SUPPRESSING_MARKER, "CLAUDE_CONFIG_DIR"],
vec![TRANSCRIPT_SUPPRESSING_MARKER, "CLAUDE_CODE_OAUTH_TOKEN"],
] {
let check = verdict(PATH, &unset, &[]);
assert_ne!(
check.status,
CheckStatus::Warn,
"must never be a Warn for {unset:?}: {}",
check.message
);
assert_eq!(check.status, CheckStatus::Fail);
}
}
#[test]
fn ok_message_reports_markers_live_in_the_environment() {
let clean = verdict(PATH, &[TRANSCRIPT_SUPPRESSING_MARKER], &[]);
assert_eq!(clean.status, CheckStatus::Ok);
assert!(
clean.message.contains("no inherited markers"),
"a clean env must say so: {}",
clean.message
);
let leaking = verdict(
PATH,
&[TRANSCRIPT_SUPPRESSING_MARKER],
&[TRANSCRIPT_SUPPRESSING_MARKER],
);
assert_eq!(
leaking.status,
CheckStatus::Ok,
"a live marker is context, not a failure: {}",
leaking.message
);
assert!(
leaking.message.contains("running with the leak present"),
"a leaking env must be reported as context: {}",
leaking.message
);
}