Skip to main content

camel_component_wasm/
host_functions.rs

1use serde_json::Value;
2use wasmtime::component::{Access, Accessor, Linker};
3
4use crate::runtime::WasmHostState;
5
6// Host marker trait must stay on WasmHostState (not HasSelf<WasmHostState>):
7// the add_to_linker bound `for<'a> D::Data<'a>: Host` resolves through the
8// `impl<T: Host> Host for &mut T` blanket impl, which requires the concrete
9// type inside &mut (_) to implement Host.
10// Sync `Host` trait impl: under the per-function import config the sync host
11// functions (get-property, set-property, host-store, host-load) live in
12// `HostWithStore` (taking an `Access`); `Host` remains an empty marker trait
13// required by the linker's where-clause.
14impl crate::bindings::camel::plugin::host::Host for WasmHostState {}
15
16impl crate::bindings::camel::plugin::host::HostWithStore<WasmHostState>
17    for wasmtime::component::HasSelf<WasmHostState>
18{
19    async fn camel_call(
20        store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
21        uri: String,
22        payload: String,
23    ) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
24        WasmHostState::camel_call_impl(store, uri, payload).await
25    }
26
27    async fn camel_poll(
28        store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
29        uri: String,
30        timeout_ms: u32,
31    ) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
32        WasmHostState::camel_poll_impl(store, uri, timeout_ms).await
33    }
34
35    fn get_property(
36        mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
37        key: String,
38    ) -> Option<String> {
39        store.get().get_property_impl(key)
40    }
41
42    fn set_property(
43        mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
44        key: String,
45        value: String,
46    ) {
47        store.get().set_property_impl(key, value)
48    }
49
50    fn host_store(
51        mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
52        key: String,
53        value: String,
54    ) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
55        WasmHostState::host_store_impl(store.get(), key, value)
56    }
57
58    fn host_load(
59        mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
60        key: String,
61    ) -> Result<Option<String>, crate::bindings::camel::plugin::types::WasmError> {
62        WasmHostState::host_load_impl(store.get(), key)
63    }
64}
65
66pub fn add_to_linker(linker: &mut Linker<WasmHostState>) -> Result<(), wasmtime::Error> {
67    // D = HasSelf<WasmHostState> uses wasmtime's built-in HasData impl
68    // (Data<'a> = &'a mut WasmHostState). The HostWithStore and Host trait
69    // impls target HasSelf<WasmHostState> and WasmHostState respectively.
70    crate::bindings::camel::plugin::host::add_to_linker::<
71        WasmHostState,
72        wasmtime::component::HasSelf<WasmHostState>,
73    >(linker, |state| state)
74}
75
76// Async helpers: the actual producer / poller logic. Pulled out of the trait
77// impls so they can also be invoked from the per-binding macro arms and from
78// tests (where we don't have an Accessor in scope).
79
80async fn run_async_call(
81    registry: std::sync::Arc<std::sync::Mutex<camel_core::Registry>>,
82    uri: String,
83    payload: String,
84) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
85    use tower::ServiceExt;
86    let scheme = uri.split(':').next().unwrap_or("").to_string();
87    if scheme.is_empty() {
88        return Err(
89            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
90                "invalid URI (no scheme): {}",
91                uri
92            )),
93        );
94    }
95
96    let component = {
97        let guard = registry.lock().map_err(|e| {
98            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
99                "registry lock poisoned: {}",
100                e
101            ))
102        })?;
103        guard.get(&scheme).ok_or_else(|| {
104            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
105                "component not found for scheme: {}",
106                scheme
107            ))
108        })?
109    };
110
111    let endpoint = component
112        .create_endpoint(&uri, &camel_component_api::NoOpComponentContext)
113        .map_err(|e| {
114            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
115                "create_endpoint failed: {}",
116                e
117            ))
118        })?;
119
120    let rt: std::sync::Arc<dyn camel_component_api::RuntimeObservability> =
121        std::sync::Arc::new(camel_component_api::NoOpComponentContext);
122    let producer = endpoint
123        .create_producer(rt, &camel_api::ProducerContext::new())
124        .map_err(|e| {
125            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
126                "create_producer failed: {}",
127                e
128            ))
129        })?;
130
131    let exchange =
132        camel_api::Exchange::new(camel_api::Message::new(camel_api::Body::Text(payload)));
133
134    let result = producer.oneshot(exchange).await.map_err(|e| {
135        crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
136            "endpoint call failed: {}",
137            e
138        ))
139    })?;
140
141    let body_str = match &result.output {
142        Some(msg) => match &msg.body {
143            camel_api::Body::Text(s) => s.clone(),
144            camel_api::Body::Json(v) => v.to_string(),
145            camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
146            camel_api::Body::Xml(s) => s.clone(),
147            camel_api::Body::Empty => String::new(),
148            camel_api::Body::Stream(_) => "<stream>".to_string(),
149        },
150        None => match &result.input.body {
151            camel_api::Body::Text(s) => s.clone(),
152            camel_api::Body::Json(v) => v.to_string(),
153            camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
154            camel_api::Body::Xml(s) => s.clone(),
155            camel_api::Body::Empty => String::new(),
156            camel_api::Body::Stream(_) => "<stream>".to_string(),
157        },
158    };
159
160    Ok(body_str)
161}
162
163async fn run_async_poll(
164    registry: std::sync::Arc<std::sync::Mutex<camel_core::Registry>>,
165    uri: String,
166    timeout_ms: u32,
167) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
168    let scheme = uri.split(':').next().unwrap_or("").to_string();
169    if scheme.is_empty() {
170        return Err(
171            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
172                "invalid URI (no scheme): {}",
173                uri
174            )),
175        );
176    }
177
178    let component = {
179        let guard = registry.lock().map_err(|e| {
180            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
181                "registry lock poisoned: {}",
182                e
183            ))
184        })?;
185        guard.get(&scheme).ok_or_else(|| {
186            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
187                "component not found for scheme: {}",
188                scheme
189            ))
190        })?
191    };
192
193    let endpoint = component
194        .create_endpoint(&uri, &camel_component_api::NoOpComponentContext)
195        .map_err(|e| {
196            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
197                "create_endpoint failed: {}",
198                e
199            ))
200        })?;
201
202    let mut poller = endpoint.polling_consumer().ok_or_else(|| {
203        crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
204            "camel_poll requires a component that supports polling consumers (scheme: {})",
205            scheme
206        ))
207    })?;
208
209    let exchange = poller
210        .receive(std::time::Duration::from_millis(timeout_ms as u64))
211        .await
212        .map_err(|e| {
213            crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
214                "poll failed: {}",
215                e
216            ))
217        })?;
218
219    let body_str = match exchange {
220        Some(ex) => {
221            let bytes = ex
222                .input
223                .body
224                .into_bytes(10 * 1024 * 1024)
225                .await
226                .map_err(|e| {
227                    crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
228                        "body read failed: {}",
229                        e
230                    ))
231                })?;
232            String::from_utf8_lossy(&bytes).to_string()
233        }
234        None => {
235            return Err(
236                crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
237                    "no message received within {}ms timeout",
238                    timeout_ms
239                )),
240            );
241        }
242    };
243
244    Ok(body_str)
245}
246
247// Macro that implements the same `HostWithStore` trait for the bean and
248// security-policy bindings modules. Those bindings define an equivalent
249// `host` interface (separate types but identical shape), so the host impl is
250// duplicated against each module's generated `HostWithStore` trait.
251macro_rules! impl_host_for_binding {
252    ($bindings_mod:ident) => {
253        // Host marker stays on WasmHostState for the `impl<T: Host> Host for &mut T`
254        // blanket bound on D::Data<'a> inside add_to_linker.
255        impl crate::$bindings_mod::camel::plugin::host::Host for WasmHostState {}
256
257        // HostWithStore targets HasSelf<WasmHostState> (wasmtime's built-in HasData)
258        // instead of a hand-rolled HasData impl on WasmHostState.
259        impl crate::$bindings_mod::camel::plugin::host::HostWithStore<WasmHostState>
260            for wasmtime::component::HasSelf<WasmHostState>
261        {
262            async fn camel_call(
263                store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
264                uri: String,
265                payload: String,
266            ) -> Result<String, crate::$bindings_mod::camel::plugin::types::WasmError> {
267                WasmHostState::camel_call_impl(store, uri, payload)
268                    .await
269                    .map_err(map_plugin_wasm_error_to)
270            }
271
272            async fn camel_poll(
273                store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
274                uri: String,
275                timeout_ms: u32,
276            ) -> Result<String, crate::$bindings_mod::camel::plugin::types::WasmError> {
277                WasmHostState::camel_poll_impl(store, uri, timeout_ms)
278                    .await
279                    .map_err(map_plugin_wasm_error_to)
280            }
281
282            fn get_property(
283                mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
284                key: String,
285            ) -> Option<String> {
286                store.get().get_property_impl(key)
287            }
288
289            fn set_property(
290                mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
291                key: String,
292                value: String,
293            ) {
294                store.get().set_property_impl(key, value)
295            }
296
297            fn host_store(
298                mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
299                key: String,
300                value: String,
301            ) -> Result<(), crate::$bindings_mod::camel::plugin::types::WasmError> {
302                WasmHostState::host_store_impl(store.get(), key, value)
303                    .map_err(map_plugin_wasm_error_to)
304            }
305
306            fn host_load(
307                mut store: Access<'_, WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
308                key: String,
309            ) -> Result<Option<String>, crate::$bindings_mod::camel::plugin::types::WasmError> {
310                WasmHostState::host_load_impl(store.get(), key).map_err(map_plugin_wasm_error_to)
311            }
312        }
313    };
314}
315
316// Helper: re-map a canonical bindings::camel::plugin::types::WasmError to
317// the same shape produced by a different bindgen invocation (bean /
318// authorization-policy). All variants have identical structure, so this is
319// a straight field-for-field clone.
320fn map_plugin_wasm_error_to<E: FromWasmErrorVariant>(
321    e: crate::bindings::camel::plugin::types::WasmError,
322) -> E {
323    match e {
324        crate::bindings::camel::plugin::types::WasmError::ProcessorError(s) => {
325            E::from_processor_error(s)
326        }
327        crate::bindings::camel::plugin::types::WasmError::TypeConversion(s) => {
328            E::from_type_conversion(s)
329        }
330        crate::bindings::camel::plugin::types::WasmError::Io(s) => E::from_io(s),
331        crate::bindings::camel::plugin::types::WasmError::Timeout => E::from_timeout(),
332    }
333}
334
335trait FromWasmErrorVariant {
336    fn from_processor_error(s: String) -> Self;
337    fn from_type_conversion(s: String) -> Self;
338    fn from_io(s: String) -> Self;
339    fn from_timeout() -> Self;
340}
341
342impl FromWasmErrorVariant for crate::bindings::camel::plugin::types::WasmError {
343    fn from_processor_error(s: String) -> Self {
344        Self::ProcessorError(s)
345    }
346    fn from_type_conversion(s: String) -> Self {
347        Self::TypeConversion(s)
348    }
349    fn from_io(s: String) -> Self {
350        Self::Io(s)
351    }
352    fn from_timeout() -> Self {
353        Self::Timeout
354    }
355}
356
357impl FromWasmErrorVariant for crate::bean_bindings::camel::plugin::types::WasmError {
358    fn from_processor_error(s: String) -> Self {
359        Self::ProcessorError(s)
360    }
361    fn from_type_conversion(s: String) -> Self {
362        Self::TypeConversion(s)
363    }
364    fn from_io(s: String) -> Self {
365        Self::Io(s)
366    }
367    fn from_timeout() -> Self {
368        Self::Timeout
369    }
370}
371
372impl FromWasmErrorVariant for crate::security_policy_bindings::camel::plugin::types::WasmError {
373    fn from_processor_error(s: String) -> Self {
374        Self::ProcessorError(s)
375    }
376    fn from_type_conversion(s: String) -> Self {
377        Self::TypeConversion(s)
378    }
379    fn from_io(s: String) -> Self {
380        Self::Io(s)
381    }
382    fn from_timeout() -> Self {
383        Self::Timeout
384    }
385}
386
387pub fn add_bean_to_linker(linker: &mut Linker<WasmHostState>) -> Result<(), wasmtime::Error> {
388    crate::bean_bindings::camel::plugin::host::add_to_linker::<
389        WasmHostState,
390        wasmtime::component::HasSelf<WasmHostState>,
391    >(linker, |state| state)
392}
393
394impl_host_for_binding!(bean_bindings);
395
396pub fn add_security_policy_to_linker(
397    linker: &mut Linker<WasmHostState>,
398) -> Result<(), wasmtime::Error> {
399    crate::security_policy_bindings::camel::plugin::host::add_to_linker::<
400        WasmHostState,
401        wasmtime::component::HasSelf<WasmHostState>,
402    >(linker, |state| state)
403}
404
405impl_host_for_binding!(security_policy_bindings);
406
407// RAII recursion guard: increments `call_depth` on construction and
408// decrements on drop. The drop runs even if the future is cancelled,
409// which is the whole point: a manual inc/dec around an `.await` leaks
410// the increment forever if the future is dropped between the two sites.
411// Returns `None` from `new` if the depth was already > 0 (a nested
412// call is in flight) so the caller can return the recursion error.
413struct DepthGuard<'a> {
414    depth: &'a std::sync::atomic::AtomicUsize,
415}
416
417impl<'a> DepthGuard<'a> {
418    fn new(depth: &'a std::sync::atomic::AtomicUsize) -> Option<Self> {
419        let prev = depth.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
420        if prev > 0 {
421            depth.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
422            return None;
423        }
424        Some(DepthGuard { depth })
425    }
426}
427
428impl Drop for DepthGuard<'_> {
429    fn drop(&mut self) {
430        self.depth.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
431    }
432}
433
434// Inherent methods on `WasmHostState` that implement the host function
435// logic. The trait impls above are thin shims that call these. Splitting
436// the logic this way gives the test suite direct access to the same code
437// paths without needing an `Accessor<WasmHostState, HasSelf<WasmHostState>>` (which
438// can only be constructed inside a `run_concurrent` scope).
439impl WasmHostState {
440    pub(crate) async fn camel_call_impl(
441        store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
442        uri: String,
443        payload: String,
444    ) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
445        // Capability gate (H4): check scheme against per-world allowlist.
446        let scheme = uri.split(':').next().unwrap_or("").to_string();
447        let can_call = store.with(|mut view| view.get().capabilities.can_call(&scheme));
448        if !can_call {
449            return Err(
450                crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
451                    "camel_call denied: scheme '{}' not in capability allowlist",
452                    scheme
453                )),
454            );
455        }
456
457        // Snapshot the registry (cloned Arc — cheap) for the async work
458        // so we don't borrow `store` across the await.
459        let registry = store.with(|mut view| view.get().registry.clone());
460
461        // Recursion guard. The RAII `DepthGuard` decrements on drop, so
462        // even if the `run_async_call` future is cancelled mid-await the
463        // counter returns to 0 — otherwise every subsequent call would
464        // fail with "recursive wasm calls not supported".
465        //
466        // Clone the Arc<AtomicUsize> from store state — no unsafe needed.
467        // The guard borrows the Arc's inner AtomicUsize for the call scope.
468        let call_depth = store.with(|mut view| view.get().call_depth.clone());
469        let _depth_guard = match DepthGuard::new(call_depth.as_ref()) {
470            Some(g) => g,
471            None => {
472                return Err(
473                    crate::bindings::camel::plugin::types::WasmError::ProcessorError(
474                        "recursive wasm calls not supported".to_string(),
475                    ),
476                );
477            }
478        };
479
480        run_async_call(registry, uri, payload).await
481    }
482
483    pub(crate) async fn camel_poll_impl(
484        store: &Accessor<WasmHostState, wasmtime::component::HasSelf<WasmHostState>>,
485        uri: String,
486        timeout_ms: u32,
487    ) -> Result<String, crate::bindings::camel::plugin::types::WasmError> {
488        let scheme = uri.split(':').next().unwrap_or("").to_string();
489        let can_call = store.with(|mut view| view.get().capabilities.can_call(&scheme));
490        if !can_call {
491            return Err(
492                crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
493                    "camel_poll denied: scheme '{}' not in capability allowlist",
494                    scheme
495                )),
496            );
497        }
498
499        // Recursion guard (RAII — see camel_call_impl for rationale).
500        let registry = store.with(|mut view| view.get().registry.clone());
501        let call_depth = store.with(|mut view| view.get().call_depth.clone());
502        let _depth_guard = match DepthGuard::new(call_depth.as_ref()) {
503            Some(g) => g,
504            None => {
505                return Err(
506                    crate::bindings::camel::plugin::types::WasmError::ProcessorError(
507                        "recursive wasm calls not supported".to_string(),
508                    ),
509                );
510            }
511        };
512
513        run_async_poll(registry, uri, timeout_ms).await
514    }
515
516    pub(crate) fn get_property_impl(&self, key: String) -> Option<String> {
517        self.properties.get(&key).map(|v| match v {
518            Value::String(s) => s.clone(),
519            other => other.to_string(),
520        })
521    }
522
523    pub(crate) fn set_property_impl(&mut self, key: String, value: String) {
524        let parsed = serde_json::from_str::<Value>(&value).unwrap_or(Value::String(value));
525        self.properties.insert(key, parsed);
526    }
527
528    pub(crate) fn host_store_impl(
529        state: &mut WasmHostState,
530        key: String,
531        value: String,
532    ) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
533        if !state.capabilities.host_kv {
534            return Err(
535                crate::bindings::camel::plugin::types::WasmError::ProcessorError(
536                    "host_store denied: host_kv capability not granted".to_string(),
537                ),
538            );
539        }
540        state
541            .state_store
542            .store(&key, &value)
543            .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
544    }
545
546    pub(crate) fn host_load_impl(
547        state: &mut WasmHostState,
548        key: String,
549    ) -> Result<Option<String>, crate::bindings::camel::plugin::types::WasmError> {
550        if !state.capabilities.host_kv {
551            return Err(
552                crate::bindings::camel::plugin::types::WasmError::ProcessorError(
553                    "host_load denied: host_kv capability not granted".to_string(),
554                ),
555            );
556        }
557        state
558            .state_store
559            .load(&key)
560            .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
561    }
562}
563
564// Inherent (non-async) versions of the impl methods for the test suite.
565// The trait methods are async + store-bound, but the unit tests want to
566// exercise the gating logic synchronously against a plain `&mut
567// WasmHostState`. Both layers share the same recursion / capability /
568// host_kv gates.
569//
570// `dead_code` allow: the `*_gate` methods are referenced only from
571// `#[cfg(test)]` code in this same module, but the rustc dead-code lint
572// analyses test code separately and reports them as unused. The
573// #[allow(dead_code)] silences the false positive.
574#[allow(dead_code)]
575impl WasmHostState {
576    /// Same gating as `camel_call_impl` but synchronous and against a
577    /// plain `&mut self`. Returns the same error variant on gate failure.
578    /// Async work is NOT run — this is purely for unit-testing the gate
579    /// logic in isolation.
580    pub(crate) fn camel_call_gate(
581        &mut self,
582        uri: String,
583    ) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
584        let scheme = uri.split(':').next().unwrap_or("").to_string();
585        if !self.capabilities.can_call(&scheme) {
586            return Err(
587                crate::bindings::camel::plugin::types::WasmError::ProcessorError(format!(
588                    "camel_call denied: scheme '{}' not in capability allowlist",
589                    scheme
590                )),
591            );
592        }
593        if self.call_depth.load(std::sync::atomic::Ordering::Relaxed) > 0 {
594            return Err(
595                crate::bindings::camel::plugin::types::WasmError::ProcessorError(
596                    "recursive wasm calls not supported".to_string(),
597                ),
598            );
599        }
600        self.call_depth
601            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
602        Ok(())
603    }
604
605    pub(crate) fn release_call_depth(&self) {
606        self.call_depth
607            .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
608    }
609
610    pub(crate) fn host_store_gate(
611        &self,
612        key: String,
613        value: String,
614    ) -> Result<(), crate::bindings::camel::plugin::types::WasmError> {
615        if !self.capabilities.host_kv {
616            return Err(
617                crate::bindings::camel::plugin::types::WasmError::ProcessorError(
618                    "host_store denied: host_kv capability not granted".to_string(),
619                ),
620            );
621        }
622        self.state_store
623            .store(&key, &value)
624            .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
625    }
626
627    pub(crate) fn host_load_gate(
628        &self,
629        key: String,
630    ) -> Result<Option<String>, crate::bindings::camel::plugin::types::WasmError> {
631        if !self.capabilities.host_kv {
632            return Err(
633                crate::bindings::camel::plugin::types::WasmError::ProcessorError(
634                    "host_load denied: host_kv capability not granted".to_string(),
635                ),
636            );
637        }
638        self.state_store
639            .load(&key)
640            .map_err(crate::bindings::camel::plugin::types::WasmError::Io)
641    }
642}
643
644#[cfg(test)]
645mod tests {
646    use super::*;
647    use camel_core::Registry;
648    use std::collections::HashMap;
649    use std::sync::Arc;
650
651    fn make_state(call_depth: usize) -> WasmHostState {
652        WasmHostState {
653            table: wasmtime::component::ResourceTable::new(),
654            wasi: wasmtime_wasi::WasiCtxBuilder::new()
655                .inherit_stderr()
656                .build(),
657            properties: HashMap::new(),
658            registry: Arc::new(std::sync::Mutex::new(Registry::new())),
659            call_depth: Arc::new(std::sync::atomic::AtomicUsize::new(call_depth)),
660            limits: wasmtime::StoreLimits::default(),
661            state_store: crate::state_store::StateStore::new(),
662            capabilities: crate::capabilities::WasmCapabilities::default(),
663        }
664    }
665
666    #[test]
667    fn test_recursion_guard_blocks_nested_calls() {
668        let state = make_state(1);
669        assert!(state.call_depth.load(std::sync::atomic::Ordering::Relaxed) > 0);
670    }
671
672    #[test]
673    fn test_recursion_guard_allows_initial_call() {
674        let state = make_state(0);
675        assert_eq!(
676            state.call_depth.load(std::sync::atomic::Ordering::Relaxed),
677            0
678        );
679    }
680
681    #[test]
682    fn test_get_property_string_value() {
683        let mut state = make_state(0);
684        state
685            .properties
686            .insert("key".to_string(), Value::String("value".to_string()));
687        assert_eq!(
688            state.get_property_impl("key".to_string()),
689            Some("value".to_string())
690        );
691    }
692
693    #[test]
694    fn test_get_property_missing_key() {
695        let state = make_state(0);
696        assert!(!state.properties.contains_key("missing"));
697    }
698
699    #[test]
700    fn test_set_property_json_value() {
701        let mut state = make_state(0);
702        state.set_property_impl("json_key".to_string(), "{\"nested\":true}".to_string());
703        assert!(state.properties.get("json_key").unwrap().is_object());
704    }
705
706    #[test]
707    fn test_uri_scheme_parsing() {
708        assert_eq!("direct".split(':').next().unwrap_or(""), "direct");
709        assert_eq!("log:info".split(':').next().unwrap_or(""), "log");
710        assert_eq!("noscheme".split(':').next().unwrap_or(""), "noscheme");
711        assert_eq!("".split(':').next().unwrap_or(""), "");
712    }
713
714    #[test]
715    fn test_camel_call_does_not_panic_inside_tokio_runtime() {
716        let mut state = make_state(0);
717        let result = state.camel_call_gate("noscheme".to_string());
718        assert!(
719            result.is_err(),
720            "should return error for empty scheme, not panic"
721        );
722    }
723
724    // Capability gating (H4 / H5)
725    // -----------------------------------------------------------------------
726    // Default capabilities are fail-closed: empty scheme allowlist, host_kv
727    // disabled. Tests below verify that the gates fire BEFORE any side effect
728    // (registry lookup, state store mutation) so a denied call cannot leak
729    // information or persist data.
730
731    #[test]
732    fn test_camel_call_denied_without_capability() {
733        let mut state = make_state(0);
734        // Default capabilities: empty allowlist
735        let result = state.camel_call_gate("log:info".to_string());
736        let err = result.unwrap_err();
737        let msg = format!("{:?}", err);
738        assert!(
739            msg.contains("denied"),
740            "expected 'denied' in error, got: {msg}"
741        );
742    }
743
744    #[test]
745    fn test_camel_poll_denied_without_capability() {
746        let state = make_state(0);
747        // The poll gate mirrors the call gate's scheme check.
748        let can_call = state.capabilities.can_call("file:foo");
749        assert!(!can_call, "default capabilities must deny 'file' scheme");
750    }
751
752    #[test]
753    fn test_host_store_denied_without_capability() {
754        let state = make_state(0);
755        let result = state.host_store_gate("key".to_string(), "val".to_string());
756        let err = result.unwrap_err();
757        let msg = format!("{:?}", err);
758        assert!(
759            msg.contains("host_kv"),
760            "expected 'host_kv' in error, got: {msg}"
761        );
762    }
763
764    #[test]
765    fn test_host_load_denied_without_capability() {
766        let state = make_state(0);
767        let result = state.host_load_gate("key".to_string());
768        let err = result.unwrap_err();
769        let msg = format!("{:?}", err);
770        assert!(
771            msg.contains("host_kv"),
772            "expected 'host_kv' in error, got: {msg}"
773        );
774    }
775
776    #[test]
777    fn test_camel_call_allowed_with_capability_passes_scheme_check() {
778        // Granting the scheme means the gate no longer denies — but with an
779        // empty registry, the next stage returns "component not found". This
780        // proves the gate runs BEFORE the registry lookup.
781        let mut state = make_state(0);
782        state.capabilities = crate::capabilities::WasmCapabilities::from_scheme_list("noscheme");
783        // The scheme check now passes; the gate increments call_depth.
784        let result = state.camel_call_gate("noscheme:foo".to_string());
785        assert!(
786            result.is_ok(),
787            "gate should pass when scheme is allowed; got: {result:?}"
788        );
789        state.release_call_depth();
790    }
791
792    #[test]
793    fn test_denied_capabilities_block_host_kv_via_field() {
794        // H5: policy worlds (denied caps) must have host_kv disabled — the
795        // gate is a single field check, not a per-key check.
796        let mut state = make_state(0);
797        state.capabilities = crate::capabilities::WasmCapabilities::denied();
798        assert!(!state.capabilities.host_kv);
799        // host_store and host_load both go through the same gate
800        assert!(
801            state
802                .host_store_gate("k".to_string(), "v".to_string())
803                .is_err()
804        );
805        assert!(state.host_load_gate("k".to_string()).is_err());
806    }
807
808    // ── DepthGuard: RAII recursion counter (C2, I2) ──────────────────────
809    //
810    // The guard increments `call_depth` on `new` and decrements on `Drop`.
811    // The three tests below cover the three behaviours the production code
812    // depends on: increment, drop-decrement, and recursion rejection.
813    //
814    // Per I2: the spec explicitly notes that "if testing through `Accessor`
815    // is too complex for unit tests, at minimum test the `DepthGuard`
816    // struct directly". The trait method signatures now use
817    // `&Accessor<WasmHostState, HasSelf<WasmHostState>>` (switched from
818    // hand-rolled HasData to wasmtime's built-in HasSelf). Accessor
819    // construction still requires `run_concurrent`, so DepthGuard unit
820    // tests remain the most practical verification path for the RAII
821    // recursion counter behaviour.
822
823    #[test]
824    fn test_depth_guard_increments_and_decrements() {
825        // new() must increment; Drop must decrement back to the original value.
826        let state = make_state(0);
827        let depth = &state.call_depth;
828        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
829        {
830            let _g = DepthGuard::new(depth).expect("first guard must succeed");
831            assert_eq!(
832                depth.load(std::sync::atomic::Ordering::SeqCst),
833                1,
834                "guard must increment to 1"
835            );
836        }
837        assert_eq!(
838            depth.load(std::sync::atomic::Ordering::SeqCst),
839            0,
840            "Drop must decrement back to 0"
841        );
842    }
843
844    #[test]
845    fn test_depth_guard_blocks_recursion() {
846        // A second guard while the first is alive must return None and must
847        // not leak an increment (the failed new() must roll back).
848        let state = make_state(0);
849        let depth = &state.call_depth;
850        let first = DepthGuard::new(depth).expect("first guard must succeed");
851        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
852        let second = DepthGuard::new(depth);
853        assert!(second.is_none(), "nested guard must be rejected");
854        // Drop the second (None) and verify the counter is still 1.
855        drop(second);
856        assert_eq!(
857            depth.load(std::sync::atomic::Ordering::SeqCst),
858            1,
859            "rejected guard must not leak an increment"
860        );
861        drop(first);
862        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
863    }
864
865    #[test]
866    fn test_depth_guard_rolls_back_on_rejection() {
867        // If new() is called when depth > 0, it must return None AND restore
868        // the counter (otherwise the increment would leak forever).
869        let state = make_state(0);
870        let depth = &state.call_depth;
871        let guard = DepthGuard::new(depth).expect("first guard");
872        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
873        // The second attempt increments-then-decrements; verify the transient
874        // bump does not affect the observed value after rejection.
875        let _rejected = DepthGuard::new(depth);
876        assert_eq!(
877            depth.load(std::sync::atomic::Ordering::SeqCst),
878            1,
879            "after rejection, counter must be unchanged"
880        );
881        drop(guard);
882        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
883    }
884
885    #[test]
886    fn test_depth_guard_decrements_on_explicit_drop() {
887        // Explicit drop() must decrement. This is the test the original
888        // manual inc/dec pattern was missing: a future that gets dropped
889        // between inc and dec would leak the counter.
890        let state = make_state(0);
891        let depth = &state.call_depth;
892        let guard = DepthGuard::new(depth).expect("first guard");
893        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 1);
894        drop(guard);
895        assert_eq!(depth.load(std::sync::atomic::Ordering::SeqCst), 0);
896    }
897}