返回 CodeWhale
diagnostic_dispatch_read_only.rs
根目录 / crates / cli / tests / diagnostic_dispatch_read_only.rs
1 //! The facade must not migrate secrets before it delegates static diagnostics.
2
3 #![cfg(unix)]
4
5 use std::fs;
6 use std::os::unix::fs::PermissionsExt;
7 use std::path::{Path, PathBuf};
8 use std::process::Command;
9
10 use codewhale_secrets::{FileKeyringStore, KeyringStore};
11 use tempfile::TempDir;
12
13 #[test]
14 fn dispatcher_diagnostics_leave_legacy_secret_state_unchanged() {
15 for (args, expected_tui_args, expects_json) in [
16 (&["doctor"][..], &["doctor"][..], false),
17 (&["doctor", "--json"][..], &["doctor", "--json"][..], false),
18 (
19 &["doctor", "--context-json"][..],
20 &["doctor", "--context-json"][..],
21 true,
22 ),
23 (
24 &["setup", "--status"][..],
25 &["setup", "--status"][..],
26 false,
27 ),
28 ] {
29 let fixture = TempDir::new().expect("fixture root");
30 let sealed_home = fixture.path().join("sealed-home");
31 let codewhale_home = fixture.path().join("sealed-codewhale-home");
32 let primary_home = sealed_home.join(".codewhale");
33 let legacy = sealed_home
34 .join(".deepseek")
35 .join("secrets")
36 .join("secrets.json");
37 let legacy_settings = sealed_home.join(".deepseek").join("settings.toml");
38 let legacy_settings_bytes = b"default_mode = \"plan\"\n";
39 FileKeyringStore::new(&legacy)
40 .set("deepseek", "synthetic-legacy-fixture")
41 .expect("seed synthetic legacy store");
42 fs::write(&legacy_settings, legacy_settings_bytes).expect("seed legacy settings");
43 let before_paths = relative_paths(&sealed_home);
44 let before_legacy = fs::read(&legacy).expect("read synthetic legacy store");
45
46 let receipt = fixture.path().join("delegated-args.txt");
47 let fake_tui = fixture.path().join("fake-codewhale-tui");
48 fs::write(
49 &fake_tui,
50 "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$DIAGNOSTIC_DISPATCH_RECEIPT\"\nif [ \"$1\" = doctor ] && [ \"$2\" = --context-json ]; then\n printf '%s\\n' '{\"entries\":[]}'\nfi\n",
51 )
52 .expect("write fake TUI");
53 let mut permissions = fs::metadata(&fake_tui)
54 .expect("fake TUI metadata")
55 .permissions();
56 permissions.set_mode(0o700);
57 fs::set_permissions(&fake_tui, permissions).expect("make fake TUI executable");
58
59 let output = Command::new(codewhale_binary())
60 .args(args)
61 .env_clear()
62 .env("HOME", &sealed_home)
63 .env("USERPROFILE", &sealed_home)
64 .env("CODEWHALE_HOME", &codewhale_home)
65 .env("CODEWHALE_SECRET_BACKEND", "file")
66 .env("DEEPSEEK_TUI_BIN", &fake_tui)
67 .env("DIAGNOSTIC_DISPATCH_RECEIPT", &receipt)
68 .output()
69 .expect("run dispatcher diagnostic");
70
71 assert!(
72 output.status.success(),
73 "dispatcher {args:?} failed\nstdout:\n{}\nstderr:\n{}",
74 String::from_utf8_lossy(&output.stdout),
75 String::from_utf8_lossy(&output.stderr)
76 );
77 assert_eq!(
78 fs::read_to_string(&receipt)
79 .expect("fake TUI receipt")
80 .lines()
81 .collect::<Vec<_>>(),
82 expected_tui_args,
83 "dispatcher must preserve the diagnostic command shape"
84 );
85 if expects_json {
86 let report: serde_json::Value = serde_json::from_slice(&output.stdout)
87 .unwrap_or_else(|error| {
88 panic!(
89 "facade {args:?} must preserve machine-readable output: {error}\nstdout:\n{}\nstderr:\n{}",
90 String::from_utf8_lossy(&output.stdout),
91 String::from_utf8_lossy(&output.stderr)
92 )
93 });
94 assert!(
95 report["entries"].is_array(),
96 "facade {args:?} must preserve the context source map\nstdout:\n{}",
97 String::from_utf8_lossy(&output.stdout)
98 );
99 }
100 assert_eq!(
101 relative_paths(&sealed_home),
102 before_paths,
103 "dispatcher {args:?} must not create or migrate state below HOME"
104 );
105 assert_eq!(
106 fs::read(&legacy).expect("read synthetic legacy store after diagnostic"),
107 before_legacy,
108 "dispatcher {args:?} must not rewrite the legacy store"
109 );
110 assert_eq!(
111 fs::read(&legacy_settings).expect("read legacy settings after diagnostic"),
112 legacy_settings_bytes,
113 "dispatcher {args:?} must not rewrite legacy settings"
114 );
115 assert!(
116 !primary_home.exists(),
117 "dispatcher {args:?} must not create a primary Codewhale home or migrated state"
118 );
119 assert!(
120 !codewhale_home.exists(),
121 "dispatcher {args:?} must not create an explicit CODEWHALE_HOME"
122 );
123 }
124 }
125
126 fn relative_paths(root: &Path) -> Vec<PathBuf> {
127 let mut paths = Vec::new();
128 collect_relative_paths(root, root, &mut paths);
129 paths.sort();
130 paths
131 }
132
133 fn collect_relative_paths(root: &Path, current: &Path, paths: &mut Vec<PathBuf>) {
134 let entries = fs::read_dir(current).expect("read synthetic state directory");
135 for entry in entries {
136 let entry = entry.expect("synthetic state directory entry");
137 let path = entry.path();
138 paths.push(
139 path.strip_prefix(root)
140 .expect("synthetic path below root")
141 .to_path_buf(),
142 );
143 if entry.file_type().expect("synthetic entry type").is_dir() {
144 collect_relative_paths(root, &path, paths);
145 }
146 }
147 }
148
149 fn codewhale_binary() -> PathBuf {
150 if let Some(path) = option_env!("CARGO_BIN_EXE_codewhale") {
151 return PathBuf::from(path);
152 }
153 if let Ok(path) = std::env::var("CARGO_BIN_EXE_codewhale") {
154 return PathBuf::from(path);
155 }
156
157 let mut path = std::env::current_exe().expect("current test executable path");
158 path.pop();
159 if path.ends_with("deps") {
160 path.pop();
161 }
162 path.push(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
163 path
164 }
165
165 lines RUST