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