返回 CodeWhale
tests.rs
1 //! Real installed Native composition, committed host bundle and local MCP peer.
2 //! No provider traffic; the Node-only suite uses a fake Core separately.
3 use super::*;
4 use crate::extension_host::composition_scope::NativePresetRef;
5 use crate::extension_host::protocol::RegisterSpecWire;
6 use crate::extension_host::registry::OwnerRegistry;
7 use crate::extension_host::tests::{FixturePlugins, fake_authority, node_for_tests};
8 use crate::extension_host::{ExtensionHostManager, HostAttachment, TestManagerGuard};
9 use crate::mcp::{McpBackend, McpPool};
10 use crate::plugins::activation::TestPolicyGuard;
11 use serde_json::json;
12 use std::path::Path;
13 use std::time::Duration;
14
15 fn selected(fixture: &FixturePlugins, tag: &str) -> Arc<PluginRegistry> {
16 let plugins = fixture.registry();
17 let (sources, problems) = crate::plugins::runtime::active_component_sources(
18 &plugins,
19 PluginActivationCapability::Native,
20 );
21 assert!(problems.is_empty(), "{problems:?}");
22 let source = sources
23 .into_iter()
24 .find(|source| source.path.ends_with(format!("native/{tag}.mjs")))
25 .unwrap();
26 let bytes = std::fs::read(&source.path).unwrap();
27 Arc::new(
28 plugins
29 .with_native_preset(NativePresetRef {
30 plugin_id: source.authority.plugin_id.to_string(),
31 content_hash: source.authority.content_hash,
32 entry: EntryRef {
33 path: source.path.to_string_lossy().into(),
34 sha256: crate::hashing::sha256_hex(&bytes),
35 },
36 })
37 .unwrap(),
38 )
39 }
40 /// Surface the existing owner diagnostic before catalog assertions discard it.
41 /// This does not admit or repair a definition; final consumer assertions remain.
42 fn assert_reviewed_owner_active(manager: &ExtensionHostManager, caller: &HostAttachment) {
43 let plugins = caller.plugin_view();
44 let entries = plugins.selected_native_entries();
45 assert!(
46 !entries.is_empty(),
47 "fixture must keep its exact reviewed selection"
48 );
49 for selected in entries {
50 let report = manager.owner_report(&selected.plugin_id);
51 let state = report.as_ref().and_then(|report| report.state.as_ref());
52 let diagnostics = report.as_ref().map(|report| &report.diagnostics);
53 assert_eq!(
54 state,
55 Some(&super::super::registry::OwnerState::Active),
56 "selected entry {:?}: existing owner diagnostics: {diagnostics:?}",
57 selected.entry
58 );
59 let scope_state = {
60 let registry = manager.shared.registry.lock().expect("registry lock");
61 registry
62 .owner(&selected.plugin_id)
63 .map(|owner| registry.check_scope(&owner.owner, Some(&selected.entry), true))
64 };
65 assert_eq!(
66 scope_state,
67 Some(Ok(())),
68 "selected entry {:?}: existing owner diagnostics: {diagnostics:?}",
69 selected.entry
70 );
71 }
72 }
73 fn pool(fixture: &FixturePlugins, caller: &HostAttachment, backend: McpBackend) -> McpPool {
74 McpPool::from_config_path_with_workspace_and_plugins(
75 &fixture.root.join("mcp.json"),
76 fixture.workspace(),
77 caller.plugin_view(),
78 )
79 .unwrap()
80 .with_backend(backend)
81 }
82 async fn connect_echo(pool: &mut McpPool) -> String {
83 let errors = pool.connect_all().await;
84 assert!(errors.is_empty(), "{errors:?}");
85 let tools = pool.all_tools();
86 assert_eq!(tools.len(), 1, "{tools:?}");
87 tools[0].0.clone()
88 }
89
90 #[tokio::test(flavor = "current_thread")]
91 async fn mixed_native_graphs_use_one_core_catalog_and_selected_child_pool_on_both_backends() {
92 let _home = crate::test_support::SealedHome::new();
93 let _policy = TestPolicyGuard::extension_host(true);
94 let Some(node) = node_for_tests("mixed_native_graphs_use_one_core_catalog") else {
95 return;
96 };
97 let fixture = FixturePlugins::new(&["raw-dsh-mcp"]).await;
98 let manager = fixture.manager(node);
99 let _manager = TestManagerGuard::install(Arc::clone(&manager));
100 let a = manager.attach(selected(&fixture, "a"));
101 let b = manager.attach(selected(&fixture, "b"));
102 a.reconcile().await.unwrap();
103 assert_reviewed_owner_active(&manager, &a);
104 let a_view = a.plugin_view();
105 let b_view = b.plugin_view();
106 let a_defs = for_plugins(&a_view).unwrap();
107 let b_defs = for_plugins(&b_view).unwrap();
108 assert_eq!(a_defs.len(), 1);
109 assert_eq!(b_defs.len(), 1);
110 assert_ne!(
111 a_defs[0].3.catalog_identity(),
112 b_defs[0].3.catalog_identity()
113 );
114 // Same owner and public names, distinct exact selected entry handles.
115 assert_eq!(a_defs[0].0, b_defs[0].0);
116 // Path::ends_with is separator-agnostic. A string suffix of "native/a.mjs"
117 // fails on Windows because scope paths keep backslashes (and may carry a
118 // verbatim prefix from canonicalize).
119 assert!(
120 Path::new(&a_defs[0].3.registration.scope.path).ends_with(Path::new("native/a.mjs")),
121 "{}",
122 a_defs[0].3.registration.scope.path
123 );
124 assert!(
125 Path::new(&b_defs[0].3.registration.scope.path).ends_with(Path::new("native/b.mjs")),
126 "{}",
127 b_defs[0].3.registration.scope.path
128 );
129 for backend in [McpBackend::Rust, McpBackend::Host] {
130 let mut parent = pool(&fixture, &a, backend);
131 let name = connect_echo(&mut parent).await;
132 let result = parent.call_tool(&name, json!({"value":1})).await.unwrap();
133 assert_eq!(result["content"][0]["text"], "a:{\"value\":1}");
134 parent.validate_native_caller(Some(&a_view)).unwrap();
135 assert!(parent.validate_native_caller(Some(&b_view)).is_err());
136 let mut child = parent.fork_for_plugins(Arc::clone(&b_view)).unwrap();
137 let child_name = connect_echo(&mut child).await;
138 assert_eq!(child_name, name);
139 child.validate_native_caller(Some(&b_view)).unwrap();
140 let result = child
141 .call_tool(&child_name, json!({"child":true}))
142 .await
143 .unwrap();
144 assert_eq!(result["content"][0]["text"], "b:{\"child\":true}");
145 }
146 let mut tools = crate::tools::registry::ToolRegistryBuilder::new().build(
147 crate::tools::ToolContext::new(fixture.workspace()).with_plugin_registry(a.plugin_view()),
148 );
149 assert_eq!(a.install_tools(&mut tools), vec!["mixed_echo"]);
150 let catalog = crate::skills::discover_for_workspace_and_dir_with_mode_and_plugins(
151 fixture.workspace(),
152 &fixture.workspace().join("empty-skills"),
153 crate::skills::SkillDiscoveryMode::CodeWhaleOnly,
154 Some(&a.plugin_view()),
155 );
156 assert!(catalog.get("raw-dsh-mcp:mixed-check").is_some());
157 assert!(
158 a.prompt_sections()
159 .await
160 .unwrap()
161 .iter()
162 .any(|section| section.text == "Selected a")
163 );
164 manager.shutdown().await;
165 }
166
167 #[tokio::test(flavor = "current_thread")]
168 async fn caller_revision_withdraws_pending_mcp_without_revoking_sibling_definition() {
169 let _home = crate::test_support::SealedHome::new();
170 let _policy = TestPolicyGuard::extension_host(true);
171 let Some(node) = node_for_tests("caller_revision_withdraws_pending_mcp") else {
172 return;
173 };
174 let fixture = FixturePlugins::new(&["raw-dsh-mcp"]).await;
175 let manager = fixture.manager(node);
176 let _manager = TestManagerGuard::install(Arc::clone(&manager));
177 let a = manager.attach(selected(&fixture, "a"));
178 let sibling = manager.attach(selected(&fixture, "a"));
179 a.reconcile().await.unwrap();
180 assert_reviewed_owner_active(&manager, &a);
181 let old = for_plugins(&a.plugin_view()).unwrap().remove(0).3;
182 let retained = for_plugins(&sibling.plugin_view()).unwrap().remove(0).3;
183 let mut pending = pool(&fixture, &a, McpBackend::Host);
184 let name = connect_echo(&mut pending).await;
185 let operation = pending.call_tool(&name, json!({"hold":true}));
186 tokio::pin!(operation);
187 tokio::select! {
188 biased;
189 result = &mut operation => panic!("held peer must remain pending: {result:?}"),
190 _ = tokio::time::sleep(Duration::from_millis(30)) => {},
191 }
192 // Existing caller authority is the cancellation source, before reconcile.
193 a.set_plugins(selected(&fixture, "b"));
194 assert!(old.validate().is_err());
195 retained.validate().unwrap();
196 tokio::time::timeout(Duration::from_secs(1), old.withdrawn())
197 .await
198 .unwrap();
199 let error = tokio::time::timeout(Duration::from_secs(2), operation)
200 .await
201 .unwrap()
202 .unwrap_err();
203 assert!(error.to_string().contains("not replayed"), "{error:#}");
204 a.reconcile().await.unwrap();
205 assert_reviewed_owner_active(&manager, &a);
206 let mut live = pool(&fixture, &sibling, McpBackend::Host);
207 let name = connect_echo(&mut live).await;
208 assert_eq!(
209 live.call_tool(&name, json!({})).await.unwrap()["content"][0]["text"],
210 "a:{}"
211 );
212 manager.shutdown().await;
213 }
214
215 #[tokio::test(flavor = "current_thread")]
216 async fn retained_root_pool_rebinds_current_revision_and_refuses_foreign_attachment() {
217 let _home = crate::test_support::SealedHome::new();
218 let _policy = TestPolicyGuard::extension_host(true);
219 let Some(node) = node_for_tests("retained_root_pool_rebinds_current_revision") else {
220 return;
221 };
222 let fixture = FixturePlugins::new(&["raw-dsh-mcp"]).await;
223 let manager = fixture.manager(node);
224 let _manager = TestManagerGuard::install(Arc::clone(&manager));
225 let caller = manager.attach(selected(&fixture, "a"));
226 let foreign = manager.attach(selected(&fixture, "a"));
227 caller.reconcile().await.unwrap();
228 assert_reviewed_owner_active(&manager, &caller);
229 let mut retained = pool(&fixture, &caller, McpBackend::Host);
230 let name = connect_echo(&mut retained).await;
231 assert!(retained.bind_caller_plugins(foreign.plugin_view()).is_err());
232 assert_eq!(
233 retained.call_tool(&name, json!({})).await.unwrap()["content"][0]["text"],
234 "a:{}"
235 );
236 caller.set_plugins(selected(&fixture, "b"));
237 caller.reconcile().await.unwrap();
238 assert_reviewed_owner_active(&manager, &caller);
239 retained.bind_caller_plugins(caller.plugin_view()).unwrap();
240 assert_eq!(connect_echo(&mut retained).await, name);
241 assert_eq!(
242 retained.call_tool(&name, json!({})).await.unwrap()["content"][0]["text"],
243 "b:{}"
244 );
245 manager.shutdown().await;
246 }
247
248 #[tokio::test(flavor = "current_thread")]
249 async fn native_mcp_receipt_refuses_persisted_disable_and_source_tamper() {
250 let _home = crate::test_support::SealedHome::new();
251 let _policy = TestPolicyGuard::extension_host(true);
252 let Some(node) = node_for_tests("native_mcp_receipt_refuses_persisted_disable") else {
253 return;
254 };
255 let fixture = FixturePlugins::new(&["raw-dsh-mcp"]).await;
256 let manager = fixture.manager(node);
257 let _manager = TestManagerGuard::install(Arc::clone(&manager));
258 let caller = manager.attach(selected(&fixture, "a"));
259 caller.reconcile().await.unwrap();
260 assert_reviewed_owner_active(&manager, &caller);
261 let mut active = pool(&fixture, &caller, McpBackend::Host);
262 let name = connect_echo(&mut active).await;
263 fixture.disable("raw-dsh-mcp");
264 assert!(active.call_tool(&name, json!({})).await.is_err());
265 assert!(for_plugins(&caller.plugin_view()).unwrap().is_empty());
266 manager.shutdown().await;
267 // A second independently reviewed fixture covers changed source bytes.
268 let fixture = FixturePlugins::new(&["raw-dsh-mcp"]).await;
269 let manager = fixture.manager(node_for_tests("native_mcp_source_tamper").unwrap());
270 let _manager = TestManagerGuard::install(Arc::clone(&manager));
271 let caller = manager.attach(selected(&fixture, "a"));
272 caller.reconcile().await.unwrap();
273 assert_reviewed_owner_active(&manager, &caller);
274 let mut active = pool(&fixture, &caller, McpBackend::Host);
275 let name = connect_echo(&mut active).await;
276 let plugin = fixture.registry().get("raw-dsh-mcp").unwrap().clone();
277 std::fs::write(
278 plugin.canonical_root.join("source/peer.mjs"),
279 "export const tampered=true",
280 )
281 .unwrap();
282 assert!(active.call_tool(&name, json!({})).await.is_err());
283 manager.shutdown().await;
284 }
285
286 fn params(owner: &OwnerRef, scope: EntryRef, name: String) -> RegisterParams {
287 RegisterParams {
288 owner: owner.clone(),
289 scope: Some(scope),
290 kind: RegisterKind::McpServer,
291 spec: RegisterSpecWire {
292 name,
293 description: "{}".into(),
294 input_schema: None,
295 argument_hint: None,
296 },
297 }
298 }
299 #[test]
300 fn mcp_definition_bounds_are_owner_wide_and_exact_unregister_cancels_only_its_handle() {
301 let mut registry = OwnerRegistry::default();
302 let owner = registry
303 .begin_owner(
304 HostTier::Plugin,
305 "fixture",
306 "fixture",
307 Some(fake_authority("fixture")),
308 "build",
309 )
310 .unwrap();
311 let a = EntryRef {
312 path: "/reviewed/a.mjs".into(),
313 sha256: "a".repeat(64),
314 };
315 let b = EntryRef {
316 path: "/reviewed/b.mjs".into(),
317 sha256: "b".repeat(64),
318 };
319 registry.begin_scope(&owner, a.clone()).unwrap();
320 registry.begin_scope(&owner, b.clone()).unwrap();
321 let config: McpServerConfig = serde_json::from_value(json!({"command":"node"})).unwrap();
322 for i in 0..MAX_PER_OWNER {
323 registry
324 .register_mcp(
325 &params(
326 &owner,
327 if i % 2 == 0 { a.clone() } else { b.clone() },
328 format!("s{i}"),
329 ),
330 config.clone(),
331 1,
332 )
333 .unwrap();
334 }
335 assert!(
336 registry
337 .register_mcp(&params(&owner, b.clone(), "over".into()), config, 1)
338 .is_err()
339 );
340 registry.mark_scope_active(&owner, &a);
341 registry.mark_scope_active(&owner, &b);
342 registry.mark_active(&owner);
343 let definitions = registry.live_mcp();
344 assert_eq!(definitions.len(), MAX_PER_OWNER);
345 let first = definitions[0].clone();
346 let second = definitions[1].clone();
347 registry.unregister(&owner, first.handle);
348 assert!(first.cancel.is_cancelled());
349 assert!(!second.cancel.is_cancelled());
350 registry.revoke_scope(&owner, &second.scope);
351 assert!(second.cancel.is_cancelled());
352 }
353
354 #[path = "remote_tests.rs"]
355 mod remote_tests;
356
356 lines RUST