use std::fs;
use std::sync::{Arc, Mutex};
use kinetik_embed::{Error, Kinetik, Value};
#[test]
fn embeds_script_registers_print_and_calls_start() {
let mut runtime = Kinetik::new();
let printed = Arc::new(Mutex::new(Vec::new()));
let captured = Arc::clone(&printed);
runtime.register_native("print", move |args| {
captured.lock().expect("print buffer lock").push(
args.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" "),
);
Ok(vec![Value::Nil])
});
let path = std::env::temp_dir().join(format!(
"kinetik_embed_{}_{}.kn",
std::process::id(),
line!()
));
fs::write(
&path,
r#"
function start(name) {
print("hello", name)
return 42
}
"#,
)
.expect("write script");
runtime.load_file(&path).expect("load script file");
let values = runtime
.call_function("start", &[Value::string("embed")])
.expect("call start");
fs::remove_file(path).expect("remove script");
assert_eq!(values, vec![Value::number(42.0)]);
assert_eq!(
printed.lock().expect("print buffer lock").as_slice(),
["hello embed"]
);
}
#[derive(Debug, Default)]
struct FakeEntity {
x: f64,
}
#[test]
fn attaches_entity_script_with_self_and_updates_position() {
let mut runtime = Kinetik::new();
let entity = Arc::new(Mutex::new(FakeEntity::default()));
let self_handle = runtime.insert_host(String::from("entity:player"));
runtime.define_global("self", self_handle.clone());
let expected_self = self_handle.clone();
let moved_entity = Arc::clone(&entity);
runtime.register_native("move_x", move |args| {
if args.first() != Some(&expected_self) {
return Err(String::from("move_x expected self handle"));
}
let Some(Value::Number(dx)) = args.get(1) else {
return Err(String::from("move_x expected number delta"));
};
moved_entity.lock().expect("entity lock").x += dx;
Ok(vec![Value::Nil])
});
runtime
.load_source(
"entity_demo.kn",
r"
function start() {
move_x(self, 1)
}
function update(dt) {
move_x(self, dt * 4)
}
",
)
.expect("load entity script");
runtime.call_function("start", &[]).expect("call start");
runtime
.call_function("update", &[Value::number(0.5)])
.expect("call update");
let x = entity.lock().expect("entity lock").x;
assert!((x - 3.0).abs() < f64::EPSILON);
}
#[test]
fn stale_host_handles_fail_safely_in_embedding_api() {
let mut runtime = Kinetik::new();
let handle = runtime.insert_host(String::from("entity:stale"));
assert_eq!(
runtime
.with_host::<String, _>(&handle, Clone::clone)
.expect("host is live"),
"entity:stale"
);
runtime.remove_host(&handle).expect("remove host");
assert!(matches!(
runtime.with_host::<String, _>(&handle, Clone::clone),
Err(Error::StaleHostHandle { .. })
));
}
#[test]
fn reload_source_rebinds_callbacks_and_keeps_old_script_after_parse_error() {
let mut runtime = Kinetik::new();
runtime
.load_source(
"player.kn",
r"
function update(dt) {
return dt + 1
}
",
)
.expect("load initial script");
assert_eq!(
runtime
.call_function("update", &[Value::number(1.0)])
.expect("call first update"),
vec![Value::number(2.0)]
);
runtime
.reload_source(
"player.kn",
r"
function update(dt) {
return dt + 2
}
",
)
.expect("reload script");
assert_eq!(
runtime
.call_function("update", &[Value::number(1.0)])
.expect("call reloaded update"),
vec![Value::number(3.0)]
);
let error = runtime
.reload_source("player.kn", "function update(dt) { return")
.expect_err("bad reload fails");
assert!(matches!(error, kinetik_embed::Error::Parse { .. }));
assert_eq!(
runtime
.call_function("update", &[Value::number(1.0)])
.expect("old update remains bound"),
vec![Value::number(3.0)]
);
}
#[test]
fn reload_source_preserves_compatible_state_and_reports_recreated_values() {
let mut runtime = Kinetik::new();
runtime
.load_source(
"state.kn",
r#"
let score = 1
let mode = "play"
function score_value() {
return score
}
function mode_value() {
return mode
}
"#,
)
.expect("load initial script");
runtime.define_global("score", Value::number(7.0));
runtime.define_global("mode", Value::string("paused"));
let report = runtime
.reload_source(
"state.kn",
r"
let score = 0
let mode = false
function score_value() {
return score
}
function mode_value() {
return mode
}
",
)
.expect("reload script");
assert_eq!(
runtime
.call_function("score_value", &[])
.expect("score survived"),
vec![Value::number(7.0)]
);
assert_eq!(
runtime
.call_function("mode_value", &[])
.expect("mode recreated"),
vec![Value::bool(false)]
);
assert_eq!(report.diagnostics.len(), 1);
assert!(report.diagnostics[0]
.message
.contains("reload recreated `mode`"));
}
#[test]
fn reload_source_cancels_stale_tasks_with_diagnostics() {
let mut runtime = Kinetik::new();
runtime
.load_source(
"tasks.kn",
r"
function work() {
task.yield(1)
return 2
}
let handle = task.spawn(work)
",
)
.expect("load task script");
let report = runtime
.reload_source(
"tasks.kn",
r"
function work() {
return 3
}
let handle = task.spawn(work)
",
)
.expect("reload task script");
assert!(report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.message.contains("stale task")));
}