use std::sync::Arc;
use jsdet_core::{Bridge, CompiledModule, EmptyBridge, Observation, SandboxConfig, Value};
#[test]
fn compiles_quickjs_wasm_module() {
let module = CompiledModule::new().expect("should compile QuickJS WASM");
drop(module);
}
#[test]
fn executes_simple_arithmetic() {
let module = CompiledModule::new().unwrap();
let result = module
.execute(
&["var x = 1 + 2;".into()],
Arc::new(EmptyBridge),
&SandboxConfig::default(),
)
.unwrap();
assert_eq!(result.scripts_executed, 1);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
assert!(!result.timed_out);
}
#[test]
fn captures_syntax_error() {
let module = CompiledModule::new().unwrap();
let result = module
.execute(
&["var x = {{{;".into()],
Arc::new(EmptyBridge),
&SandboxConfig::default(),
)
.unwrap();
assert_eq!(result.scripts_executed, 1);
assert!(
!result.errors.is_empty()
|| result
.observations
.iter()
.any(|o| matches!(o, Observation::Error { .. })),
"syntax error should produce an error observation"
);
}
#[test]
fn respects_fuel_limit() {
let module = CompiledModule::new().unwrap();
let config = SandboxConfig {
max_fuel: 50_000_000,
timeout_ms: 10_000,
..SandboxConfig::default()
};
let result = module.execute(
&["for(var i=0; i<1000000; i++) { var x = i * i; }".into()],
Arc::new(EmptyBridge),
&config,
);
match result {
Ok(r) => {
let has_fuel_limit = r.observations.iter().any(|o| {
matches!(
o,
Observation::ResourceLimit {
kind: jsdet_core::observation::ResourceLimitKind::Fuel,
..
}
)
});
assert!(
has_fuel_limit || r.scripts_executed == 1,
"should either hit fuel limit or complete: scripts={}, errors={:?}, obs={}",
r.scripts_executed,
r.errors,
r.observations.len()
);
}
Err(jsdet_core::Error::FuelExhausted { .. }) => {} Err(jsdet_core::Error::Trap(msg)) if msg.contains("fuel") => {} Err(e) => panic!("unexpected error: {e}"),
}
}
#[test]
fn multiple_scripts_execute_sequentially() {
let module = CompiledModule::new().unwrap();
let result = module
.execute(
&[
"globalThis.a = 10;".into(),
"globalThis.b = globalThis.a + 20;".into(),
"globalThis.c = globalThis.b + 30;".into(),
],
Arc::new(EmptyBridge),
&SandboxConfig::default(),
)
.unwrap();
assert_eq!(result.scripts_executed, 3);
assert!(
result.errors.is_empty(),
"errors: {:?}\nobservations: {:?}",
result.errors,
result.observations
);
}
#[test]
fn isolates_between_executions() {
let module = CompiledModule::new().unwrap();
let bridge = Arc::new(EmptyBridge);
let r1 = module
.execute(
&["globalThis.leak = 'secret';".into()],
bridge.clone(),
&SandboxConfig::default(),
)
.unwrap();
assert_eq!(r1.scripts_executed, 1);
let r2 = module
.execute(
&["if(typeof globalThis.leak !== 'undefined') throw 'LEAK';".into()],
bridge,
&SandboxConfig::default(),
)
.unwrap();
let leaked = r2.observations.iter().any(|o| {
if let Observation::Error { message, .. } = o {
message.contains("LEAK")
} else {
false
}
});
assert!(!leaked, "state should not leak between executions");
}
#[test]
fn bridge_return_values_flow_to_js() {
let module = CompiledModule::new().unwrap();
struct TestBridge;
impl Bridge for TestBridge {
fn call(&self, api: &str, _args: &[Value]) -> Result<Value, String> {
match api {
"getNumber" => Ok(Value::Int(42)),
"getString" => Ok(Value::string("hello from bridge")),
"getJson" => Ok(Value::json(r#"{"key":"value"}"#)),
"verify" => Ok(Value::Bool(true)),
_ => Err(format!("{api} not found")),
}
}
fn get_property(&self, _: &str, _: &str) -> Result<Value, String> {
Err("not found".into())
}
fn set_property(&self, _: &str, _: &str, _: &Value) -> Result<(), String> {
Ok(())
}
fn provided_globals(&self) -> Vec<String> {
vec!["testApi".into()]
}
fn bootstrap_js(&self) -> String {
r#"
globalThis.testApi = {
getNumber: function() { return __jsdet_bridge_call("getNumber", "[]"); },
getString: function() { return __jsdet_bridge_call("getString", "[]"); },
getJson: function() { return __jsdet_bridge_call("getJson", "[]"); },
verify: function(v) { return __jsdet_bridge_call("verify", JSON.stringify([v])); },
};
"#
.into()
}
}
let result = module
.execute(
&[r#"
var n = testApi.getNumber();
if (n !== 42) throw "number failed: " + n + " type:" + typeof n;
var s = testApi.getString();
if (s !== "hello from bridge") throw "string failed: " + s;
var j = testApi.getJson();
if (j.key !== "value") throw "json failed: " + JSON.stringify(j);
"#
.into()],
Arc::new(TestBridge),
&SandboxConfig::default(),
)
.unwrap();
assert!(
result.errors.is_empty(),
"errors: {:?}\nobs: {:?}",
result.errors,
result.observations
);
let api_calls = result
.observations
.iter()
.filter(|o| matches!(o, Observation::ApiCall { .. }))
.count();
assert!(api_calls >= 3, "expected 3+ API calls, got {api_calls}");
}