use std::sync::{Arc, Mutex};
use std::time::Duration;
use arrow_array::Float64Array;
use datafusion::logical_expr::ColumnarValue;
use datafusion::scalar::ScalarValue;
use uni_plugin::{
Capability, CapabilitySet, FnError, HttpEgress, HttpResponse, PluginId, PluginRegistrar,
PluginRegistry, QName,
};
use uni_plugin_extism::{ExtismLoader, register_default_host_svc};
const WASM_PATH: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../examples/example-extism-net/target/wasm32-unknown-unknown/release/example_extism_net.wasm",
);
fn load_wasm_bytes() -> Vec<u8> {
std::fs::read(WASM_PATH).unwrap_or_else(|e| {
panic!(
"wasm artifact missing at {WASM_PATH}: {e}\n\
Run `./scripts/build-wasm-fixtures.sh` from the repo root first."
);
})
}
#[derive(Default)]
struct RecordingEgress {
last_url: Mutex<Option<String>>,
}
impl HttpEgress for RecordingEgress {
fn get(
&self,
url: &str,
_timeout: Duration,
_max_bytes: usize,
_traceparent: Option<&str>,
) -> Result<HttpResponse, FnError> {
*self.last_url.lock().unwrap() = Some(url.to_owned());
Ok(HttpResponse {
status: 200,
body: b"pong".to_vec(),
})
}
fn post(
&self,
url: &str,
_body: &[u8],
_timeout: Duration,
_max_bytes: usize,
_traceparent: Option<&str>,
) -> Result<HttpResponse, FnError> {
*self.last_url.lock().unwrap() = Some(url.to_owned());
Ok(HttpResponse {
status: 200,
body: Vec::new(),
})
}
}
fn network_grant() -> CapabilitySet {
CapabilitySet::from_iter_of([
Capability::ScalarFn,
Capability::Network {
allow: vec!["**".into()],
},
])
}
fn invoke_status(registry: &PluginRegistry) -> Result<f64, String> {
let qname = QName::parse("ai.example.net.fetch_status").expect("qname");
let entry = registry.scalar_fn(&qname).expect("registered");
let args = vec![ColumnarValue::Scalar(ScalarValue::Float64(Some(0.0)))];
let out = entry
.function
.invoke(&args, 1)
.map_err(|e| format!("{e}"))?;
let arr = match out {
ColumnarValue::Array(a) => a,
ColumnarValue::Scalar(s) => panic!("expected array, got {s:?}"),
};
let f = arr
.as_any()
.downcast_ref::<Float64Array>()
.expect("Float64Array");
Ok(f.value(0))
}
#[test]
fn granted_with_egress_round_trips_through_uni_http_get() {
let bytes = load_wasm_bytes();
let egress = Arc::new(RecordingEgress::default());
let mut loader = ExtismLoader::new().with_http(egress.clone());
register_default_host_svc(&mut loader);
let registry = PluginRegistry::new();
let caps = CapabilitySet::from_iter_of([Capability::ScalarFn]);
let mut r = PluginRegistrar::new(PluginId::new("extism.net"), &caps, ®istry);
let outcome = loader.load(&bytes, &network_grant(), &mut r).expect("load");
r.commit_to_registry().expect("commit");
assert_eq!(outcome.plugin_id, "ai.example.net");
assert!(
outcome
.scalars_registered
.iter()
.any(|q| q == "ai.example.net.fetch_status"),
"scalars_registered: {:?}",
outcome.scalars_registered
);
let status = invoke_status(®istry).expect("invoke");
assert!((status - 200.0).abs() < f64::EPSILON, "status: {status}");
assert_eq!(
egress.last_url.lock().unwrap().as_deref(),
Some("https://api.example.com/ping"),
"uni_http_get must have dispatched the guest's request"
);
}
#[test]
fn granted_without_egress_fails_loudly_at_call_time() {
let bytes = load_wasm_bytes();
let mut loader = ExtismLoader::new();
register_default_host_svc(&mut loader);
let registry = PluginRegistry::new();
let caps = CapabilitySet::from_iter_of([Capability::ScalarFn]);
let mut r = PluginRegistrar::new(PluginId::new("extism.net.noegress"), &caps, ®istry);
loader
.load(&bytes, &network_grant(), &mut r)
.expect("load (manifest reads; uni_http_get present but unconfigured)");
r.commit_to_registry().expect("commit");
let err = invoke_status(®istry).expect_err("must fail loudly without egress");
assert!(
err.contains("invoke_ai_example_net_fetch_status") || err.to_lowercase().contains("egress"),
"expected the host-net call to fail loudly when egress is unconfigured, got: {err}"
);
}
#[test]
fn network_not_granted_fails_at_link_time() {
let bytes = load_wasm_bytes();
let egress = Arc::new(RecordingEgress::default());
let mut loader = ExtismLoader::new().with_http(egress);
register_default_host_svc(&mut loader);
let registry = PluginRegistry::new();
let caps = CapabilitySet::from_iter_of([Capability::ScalarFn]);
let mut r = PluginRegistrar::new(PluginId::new("extism.net.ungranted"), &caps, ®istry);
let scalar_caps = CapabilitySet::from_iter_of([Capability::ScalarFn]);
let load_result = loader.load(&bytes, &scalar_caps, &mut r);
let msg = match load_result {
Err(e) => format!("{e}"),
Ok(_) => {
r.commit_to_registry().expect("commit");
invoke_status(®istry).expect_err("ungranted call must fail")
}
};
let lower = msg.to_lowercase();
assert!(
lower.contains("uni_http_get") || lower.contains("import") || lower.contains("function"),
"expected a link-time import failure mentioning uni_http_get, got: {msg}"
);
}