返回 CodeWhale
shortlist_tests.rs
根目录 / crates / tui / src / tools / workflow / shortlist_tests.rs
1 //! Exercise native plan lowering and the real provider-binding spawn boundary.
2
3 use super::*;
4 use crate::client::CodewhaleClient;
5 use crate::config::{Config, ProviderKind};
6 use crate::fleet::exact::{ExactFleetWorkflow, StaticFleetRouter};
7 use crate::fleet::members::add_fleet_model;
8 use crate::fleet::store::{FleetFile, FleetScope, save_fleet, set_selected};
9 use crate::tools::subagent::new_shared_subagent_manager;
10 use codewhale_workflow::{FleetDocument, QualifiedFleetId};
11 use std::sync::atomic::AtomicUsize;
12 use std::time::Duration;
13
14 const TARGET_MODEL: &str = "openai/gpt-4.1";
15 const TARGET_SELECTOR: &str = "openrouter/openai/gpt-4.1";
16
17 struct RouteFixture {
18 config: Config,
19 runtime: SubAgentRuntime,
20 parent_calls: Arc<AtomicUsize>,
21 target_calls: Arc<AtomicUsize>,
22 target_bodies: Arc<Mutex<Vec<Value>>>,
23 }
24
25 impl RouteFixture {
26 async fn new(workspace: &Path) -> Self {
27 // WorkflowVm dispatches on its own thread. Keep saved routes in the
28 // explicit workspace: the test-only personal-home resolver correctly
29 // refuses another thread's environment guard.
30 let fleet = FleetFile::new("Workflow fixture".into(), None).expect("empty saved Fleet");
31 save_fleet(&fleet, FleetScope::Workspace, workspace).expect("save fixture Fleet");
32 set_selected(&fleet.name, FleetScope::Workspace, workspace).expect("select fixture Fleet");
33 let (parent, parent_calls, _, _) = tests::fake_chat_client_capturing("parent route").await;
34 let (target, target_calls, target_bodies, _) =
35 tests::fake_chat_client_capturing("frozen route result").await;
36 let mut config = Config {
37 ..Default::default()
38 }
39 .with_legacy_root(
40 Some("fixture-key".into()),
41 Some(parent.base_url().to_string()),
42 );
43 config
44 .set_provider_api_key_override(
45 &config.test_identity_for_kind(ProviderKind::Openrouter),
46 Some("fixture-key".into()),
47 )
48 .unwrap();
49 config
50 .set_provider_base_url_override(
51 &config.test_identity_for_kind(ProviderKind::Openrouter),
52 Some(target.base_url().to_string()),
53 )
54 .unwrap();
55 let client = CodewhaleClient::new(&config).expect("parent fixture client");
56 let manager = new_shared_subagent_manager(workspace.to_path_buf(), 4);
57 let runtime = SubAgentRuntime::new(
58 client,
59 "deepseek-v4-pro".into(),
60 ToolContext::new(workspace.to_path_buf()).with_state_namespace("session-test"),
61 true,
62 None,
63 manager,
64 )
65 .with_api_config(config.clone());
66 Self {
67 config,
68 runtime,
69 parent_calls,
70 target_calls,
71 target_bodies,
72 }
73 }
74
75 fn driver(&self, fleet: WorkflowFleetBinding) -> Arc<SubAgentWorkflowDriver> {
76 let workspace = self.runtime.context.workspace.clone();
77 let state = WorkflowWorkspaceState::open(&workspace);
78 let run_id = format!("shortlist-{}", Uuid::new_v4());
79 state.runs.lock().expect("runs").insert(
80 run_id.clone(),
81 WorkflowRunRecord::new(
82 run_id.clone(),
83 Some("session-test".into()),
84 None,
85 None,
86 None,
87 ),
88 );
89 SubAgentWorkflowDriver::new(
90 run_id,
91 "session-test".into(),
92 self.runtime.manager.clone(),
93 self.runtime.clone(),
94 state,
95 None,
96 fleet,
97 Vec::new(),
98 workspace,
99 )
100 }
101
102 fn assert_target_request(&self) {
103 assert_eq!(
104 self.parent_calls.load(Ordering::SeqCst),
105 0,
106 "parent route must not receive the child"
107 );
108 assert!(
109 self.target_calls.load(Ordering::SeqCst) > 0,
110 "the child must actually reach the selected provider"
111 );
112 let bodies = self.target_bodies.lock().expect("request bodies");
113 assert!(!bodies.is_empty());
114 for body in bodies.iter() {
115 assert_eq!(
116 body["model"], TARGET_MODEL,
117 "provider wire model must match the saved route"
118 );
119 }
120 }
121 }
122
123 /// The real VM has its own OS thread. Enroll it in this fixture's sealed test
124 /// environment before provider construction reads Config, and retain enrollment
125 /// while the real child tasks run on that same reactor. Otherwise Config's test
126 /// reader blocks on the environment lock held by the test awaiting the VM.
127 struct ScopedWorkflowDriver {
128 inner: Arc<SubAgentWorkflowDriver>,
129 ticket: crate::test_support::EnvScopeTicket,
130 membership: Mutex<Option<crate::test_support::EnvScopeMembership>>,
131 }
132
133 #[async_trait]
134 impl WorkflowDriver for ScopedWorkflowDriver {
135 async fn spawn_task(&self, request: TaskRequest) -> Result<SpawnedTask, DriverError> {
136 {
137 let mut membership = self.membership.lock().expect("VM environment membership");
138 if membership.is_none() {
139 *membership = Some(
140 crate::test_support::join_env_scope(Some(self.ticket))
141 .expect("the originating test still owns its environment"),
142 );
143 }
144 }
145 self.inner.spawn_task(request).await
146 }
147
148 fn cancel_all(&self) {
149 self.inner.cancel_all();
150 }
151
152 fn budget(&self) -> BudgetSnapshot {
153 self.inner.budget()
154 }
155
156 fn progress(&self, event: ProgressEvent) {
157 self.inner.progress(event);
158 }
159 }
160
161 async fn run_script(source: &str, driver: Arc<SubAgentWorkflowDriver>) -> Result<Value, String> {
162 let scoped = Arc::new(ScopedWorkflowDriver {
163 inner: driver.clone(),
164 ticket: crate::test_support::env_scope_ticket().expect("fixture owns test environment"),
165 membership: Mutex::new(None),
166 });
167 let result = tokio::time::timeout(
168 Duration::from_secs(10),
169 WorkflowVm::new().run_script(source, json!({}), scoped),
170 )
171 .await;
172 driver.cancel_all();
173 result
174 .expect("workflow fixture must settle within ten seconds")
175 .map_err(|error| error.to_string())
176 }
177
178 fn native_script(child: Value) -> String {
179 let spec = structured_plan_to_workflow_spec(&json!({
180 "goal": "review the route fixture", "risk": "read_only", "children": [child],
181 }))
182 .expect("valid native plan");
183 lower_declarative_workflow_to_imperative_js(&spec).expect("lower native plan")
184 }
185
186 fn select_role(workspace: &Path, model: &str, provider: &str) {
187 let fleet = FleetFile::parse(&format!(
188 "schema = 'fleet'\nschema_revision = 2\nname = 'Selected roster'\n\
189 [[members]]\nid = 'auditor'\nrole = 'reviewer'\nprovider = '{provider}'\n\
190 model = '{model}'\nreasoning = 'off'\n"
191 ))
192 .expect("saved roster");
193 save_fleet(&fleet, FleetScope::Workspace, workspace).expect("save selected roster");
194 set_selected(&fleet.name, FleetScope::Workspace, workspace).expect("select roster");
195 }
196
197 fn exact_document(reasoning: &str) -> FleetDocument {
198 let router = if reasoning == "auto" {
199 "reasoning_router = 'fixture-router'\n"
200 } else {
201 ""
202 };
203 FleetDocument::parse(&format!(
204 "name = 'frozen-audit'\nschema = 'exact'\n{router}\
205 [[members]]\nid = 'auditor'\nrole = 'reviewer'\nprovider = 'openrouter'\n\
206 model = '{TARGET_MODEL}'\nreasoning = '{reasoning}'\n"
207 ))
208 .expect("exact Fleet document")
209 }
210
211 #[tokio::test]
212 #[allow(clippy::await_holding_lock)]
213 async fn native_shortlisted_model_reaches_its_configured_provider_request() {
214 let _retry = tests::workflow_test_retry_guard();
215 let _env = crate::test_support::lock_test_env();
216 let root = tempfile::tempdir().unwrap();
217 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
218 let fixture = RouteFixture::new(root.path()).await;
219 add_fleet_model(root.path(), "openrouter", TARGET_MODEL, &[]).unwrap();
220 let source = native_script(json!({
221 "prompt": "read-only route check", "type": "reviewer", "model": TARGET_SELECTOR,
222 }));
223 run_script(&source, fixture.driver(WorkflowFleetBinding::None))
224 .await
225 .expect("shortlisted native child runs");
226 fixture.assert_target_request();
227 }
228
229 #[tokio::test]
230 #[allow(clippy::await_holding_lock)]
231 async fn native_non_shortlisted_model_is_rejected_without_a_provider_call() {
232 let _retry = tests::workflow_test_retry_guard();
233 let _env = crate::test_support::lock_test_env();
234 let root = tempfile::tempdir().unwrap();
235 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
236 let fixture = RouteFixture::new(root.path()).await;
237 add_fleet_model(root.path(), "openrouter", TARGET_MODEL, &[]).unwrap();
238 let source = native_script(json!({
239 "prompt": "read-only route check", "type": "reviewer", "model": "openrouter/outside-fixture",
240 }));
241 let error = run_script(&source, fixture.driver(WorkflowFleetBinding::None))
242 .await
243 .expect_err("closed shortlist");
244 assert!(error.contains("outside the selected Fleet"), "{error}");
245 assert_eq!(fixture.parent_calls.load(Ordering::SeqCst), 0);
246 assert_eq!(fixture.target_calls.load(Ordering::SeqCst), 0);
247 }
248
249 #[tokio::test]
250 #[allow(clippy::await_holding_lock)]
251 async fn native_role_without_model_uses_the_saved_provider_and_thinking() {
252 let _retry = tests::workflow_test_retry_guard();
253 let _env = crate::test_support::lock_test_env();
254 let root = tempfile::tempdir().unwrap();
255 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
256 let fixture = RouteFixture::new(root.path()).await;
257 select_role(root.path(), TARGET_MODEL, "openrouter");
258 let source = native_script(json!({"prompt": "read-only route check", "profile": "auditor"}));
259 let driver = fixture.driver(WorkflowFleetBinding::None);
260 run_script(&source, driver.clone())
261 .await
262 .expect("saved role runs");
263 fixture.assert_target_request();
264 let runs = driver.state.runs.lock().expect("runs");
265 let run = runs.get(&driver.run_id).expect("run record");
266 let started = run
267 .events
268 .iter()
269 .find(|event| event.event_type() == "task_started")
270 .expect("task receipt");
271 let WorkflowUiEventKind::TaskStarted(started) = &started.kind else {
272 unreachable!()
273 };
274 assert_eq!(started.resolved_profile.as_deref(), Some("auditor"));
275 assert_eq!(started.resolved_provider, "openrouter");
276 assert_eq!(started.resolved_model, TARGET_MODEL);
277 assert_eq!(started.effective_reasoning.as_deref(), Some("off"));
278 }
279
280 #[tokio::test]
281 #[allow(clippy::await_holding_lock)]
282 async fn exact_fleet_frozen_route_survives_a_conflicting_selected_roster() {
283 let _retry = tests::workflow_test_retry_guard();
284 let _env = crate::test_support::lock_test_env();
285 let root = tempfile::tempdir().unwrap();
286 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
287 let fixture = RouteFixture::new(root.path()).await;
288 // Capture first. Later selected-Pod edits must affect neither this member
289 // nor the preflighted provider endpoint, even when the member id collides.
290 let operation = ExactFleetWorkflow::capture(
291 &exact_document("off"),
292 QualifiedFleetId {
293 name: "frozen-audit".into(),
294 origin: "workspace".into(),
295 },
296 "2026-09-13T00:00:00Z",
297 Some(&fixture.config),
298 &[],
299 )
300 .expect("preflight exact route against loopback provider");
301 select_role(root.path(), "deepseek-v4-pro", "deepseek");
302 let source =
303 native_script(json!({"prompt": "read-only frozen route check", "profile": "auditor"}));
304 run_script(
305 &source,
306 fixture.driver(WorkflowFleetBinding::Exact(Arc::new(operation))),
307 )
308 .await
309 .expect("frozen exact child runs independently of selected Fleet");
310 fixture.assert_target_request();
311 }
312
313 #[tokio::test]
314 #[allow(clippy::await_holding_lock)]
315 async fn exact_fleet_model_override_is_rejected_before_router_or_provider() {
316 let _retry = tests::workflow_test_retry_guard();
317 let _env = crate::test_support::lock_test_env();
318 let root = tempfile::tempdir().unwrap();
319 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
320 let fixture = RouteFixture::new(root.path()).await;
321 let router = StaticFleetRouter::new(r#"{"reasoning":"high"}"#);
322 let operation = ExactFleetWorkflow::for_tests(
323 &exact_document("auto"),
324 QualifiedFleetId {
325 name: "frozen-audit".into(),
326 origin: "workspace".into(),
327 },
328 Some(router.clone()),
329 );
330 let error = run_script(
331 "return await task({description:'read-only check', profile:'auditor', writeAuthority:'read_only', model:'openrouter/outside-fixture'});",
332 fixture.driver(WorkflowFleetBinding::Exact(Arc::new(operation))),
333 ).await.expect_err("exact Fleet refuses task route overrides");
334 assert!(
335 error.contains("task option `model` is not allowed"),
336 "{error}"
337 );
338 assert!(
339 router.seen.lock().unwrap().is_empty(),
340 "rejection must precede reasoning spend"
341 );
342 assert_eq!(fixture.parent_calls.load(Ordering::SeqCst), 0);
343 assert_eq!(fixture.target_calls.load(Ordering::SeqCst), 0);
344 }
345
346 #[tokio::test]
347 #[allow(clippy::await_holding_lock)]
348 async fn exact_fleet_changed_provider_endpoint_is_rejected_before_dispatch() {
349 let _retry = tests::workflow_test_retry_guard();
350 let _env = crate::test_support::lock_test_env();
351 let root = tempfile::tempdir().unwrap();
352 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
353 let mut fixture = RouteFixture::new(root.path()).await;
354 let operation = ExactFleetWorkflow::capture(
355 &exact_document("off"),
356 QualifiedFleetId {
357 name: "frozen-audit".into(),
358 origin: "workspace".into(),
359 },
360 "2026-09-13T00:00:00Z",
361 Some(&fixture.config),
362 &[],
363 )
364 .expect("capture exact route");
365 let mut changed = fixture.config.clone();
366 changed
367 .set_provider_base_url_override(
368 &changed.test_identity_for_kind(ProviderKind::Openrouter),
369 Some(fixture.runtime.client.base_url().to_string()),
370 )
371 .unwrap();
372 fixture.runtime.api_config = Some(Arc::new(changed));
373 let error = run_script(
374 "return await task({description:'read-only check', profile:'auditor', writeAuthority:'read_only'});",
375 fixture.driver(WorkflowFleetBinding::Exact(Arc::new(operation))),
376 )
377 .await
378 .expect_err("changed endpoint cannot inherit the frozen receipt");
379 assert!(error.contains("exact Fleet route changed"), "{error}");
380 assert_eq!(fixture.parent_calls.load(Ordering::SeqCst), 0);
381 assert_eq!(fixture.target_calls.load(Ordering::SeqCst), 0);
382 }
383
384 #[tokio::test]
385 #[allow(clippy::await_holding_lock)]
386 async fn native_exact_fleet_builder_keeps_the_plan_read_only_ceiling() {
387 let _retry = tests::workflow_test_retry_guard();
388 let _env = crate::test_support::lock_test_env();
389 let root = tempfile::tempdir().unwrap();
390 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
391 let fixture = RouteFixture::new(root.path()).await;
392 let document = FleetDocument::parse(&format!(
393 "name = 'frozen-builder'\nschema = 'exact'\n[[members]]\n\
394 id = 'builder-one'\nrole = 'builder'\nprovider = 'openrouter'\n\
395 model = '{TARGET_MODEL}'\nreasoning = 'off'\n"
396 ))
397 .unwrap();
398 let operation = ExactFleetWorkflow::capture(
399 &document,
400 QualifiedFleetId {
401 name: "frozen-builder".into(),
402 origin: "workspace".into(),
403 },
404 "2026-09-13T00:00:00Z",
405 Some(&fixture.config),
406 &[],
407 )
408 .expect("capture exact builder");
409 let source =
410 native_script(json!({"prompt": "inspect without changing files", "role": "builder"}));
411 run_script(
412 &source,
413 fixture.driver(WorkflowFleetBinding::Exact(Arc::new(operation))),
414 )
415 .await
416 .expect("a native plan may narrow an exact builder");
417 fixture.assert_target_request();
418 let manager = fixture.runtime.manager.read().await;
419 let records = manager.list_worker_records();
420 assert_eq!(records.len(), 1);
421 let profile = &records[0].spec.runtime_profile;
422 assert!(
423 !profile.permissions.write,
424 "the authored read_only mode must remain executable policy"
425 );
426 assert_eq!(profile.shell, crate::worker_profile::ShellPolicy::ReadOnly);
427 assert_eq!(profile.tools, crate::worker_profile::ToolScope::Inherit);
428 assert_eq!(
429 records[0].spec.child_route.as_ref().unwrap().provider_id,
430 "openrouter"
431 );
432 }
433
434 #[tokio::test]
435 #[allow(clippy::await_holding_lock)]
436 async fn exact_fleet_task_cannot_widen_a_read_only_role_before_router_or_provider() {
437 let _retry = tests::workflow_test_retry_guard();
438 let _env = crate::test_support::lock_test_env();
439 let root = tempfile::tempdir().unwrap();
440 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", root.path().join("state"));
441 let fixture = RouteFixture::new(root.path()).await;
442 let router = StaticFleetRouter::new(r#"{"reasoning":"high"}"#);
443 let operation = ExactFleetWorkflow::for_tests(
444 &exact_document("auto"),
445 QualifiedFleetId {
446 name: "frozen-audit".into(),
447 origin: "workspace".into(),
448 },
449 Some(router.clone()),
450 );
451 let error = run_script(
452 "return await task({description:'attempt widening', profile:'auditor', writeAuthority:'workspace_write', writeRoots:['src']});",
453 fixture.driver(WorkflowFleetBinding::Exact(Arc::new(operation))),
454 ).await.expect_err("task cannot widen the Runtime role");
455 assert!(error.contains("cannot request write authority"), "{error}");
456 assert!(router.seen.lock().unwrap().is_empty());
457 assert_eq!(fixture.parent_calls.load(Ordering::SeqCst), 0);
458 assert_eq!(fixture.target_calls.load(Ordering::SeqCst), 0);
459 }
460
460 lines RUST