use afterburner::core::ScriptId;
use afterburner::{Afterburner, AfterburnerError, EngineMode, FsAccess, FuelGauge, Manifold};
use serde_json::json;
#[test]
fn register_and_run_default() {
let ab = Afterburner::new().expect("Afterburner::new");
let id = ab
.register("module.exports = (d) => d.n + 1")
.expect("register");
let out = ab.run(&id, &json!({ "n": 41 })).expect("run");
assert_eq!(out, json!(42));
}
#[test]
fn register_is_content_addressed() {
let ab = Afterburner::new().expect("Afterburner::new");
let src = "module.exports = (d) => d.x * 2";
let id1 = ab.register(src).expect("register 1");
let id2 = ab.register(src).expect("register 2 (same source)");
assert_eq!(id1, id2, "same source must yield identical ScriptId");
let out = ab.run(&id1, &json!({ "x": 7 })).expect("run via id1");
assert_eq!(out, json!(14));
let out2 = ab.run(&id2, &json!({ "x": 9 })).expect("run via id2");
assert_eq!(out2, json!(18));
}
#[test]
fn distinct_sources_yield_distinct_ids() {
let ab = Afterburner::new().expect("Afterburner::new");
let a = ab
.register("module.exports = (d) => d.n")
.expect("register a");
let b = ab
.register("module.exports = (d) => -d.n")
.expect("register b");
assert_ne!(a, b, "different sources must yield different ScriptIds");
}
#[test]
fn run_is_reusable_across_many_inputs() {
let ab = Afterburner::new().expect("Afterburner::new");
let id = ab
.register("module.exports = (d) => ({ doubled: d.n * 2, src: d.tag })")
.expect("register");
for n in 0..32 {
let out = ab
.run(&id, &json!({ "n": n, "tag": format!("t{n}") }))
.expect("run");
assert_eq!(out, json!({ "doubled": n * 2, "src": format!("t{n}") }));
}
}
#[test]
fn run_with_explicit_limits_does_not_mutate_default_gauge() {
let ab = Afterburner::builder()
.fuel(1_000_000_000)
.build()
.expect("build");
let id = ab
.register("module.exports = (d) => d.n + 1")
.expect("register");
let tight = FuelGauge {
fuel: Some(500),
..FuelGauge::unlimited()
};
let _ = ab.run_with(&id, &json!({ "n": 1 }), &tight);
let out = ab.run(&id, &json!({ "n": 41 })).expect("run with default");
assert_eq!(out, json!(42));
}
#[test]
fn run_batch_whole_array_in_whole_array_out() {
let ab = Afterburner::new().expect("Afterburner::new");
let id = ab
.register("module.exports = (rows) => rows.map(r => ({ doubled: r.n * 2 }))")
.expect("register");
let inputs = json!([
{ "n": 1 }, { "n": 2 }, { "n": 3 }, { "n": 4 }
]);
let out = ab.run_batch(&id, &inputs).expect("run_batch");
assert_eq!(
out,
json!([
{ "doubled": 2 }, { "doubled": 4 },
{ "doubled": 6 }, { "doubled": 8 }
])
);
}
#[test]
fn run_batch_rejects_non_array_input() {
let ab = Afterburner::new().expect("Afterburner::new");
let id = ab
.register("module.exports = (row) => row")
.expect("register");
let err = ab
.run_batch(&id, &json!({ "not": "an array" }))
.expect_err("non-array input should error");
let msg = format!("{err:?}");
assert!(
msg.to_lowercase().contains("array"),
"expected 'array' in error: {msg}"
);
}
#[test]
fn unload_then_re_register_works() {
let ab = Afterburner::new().expect("Afterburner::new");
let src = "module.exports = (d) => d.n";
let id = ab.register(src).expect("register");
assert_eq!(ab.run(&id, &json!({ "n": 1 })).expect("run"), json!(1));
ab.unload(&id);
let id2 = ab.register(src).expect("re-register");
assert_eq!(id, id2);
assert_eq!(ab.run(&id2, &json!({ "n": 2 })).expect("run"), json!(2));
}
#[test]
fn unload_unknown_id_is_noop() {
let ab = Afterburner::new().expect("Afterburner::new");
let fake = ScriptId {
hash: [0xff; 32],
mode: EngineMode::Wasm,
};
ab.unload(&fake);
}
#[test]
fn fuel_exhaustion_surfaces_typed_error() {
let ab = Afterburner::builder()
.fuel(1_000)
.build()
.expect("build with tight fuel");
let id = ab
.register(
"module.exports = (d) => {\n\
let x = 0;\n\
for (let i = 0; i < 10_000_000; i++) x += i;\n\
return x;\n\
}",
)
.expect("register");
let err = ab
.run(&id, &json!({}))
.expect_err("tight fuel + busy loop should exhaust");
assert!(
matches!(err, AfterburnerError::FuelExhausted) || matches!(err, AfterburnerError::Timeout),
"expected FuelExhausted or Timeout, got: {err:?}"
);
}
#[test]
fn sealed_manifold_blocks_fs_read() {
let ab = Afterburner::builder()
.manifold(Manifold {
fs: FsAccess::None,
..Manifold::sealed()
})
.build()
.expect("build");
let id = ab
.register(
"module.exports = (d) => {\n\
const fs = require('fs');\n\
try { fs.readFileSync('/etc/hostname'); return 'NO_THROW'; }\n\
catch (e) { return e.code || 'EOTHER'; }\n\
}",
)
.expect("register");
let out = ab.run(&id, &json!({})).expect("run");
assert_eq!(out, json!("EACCES"), "sealed fs must yield EACCES");
}
#[test]
fn null_input_is_passed_through() {
let ab = Afterburner::new().expect("Afterburner::new");
let id = ab
.register("module.exports = (d) => d === null ? 'null-ok' : 'wrong'")
.expect("register");
let out = ab.run(&id, &json!(null)).expect("run with null");
assert_eq!(out, json!("null-ok"));
}
#[test]
fn deeply_nested_input_round_trips() {
let ab = Afterburner::new().expect("Afterburner::new");
let id = ab.register("module.exports = (d) => d").expect("register");
let input = json!({
"users": [
{ "id": 1, "name": "alice", "tags": ["admin", "ops"] },
{ "id": 2, "name": "bob", "tags": ["dev"], "meta": { "level": 3 } },
],
"count": 2,
"nullable": null,
"negatives": [-1, -2, -3.5],
});
let out = ab.run(&id, &input).expect("identity transform");
assert_eq!(out, input);
}