返回 CodeWhale
persona_receipt.rs
根目录 / crates / tui / src / tools / subagent / tests / persona_receipt.rs
1 //! Actual child loop, selected Native templates and an approved local route replacement.
2 use super::*;
3 use crate::extension_host::TestManagerGuard;
4 use crate::extension_host::composition_scope::NativePresetRef;
5 use crate::extension_host::protocol::EntryRef;
6 use crate::extension_host::tests::{FixturePlugins, node_for_tests};
7 use crate::plugins::activation::{PluginActivationCapability, TestPolicyGuard};
8
9 #[cfg(test)]
10 fn preset(fixture: &FixturePlugins, tag: &str) -> NativePresetRef {
11 let plugins = fixture.registry();
12 let (sources, problems) = crate::plugins::runtime::active_component_sources(
13 &plugins,
14 PluginActivationCapability::Native,
15 );
16 assert!(problems.is_empty(), "{problems:?}");
17 let source = sources
18 .into_iter()
19 .find(|source| source.path.ends_with(format!("{tag}.mjs")))
20 .unwrap();
21 NativePresetRef {
22 plugin_id: source.authority.plugin_id.to_string(),
23 content_hash: source.authority.content_hash,
24 entry: EntryRef {
25 path: source.path.to_string_lossy().into(),
26 sha256: crate::hashing::sha256_hex(std::fs::read(source.path).unwrap()),
27 },
28 }
29 }
30
31 #[tokio::test(flavor = "current_thread")]
32 async fn actual_child_persona_selection_and_approved_route_replace_use_current_model_and_cwd() {
33 let _home = crate::test_support::SealedHome::new();
34 let _policy = TestPolicyGuard::extension_host(true);
35 let Some(node) = node_for_tests("actual_child_persona_selection_and_route_replace") else {
36 return;
37 };
38 let fixture = FixturePlugins::new(&["child-persona"]).await;
39 let manager = fixture.manager(node);
40 let _manager = TestManagerGuard::install(Arc::clone(&manager));
41 let original = fixture.registry();
42 let a = preset(&fixture, "a");
43 let b = preset(&fixture, "b");
44 let parent = manager.attach(Arc::new(original.with_native_preset(a.clone()).unwrap()));
45 parent.reconcile().await.unwrap();
46 assert_eq!(
47 parent.prompt_sections().await.unwrap()[0].text,
48 "Persona A model={{model}} cwd={{cwd}}"
49 );
50
51 // Reuse the existing actual HTTP chat recorder and approved-replacement
52 // config authority. No prompt store, fake Engine or alternate renderer.
53 let (backup, backup_calls, bodies, _fixture_config) =
54 delayed_chat_client(Duration::ZERO, "persona done").await;
55 let (pin_url, pin_calls) = refusing_chat_server().await;
56 let config_path = fixture.workspace().join("persona-route.toml");
57 write_replacement_config(
58 &config_path,
59 &pin_url,
60 backup.base_url(),
61 r#"
62 [subagents.roles.reviewer]
63 model = "PinRoute/fixture-pin-model"
64 replacements = ["BackupRoute/fixture-backup-model"]
65 "#,
66 )
67 .await;
68 let mut config = crate::config::Config::load(Some(config_path), None).unwrap();
69 config.set_feature("extension_host", true).unwrap();
70 let mut runtime = stub_runtime().with_api_config(config.clone());
71 runtime.client = CodewhaleClient::new(&config).unwrap();
72 runtime.context = ToolContext::new(fixture.workspace())
73 .with_features(config.features())
74 .with_plugin_registry(parent.plugin_view());
75 runtime.manager = new_shared_subagent_manager(fixture.workspace().to_path_buf(), 2);
76 let request =
77 parse_spawn_request(&json!({"type":"reviewer","prompt":"Report your selected persona."}))
78 .unwrap();
79 let (_, source, _) = bind_spawn_model_route(&mut runtime, &request, None, true, true)
80 .await
81 .unwrap();
82 assert_eq!(source, SpawnRouteSource::RolePin);
83 assert_eq!(runtime.model, "fixture-pin-model");
84 assert_eq!(runtime.route_replacements.len(), 1);
85
86 // The parent keeps A alive while B executes under the same admitted owner.
87 // Then a second child selects A; no sibling or previous-child template leaks.
88 for (tag, selected) in [("B", b), ("A", a)] {
89 let mut assignment = request.assignment.clone();
90 assignment.native_preset = Some(selected);
91 let result = tokio::time::timeout(Duration::from_secs(15), async {
92 let route = mint_child_route_receipt(
93 &RequestedChildRoute {
94 requested_type: request.agent_type.as_str().to_owned(),
95 requested_profile: request.profile.clone(),
96 requested_reasoning: subagent_thinking_label(request.thinking).to_owned(),
97 },
98 &request,
99 None,
100 &runtime,
101 runtime.model.clone(),
102 source.as_str(),
103 None,
104 )
105 .unwrap();
106 let started = runtime
107 .manager
108 .write()
109 .await
110 .spawn_background_with_assignment_options(
111 Arc::clone(&runtime.manager),
112 runtime.clone(),
113 request.agent_type.clone(),
114 "Report your selected persona.".into(),
115 assignment,
116 Some(Vec::new()),
117 SubAgentSpawnOptions {
118 name: Some(format!("persona_child_{tag}")),
119 model: Some(runtime.model.clone()),
120 model_route: Some(ModelRoute::Fixed(runtime.model.clone())),
121 child_route: Some(route),
122 max_steps: Some(3),
123 ..Default::default()
124 },
125 None,
126 )
127 .expect("actual persona child admission");
128 assert_eq!(started.workspace.as_deref(), Some(fixture.workspace()));
129 assert_eq!(
130 started.child_route.as_ref().unwrap().provider_id,
131 "PinRoute"
132 );
133 loop {
134 let result = runtime
135 .manager
136 .read()
137 .await
138 .get_result(&started.agent_id)
139 .expect("admitted persona child remains registered");
140 if result.status != SubAgentStatus::Running {
141 break result;
142 }
143 tokio::time::sleep(Duration::from_millis(10)).await;
144 }
145 })
146 .await
147 .expect("local child settles");
148 assert_eq!(
149 result.status,
150 SubAgentStatus::Completed,
151 "{:?}",
152 result.status
153 );
154 assert_eq!(result.result.as_deref(), Some("persona done"));
155 let expected = format!(
156 "Persona {tag} model=fixture-backup-model cwd={}",
157 fixture.workspace().display()
158 );
159 let body = bodies.lock().unwrap().last().unwrap().clone();
160 assert_eq!(body["model"], "fixture-backup-model");
161 assert!(
162 body.to_string().contains(&expected),
163 "the actual replacement request must include the new captured model: {body}"
164 );
165 let other = if tag == "B" { "A" } else { "B" };
166 assert!(
167 !body
168 .to_string()
169 .contains(&format!("Persona {other} model=")),
170 "an owner union must not cross selected entries"
171 );
172 let checkpoint = result
173 .checkpoint
174 .as_ref()
175 .expect("actual child transcript checkpoint");
176 assert!(
177 !checkpoint.messages.iter().any(|message| {
178 crate::runtime_handoff::is_internal_runtime_handoff(message)
179 && message.content.iter().any(|block| {
180 matches!(block, ContentBlock::Text { text, .. }
181 if text.contains("<codewhale:subagent.done>"))
182 })
183 }),
184 "a persona child's checkpoint must not consume its root parent's sibling completions"
185 );
186 let latest = checkpoint
187 .messages
188 .iter()
189 .rev()
190 .find_map(crate::runtime_handoff::extension_prompt_contributions_display)
191 .unwrap();
192 assert!(
193 latest.contains(&expected),
194 "latest full snapshot follows the installed replacement: {latest}"
195 );
196 assert!(!latest.contains("model=fixture-pin-model"));
197 }
198 assert_eq!(
199 pin_calls.load(Ordering::SeqCst),
200 2,
201 "each explicit child tries the exact pin once"
202 );
203 assert_eq!(
204 backup_calls.load(Ordering::SeqCst),
205 2,
206 "each approved replacement receives one request"
207 );
208 assert_eq!(
209 parent.prompt_sections().await.unwrap()[0].text,
210 "Persona A model={{model}} cwd={{cwd}}",
211 "Core-captured turn facts never mutate the registered template"
212 );
213 manager.shutdown().await;
214 }
215
215 lines RUST