返回 CodeWhale
telemetry_kill_switch_dispatch.rs
根目录 / crates / cli / tests / telemetry_kill_switch_dispatch.rs
1 //! The kill switch has to reach the process that would emit.
2 //!
3 //! The dispatcher itself almost never emits: every interactive and headless
4 //! session is delegated to the sibling `codewhale-tui` binary, which re-resolves
5 //! telemetry from *its own* environment and config file. So the switch is only
6 //! real if the resolved value — not the raw flag — is in that child's
7 //! environment. This drives the real `codewhale` binary and reads what the
8 //! child actually received.
9
10 #![cfg(unix)]
11
12 use std::collections::BTreeMap;
13 use std::fs;
14 use std::os::unix::fs::PermissionsExt;
15 use std::path::PathBuf;
16 use std::process::Command;
17
18 use codewhale_config::{SetupState, TELEMETRY_NOTICE_VERSION};
19 use tempfile::TempDir;
20
21 /// `CODEWHALE_TELEMETRY=0` beats `--telemetry true`, end to end.
22 ///
23 /// The notice decision is recorded on this home on purpose: without it the run
24 /// would be off for want of consent, and the test would pass without the kill
25 /// switch ever being consulted.
26 #[test]
27 fn env_off_beats_cli_on_end_to_end() {
28 // Positive control first: the flag does reach the child, so the assertion
29 // below is about the floor and not about a flag that goes nowhere.
30 let on = dispatch_and_read_child_env(&["--telemetry", "true", "exec", "hi"], None);
31 assert_eq!(
32 on.get("CODEWHALE_TELEMETRY").map(String::as_str),
33 Some("true"),
34 "`--telemetry true` must reach the delegated child at all"
35 );
36
37 let off = dispatch_and_read_child_env(&["--telemetry", "true", "exec", "hi"], Some("0"));
38 assert_eq!(
39 off.get("CODEWHALE_TELEMETRY").map(String::as_str),
40 Some("false"),
41 "`CODEWHALE_TELEMETRY=0` must beat `--telemetry true` in the child's environment"
42 );
43 assert_eq!(
44 off.get("DEEPSEEK_TELEMETRY").map(String::as_str),
45 Some("false"),
46 "the legacy alias must carry the same resolved value"
47 );
48 }
49
50 /// A value the resolver cannot parse resolves to off, rather than falling
51 /// through to the flag.
52 #[test]
53 fn an_unparseable_telemetry_env_value_reaches_the_child_as_off() {
54 let child = dispatch_and_read_child_env(&["--telemetry", "true", "exec", "hi"], Some("maybe"));
55 assert_eq!(
56 child.get("CODEWHALE_TELEMETRY").map(String::as_str),
57 Some("false"),
58 "a typo in the kill switch must never resolve to on"
59 );
60 }
61
62 /// Run the real dispatcher against a fake sibling TUI that dumps its
63 /// environment, and return that environment.
64 fn dispatch_and_read_child_env(
65 args: &[&str],
66 telemetry_env: Option<&str>,
67 ) -> BTreeMap<String, String> {
68 let fixture = TempDir::new().expect("fixture root");
69 let home = fixture.path().join("home");
70 let codewhale_home = fixture.path().join("codewhale-home");
71 let workspace = fixture.path().join("workspace");
72 for dir in [&home, &codewhale_home, &workspace] {
73 fs::create_dir_all(dir).expect("create fixture dir");
74 }
75
76 // A recorded acceptance, so the run is not off for want of consent.
77 let mut state = SetupState::default();
78 state.record_telemetry_notice(TELEMETRY_NOTICE_VERSION, true);
79 state
80 .save_to(&codewhale_home.join("setup_state.json"))
81 .expect("write setup state");
82
83 let config_path = fixture.path().join("config.toml");
84 fs::write(&config_path, "telemetry = true\n").expect("write config");
85
86 let receipt = fixture.path().join("child-env.txt");
87 let fake_tui = fixture.path().join("fake-codewhale-tui");
88 fs::write(
89 &fake_tui,
90 format!("#!/bin/sh\nenv > '{}'\n", receipt.display()),
91 )
92 .expect("write fake TUI");
93 let mut permissions = fs::metadata(&fake_tui)
94 .expect("fake TUI metadata")
95 .permissions();
96 permissions.set_mode(0o700);
97 fs::set_permissions(&fake_tui, permissions).expect("make fake TUI executable");
98
99 let mut command = Command::new(codewhale_binary());
100 command
101 .current_dir(&workspace)
102 .env_clear()
103 .env("PATH", std::env::var_os("PATH").expect("PATH"))
104 .env("HOME", &home)
105 .env("USERPROFILE", &home)
106 .env("CODEWHALE_HOME", &codewhale_home)
107 .env("CODEWHALE_SECRET_BACKEND", "file")
108 .env("CODEWHALE_TUI_BIN", &fake_tui)
109 .env(
110 "CODEWHALE_RELEASE_BASE_URL",
111 "https://example.invalid/releases",
112 )
113 .arg("--config")
114 .arg(&config_path)
115 .args(args);
116 if let Some(value) = telemetry_env {
117 command.env("CODEWHALE_TELEMETRY", value);
118 }
119 let output = command.output().expect("run codewhale dispatcher");
120
121 let dumped = fs::read_to_string(&receipt).unwrap_or_else(|error| {
122 panic!(
123 "the delegated child must have run and dumped its environment: {error}\n\
124 stdout:\n{}\nstderr:\n{}",
125 String::from_utf8_lossy(&output.stdout),
126 String::from_utf8_lossy(&output.stderr)
127 )
128 });
129
130 // A dispatcher run that never emits must also never create telemetry state
131 // in the operator's home.
132 assert!(
133 !codewhale_home.join("telemetry").exists(),
134 "the dispatcher must not create telemetry state for a delegated command"
135 );
136
137 dumped
138 .lines()
139 .filter_map(|line| {
140 line.split_once('=')
141 .map(|(key, value)| (key.to_string(), value.to_string()))
142 })
143 .collect()
144 }
145
146 fn codewhale_binary() -> PathBuf {
147 if let Some(path) = option_env!("CARGO_BIN_EXE_codewhale") {
148 return PathBuf::from(path);
149 }
150 if let Ok(path) = std::env::var("CARGO_BIN_EXE_codewhale") {
151 return PathBuf::from(path);
152 }
153 let mut path = std::env::current_exe().expect("current test executable path");
154 path.pop();
155 if path.ends_with("deps") {
156 path.pop();
157 }
158 path.push(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
159 path
160 }
161
161 lines RUST