kinetik-embed 0.1.0-alpha.0

Rust-native scripting language runtime and CLI for game engines.
Documentation
//! Integration tests for the Kinetik embedding API.

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")));
}