|
1 | | -use super::*; |
2 | | -use std::time::Duration; |
3 | | - |
4 | | -use axum::routing::get; |
5 | | -use axum::{Json, Router}; |
6 | | -use rexos::config::{ProviderKind, RexosConfig}; |
7 | | -use rexos::paths::RexosPaths; |
8 | | -use serde_json::json; |
9 | | - |
10 | | -#[tokio::test] |
11 | | -async fn doctor_suggests_running_init_when_core_files_are_missing() { |
12 | | - let tmp = tempfile::tempdir().unwrap(); |
13 | | - let paths = RexosPaths { |
14 | | - base_dir: tmp.path().join(".loopforge"), |
15 | | - }; |
16 | | - std::fs::create_dir_all(&paths.base_dir).unwrap(); |
17 | | - |
18 | | - let report = run_doctor(DoctorOptions { |
19 | | - paths, |
20 | | - timeout: Duration::from_millis(200), |
21 | | - }) |
22 | | - .await |
23 | | - .unwrap(); |
24 | | - |
25 | | - let value = serde_json::to_value(&report).unwrap(); |
26 | | - let next_actions = value |
27 | | - .get("next_actions") |
28 | | - .and_then(|item| item.as_array()) |
29 | | - .cloned() |
30 | | - .unwrap_or_default(); |
31 | | - assert!( |
32 | | - next_actions |
33 | | - .iter() |
34 | | - .any(|item| item.as_str().unwrap_or("").contains("loopforge init")), |
35 | | - "expected init guidance in next_actions, got: {next_actions:?}" |
36 | | - ); |
37 | | - assert!( |
38 | | - report.to_text().contains("Suggested next steps"), |
39 | | - "expected text output to include suggested next steps, got: {}", |
40 | | - report.to_text() |
41 | | - ); |
42 | | -} |
43 | | - |
44 | | -#[tokio::test] |
45 | | -async fn doctor_suggests_missing_provider_env_vars() { |
46 | | - let tmp = tempfile::tempdir().unwrap(); |
47 | | - let paths = RexosPaths { |
48 | | - base_dir: tmp.path().join(".loopforge"), |
49 | | - }; |
50 | | - std::fs::create_dir_all(&paths.base_dir).unwrap(); |
51 | | - |
52 | | - let mut cfg = RexosConfig::default(); |
53 | | - cfg.providers.insert( |
54 | | - "anthropic".to_string(), |
55 | | - rexos::config::ProviderConfig { |
56 | | - kind: ProviderKind::Anthropic, |
57 | | - base_url: "https://api.anthropic.com".to_string(), |
58 | | - api_key_env: "ANTHROPIC_API_KEY".to_string(), |
59 | | - default_model: "claude-3-5-sonnet-latest".to_string(), |
60 | | - aws_bedrock: None, |
61 | | - }, |
62 | | - ); |
63 | | - std::fs::write(paths.config_path(), toml::to_string(&cfg).unwrap()).unwrap(); |
64 | | - std::env::remove_var("ANTHROPIC_API_KEY"); |
65 | | - |
66 | | - let report = run_doctor(DoctorOptions { |
67 | | - paths, |
68 | | - timeout: Duration::from_millis(200), |
69 | | - }) |
70 | | - .await |
71 | | - .unwrap(); |
72 | | - |
73 | | - let value = serde_json::to_value(&report).unwrap(); |
74 | | - let next_actions = value |
75 | | - .get("next_actions") |
76 | | - .and_then(|item| item.as_array()) |
77 | | - .cloned() |
78 | | - .unwrap_or_default(); |
79 | | - assert!( |
80 | | - next_actions |
81 | | - .iter() |
82 | | - .any(|item| item.as_str().unwrap_or("").contains("ANTHROPIC_API_KEY")), |
83 | | - "expected provider env guidance in next_actions, got: {next_actions:?}" |
84 | | - ); |
85 | | -} |
86 | | - |
87 | | -#[tokio::test] |
88 | | -async fn doctor_probes_local_ollama_models_and_cdp_version() { |
89 | | - async fn models() -> Json<serde_json::Value> { |
90 | | - Json(json!({ "data": [] })) |
91 | | - } |
92 | | - async fn cdp_version() -> Json<serde_json::Value> { |
93 | | - Json(json!({ "Browser": "Chrome/1.0" })) |
94 | | - } |
95 | | - |
96 | | - let app = Router::new() |
97 | | - .route("/v1/models", get(models)) |
98 | | - .route("/json/version", get(cdp_version)); |
99 | | - |
100 | | - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); |
101 | | - let addr = listener.local_addr().unwrap(); |
102 | | - let server = tokio::spawn(async move { |
103 | | - axum::serve(listener, app).await.unwrap(); |
104 | | - }); |
105 | | - |
106 | | - let tmp = tempfile::tempdir().unwrap(); |
107 | | - let paths = RexosPaths { |
108 | | - base_dir: tmp.path().join(".loopforge"), |
109 | | - }; |
110 | | - std::fs::create_dir_all(&paths.base_dir).unwrap(); |
111 | | - |
112 | | - let cfg = RexosConfig { |
113 | | - llm: rexos::config::LlmConfig::default(), |
114 | | - providers: [( |
115 | | - "ollama".to_string(), |
116 | | - rexos::config::ProviderConfig { |
117 | | - kind: ProviderKind::OpenAiCompatible, |
118 | | - base_url: format!("http://{addr}/v1"), |
119 | | - api_key_env: "".to_string(), |
120 | | - default_model: "x".to_string(), |
121 | | - aws_bedrock: None, |
122 | | - }, |
123 | | - )] |
124 | | - .into_iter() |
125 | | - .collect(), |
126 | | - router: rexos::config::RouterConfig::default(), |
127 | | - security: Default::default(), |
128 | | - }; |
129 | | - std::fs::write(paths.config_path(), toml::to_string(&cfg).unwrap()).unwrap(); |
130 | | - std::env::set_var("LOOPFORGE_BROWSER_CDP_HTTP", format!("http://{addr}")); |
131 | | - |
132 | | - let report = run_doctor(DoctorOptions { |
133 | | - paths, |
134 | | - timeout: Duration::from_millis(500), |
135 | | - }) |
136 | | - .await |
137 | | - .unwrap(); |
138 | | - |
139 | | - let statuses: std::collections::BTreeMap<String, CheckStatus> = report |
140 | | - .checks |
141 | | - .iter() |
142 | | - .map(|check| (check.id.clone(), check.status)) |
143 | | - .collect(); |
144 | | - assert_eq!(statuses.get("ollama.http"), Some(&CheckStatus::Ok)); |
145 | | - assert_eq!(statuses.get("browser.cdp_http"), Some(&CheckStatus::Ok)); |
146 | | - |
147 | | - std::env::remove_var("LOOPFORGE_BROWSER_CDP_HTTP"); |
148 | | - server.abort(); |
149 | | -} |
150 | | - |
151 | | -#[tokio::test] |
152 | | -async fn doctor_reports_security_posture_checks() { |
153 | | - let tmp = tempfile::tempdir().unwrap(); |
154 | | - let paths = RexosPaths { |
155 | | - base_dir: tmp.path().join(".loopforge"), |
156 | | - }; |
157 | | - std::fs::create_dir_all(&paths.base_dir).unwrap(); |
158 | | - |
159 | | - let mut cfg = RexosConfig { |
160 | | - llm: rexos::config::LlmConfig::default(), |
161 | | - providers: [( |
162 | | - "ollama".to_string(), |
163 | | - rexos::config::ProviderConfig { |
164 | | - kind: ProviderKind::OpenAiCompatible, |
165 | | - base_url: "http://127.0.0.1:11434/v1".to_string(), |
166 | | - api_key_env: "".to_string(), |
167 | | - default_model: "x".to_string(), |
168 | | - aws_bedrock: None, |
169 | | - }, |
170 | | - )] |
171 | | - .into_iter() |
172 | | - .collect(), |
173 | | - router: rexos::config::RouterConfig::default(), |
174 | | - security: Default::default(), |
175 | | - }; |
176 | | - cfg.security.leaks.mode = rexos::security::LeakMode::Redact; |
177 | | - cfg.security.egress.rules.push(rexos::security::EgressRule { |
178 | | - tool: "web_fetch".to_string(), |
179 | | - host: "docs.rs".to_string(), |
180 | | - path_prefix: "/".to_string(), |
181 | | - methods: vec!["GET".to_string()], |
182 | | - }); |
183 | | - std::fs::write(paths.config_path(), toml::to_string(&cfg).unwrap()).unwrap(); |
184 | | - |
185 | | - let report = run_doctor(DoctorOptions { |
186 | | - paths, |
187 | | - timeout: Duration::from_millis(200), |
188 | | - }) |
189 | | - .await |
190 | | - .unwrap(); |
191 | | - |
192 | | - let statuses: std::collections::BTreeMap<String, CheckStatus> = report |
193 | | - .checks |
194 | | - .iter() |
195 | | - .map(|check| (check.id.clone(), check.status)) |
196 | | - .collect(); |
197 | | - assert_eq!( |
198 | | - statuses.get("security.secrets.mode"), |
199 | | - Some(&CheckStatus::Ok) |
200 | | - ); |
201 | | - assert_eq!(statuses.get("security.leaks.mode"), Some(&CheckStatus::Ok)); |
202 | | - assert_eq!( |
203 | | - statuses.get("security.egress.rules"), |
204 | | - Some(&CheckStatus::Ok) |
205 | | - ); |
206 | | -} |
207 | | - |
208 | | -#[tokio::test] |
209 | | -async fn doctor_suggests_leak_guard_and_egress_hardening_when_defaults_are_open() { |
210 | | - let tmp = tempfile::tempdir().unwrap(); |
211 | | - let paths = RexosPaths { |
212 | | - base_dir: tmp.path().join(".loopforge"), |
213 | | - }; |
214 | | - std::fs::create_dir_all(&paths.base_dir).unwrap(); |
215 | | - |
216 | | - let cfg = RexosConfig { |
217 | | - llm: rexos::config::LlmConfig::default(), |
218 | | - providers: [( |
219 | | - "ollama".to_string(), |
220 | | - rexos::config::ProviderConfig { |
221 | | - kind: ProviderKind::OpenAiCompatible, |
222 | | - base_url: "http://127.0.0.1:11434/v1".to_string(), |
223 | | - api_key_env: "".to_string(), |
224 | | - default_model: "x".to_string(), |
225 | | - aws_bedrock: None, |
226 | | - }, |
227 | | - )] |
228 | | - .into_iter() |
229 | | - .collect(), |
230 | | - router: rexos::config::RouterConfig::default(), |
231 | | - security: Default::default(), |
232 | | - }; |
233 | | - std::fs::write(paths.config_path(), toml::to_string(&cfg).unwrap()).unwrap(); |
234 | | - |
235 | | - let report = run_doctor(DoctorOptions { |
236 | | - paths, |
237 | | - timeout: Duration::from_millis(200), |
238 | | - }) |
239 | | - .await |
240 | | - .unwrap(); |
241 | | - |
242 | | - let statuses: std::collections::BTreeMap<String, CheckStatus> = report |
243 | | - .checks |
244 | | - .iter() |
245 | | - .map(|check| (check.id.clone(), check.status)) |
246 | | - .collect(); |
247 | | - assert_eq!( |
248 | | - statuses.get("security.leaks.mode"), |
249 | | - Some(&CheckStatus::Warn) |
250 | | - ); |
251 | | - assert_eq!( |
252 | | - statuses.get("security.egress.rules"), |
253 | | - Some(&CheckStatus::Warn) |
254 | | - ); |
255 | | - assert!( |
256 | | - report |
257 | | - .next_actions |
258 | | - .iter() |
259 | | - .any(|item| item.contains("security.leaks")), |
260 | | - "expected leak-guard guidance, got: {:?}", |
261 | | - report.next_actions |
262 | | - ); |
263 | | - assert!( |
264 | | - report |
265 | | - .next_actions |
266 | | - .iter() |
267 | | - .any(|item| item.contains("security.egress")), |
268 | | - "expected egress guidance, got: {:?}", |
269 | | - report.next_actions |
270 | | - ); |
271 | | -} |
272 | | - |
273 | | -#[tokio::test] |
274 | | -async fn doctor_reports_bedrock_feature_status_when_routed() { |
275 | | - let tmp = tempfile::tempdir().unwrap(); |
276 | | - let paths = RexosPaths { |
277 | | - base_dir: tmp.path().join(".loopforge"), |
278 | | - }; |
279 | | - std::fs::create_dir_all(&paths.base_dir).unwrap(); |
280 | | - |
281 | | - let mut cfg = RexosConfig::default(); |
282 | | - cfg.router.coding.provider = "bedrock".to_string(); |
283 | | - cfg.router.coding.model = "default".to_string(); |
284 | | - if let Some(provider) = cfg.providers.get_mut("bedrock") { |
285 | | - provider.default_model = "anthropic.claude-3-5-sonnet-20241022-v2:0".to_string(); |
286 | | - } |
287 | | - std::fs::write(paths.config_path(), toml::to_string(&cfg).unwrap()).unwrap(); |
288 | | - |
289 | | - let report = run_doctor(DoctorOptions { |
290 | | - paths, |
291 | | - timeout: Duration::from_millis(200), |
292 | | - }) |
293 | | - .await |
294 | | - .unwrap(); |
295 | | - |
296 | | - let statuses: std::collections::BTreeMap<String, CheckStatus> = report |
297 | | - .checks |
298 | | - .iter() |
299 | | - .map(|check| (check.id.clone(), check.status)) |
300 | | - .collect(); |
301 | | - |
302 | | - assert_eq!( |
303 | | - statuses.get("bedrock.router.coding.model"), |
304 | | - Some(&CheckStatus::Ok) |
305 | | - ); |
306 | | - assert_eq!( |
307 | | - statuses.get("bedrock.providers.bedrock.region"), |
308 | | - Some(&CheckStatus::Ok) |
309 | | - ); |
310 | | - assert_eq!( |
311 | | - statuses.get("bedrock.feature"), |
312 | | - Some(if cfg!(feature = "bedrock") { |
313 | | - &CheckStatus::Ok |
314 | | - } else { |
315 | | - &CheckStatus::Error |
316 | | - }) |
317 | | - ); |
318 | | - |
319 | | - if !cfg!(feature = "bedrock") { |
320 | | - assert!( |
321 | | - report |
322 | | - .next_actions |
323 | | - .iter() |
324 | | - .any(|item| item.contains("features bedrock")), |
325 | | - "expected bedrock rebuild guidance, got: {:?}", |
326 | | - report.next_actions |
327 | | - ); |
328 | | - } |
329 | | -} |
330 | | - |
331 | | -#[tokio::test] |
332 | | -async fn doctor_flags_missing_bedrock_model_and_region() { |
333 | | - let tmp = tempfile::tempdir().unwrap(); |
334 | | - let paths = RexosPaths { |
335 | | - base_dir: tmp.path().join(".loopforge"), |
336 | | - }; |
337 | | - std::fs::create_dir_all(&paths.base_dir).unwrap(); |
338 | | - |
339 | | - let mut cfg = RexosConfig::default(); |
340 | | - cfg.router.coding.provider = "bedrock".to_string(); |
341 | | - cfg.router.coding.model = "default".to_string(); |
342 | | - if let Some(provider) = cfg.providers.get_mut("bedrock") { |
343 | | - provider.default_model = "".to_string(); |
344 | | - if let Some(aws) = provider.aws_bedrock.as_mut() { |
345 | | - aws.region = "".to_string(); |
346 | | - } |
347 | | - } |
348 | | - std::fs::write(paths.config_path(), toml::to_string(&cfg).unwrap()).unwrap(); |
349 | | - |
350 | | - let report = run_doctor(DoctorOptions { |
351 | | - paths, |
352 | | - timeout: Duration::from_millis(200), |
353 | | - }) |
354 | | - .await |
355 | | - .unwrap(); |
356 | | - |
357 | | - let statuses: std::collections::BTreeMap<String, CheckStatus> = report |
358 | | - .checks |
359 | | - .iter() |
360 | | - .map(|check| (check.id.clone(), check.status)) |
361 | | - .collect(); |
362 | | - |
363 | | - assert_eq!( |
364 | | - statuses.get("bedrock.router.coding.model"), |
365 | | - Some(&CheckStatus::Error) |
366 | | - ); |
367 | | - assert_eq!( |
368 | | - statuses.get("bedrock.providers.bedrock.region"), |
369 | | - Some(&CheckStatus::Error) |
370 | | - ); |
371 | | -} |
| 1 | +mod bedrock; |
| 2 | +mod common; |
| 3 | +mod guidance; |
| 4 | +mod probes; |
| 5 | +mod security; |
0 commit comments