use std::collections::HashMap;
use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::mpsc;
use std::time::Duration;
use std::time::Instant;
use boa_engine::property::PropertyKey;
use boa_engine::realm::Realm;
use boa_engine::{Context, JsObject, JsValue, Script, Source, js_string};
use super::boa::{ResolvedJsLimits, resolve_js_limits};
use super::integrity::IntegrityBaseline;
use crate::{
bindings,
engine::{JsEvalResult, JsExchange},
error::JsLanguageError,
value::js_to_value,
};
#[cfg(test)]
use serde_json::Value;
const JOB_CHANNEL_CAPACITY: usize = 1024;
const SCRIPT_CACHE_CAPACITY: usize = 256;
pub(super) enum JsJob {
Eval {
source: Arc<str>,
exchange: JsExchange,
timeout_ms: u64,
enqueued: Instant,
reply: mpsc::SyncSender<Result<JsEvalResult, JsLanguageError>>,
},
Validate {
source: Arc<str>,
reply: mpsc::SyncSender<Result<(), JsLanguageError>>,
},
#[cfg(test)]
TestBlock {
millis: u64,
reply: mpsc::SyncSender<Result<JsEvalResult, JsLanguageError>>,
},
#[cfg(test)]
TestPanic {
reply: mpsc::SyncSender<Result<JsEvalResult, JsLanguageError>>,
},
}
#[derive(Debug)]
pub(super) struct JsWorkerHandle {
tx: mpsc::SyncSender<JsJob>,
}
impl JsWorkerHandle {
pub(super) fn spawn(limits: camel_language_api::JsLimitsConfig) -> JsWorkerHandle {
let (tx, rx) = mpsc::sync_channel(JOB_CHANNEL_CAPACITY);
let _ = std::thread::Builder::new()
.name("camel-js-worker".to_string())
.spawn(move || worker_loop(rx, limits));
JsWorkerHandle { tx }
}
pub(super) fn send(&self, job: JsJob) -> Result<(), JsLanguageError> {
self.tx.send(job).map_err(|_| worker_unavailable())
}
}
pub(super) fn worker_unavailable() -> JsLanguageError {
JsLanguageError::Execution {
message: "JS worker unavailable".to_string(),
}
}
fn escape_js_string_literal(src: &str) -> String {
let mut out = String::with_capacity(src.len() + 2);
for c in src.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\u{2028}' => out.push_str("\\u2028"),
'\u{2029}' => out.push_str("\\u2029"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
struct ScriptCache {
map: HashMap<Arc<str>, boa_engine::Script>,
order: VecDeque<Arc<str>>,
cap: usize,
}
impl ScriptCache {
fn new(cap: usize) -> Self {
Self {
map: HashMap::new(),
order: VecDeque::new(),
cap,
}
}
fn get(&mut self, key: &Arc<str>) -> Option<&boa_engine::Script> {
if self.map.contains_key(key) {
self.touch(key);
}
self.map.get(key)
}
fn insert(&mut self, key: Arc<str>, script: boa_engine::Script) {
if self.map.contains_key(&key) {
self.touch(&key);
self.map.insert(key, script);
return;
}
while self.map.len() >= self.cap {
match self.order.pop_front() {
Some(evicted) => {
self.map.remove(&evicted);
}
None => break,
}
}
self.order.push_back(key.clone());
self.map.insert(key, script);
}
#[allow(dead_code)]
fn len(&self) -> usize {
self.map.len()
}
fn clear(&mut self) {
self.map.clear();
self.order.clear();
}
fn touch(&mut self, key: &Arc<str>) {
if let Some(pos) = self.order.iter().position(|k| k == key)
&& let Some(k) = self.order.remove(pos)
{
self.order.push_back(k);
}
}
}
struct InstalledGlobals {
console: JsObject,
camel: JsObject,
}
enum InstallError {
Data(JsLanguageError),
Rejected,
}
fn install_bindings(
ctx: &mut Context,
exchange: &JsExchange,
) -> Result<InstalledGlobals, InstallError> {
let console = bindings::register_console(ctx).map_err(|_| InstallError::Rejected)?;
let camel_obj = bindings::build_camel_global(exchange, ctx).map_err(|e| {
InstallError::Data(JsLanguageError::Execution {
message: e.to_string(),
})
})?;
ctx.global_object()
.set(
js_string!("camel"),
JsValue::from(camel_obj.clone()),
true,
ctx,
)
.map_err(|_| InstallError::Rejected)?;
Ok(InstalledGlobals {
console,
camel: camel_obj,
})
}
pub(super) struct WorkerState {
ctx: Context,
realm: Realm,
cache: ScriptCache,
limits: ResolvedJsLimits,
baseline: IntegrityBaseline,
#[allow(dead_code)]
recycle_count: u64,
}
impl WorkerState {
pub(super) fn new(limits: ResolvedJsLimits) -> Self {
let mut ctx = Context::default();
apply_runtime_limits(&mut ctx, &limits);
let realm = ctx.realm().clone();
ctx.enter_realm(realm.clone());
let mut state = Self {
ctx,
realm,
cache: ScriptCache::new(SCRIPT_CACHE_CAPACITY),
limits,
baseline: IntegrityBaseline::empty(),
recycle_count: 0,
};
if install_bindings(&mut state.ctx, &JsExchange::default()).is_ok() {
state.baseline = IntegrityBaseline::capture(&mut state.ctx);
}
state
}
pub(super) fn run_eval(
&mut self,
source: &str,
exchange: JsExchange,
deadline: Instant,
) -> Result<JsEvalResult, JsLanguageError> {
if Instant::now() > deadline {
return Err(JsLanguageError::Execution {
message: "JS execution timeout".to_string(),
});
}
self.ctx.enter_realm(self.realm.clone());
let mut verified = false;
for attempt in 0..2 {
self.scrub_globals();
match install_bindings(&mut self.ctx, &exchange) {
Ok(installed) => {
if self.install_verify(&installed) {
verified = true;
break;
}
}
Err(InstallError::Data(e)) => return Err(e),
Err(InstallError::Rejected) => {}
}
self.recycle();
if attempt == 0 && Instant::now() > deadline {
return Err(JsLanguageError::Execution {
message: "JS execution timeout".to_string(),
});
}
}
if !verified {
return Err(JsLanguageError::Execution {
message: "JS engine realm failed verification".to_string(),
});
}
let outcome = self.eval_cached(source);
if !self.integrity_check() {
self.recycle();
}
outcome
}
fn eval_cached(&mut self, source: &str) -> Result<JsEvalResult, JsLanguageError> {
let WorkerState { ctx, cache, .. } = self;
let key: Arc<str> = Arc::from(source);
let outcome = match cache.get(&key) {
Some(script) => script.evaluate(ctx),
None => {
let wrapper = format!("eval(\"{}\")", escape_js_string_literal(source));
let script = Script::parse(Source::from_bytes(wrapper.as_bytes()), None, ctx)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
let outcome = script.evaluate(ctx);
cache.insert(key, script);
outcome
}
};
let result = outcome.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
let return_value = js_to_value(&result, ctx)?;
let modified = bindings::extract_camel_state(ctx)?;
Ok(JsEvalResult {
return_value,
headers: modified.headers,
body: modified.body,
properties: modified.properties,
})
}
fn scrub_globals(&mut self) {
let global = self.ctx.global_object();
let Ok(keys) = global.own_property_keys(&mut self.ctx) else {
return;
};
for key in keys {
if !self.baseline.contains_global_key(&key) {
let _ = global.delete_property_or_throw(key, &mut self.ctx);
}
}
}
fn install_verify(&mut self, installed: &InstalledGlobals) -> bool {
let Some(eval_object) = self.baseline.eval_object() else {
return false;
};
let eval_object = eval_object.clone();
let global = self.ctx.global_object();
for (key, expected) in [
(js_string!("camel"), JsValue::from(installed.camel.clone())),
(
js_string!("console"),
JsValue::from(installed.console.clone()),
),
(js_string!("eval"), JsValue::from(eval_object)),
] {
let Some(desc) = global.borrow().properties().get(&PropertyKey::from(key)) else {
return false;
};
let Some(value) = desc.value() else {
return false;
};
if !value.strict_equals(&expected) {
return false;
}
}
true
}
fn integrity_check(&mut self) -> bool {
let WorkerState { ctx, baseline, .. } = self;
baseline.verify(ctx)
}
fn recycle(&mut self) {
self.recycle_count += 1;
let limits = self.limits.clone();
if let Ok(realm) = self.ctx.create_realm() {
let _previous = self.ctx.enter_realm(realm.clone());
self.realm = realm;
apply_runtime_limits(&mut self.ctx, &self.limits);
if install_bindings(&mut self.ctx, &JsExchange::default()).is_ok() {
self.baseline = IntegrityBaseline::capture(&mut self.ctx);
self.cache.clear();
return;
}
}
let count = self.recycle_count;
*self = WorkerState::new(limits);
self.recycle_count = count;
}
pub(super) fn run_validate(&mut self, source: &str) -> Result<(), JsLanguageError> {
let _script = Script::parse(Source::from_bytes(source.as_bytes()), None, &mut self.ctx)
.map_err(|e| JsLanguageError::Parse {
message: e.to_string(),
})?;
Ok(())
}
#[cfg(test)]
fn run_test_block(&self, millis: u64) -> Result<JsEvalResult, JsLanguageError> {
std::thread::sleep(Duration::from_millis(millis));
Ok(JsEvalResult {
return_value: Value::Null,
headers: HashMap::new(),
body: Value::Null,
properties: HashMap::new(),
})
}
#[cfg(test)]
fn run_test_panic(&self) -> Result<JsEvalResult, JsLanguageError> {
panic!("TestPanic: deliberate worker panic for the self-heal test");
}
}
fn apply_runtime_limits(ctx: &mut Context, limits: &ResolvedJsLimits) {
let runtime_limits = ctx.runtime_limits_mut();
runtime_limits.set_loop_iteration_limit(limits.max_loop_iterations);
runtime_limits.set_recursion_limit(limits.max_recursion_depth);
runtime_limits.set_stack_size_limit(limits.max_stack_size);
}
fn worker_loop(rx: mpsc::Receiver<JsJob>, limits: camel_language_api::JsLimitsConfig) {
let resolved = resolve_js_limits(&limits);
let mut state = WorkerState::new(resolved);
while let Ok(job) = rx.recv() {
let panicked = match job {
JsJob::Eval {
source,
exchange,
timeout_ms,
enqueued,
reply,
} => {
if enqueued.elapsed() > Duration::from_millis(timeout_ms) {
let _ = reply.send(Err(JsLanguageError::Execution {
message: "JS execution timeout".to_string(),
}));
continue;
}
let deadline = enqueued + Duration::from_millis(timeout_ms);
let attempt = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
state.run_eval(&source, exchange, deadline)
}));
reply_outcome(&reply, attempt)
}
JsJob::Validate { source, reply } => {
let attempt = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
state.run_validate(&source)
}));
reply_outcome(&reply, attempt)
}
#[cfg(test)]
JsJob::TestBlock { millis, reply } => {
let attempt = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
state.run_test_block(millis)
}));
reply_outcome(&reply, attempt)
}
#[cfg(test)]
JsJob::TestPanic { reply } => {
let attempt = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
state.run_test_panic()
}));
reply_outcome(&reply, attempt)
}
};
if panicked {
let count = state.recycle_count;
state = WorkerState::new(resolve_js_limits(&limits));
state.recycle_count = count;
}
}
}
fn reply_outcome<T>(
reply: &mpsc::SyncSender<Result<T, JsLanguageError>>,
attempt: std::thread::Result<Result<T, JsLanguageError>>,
) -> bool {
match attempt {
Ok(outcome) => {
let _ = reply.send(outcome);
false
}
Err(_) => {
let _ = reply.send(Err(JsLanguageError::Execution {
message: "JS worker panic recovered".to_string(),
}));
true
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::mpsc;
use camel_language_api::{Exchange, JsLimitsConfig, Language, Message};
use crate::engine::JsEngine as _;
use crate::language::JsLanguage;
fn eval_now(
state: &mut WorkerState,
source: &str,
exchange: JsExchange,
) -> Result<JsEvalResult, JsLanguageError> {
state.run_eval(source, exchange, Instant::now() + Duration::from_secs(5))
}
fn worker_state() -> WorkerState {
WorkerState::new(resolve_js_limits(&JsLimitsConfig::default()))
}
#[test]
fn expired_queued_job_is_skipped() {
let handle = JsWorkerHandle::spawn(JsLimitsConfig::default());
let (block_tx, _block_rx) = mpsc::sync_channel(1);
handle
.tx
.send(JsJob::TestBlock {
millis: 300,
reply: block_tx,
})
.unwrap();
let (tx, rx) = mpsc::sync_channel(1);
handle
.tx
.send(JsJob::Eval {
source: Arc::from("1+1"),
exchange: JsExchange::default(),
timeout_ms: 1,
enqueued: Instant::now(),
reply: tx,
})
.unwrap();
let reply = rx.recv().unwrap();
let err = reply.expect_err("expired job must be skipped with an error");
assert!(
err.to_string().contains("timeout"),
"expected timeout error, got: {err}"
);
let (tx, rx) = mpsc::sync_channel(1);
handle
.tx
.send(JsJob::Eval {
source: Arc::from("1+1"),
exchange: JsExchange::default(),
timeout_ms: 5_000,
enqueued: Instant::now(),
reply: tx,
})
.unwrap();
let result = rx.recv().unwrap().unwrap();
assert_eq!(result.return_value.as_i64().unwrap(), 2);
}
#[test]
fn worker_self_heals_after_panic() {
let handle = JsWorkerHandle::spawn(JsLimitsConfig::default());
let (tx, rx) = mpsc::sync_channel(1);
handle.tx.send(JsJob::TestPanic { reply: tx }).unwrap();
let reply = rx.recv().unwrap();
let err = reply.expect_err("TestPanic must produce an error");
assert!(
err.to_string().contains("panic"),
"expected panic error, got: {err}"
);
let (tx, rx) = mpsc::sync_channel(1);
handle
.tx
.send(JsJob::Eval {
source: Arc::from("1+1"),
exchange: JsExchange::default(),
timeout_ms: 5_000,
enqueued: Instant::now(),
reply: tx,
})
.unwrap();
let result = rx.recv().unwrap().unwrap();
assert_eq!(result.return_value.as_i64().unwrap(), 2);
}
#[test]
fn lexical_declarations_are_fresh_per_eval() {
let engine = super::super::boa::BoaEngine::default();
let source = "let count = 0; count++; count";
let r1 = engine.eval(source, JsExchange::default()).unwrap();
let r2 = engine.eval(source, JsExchange::default()).unwrap();
assert_eq!(r1.return_value.as_i64().unwrap(), 1);
assert_eq!(r2.return_value.as_i64().unwrap(), 1);
}
#[test]
fn wrapper_escapes_adversarial_sources() {
let table: Vec<(String, String)> = vec![
(r#"'it\'s'"#.to_string(), "it's".to_string()),
(r#""quo\"te""#.to_string(), "quo\"te".to_string()),
(r#""back\\slash""#.to_string(), "back\\slash".to_string()),
("\"cr\\rLf\\n\"".to_string(), "cr\rLf\n".to_string()),
(
"\"ls\u{2028}ps\u{2029}\"".to_string(),
"ls\u{2028}ps\u{2029}".to_string(),
),
("\"trailing\\\\\"".to_string(), "trailing\\".to_string()),
];
let distinct_sources = table.len();
let mut state = worker_state();
for (source, expected) in &table {
for _ in 0..3 {
let result = eval_now(&mut state, source, JsExchange::default())
.unwrap_or_else(|e| panic!("eval must succeed for {source:?}: {e}"));
assert_eq!(
result.return_value.as_str(),
Some(expected.as_str()),
"adversarial source must round-trip: {source:?}"
);
}
}
assert_eq!(state.cache.len(), distinct_sources);
}
#[test]
fn invalid_source_maps_to_execution_error() {
let mut state = worker_state();
let err = eval_now(&mut state, "let x = {{{", JsExchange::default())
.expect_err("invalid source must fail eval");
assert!(
matches!(err, JsLanguageError::Execution { .. }),
"eval of invalid source must be Execution, got: {err:?}"
);
let verr = state
.run_validate("let x = {{{")
.expect_err("invalid source must fail validate");
assert!(
matches!(verr, JsLanguageError::Parse { .. }),
"validate of invalid source must be Parse, got: {verr:?}"
);
}
#[test]
fn identical_results_across_repeats() {
let mut state = worker_state();
let source = "1 + 1";
let exchange = JsExchange::default();
let r1 = eval_now(&mut state, source, exchange.clone()).unwrap();
let r2 = eval_now(&mut state, source, exchange.clone()).unwrap();
let r3 = eval_now(&mut state, source, exchange).unwrap();
assert_eq!(r1, r2);
assert_eq!(r2, r3);
}
#[test]
fn identical_errors_first_and_cached() {
let mut state = worker_state();
let source = "throw new Error('boom')";
let e1 =
eval_now(&mut state, source, JsExchange::default()).expect_err("first eval must fail");
let e2 = eval_now(&mut state, source, JsExchange::default())
.expect_err("cached eval must fail identically");
let message_of = |e: &JsLanguageError| match e {
JsLanguageError::Execution { message } => message.clone(),
other => panic!("expected Execution error, got: {other:?}"),
};
let m1 = message_of(&e1);
let m2 = message_of(&e2);
assert!(m1.contains("boom"), "error must carry the JS message: {m1}");
assert_eq!(m1, m2, "first and cached error must be identical");
}
#[tokio::test]
async fn route_rebuild_reuses_cache_entry() {
let mut state = worker_state();
let source = "1 + 1";
let r1 = eval_now(&mut state, source, JsExchange::default()).unwrap();
let r2 = eval_now(&mut state, source, JsExchange::default()).unwrap();
assert_eq!(r1, r2);
assert_eq!(state.cache.len(), 1);
let lang = JsLanguage::new();
let expr1 = lang.create_expression("1 + 1").unwrap();
let expr2 = lang.create_expression("1 + 1").unwrap();
let exchange = Exchange::new(Message::default());
let v1 = expr1.evaluate(&exchange).await.unwrap();
let v2 = expr2.evaluate(&exchange).await.unwrap();
assert_eq!(v1, v2);
}
#[test]
fn lru_eviction_bounds_cache_at_256() {
let mut ctx = Context::default();
let mut cache = ScriptCache::new(256);
let keys: Vec<Arc<str>> = (0..257)
.map(|i| Arc::<str>::from(format!("source-{i}").as_str()))
.collect();
for key in &keys {
let script = Script::parse(Source::from_bytes(b"0"), None, &mut ctx).unwrap();
cache.insert(key.clone(), script);
}
assert_eq!(cache.len(), 256, "cache must stay bounded at the cap");
assert!(cache.get(&keys[0]).is_none(), "oldest key must be evicted");
assert!(
cache.get(&keys[256]).is_some(),
"most recently inserted key must be present"
);
}
#[test]
fn global_assignment_does_not_persist() {
let mut state = worker_state();
let r = eval_now(&mut state, "globalThis.leak = 42; 0", JsExchange::default()).unwrap();
assert_eq!(r.return_value.as_i64(), Some(0));
let r = eval_now(&mut state, "typeof leak", JsExchange::default()).unwrap();
assert_eq!(
r.return_value.as_str(),
Some("undefined"),
"global assignment must not persist across evals"
);
}
#[test]
fn poisoned_eval_recovers() {
let engine = super::super::boa::BoaEngine::default();
let r1 = engine.eval("eval = 1; 0", JsExchange::default()).unwrap();
assert_eq!(r1.return_value.as_i64(), Some(0));
let r2 = engine.eval("typeof eval", JsExchange::default()).unwrap();
assert_eq!(
r2.return_value.as_str(),
Some("function"),
"poisoned eval must be restored via realm recycle before the next eval"
);
}
#[test]
fn undeletable_global_recycles_realm() {
let mut state = worker_state();
eval_now(
&mut state,
"Object.defineProperty(globalThis, 'pwn', { value: 1, configurable: false }); 0",
JsExchange::default(),
)
.unwrap();
let r = eval_now(&mut state, "typeof pwn", JsExchange::default()).unwrap();
assert_eq!(
r.return_value.as_str(),
Some("undefined"),
"undeletable global key must be detected as drift and the realm recycled"
);
}
#[test]
fn frozen_prototype_recycles_realm() {
let mut state = worker_state();
eval_now(
&mut state,
"Object.freeze(Object.prototype); 0",
JsExchange::default(),
)
.unwrap();
let r = eval_now(
&mut state,
"Object.isFrozen(Object.prototype)",
JsExchange::default(),
)
.unwrap();
assert_eq!(
r.return_value.as_bool(),
Some(false),
"descriptor drift on Object.prototype must recycle the realm"
);
}
#[test]
fn failed_eval_residue_is_cleaned() {
let mut state = worker_state();
let err = eval_now(
&mut state,
"globalThis.x = 1; throw new Error('boom')",
JsExchange::default(),
);
assert!(err.is_err(), "eval must fail with the thrown error");
let r = eval_now(&mut state, "typeof x", JsExchange::default()).unwrap();
assert_eq!(
r.return_value.as_str(),
Some("undefined"),
"cleanup must run on the error path"
);
}
#[test]
fn limit_failure_residue_is_cleaned() {
let mut state = worker_state();
let err = eval_now(
&mut state,
"globalThis.x = 1; while (true) {}",
JsExchange::default(),
);
assert!(
err.is_err(),
"infinite loop must trip the loop-iteration limit"
);
let r = eval_now(&mut state, "typeof x", JsExchange::default()).unwrap();
assert_eq!(
r.return_value.as_str(),
Some("undefined"),
"cleanup must run on the limit-failure path"
);
}
#[test]
fn console_and_camel_are_fresh_each_eval() {
let mut state = worker_state();
eval_now(
&mut state,
"console.log = function(){}; camel.headers = 1; 0",
JsExchange::default(),
)
.unwrap();
let exchange = JsExchange::from_headers_body_properties(
[("k".to_string(), serde_json::json!("v"))]
.into_iter()
.collect(),
serde_json::Value::Null,
HashMap::new(),
);
let r = eval_now(
&mut state,
"console.log('x'); camel.headers.get('k')",
exchange,
)
.unwrap();
assert_eq!(
r.return_value.as_str(),
Some("v"),
"camel/console must be re-installed fresh with the new exchange"
);
}
#[test]
fn spoofed_non_writable_binding_recycles() {
let mut state = worker_state();
eval_now(
&mut state,
"Object.defineProperty(globalThis, 'camel', { value: { pwn: 1 }, writable: false }); 0",
JsExchange::default(),
)
.unwrap();
let r = eval_now(&mut state, "camel.pwn", JsExchange::default()).unwrap();
assert!(
r.return_value.is_null(),
"spoofed non-writable camel must be replaced via realm recycle before the eval, got {:?}",
r.return_value
);
let exchange = JsExchange::from_headers_body_properties(
HashMap::new(),
serde_json::Value::String("payload".to_string()),
HashMap::new(),
);
let r = eval_now(&mut state, "camel.body", exchange).unwrap();
assert_eq!(r.return_value.as_str(), Some("payload"));
}
#[test]
fn date_prototype_persists_until_recycle() {
let mut state = worker_state();
eval_now(
&mut state,
"Date.prototype.probe = 1; 0",
JsExchange::default(),
)
.unwrap();
let r = eval_now(&mut state, "new Date().probe", JsExchange::default()).unwrap();
assert_eq!(
r.return_value.as_i64(),
Some(1),
"state outside the integrity set persists within a realm generation"
);
eval_now(
&mut state,
"Object.defineProperty(globalThis, 'pwn', { value: 1, configurable: false }); 0",
JsExchange::default(),
)
.unwrap();
let r = eval_now(&mut state, "new Date().probe", JsExchange::default()).unwrap();
assert!(
r.return_value.is_null(),
"recycled realm must drop the persisted Date.prototype state, got {:?}",
r.return_value
);
}
#[test]
fn no_recycle_storm_on_steady_state() {
let mut state = worker_state();
let exchange = JsExchange::from_headers_body_properties(
HashMap::new(),
serde_json::Value::String("payload".to_string()),
HashMap::new(),
);
for _ in 0..100 {
eval_now(&mut state, "camel.body", exchange.clone()).unwrap();
}
assert_eq!(
state.recycle_count, 0,
"steady-state evals must not trigger realm recycles"
);
}
#[test]
#[ignore = "slow test: timing diagnostic; run via cargo test -p camel-language-js integrity_detector -- --ignored --nocapture; printed mean feeds the Req-4 shrink decision"]
fn integrity_detector_cost_diagnostic() {
let mut state = worker_state();
for _ in 0..20 {
state.integrity_check();
}
let start = Instant::now();
for _ in 0..200 {
state.integrity_check();
}
let mean_us = start.elapsed().as_micros() as f64 / 200.0;
println!(
"integrity_detector_cost_diagnostic: mean {mean_us:.2} µs per integrity_check over 200 calls"
);
}
}