返回 CodeWhale
roster_routes.rs
根目录 / crates / tui / src / tools / subagent / tests / roster_routes.rs
1 //! Consumer regressions for operator-visible routes (#5915/#5955).
2 use super::*;
3
4 #[tokio::test]
5 async fn roster_matches_actual_start_receipts_and_refreshes_live_role_defaults() {
6 let _env = crate::test_support::lock_test_env();
7 let root = tempdir().unwrap();
8 let (client, calls, _, _) = delayed_chat_client(Duration::ZERO, "done").await;
9 let config = crate::config::Config {
10 subagents: Some(crate::config::SubagentsConfig {
11 worker_model: Some("deepseek-v4-flash".into()),
12 ..Default::default()
13 }),
14 ..Default::default()
15 }
16 .with_legacy_root(Some("test-key".into()), Some(client.base_url().into()));
17 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 8);
18 let context = ToolContext::new(root.path()).with_state_namespace("roster-route-consumer");
19 let mut runtime = SubAgentRuntime::new(
20 client,
21 "deepseek-v4-pro".into(),
22 context.clone(),
23 false,
24 None,
25 manager.clone(),
26 )
27 .with_api_config(config);
28 runtime
29 .role_models
30 .insert("general".into(), "deepseek-v4-pro".into());
31 let tool = AgentTool::new(manager.clone(), runtime);
32 let query = tool
33 .execute(json!({"action":"roster"}), &context)
34 .await
35 .unwrap();
36 let roster: Value = serde_json::from_str(&query.content).unwrap();
37 assert_eq!(
38 calls.load(Ordering::SeqCst),
39 0,
40 "discovery must not send inference"
41 );
42 let rows = roster["members"].as_array().unwrap();
43 assert_eq!(rows.len(), 8);
44 assert_eq!(
45 rows[0]["route"]["model"], "deepseek-v4-flash",
46 "live config supersedes launch default"
47 );
48 for row in rows {
49 assert!(row["route"].is_object(), "route missing: {row}");
50 assert_eq!(row["route"]["reachability"], "unverified");
51 let role = row["role"].as_str().unwrap();
52 let mut request = json!({"action":"start", "type":role, "prompt":"Say done."});
53 if role == "custom" {
54 request["allowed_tools"] = json!(["Read"]);
55 }
56 let started = tool.execute(request, &context).await.unwrap();
57 let metadata = started.metadata.as_ref().unwrap();
58 let receipt = &metadata["child_route"];
59 for (discovery, dispatch) in [
60 ("provider", "provider_id"),
61 ("model", "model_id"),
62 ("reasoning_effort", "effective_reasoning"),
63 ("source", "route_source"),
64 ] {
65 assert_eq!(
66 row["route"][discovery], receipt[dispatch],
67 "{role}: {discovery}"
68 );
69 }
70 manager
71 .write()
72 .await
73 .cancel_agent(metadata["agent_id"].as_str().unwrap())
74 .unwrap();
75 }
76 }
77
78 #[tokio::test]
79 async fn roster_preserves_unknown_and_non_metered_costs_and_invalid_role_errors() {
80 let _env = crate::test_support::lock_test_env();
81 let _live = crate::provider_lake::lock_live_snapshot();
82 crate::provider_lake::clear_live_snapshot();
83 for (provider, model, vendor, expected_cost, reason) in [
84 ("deepseek", "deepseek-v4-flash", None, "paid", None),
85 (
86 "openrouter",
87 "qwen/qwen3.7-plus",
88 Some("cerebras"),
89 "unknown",
90 Some("routing_dependent_price"),
91 ),
92 (
93 "ollama",
94 "fixture-local-model",
95 None,
96 "not_money_metered",
97 Some("not_money_metered"),
98 ),
99 ] {
100 let root = tempdir().unwrap();
101 let mut config = crate::config::Config {
102 provider: Some(provider.into()),
103 ..Default::default()
104 };
105 let selected = config
106 .provider_config_for_mut(
107 &config.test_identity_for_kind(ProviderKind::parse(provider).unwrap()),
108 )
109 .unwrap();
110 selected.api_key = Some("roster-private-fixture-key".into());
111 selected.model = Some(model.into());
112 selected.vendor = vendor.map(str::to_string);
113 let client = CodewhaleClient::new(&config).unwrap();
114 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 1);
115 let runtime = SubAgentRuntime::new(
116 client,
117 model.into(),
118 ToolContext::new(root.path()),
119 false,
120 None,
121 manager,
122 )
123 .with_api_config(config);
124 let row =
125 resolved_role_roster_entry(&runtime, &spawn_roster(&runtime), &FleetRole::Worker).await;
126 assert_eq!(
127 row["route"]["cost_class"], expected_cost,
128 "{provider}: {row}"
129 );
130 assert_eq!(row["route"]["unpriced_reason"], json!(reason));
131 assert!(!row.to_string().contains("roster-private-fixture-key"));
132 }
133 let mut runtime = stub_runtime();
134 runtime
135 .role_models
136 .insert("general".into(), "invalid\nmodel".into());
137 let row =
138 resolved_role_roster_entry(&runtime, &spawn_roster(&runtime), &FleetRole::Worker).await;
139 assert!(row["route"].is_null());
140 assert!(row["route_error"].as_str().is_some());
141 let other =
142 resolved_role_roster_entry(&runtime, &spawn_roster(&runtime), &FleetRole::Reviewer).await;
143 assert!(
144 other["route"].is_object(),
145 "one bad role must not hide other routes: {other}"
146 );
147 }
148
149 #[tokio::test]
150 async fn advertised_task_route_overrides_reach_start_and_foreign_models_fail_before_admission() {
151 let _env = crate::test_support::lock_test_env();
152 let root = tempdir().unwrap();
153 let (client, _, _, _) = delayed_chat_client(Duration::ZERO, "done").await;
154 let config = crate::config::Config {
155 ..Default::default()
156 }
157 .with_legacy_root(Some("test-key".into()), Some(client.base_url().into()));
158 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 2);
159 let context = ToolContext::new(root.path()).with_state_namespace("explicit-task-route");
160 let runtime = SubAgentRuntime::new(
161 client,
162 "deepseek-v4-flash".into(),
163 context.clone(),
164 false,
165 None,
166 manager.clone(),
167 )
168 .with_api_config(config);
169 let tool = AgentTool::new(manager.clone(), runtime);
170 let schema = tool.input_schema();
171 for field in ["model", "model_strength", "thinking"] {
172 assert!(schema["properties"].get(field).is_some());
173 }
174 let started = tool.execute(json!({"action":"start", "type":"explore", "prompt":"Say done.", "model":"deepseek-v4-pro", "model_strength":"faster", "thinking":"high"}), &context).await.unwrap();
175 let metadata = started.metadata.as_ref().unwrap();
176 let receipt = &metadata["child_route"];
177 assert_eq!(receipt["model_id"], "deepseek-v4-pro");
178 assert_eq!(receipt["route_source"], "task.model");
179 assert_eq!(receipt["effective_reasoning"], "high");
180 manager
181 .write()
182 .await
183 .cancel_agent(metadata["agent_id"].as_str().unwrap())
184 .unwrap();
185 let error = tool.execute(json!({"action":"start", "type":"explore", "prompt":"Say done.", "model":"claude-fable-5"}), &context).await.unwrap_err();
186 assert!(error.to_string().contains("provider"), "{error}");
187 }
188
189 struct ProjectProfilesGuard(bool);
190 impl ProjectProfilesGuard {
191 fn enabled() -> Self {
192 let previous = crate::fleet::roster::project_agent_profiles_enabled();
193 crate::fleet::roster::set_project_agent_profiles_enabled(true);
194 Self(previous)
195 }
196 }
197 impl Drop for ProjectProfilesGuard {
198 fn drop(&mut self) {
199 crate::fleet::roster::set_project_agent_profiles_enabled(self.0);
200 }
201 }
202
203 #[tokio::test]
204 async fn saved_profile_discovery_and_actual_start_share_current_instructions_route_and_trust() {
205 let _env = crate::test_support::lock_test_env();
206 let root = tempdir().unwrap();
207 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
208 let _project = ProjectProfilesGuard::enabled();
209 let profile_dir = root.path().join(".codewhale/agents");
210 std::fs::create_dir_all(&profile_dir).unwrap();
211 let profile = profile_dir.join("bug-hunter.toml");
212 let (client, calls, bodies, _) = delayed_chat_client(Duration::ZERO, "done").await;
213 let config = crate::config::Config {
214 subagents: Some(crate::config::SubagentsConfig {
215 explorer_model: Some("invalid\nrole-default".into()),
216 ..Default::default()
217 }),
218 ..Default::default()
219 }
220 .with_legacy_root(Some("test-key".into()), Some(client.base_url().into()));
221 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 2);
222 let context = ToolContext::new(root.path()).with_state_namespace("saved-profile-consumer");
223 let runtime = SubAgentRuntime::new(
224 client,
225 "deepseek-v4-flash".into(),
226 context.clone(),
227 false,
228 None,
229 manager.clone(),
230 )
231 .with_api_config(config);
232 let tool = AgentTool::new(manager.clone(), runtime);
233 for (model, instruction) in [
234 ("deepseek-v4-pro", "Inspect only changed parser branches."),
235 ("deepseek-v4-flash", "Inspect the new queue consumer."),
236 ] {
237 std::fs::write(&profile, format!("id = \"bug-hunter\"\nbase_role = \"scout\"\nmodel = \"{model}\"\nreasoning_effort = \"high\"\npersona = \"{instruction}\"\n")).unwrap();
238 let before = calls.load(Ordering::SeqCst);
239 let discovered = tool
240 .execute(json!({"action":"roster"}), &context)
241 .await
242 .unwrap();
243 assert_eq!(
244 calls.load(Ordering::SeqCst),
245 before,
246 "roster must never infer"
247 );
248 let roster: Value = serde_json::from_str(&discovered.content).unwrap();
249 let row = roster["profiles"]
250 .as_array()
251 .unwrap()
252 .iter()
253 .find(|row| row["member_id"] == "bug-hunter")
254 .unwrap();
255 assert_eq!(row["route"]["model"], model);
256 assert_eq!(row["route"]["reasoning_effort"], "high");
257 let started = tool
258 .execute(
259 json!({"profile":"bug-hunter", "prompt":"Inspect the assigned slice."}),
260 &context,
261 )
262 .await
263 .unwrap();
264 let meta = started.metadata.as_ref().unwrap();
265 let receipt = &meta["child_route"];
266 assert_eq!(receipt["resolved_profile_id"], "bug-hunter");
267 assert_eq!(receipt["profile_origin"], "project");
268 assert_eq!(receipt["model_id"], row["route"]["model"]);
269 assert_eq!(
270 receipt["effective_reasoning"],
271 row["route"]["reasoning_effort"]
272 );
273 assert_eq!(receipt["route_source"], "agent_profile.model");
274 tokio::time::timeout(Duration::from_secs(5), async {
275 while calls.load(Ordering::SeqCst) == before {
276 tokio::time::sleep(Duration::from_millis(10)).await;
277 }
278 })
279 .await
280 .expect("local provider receives the saved profile prompt");
281 let body = bodies.lock().unwrap().last().unwrap().clone();
282 assert!(
283 body.to_string().contains(instruction),
284 "saved instructions must reach the actual request"
285 );
286 let id = meta["agent_id"].as_str().unwrap();
287 let mut guard = manager.write().await;
288 let worker = guard.worker_records.get(id).unwrap();
289 assert!(
290 worker
291 .spec
292 .launch_manifest
293 .as_ref()
294 .unwrap()
295 .prompt
296 .contains(instruction)
297 );
298 assert!(!worker.spec.runtime_profile.permissions.write);
299 if guard.agents[id].status == SubAgentStatus::Running {
300 guard.cancel_agent(id).unwrap();
301 }
302 }
303 crate::fleet::roster::set_project_agent_profiles_enabled(false);
304 let error = tool
305 .execute(
306 json!({"profile":"bug-hunter", "prompt":"Inspect."}),
307 &context,
308 )
309 .await
310 .unwrap_err();
311 assert!(
312 error.to_string().contains("Unknown Fleet role/profile"),
313 "{error}"
314 );
315 }
316
317 #[tokio::test]
318 async fn saved_provider_pin_reaches_actual_request_and_conflicts_fail_before_admission() {
319 let _env = crate::test_support::lock_test_env();
320 let root = tempdir().unwrap();
321 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
322 let _project = ProjectProfilesGuard::enabled();
323 let profile_dir = root.path().join(".codewhale/agents");
324 std::fs::create_dir_all(&profile_dir).unwrap();
325 std::fs::write(profile_dir.join("router-review.toml"), "id = \"router-review\"\nbase_role = \"reviewer\"\nprovider = \"openrouter\"\nmodel = \"qwen/qwen3.7-plus\"\nreasoning_effort = \"low\"\n").unwrap();
326 let (client, calls, bodies, _) = delayed_chat_client(Duration::ZERO, "done").await;
327 let mut config = crate::config::Config {
328 ..Default::default()
329 }
330 .with_legacy_root(Some("test-key".into()), Some(client.base_url().into()));
331 let router = config
332 .provider_config_for_mut(&config.test_identity_for_kind(ProviderKind::Openrouter))
333 .unwrap();
334 router.api_key = Some("test-router-key".into());
335 router.base_url = Some(client.base_url().into());
336 router.vendor = Some("cerebras".into());
337 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 2);
338 let context = ToolContext::new(root.path()).with_state_namespace("saved-provider-consumer");
339 let runtime = SubAgentRuntime::new(
340 client,
341 "deepseek-v4-flash".into(),
342 context.clone(),
343 false,
344 None,
345 manager.clone(),
346 )
347 .with_api_config(config);
348 let tool = AgentTool::new(manager.clone(), runtime);
349 for extra in [
350 json!({"model":"deepseek-v4-pro"}),
351 json!({"model_strength":"faster"}),
352 json!({"type":"builder"}),
353 ] {
354 let mut input = json!({"profile":"router-review", "prompt":"Inspect."});
355 input
356 .as_object_mut()
357 .unwrap()
358 .extend(extra.as_object().unwrap().clone());
359 assert!(tool.execute(input, &context).await.is_err());
360 }
361 assert!(manager.read().await.agents.is_empty());
362 assert_eq!(calls.load(Ordering::SeqCst), 0);
363 let discovered = tool
364 .execute(json!({"action":"roster"}), &context)
365 .await
366 .unwrap();
367 let roster: Value = serde_json::from_str(&discovered.content).unwrap();
368 let row = roster["profiles"]
369 .as_array()
370 .unwrap()
371 .iter()
372 .find(|row| row["member_id"] == "router-review")
373 .unwrap();
374 assert_eq!(row["route"]["provider"], "openrouter");
375 assert_eq!(row["route"]["openrouter_vendor"], "cerebras");
376 assert_eq!(row["route"]["cost_class"], "unknown");
377 let started = tool
378 .execute(
379 json!({"profile":"router-review", "prompt":"Say done.", "thinking":"high"}),
380 &context,
381 )
382 .await
383 .unwrap();
384 let meta = started.metadata.as_ref().unwrap();
385 assert_eq!(meta["child_route"]["provider_id"], "openrouter");
386 assert_eq!(meta["child_route"]["model_id"], "qwen/qwen3.7-plus");
387 assert_eq!(meta["child_route"]["effective_reasoning"], "high");
388 tokio::time::timeout(Duration::from_secs(5), async {
389 while calls.load(Ordering::SeqCst) == 0 {
390 tokio::time::sleep(Duration::from_millis(10)).await;
391 }
392 })
393 .await
394 .expect("local provider fixture receives child request");
395 let body = bodies.lock().unwrap()[0].clone();
396 assert_eq!(body["model"], "qwen/qwen3.7-plus");
397 assert_eq!(body["provider"]["order"], json!(["cerebras"]));
398 assert_eq!(body["provider"]["allow_fallbacks"], false);
399 assert!(
400 !serde_json::to_string(meta)
401 .unwrap()
402 .contains("test-router-key")
403 );
404 let id = meta["agent_id"].as_str().unwrap();
405 if manager.read().await.agents[id].status == SubAgentStatus::Running {
406 manager.write().await.cancel_agent(id).unwrap();
407 }
408 }
409
410 #[tokio::test]
411 async fn saved_profile_cannot_widen_parent_posture_or_depth_and_missing_provider_fails_closed() {
412 let _env = crate::test_support::lock_test_env();
413 let root = tempdir().unwrap();
414 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
415 let (client, calls, _, _) = delayed_chat_client(Duration::ZERO, "done").await;
416 let mut profile = codewhale_config::FleetProfile::default();
417 profile.role.name = "builder".into();
418 profile.model = Some("deepseek-v4-flash".into());
419 profile.delegation.max_spawn_depth = Some(0);
420 profile.permissions.allow_shell = true;
421 profile.permissions.trust = true;
422 let mut config = crate::config::Config {
423 ..Default::default()
424 }
425 .with_legacy_root(Some("test-key".into()), Some(client.base_url().into()));
426 let mut fleet = codewhale_config::FleetConfigToml::default();
427 fleet
428 .profiles
429 .insert("bounded-builder".into(), profile.clone());
430 profile.provider = Some("unconfigured-private-route".into());
431 fleet.profiles.insert("missing-route".into(), profile);
432 config.fleet = Some(fleet);
433 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 2);
434 let context = ToolContext::new(root.path()).with_state_namespace("saved-profile-ceiling");
435 let mut runtime = SubAgentRuntime::new(
436 client,
437 "deepseek-v4-flash".into(),
438 context.clone(),
439 false,
440 None,
441 manager.clone(),
442 )
443 .with_api_config(config);
444 runtime.worker_profile = WorkerRuntimeProfile::for_role(FleetRole::Scout);
445 runtime.worker_profile.shell = ShellPolicy::None;
446 let tool = AgentTool::new(manager.clone(), runtime);
447 assert!(
448 tool.execute(
449 json!({"profile":"missing-route", "prompt":"Inspect."}),
450 &context
451 )
452 .await
453 .is_err()
454 );
455 assert!(manager.read().await.agents.is_empty());
456 assert_eq!(calls.load(Ordering::SeqCst), 0);
457 let started = tool
458 .execute(
459 json!({"profile":"bounded-builder", "prompt":"Inspect only.", "max_depth":2}),
460 &context,
461 )
462 .await
463 .unwrap();
464 let id = started.metadata.as_ref().unwrap()["agent_id"]
465 .as_str()
466 .unwrap();
467 let mut guard = manager.write().await;
468 let worker = guard.worker_records.get(id).unwrap();
469 assert!(!worker.spec.runtime_profile.permissions.write);
470 assert_eq!(worker.spec.runtime_profile.shell, ShellPolicy::None);
471 assert_eq!(worker.spec.runtime_profile.max_spawn_depth, 1);
472 assert_eq!(worker.spec.runtime_profile.spawn_depth, 1);
473 assert!(!worker.spec.runtime_profile.can_spawn_child());
474 guard.cancel_agent(id).unwrap();
475 }
476
477 #[tokio::test]
478 async fn selected_fleet_capability_and_broken_selection_refuse_actual_start() {
479 use crate::fleet::store::{FleetFile, FleetScope, save_fleet, set_selected};
480 let _env = crate::test_support::lock_test_env();
481 let root = tempdir().unwrap();
482 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
483 let (client, calls, _, _) = delayed_chat_client(Duration::ZERO, "done").await;
484 let config = crate::config::Config {
485 ..Default::default()
486 }
487 .with_legacy_root(Some("test-key".into()), Some(client.base_url().into()));
488 let mut fleet = FleetFile::new("Capability fixture".into(), None).unwrap();
489 fleet.members.push(serde_json::from_value(json!({
490 "id":"visual-review", "role":"reviewer", "provider":"deepseek", "model":"deepseek-v4-flash", "requires":["vision"]
491 })).unwrap());
492 let path = save_fleet(&fleet, FleetScope::Workspace, root.path()).unwrap();
493 set_selected(&fleet.name, FleetScope::Workspace, root.path()).unwrap();
494 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 1);
495 let context =
496 ToolContext::new(root.path()).with_state_namespace("selected-capability-consumer");
497 let runtime = SubAgentRuntime::new(
498 client,
499 "deepseek-v4-flash".into(),
500 context.clone(),
501 false,
502 None,
503 manager.clone(),
504 )
505 .with_api_config(config);
506 let tool = AgentTool::new(manager.clone(), runtime);
507 let error = tool
508 .execute(
509 json!({"profile":"visual-review", "prompt":"Inspect image."}),
510 &context,
511 )
512 .await
513 .unwrap_err();
514 assert!(error.to_string().contains("requires vision"), "{error}");
515 assert!(manager.read().await.agents.is_empty());
516 assert_eq!(calls.load(Ordering::SeqCst), 0);
517 std::fs::write(path, "this is not a Fleet document").unwrap();
518 let error = tool
519 .execute(json!({"type":"reviewer", "prompt":"Inspect."}), &context)
520 .await
521 .unwrap_err();
522 assert!(error.to_string().contains("Selected"), "{error}");
523 assert!(manager.read().await.agents.is_empty());
524 assert_eq!(calls.load(Ordering::SeqCst), 0);
525 }
526
527 #[tokio::test]
528 async fn selected_models_reach_exact_provider_and_off_list_refuses_before_admission() {
529 use crate::fleet::store::{FleetFile, FleetScope, save_fleet, set_selected};
530 let _env = crate::test_support::lock_test_env();
531 let root = tempdir().unwrap();
532 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
533 let (client, calls, bodies, _) = delayed_chat_client(Duration::ZERO, "done").await;
534 let mut config = crate::config::Config {
535 ..Default::default()
536 }
537 .with_legacy_root(Some("test-key".into()), Some(client.base_url().into()));
538 let router = config
539 .provider_config_for_mut(&config.test_identity_for_kind(ProviderKind::Openrouter))
540 .unwrap();
541 router.api_key = Some("test-router-key".into());
542 router.base_url = Some(client.base_url().into());
543 router.vendor = Some("cerebras".into());
544 let mut fleet = FleetFile::new("Selected routes".into(), None).unwrap();
545 fleet.members.push(serde_json::from_value(json!({
546 "id":"review-choice", "role":"reviewer", "provider":"openrouter", "model":"qwen/qwen3.7-plus"
547 })).unwrap());
548 save_fleet(&fleet, FleetScope::Workspace, root.path()).unwrap();
549 set_selected(&fleet.name, FleetScope::Workspace, root.path()).unwrap();
550 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 2);
551 let context = ToolContext::new(root.path()).with_state_namespace("shortlist-consumer");
552 let runtime = SubAgentRuntime::new(
553 client,
554 "deepseek-v4-flash".into(),
555 context.clone(),
556 false,
557 None,
558 manager.clone(),
559 )
560 .with_api_config(config);
561 let tool = AgentTool::new(manager.clone(), runtime);
562 let roster = tool
563 .execute(json!({"action":"roster"}), &context)
564 .await
565 .unwrap();
566 let rows: Value = serde_json::from_str(&roster.content).unwrap();
567 assert_eq!(rows["model_total_count"], 1);
568 assert_eq!(
569 rows["models"][0]["selector"]["model"],
570 "openrouter/qwen/qwen3.7-plus"
571 );
572 assert_eq!(rows["models"][0]["route"]["provider"], "openrouter");
573 assert_eq!(rows["models"][0]["route"]["openrouter_vendor"], "cerebras");
574 assert_eq!(rows["models"][0]["route"]["reachability"], "unverified");
575 assert_eq!(calls.load(Ordering::SeqCst), 0, "discovery must not infer");
576 let error = tool
577 .execute(
578 json!({"type":"explore", "model":"deepseek-v4-pro", "prompt":"Inspect."}),
579 &context,
580 )
581 .await
582 .unwrap_err();
583 assert!(
584 error.to_string().contains("outside the selected Fleet"),
585 "{error}"
586 );
587 assert!(
588 error.to_string().contains("openrouter/qwen/qwen3.7-plus"),
589 "{error}"
590 );
591 assert!(
592 error.to_string().contains("deepseek/deepseek-v4-flash"),
593 "{error}"
594 );
595 assert!(manager.read().await.agents.is_empty());
596 assert_eq!(calls.load(Ordering::SeqCst), 0);
597 let started = tool
598 .execute(
599 json!({"type":"explore", "model":"openrouter/qwen/qwen3.7-plus", "prompt":"Say done."}),
600 &context,
601 )
602 .await
603 .unwrap();
604 let meta = started.metadata.as_ref().unwrap();
605 assert_eq!(meta["child_route"]["provider_id"], "openrouter");
606 assert_eq!(meta["child_route"]["model_id"], "qwen/qwen3.7-plus");
607 assert!(
608 meta["child_route"]["resolved_profile_id"].is_null(),
609 "model choice does not invent a saved profile"
610 );
611 tokio::time::timeout(Duration::from_secs(5), async {
612 while calls.load(Ordering::SeqCst) == 0 {
613 tokio::time::sleep(Duration::from_millis(10)).await;
614 }
615 })
616 .await
617 .expect("local fixture receives selected model request");
618 let body = bodies.lock().unwrap()[0].clone();
619 assert_eq!(body["model"], "qwen/qwen3.7-plus");
620 assert_eq!(body["provider"]["order"], json!(["cerebras"]));
621 assert_eq!(body["provider"]["allow_fallbacks"], false);
622 let id = meta["agent_id"].as_str().unwrap();
623 if manager.read().await.agents[id].status == SubAgentStatus::Running {
624 manager.write().await.cancel_agent(id).unwrap();
625 }
626 let session = tool
627 .execute(
628 json!({"type":"explore", "model":"deepseek/deepseek-v4-flash", "prompt":"Say done."}),
629 &context,
630 )
631 .await
632 .unwrap();
633 assert_eq!(
634 session.metadata.as_ref().unwrap()["child_route"]["provider_id"],
635 "deepseek"
636 );
637 let id = session.metadata.as_ref().unwrap()["agent_id"]
638 .as_str()
639 .unwrap();
640 if manager.read().await.agents[id].status == SubAgentStatus::Running {
641 manager.write().await.cancel_agent(id).unwrap();
642 }
643 fleet.members.clear();
644 save_fleet(&fleet, FleetScope::Workspace, root.path()).unwrap();
645 let empty = tool
646 .execute(json!({"action":"roster"}), &context)
647 .await
648 .unwrap();
649 let rows: Value = serde_json::from_str(&empty.content).unwrap();
650 assert_eq!(
651 rows["model_total_count"], 0,
652 "live removal must not preserve stale choices"
653 );
654 let error = tool
655 .execute(
656 json!({"type":"explore", "model":"openrouter/qwen/qwen3.7-plus", "prompt":"Inspect."}),
657 &context,
658 )
659 .await
660 .unwrap_err();
661 assert!(error.to_string().contains("provider DeepSeek"), "{error}");
662 }
663
664 #[tokio::test]
665 async fn shortlisted_model_on_multiple_providers_requires_exact_selector() {
666 use crate::fleet::store::{FleetFile, FleetScope, save_fleet, set_selected};
667 let _env = crate::test_support::lock_test_env();
668 let root = tempdir().unwrap();
669 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
670 let (client, calls, _, fixture_config) = delayed_chat_client(Duration::ZERO, "done").await;
671 let mut fleet = FleetFile::new("Ambiguous routes".into(), None).unwrap();
672 for (id, provider) in [("review-a", "openrouter"), ("review-b", "openai")] {
673 fleet.members.push(
674 serde_json::from_value(json!({
675 "id":id, "role":"reviewer", "provider":provider, "model":"shared-wire-model"
676 }))
677 .unwrap(),
678 );
679 }
680 save_fleet(&fleet, FleetScope::Workspace, root.path()).unwrap();
681 set_selected(&fleet.name, FleetScope::Workspace, root.path()).unwrap();
682 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 1);
683 let context = ToolContext::new(root.path()).with_state_namespace("ambiguous-model-consumer");
684 let runtime = SubAgentRuntime::new(
685 client,
686 "deepseek-v4-flash".into(),
687 context.clone(),
688 false,
689 None,
690 manager.clone(),
691 )
692 .with_api_config(fixture_config);
693 let tool = AgentTool::new(manager.clone(), runtime);
694 let error = tool
695 .execute(
696 json!({"type":"explore", "model":"shared-wire-model", "prompt":"Inspect."}),
697 &context,
698 )
699 .await
700 .unwrap_err();
701 assert!(error.to_string().contains("multiple providers"), "{error}");
702 assert!(manager.read().await.agents.is_empty());
703 assert_eq!(calls.load(Ordering::SeqCst), 0);
704 }
705
706 fn write_restart_route_config(path: &std::path::Path, base_url: &str, role_pins: &str) {
707 std::fs::write(
708 path,
709 format!(
710 r#"
711 provider = "deepseek"
712 model = "deepseek-v4-flash"
713 api_key = "fixture-key"
714 base_url = "{base_url}"
715
716 [providers.ReviewerRoute]
717 kind = "openai-compatible"
718 api_key = "fixture-review-key"
719 base_url = "{base_url}"
720 model = "fixture-review-model"
721
722 [providers.OtherRoute]
723 kind = "openai-compatible"
724 api_key = "fixture-other-key"
725 base_url = "{base_url}"
726 model = "fixture-review-model"
727
728 {role_pins}
729 "#
730 ),
731 )
732 .unwrap();
733 }
734
735 async fn assert_admitted_route(
736 manager: &SharedSubAgentManager,
737 started: &crate::tools::spec::ToolResult,
738 expected: Value,
739 ) -> String {
740 let content: Value = serde_json::from_str(&started.content).unwrap();
741 let metadata = started.metadata.as_ref().unwrap();
742 let receipt = &metadata["child_route"];
743 assert_eq!(&content["child_route"], receipt);
744 for (field, value) in expected.as_object().unwrap() {
745 assert_eq!(&receipt[field], value, "admitted {field}: {receipt}");
746 }
747 let id = metadata["agent_id"].as_str().unwrap().to_string();
748 let manager = manager.read().await;
749 let spec = &manager.worker_records[&id].spec;
750 assert_eq!(serde_json::to_value(&spec.child_route).unwrap(), *receipt);
751 assert_eq!(spec.model, receipt["model_id"].as_str().unwrap());
752 assert_eq!(spec.agent_type.as_str(), receipt["canonical_role"]);
753 assert_eq!(manager.agents[&id].model, spec.model);
754 let manifest = spec
755 .launch_manifest
756 .as_ref()
757 .expect("persisted launch authority");
758 assert_eq!(manifest.child_id, id);
759 assert_eq!(manifest.profile, spec.runtime_profile);
760 assert_eq!(manifest.profile.role, spec.agent_type);
761 assert_eq!(
762 manifest.profile.model,
763 crate::worker_profile::ModelRoute::Fixed(spec.model.clone())
764 );
765 assert_eq!(
766 manifest.profile.provider.as_deref(),
767 receipt["provider_id"].as_str()
768 );
769 assert_eq!(
770 manifest.profile.reasoning_effort.as_deref(),
771 receipt["effective_reasoning"].as_str()
772 );
773 id
774 }
775
776 async fn wait_for_queued_child(mailbox: &mut MailboxReceiver, id: &str) {
777 tokio::time::timeout(Duration::from_secs(2), async {
778 loop {
779 let envelope = mailbox.recv().await.expect("child progress channel");
780 if matches!(envelope.message, MailboxMessage::Progress { agent_id, status }
781 if agent_id == id && status.contains("queued"))
782 {
783 break;
784 }
785 }
786 })
787 .await
788 .expect("actual child reaches the held launch gate");
789 }
790
791 #[tokio::test]
792 async fn fleet_editor_save_reload_reaches_type_only_admission_without_a_model_request() {
793 use crate::fleet::store::{
794 FleetFile, FleetScope, load_fleet_in_scope, save_fleet, set_selected,
795 };
796 use crate::tui::views::fleet_detail::FleetDetailView;
797 use crate::tui::views::{ModalView, ViewAction, ViewEvent};
798 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
799
800 let _env = crate::test_support::lock_test_env();
801 let root = tempdir().unwrap();
802 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
803 let _project = ProjectProfilesGuard::enabled();
804 let (fixture_client, calls, bodies, _) = delayed_chat_client(Duration::ZERO, "done").await;
805 let _provider = crate::test_support::EnvVarGuard::set("CODEWHALE_PROVIDER", "deepseek");
806 let _endpoint =
807 crate::test_support::EnvVarGuard::set("CODEWHALE_BASE_URL", fixture_client.base_url());
808 let _model = crate::test_support::EnvVarGuard::set("CODEWHALE_MODEL", "deepseek-v4-flash");
809 let config_path = root.path().join("config.toml");
810 write_restart_route_config(&config_path, fixture_client.base_url(), "");
811 let config = crate::config::Config::load(Some(config_path.clone()), None).unwrap();
812 let mut fleet = FleetFile::new("Editor restart acceptance".into(), None).unwrap();
813 fleet.members.push(
814 serde_json::from_value(json!({
815 "id":"review-pin", "role":"reviewer", "instructions":"SAVED_REVIEW_INSTRUCTION"
816 }))
817 .unwrap(),
818 );
819 save_fleet(&fleet, FleetScope::Workspace, root.path()).unwrap();
820 set_selected(&fleet.name, FleetScope::Workspace, root.path()).unwrap();
821
822 let mut app =
823 crate::tui::app::App::new(crate::test_support::test_tui_options(root.path()), &config);
824 app.workspace = root.path().to_path_buf();
825 let mut view = FleetDetailView::open_for_member(
826 &app,
827 &config,
828 &fleet.name,
829 FleetScope::Workspace,
830 Some("review-pin"),
831 )
832 .unwrap();
833 let key = |code| KeyEvent::new(code, KeyModifiers::NONE);
834 view.handle_key(key(KeyCode::Char('e')));
835 for ch in "ReviewerRoute".chars() {
836 view.handle_key(key(KeyCode::Char(ch)));
837 }
838 view.handle_key(key(KeyCode::Enter));
839 // Off survives generic route normalization and distinguishes the saved
840 // choice from default reasoning without inventing fixture capabilities.
841 view.handle_key(key(KeyCode::Char('t')));
842 assert!(matches!(
843 view.handle_key(key(KeyCode::Char('s'))),
844 ViewAction::EmitAndClose(ViewEvent::FleetStoreChanged { .. })
845 ));
846 let (saved, _) = load_fleet_in_scope(&fleet.name, FleetScope::Workspace, root.path()).unwrap();
847 let pin = &saved.members[0];
848 assert!(!pin.shortlist);
849 assert_eq!(pin.role, "reviewer");
850 assert_eq!(pin.provider.as_deref(), Some("ReviewerRoute"));
851 assert_eq!(pin.model.as_deref(), Some("fixture-review-model"));
852 assert_eq!(pin.reasoning.as_deref(), Some("off"));
853 drop((view, app, config, fixture_client));
854
855 // Restart from the ordinary file loader; no old UI state or runtime roster survives.
856 let reloaded = crate::config::Config::load(Some(config_path), None).unwrap();
857 let client = CodewhaleClient::new(&reloaded).unwrap();
858 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 1);
859 let gate = manager.read().await.launch_gate.clone();
860 let held_permit = gate.acquire().await;
861 let (mailbox, mut mailbox_rx) = Mailbox::new(CancellationToken::new());
862 let context = ToolContext::new(root.path()).with_state_namespace("fleet-editor-restarted");
863 let mut runtime = SubAgentRuntime::new(
864 client,
865 "deepseek-v4-flash".into(),
866 context.clone(),
867 false,
868 None,
869 manager.clone(),
870 )
871 .with_api_config(reloaded);
872 runtime.mailbox = Some(mailbox);
873 let loaded_roster = spawn_roster(&runtime);
874 let loaded_pin = loaded_roster
875 .members()
876 .iter()
877 .find(|member| member.id == "review-pin")
878 .unwrap();
879 assert_eq!(loaded_pin.profile.reasoning_effort.as_deref(), Some("off"));
880 let tool = AgentTool::new(manager.clone(), runtime);
881 for extra in [
882 json!({"model":"OtherRoute/fixture-review-model"}),
883 json!({"model_strength":"faster"}),
884 ] {
885 let mut request = json!({"type":"reviewer", "prompt":"Review without execution."});
886 request
887 .as_object_mut()
888 .unwrap()
889 .extend(extra.as_object().unwrap().clone());
890 assert!(tool.execute(request, &context).await.is_err());
891 assert!(manager.read().await.agents.is_empty());
892 assert!(manager.read().await.worker_records.is_empty());
893 assert_eq!(calls.load(Ordering::SeqCst), 0);
894 }
895 let started = tool
896 .execute(
897 json!({"type":"reviewer", "prompt":"Review without execution."}),
898 &context,
899 )
900 .await
901 .unwrap();
902 let id = assert_admitted_route(
903 &manager,
904 &started,
905 json!({
906 "requested_type":"reviewer", "requested_profile":null,
907 "resolved_profile_id":"review-pin", "profile_origin":"project",
908 "canonical_role":"reviewer", "provider_id":"ReviewerRoute",
909 "model_id":"fixture-review-model", "route_source":"agent_profile.model",
910 "requested_reasoning":"inherit", "effective_reasoning":"off"
911 }),
912 )
913 .await;
914 assert!(
915 manager.read().await.worker_records[&id]
916 .spec
917 .launch_manifest
918 .as_ref()
919 .unwrap()
920 .prompt
921 .contains("SAVED_REVIEW_INSTRUCTION")
922 );
923 wait_for_queued_child(&mut mailbox_rx, &id).await;
924 assert_eq!(calls.load(Ordering::SeqCst), 0);
925 assert!(bodies.lock().unwrap().is_empty());
926 let requested = manager.write().await.cancel_agent(&id).unwrap();
927 let settled = settle_requested_child(&manager, requested).await;
928 assert_eq!(settled.status, SubAgentStatus::Cancelled);
929 assert_eq!(
930 manager.read().await.agents[&id].status,
931 SubAgentStatus::Cancelled
932 );
933 drop(held_permit);
934 }
935
936 #[tokio::test]
937 async fn reloaded_manual_role_pin_and_explicit_profile_keep_distinct_shortlist_receipts() {
938 use crate::fleet::store::{FleetFile, FleetScope, save_fleet, set_selected};
939 let _env = crate::test_support::lock_test_env();
940 let root = tempdir().unwrap();
941 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
942 let _project = ProjectProfilesGuard::enabled();
943 let (fixture_client, calls, bodies, _) = delayed_chat_client(Duration::ZERO, "done").await;
944 let _provider = crate::test_support::EnvVarGuard::set("CODEWHALE_PROVIDER", "deepseek");
945 let _endpoint =
946 crate::test_support::EnvVarGuard::set("CODEWHALE_BASE_URL", fixture_client.base_url());
947 let _model = crate::test_support::EnvVarGuard::set("CODEWHALE_MODEL", "deepseek-v4-flash");
948 let config_path = root.path().join("config.toml");
949 write_restart_route_config(
950 &config_path,
951 fixture_client.base_url(),
952 r#"
953 [subagents.models]
954 reviewer = "deepseek-v4-flash"
955 default = "deepseek-v4-pro"
956 [subagents.roles.reviewer]
957 model = "deepseek-v4-pro"
958 "#,
959 );
960 let mut fleet = FleetFile::new("Manual pin precedence".into(), None).unwrap();
961 for member in [
962 json!({"id":"review-choice", "shortlist":true, "provider":"ReviewerRoute", "model":"fixture-review-model"}),
963 // Off stays distinct from the default on this generic custom route.
964 json!({"id":"review-pin", "role":"reviewer", "provider":"ReviewerRoute", "model":"fixture-review-model", "reasoning":"off"}),
965 ] {
966 fleet.members.push(serde_json::from_value(member).unwrap());
967 }
968 save_fleet(&fleet, FleetScope::Workspace, root.path()).unwrap();
969 set_selected(&fleet.name, FleetScope::Workspace, root.path()).unwrap();
970 let config = crate::config::Config::load(Some(config_path), None).unwrap();
971 let overrides = config.subagent_model_overrides();
972 assert_eq!(overrides["reviewer"].model, "deepseek-v4-pro");
973 assert_eq!(overrides["reviewer"].provider, None);
974 let client = CodewhaleClient::new(&config).unwrap();
975 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 1);
976 let gate = manager.read().await.launch_gate.clone();
977 let held_permit = gate.acquire().await;
978 let (mailbox, mut mailbox_rx) = Mailbox::new(CancellationToken::new());
979 let context = ToolContext::new(root.path()).with_state_namespace("manual-role-restarted");
980 let mut runtime = SubAgentRuntime::new(
981 client,
982 "deepseek-v4-flash".into(),
983 context.clone(),
984 false,
985 None,
986 manager.clone(),
987 )
988 .with_api_config(config);
989 runtime.mailbox = Some(mailbox);
990 let loaded_roster = spawn_roster(&runtime);
991 let loaded_pin = loaded_roster
992 .members()
993 .iter()
994 .find(|member| member.id == "review-pin")
995 .unwrap();
996 assert_eq!(loaded_pin.profile.reasoning_effort.as_deref(), Some("off"));
997 let tool = AgentTool::new(manager.clone(), runtime);
998 let result = tool
999 .execute(json!({"action":"roster"}), &context)
1000 .await
1001 .unwrap();
1002 let roster: Value = serde_json::from_str(&result.content).unwrap();
1003 let model = roster["models"]
1004 .as_array()
1005 .unwrap()
1006 .iter()
1007 .find(|row| row["selector"]["model"] == "ReviewerRoute/fixture-review-model")
1008 .expect("shortlisted model row");
1009 assert_eq!(model["route"]["provider"], "ReviewerRoute");
1010 assert_eq!(model["route"]["model"], "fixture-review-model");
1011 assert_ne!(model["route"]["source"], "role.pin");
1012 for role in ["general", "reviewer"] {
1013 let row = roster["members"]
1014 .as_array()
1015 .unwrap()
1016 .iter()
1017 .find(|row| row["role"] == role)
1018 .unwrap();
1019 assert_eq!(row["route"]["model"], "deepseek-v4-pro");
1020 assert_eq!(row["route"]["source"], "role.pin");
1021 }
1022 for extra in [
1023 json!({"model":"ReviewerRoute/fixture-review-model"}),
1024 json!({"model_strength":"faster"}),
1025 ] {
1026 let mut request = json!({"type":"reviewer", "prompt":"Review."});
1027 request
1028 .as_object_mut()
1029 .unwrap()
1030 .extend(extra.as_object().unwrap().clone());
1031 assert!(tool.execute(request, &context).await.is_err());
1032 assert!(manager.read().await.agents.is_empty());
1033 assert!(manager.read().await.worker_records.is_empty());
1034 }
1035 for (request, expected) in [
1036 (
1037 json!({"type":"reviewer", "prompt":"Review."}),
1038 json!({
1039 "requested_profile":null, "resolved_profile_id":null,
1040 "provider_id":"deepseek", "model_id":"deepseek-v4-pro", "route_source":"role.pin"
1041 }),
1042 ),
1043 (
1044 json!({"type":"reviewer", "model":"deepseek/deepseek-v4-pro", "prompt":"Review."}),
1045 json!({
1046 "requested_profile":null, "resolved_profile_id":null,
1047 "provider_id":"deepseek", "model_id":"deepseek-v4-pro", "route_source":"role.pin"
1048 }),
1049 ),
1050 (
1051 json!({"profile":"member:review-pin", "prompt":"Review."}),
1052 json!({
1053 "requested_profile":"member:review-pin", "resolved_profile_id":"review-pin",
1054 "provider_id":"ReviewerRoute", "model_id":"fixture-review-model",
1055 "canonical_role":"reviewer", "requested_reasoning":"inherit",
1056 "effective_reasoning":"off", "route_source":"agent_profile.model"
1057 }),
1058 ),
1059 (
1060 json!({"profile":"member:review-pin", "thinking":"low", "prompt":"Review."}),
1061 json!({
1062 "requested_profile":"member:review-pin", "resolved_profile_id":"review-pin",
1063 "provider_id":"ReviewerRoute", "model_id":"fixture-review-model",
1064 "canonical_role":"reviewer", "requested_reasoning":"low",
1065 "effective_reasoning":"high", "route_source":"agent_profile.model"
1066 }),
1067 ),
1068 ] {
1069 let started = tool.execute(request, &context).await.unwrap();
1070 let id = assert_admitted_route(&manager, &started, expected).await;
1071 wait_for_queued_child(&mut mailbox_rx, &id).await;
1072 assert_eq!(calls.load(Ordering::SeqCst), 0);
1073 assert!(bodies.lock().unwrap().is_empty());
1074 let requested = manager.write().await.cancel_agent(&id).unwrap();
1075 let settled = settle_requested_child(&manager, requested).await;
1076 assert_eq!(settled.status, SubAgentStatus::Cancelled);
1077 assert_eq!(
1078 manager.read().await.agents[&id].status,
1079 SubAgentStatus::Cancelled
1080 );
1081 }
1082 drop(held_permit);
1083 }
1084
1085 #[tokio::test]
1086 async fn loaded_structured_role_routes_bind_exact_providers_and_legacy_namespaces_stay_opaque() {
1087 let _env = crate::test_support::lock_test_env();
1088 for (
1089 name,
1090 parent_provider,
1091 parent_model,
1092 declaration,
1093 pin_provider,
1094 pin_model,
1095 admitted_provider,
1096 ) in [
1097 (
1098 "structured-route",
1099 "deepseek",
1100 "deepseek-v4-flash",
1101 "[subagents.roles.reviewer]\nmodel = \"ReviewerRoute/fixture-review-model\"",
1102 Some("ReviewerRoute"),
1103 "fixture-review-model",
1104 Some("ReviewerRoute"),
1105 ),
1106 (
1107 "unknown-route",
1108 "deepseek",
1109 "deepseek-v4-flash",
1110 "[subagents.roles.reviewer]\nmodel = \"MissingRoute/fixture-review-model\"",
1111 Some("MissingRoute"),
1112 "fixture-review-model",
1113 None,
1114 ),
1115 (
1116 "legacy-namespace",
1117 "openrouter",
1118 "deepseek/deepseek-v4-flash",
1119 "[subagents.models]\nreviewer = \"deepseek/deepseek-v4-pro\"",
1120 None,
1121 "deepseek/deepseek-v4-pro",
1122 Some("openrouter"),
1123 ),
1124 ] {
1125 let root = tempdir().unwrap();
1126 let _home =
1127 crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
1128 let (fixture_client, calls, bodies, _) = delayed_chat_client(Duration::ZERO, "done").await;
1129 let _provider =
1130 crate::test_support::EnvVarGuard::set("CODEWHALE_PROVIDER", parent_provider);
1131 let _endpoint =
1132 crate::test_support::EnvVarGuard::set("CODEWHALE_BASE_URL", fixture_client.base_url());
1133 let _model = crate::test_support::EnvVarGuard::set("CODEWHALE_MODEL", parent_model);
1134 let config_path = root.path().join("config.toml");
1135 // `deepseek` is a real configured provider as well as the legacy wire
1136 // namespace. The old map must not reinterpret that namespace as a pin.
1137 let declarations = format!(
1138 r#"
1139 [providers.deepseek]
1140 api_key = "fixture-deepseek-key"
1141 base_url = "{base_url}"
1142 model = "deepseek-v4-flash"
1143 [providers.openrouter]
1144 api_key = "fixture-router-key"
1145 base_url = "{base_url}"
1146 model = "deepseek/deepseek-v4-flash"
1147 {declaration}
1148 "#,
1149 base_url = fixture_client.base_url()
1150 );
1151 write_restart_route_config(&config_path, fixture_client.base_url(), &declarations);
1152 let config = crate::config::Config::load(Some(config_path), None).unwrap();
1153 let overrides = config.subagent_model_overrides();
1154 assert_eq!(
1155 overrides["reviewer"].provider.as_deref(),
1156 pin_provider,
1157 "{name}"
1158 );
1159 assert_eq!(overrides["reviewer"].model, pin_model, "{name}");
1160 assert_eq!(
1161 config.active_provider_identity().unwrap().key.as_str(),
1162 parent_provider
1163 );
1164 let client = CodewhaleClient::new(&config).unwrap();
1165 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 1);
1166 let gate = manager.read().await.launch_gate.clone();
1167 let held_permit = gate.acquire().await;
1168 let (mailbox, mut mailbox_rx) = Mailbox::new(CancellationToken::new());
1169 let context = ToolContext::new(root.path()).with_state_namespace(name);
1170 let mut runtime = SubAgentRuntime::new(
1171 client,
1172 parent_model.into(),
1173 context.clone(),
1174 false,
1175 None,
1176 manager.clone(),
1177 )
1178 .with_api_config(config);
1179 runtime.mailbox = Some(mailbox);
1180 let tool = AgentTool::new(manager.clone(), runtime);
1181 let roster_result = tool
1182 .execute(json!({"action":"roster"}), &context)
1183 .await
1184 .unwrap();
1185 let roster: Value = serde_json::from_str(&roster_result.content).unwrap();
1186 let row = roster["members"]
1187 .as_array()
1188 .unwrap()
1189 .iter()
1190 .find(|row| row["role"] == "reviewer")
1191 .unwrap();
1192 let request = json!({"type":"reviewer", "prompt":"Review before model execution."});
1193 let Some(expected_provider) = admitted_provider else {
1194 assert!(row["route"].is_null());
1195 assert!(
1196 row["route_error"]
1197 .as_str()
1198 .unwrap()
1199 .contains("MissingRoute")
1200 );
1201 let error = tool.execute(request, &context).await.unwrap_err();
1202 assert!(error.to_string().contains("MissingRoute"), "{error}");
1203 assert!(manager.read().await.agents.is_empty());
1204 assert!(manager.read().await.worker_records.is_empty());
1205 assert_eq!(calls.load(Ordering::SeqCst), 0);
1206 assert!(bodies.lock().unwrap().is_empty());
1207 drop(held_permit);
1208 continue;
1209 };
1210 assert_eq!(row["route"]["provider"], expected_provider, "{name}");
1211 assert_eq!(row["route"]["model"], pin_model, "{name}");
1212 assert_eq!(row["route"]["source"], "role.pin", "{name}");
1213 if pin_provider.is_some() {
1214 for extra in [
1215 json!({"model":"OtherRoute/fixture-review-model"}),
1216 json!({"model_strength":"faster"}),
1217 ] {
1218 let mut conflicting = request.clone();
1219 conflicting
1220 .as_object_mut()
1221 .unwrap()
1222 .extend(extra.as_object().unwrap().clone());
1223 let error = tool.execute(conflicting, &context).await.unwrap_err();
1224 assert!(error.to_string().contains("pins"), "{error}");
1225 assert!(manager.read().await.agents.is_empty());
1226 assert!(manager.read().await.worker_records.is_empty());
1227 assert_eq!(calls.load(Ordering::SeqCst), 0);
1228 }
1229 }
1230 let mut matching = request.clone();
1231 matching["model"] = json!(format!("{expected_provider}/{pin_model}"));
1232 for request in [request, matching] {
1233 let started = tool.execute(request, &context).await.unwrap();
1234 let id = assert_admitted_route(
1235 &manager,
1236 &started,
1237 json!({
1238 "requested_type":"reviewer", "canonical_role":"reviewer",
1239 "requested_profile":null, "resolved_profile_id":null, "profile_origin":null,
1240 "provider_id":expected_provider, "model_id":pin_model, "route_source":"role.pin"
1241 }),
1242 )
1243 .await;
1244 wait_for_queued_child(&mut mailbox_rx, &id).await;
1245 assert_eq!(calls.load(Ordering::SeqCst), 0);
1246 assert!(bodies.lock().unwrap().is_empty());
1247 let requested = manager.write().await.cancel_agent(&id).unwrap();
1248 let settled = settle_requested_child(&manager, requested).await;
1249 assert_eq!(settled.status, SubAgentStatus::Cancelled);
1250 assert_eq!(
1251 manager.read().await.agents[&id].status,
1252 SubAgentStatus::Cancelled
1253 );
1254 }
1255 drop(held_permit);
1256 }
1257 }
1258
1259 #[tokio::test]
1260 async fn issue_6117_invalid_personal_profile_is_visible_and_never_admitted_as_builtin() {
1261 let _env = crate::test_support::lock_test_env();
1262 let root = tempdir().unwrap();
1263 let home = root.path().join("state");
1264 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &home);
1265 std::fs::create_dir_all(home.join("agents")).unwrap();
1266 let profile = home.join("agents/scout.toml");
1267 std::fs::write(&profile, "provider = \"openrouter\"\nmodel = \"qwen/qwen3.7-plus\"\nallow_shell = false\ntrust = false\n").unwrap();
1268 let (client, calls, _, _) = delayed_chat_client(Duration::ZERO, "done").await;
1269 let config = crate::config::Config {
1270 ..Default::default()
1271 }
1272 .with_legacy_root(Some("test-key".into()), Some(client.base_url().into()));
1273 let manager = new_shared_subagent_manager(root.path().to_path_buf(), 2);
1274 let context = ToolContext::new(root.path()).with_state_namespace("issue-6117");
1275 let runtime = SubAgentRuntime::new(
1276 client,
1277 "deepseek-v4-flash".into(),
1278 context.clone(),
1279 false,
1280 None,
1281 manager.clone(),
1282 )
1283 .with_api_config(config);
1284 let tool = AgentTool::new(manager.clone(), runtime);
1285 let discovered = tool
1286 .execute(json!({"action":"roster"}), &context)
1287 .await
1288 .unwrap();
1289 let roster: Value = serde_json::from_str(&discovered.content).unwrap();
1290 assert_eq!(roster["profile_load_issue_count"], 1);
1291 assert_eq!(roster["profile_load_issues"][0]["id"], "scout");
1292 for selector in ["scout", "explore", "member:SCOUT"] {
1293 let error = tool
1294 .execute(
1295 json!({"action":"start", "profile":selector, "prompt":"Inspect."}),
1296 &context,
1297 )
1298 .await
1299 .unwrap_err();
1300 assert!(error.to_string().contains("scout.toml"), "{error}");
1301 }
1302 assert_eq!(calls.load(Ordering::SeqCst), 0);
1303 assert!(manager.read().await.agents.is_empty());
1304 std::fs::write(&profile, "base_role = \"explore\"\nprovider = \"deepseek\"\nmodel = \"deepseek-v4-pro\"\nreasoning_effort = \"low\"\n[permissions]\nallow_shell = false\ntrust = false\n").unwrap();
1305 let started = tool
1306 .execute(
1307 json!({"action":"start", "profile":"scout", "prompt":"Say done."}),
1308 &context,
1309 )
1310 .await
1311 .unwrap();
1312 let meta = started.metadata.as_ref().unwrap();
1313 let receipt = &meta["child_route"];
1314 assert_eq!(receipt["resolved_profile_id"], "scout");
1315 assert_eq!(receipt["profile_origin"], "personal");
1316 assert_eq!(receipt["provider_id"], "deepseek");
1317 assert_eq!(receipt["model_id"], "deepseek-v4-pro");
1318 assert_eq!(receipt["effective_reasoning"], "low");
1319 assert_eq!(receipt["route_source"], "agent_profile.model");
1320 let id = meta["agent_id"].as_str().unwrap();
1321 let mut guard = manager.write().await;
1322 assert!(
1323 !guard.worker_records[id]
1324 .spec
1325 .runtime_profile
1326 .permissions
1327 .write
1328 );
1329 if guard.agents[id].status == SubAgentStatus::Running {
1330 guard.cancel_agent(id).unwrap();
1331 }
1332 }
1333
1333 lines RUST