use super::{
EngineHandleJsonResult, EngineIdJsonRequest, EngineNewJsonRequest, FFI_ENGINE_COUNTER,
FfiEngineSlot, SkillConfigGetJsonRequest, SkillConfigListJsonRequest,
SkillConfigSetJsonRequest, encode_json_buffer, ffi_engine_registry, lock_ffi_engine_registry,
luaskills_ffi_call_skill_json, luaskills_ffi_describe_json, luaskills_ffi_engine_free_json,
luaskills_ffi_engine_new_json, luaskills_ffi_is_skill_json, luaskills_ffi_list_entries_json,
luaskills_ffi_list_skill_help_json, luaskills_ffi_managed_runtime_resolve_json,
luaskills_ffi_managed_session_events_poll_json, luaskills_ffi_managed_session_events_wait_json,
luaskills_ffi_prompt_argument_completions_json, luaskills_ffi_render_skill_help_detail_json,
luaskills_ffi_run_lua_json, luaskills_ffi_runtime_lease_close_json,
luaskills_ffi_runtime_lease_create_json, luaskills_ffi_runtime_lease_eval_json,
luaskills_ffi_runtime_lease_list_json, luaskills_ffi_skill_config_delete_json,
luaskills_ffi_skill_config_get_json, luaskills_ffi_skill_config_list_json,
luaskills_ffi_skill_config_set_json, luaskills_ffi_skill_name_for_tool_json,
luaskills_ffi_system_private_install_skill_from_url_manifest_json,
luaskills_ffi_system_runtime_lease_close_json, luaskills_ffi_system_runtime_lease_create_json,
luaskills_ffi_system_runtime_lease_eval_json, luaskills_ffi_system_runtime_lease_list_json,
luaskills_ffi_system_runtime_lease_status_json, with_engine, with_engine_mut,
};
use crate::ffi_standard::{
FfiBorrowedBuffer, FfiOwnedBuffer, luaskills_ffi_buffer_clone, luaskills_ffi_buffer_free,
luaskills_ffi_engine_free, luaskills_ffi_managed_session_events_poll,
luaskills_ffi_managed_session_events_wait, luaskills_ffi_set_managed_session_wake_callback,
luaskills_ffi_system_runtime_lease_close, luaskills_ffi_system_runtime_lease_create,
luaskills_ffi_system_runtime_lease_eval,
};
use crate::runtime::managed_session_events::{
ManagedSessionEventCenter, ManagedSessionEventToken, RuntimeManagedSessionEventKind,
};
use crate::runtime::render_host_visible_path;
use crate::{
LuaEngine, LuaEngineOptions, LuaRuntimeHostOptions, LuaVmPoolConfig, RuntimeSkillRoot,
SkillManagementAuthority,
};
use serde::ser::{Serialize, Serializer};
use std::ffi::{CString, c_void};
use std::panic::{self, AssertUnwindSafe};
use std::path::{Path, PathBuf};
use std::ptr;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Barrier, Mutex, MutexGuard, OnceLock, TryLockError};
use std::thread;
use std::time::{Duration, Instant};
unsafe fn decode_response_json(buffer: FfiOwnedBuffer) -> serde_json::Value {
let bytes = if buffer.ptr.is_null() {
assert_eq!(buffer.len, 0, "null response pointer must have zero len");
&[][..]
} else {
unsafe { std::slice::from_raw_parts(buffer.ptr, buffer.len) }
};
let text = std::str::from_utf8(bytes).expect("ffi json must be utf-8");
let value = serde_json::from_str(text).expect("ffi json must parse");
unsafe { luaskills_ffi_buffer_free(buffer) };
value
}
unsafe fn take_owned_buffer_text(buffer: FfiOwnedBuffer) -> String {
let text = if buffer.ptr.is_null() {
assert_eq!(buffer.len, 0, "null owned buffer must have zero len");
String::new()
} else {
let bytes = unsafe { std::slice::from_raw_parts(buffer.ptr, buffer.len) };
std::str::from_utf8(bytes)
.expect("owned FFI buffer must be utf-8")
.to_string()
};
unsafe { luaskills_ffi_buffer_free(buffer) };
text
}
struct TestManagedSessionWakeState {
callback_count: AtomicUsize,
last_engine_id: AtomicU64,
}
struct TestRetryingManagedSessionWakeState {
callback_count: AtomicUsize,
}
unsafe extern "C" fn reject_first_managed_session_wake(
_engine_id: u64,
user_data: *mut c_void,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
if !error_out.is_null() {
unsafe {
*error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
}
}
if user_data.is_null() {
return 1;
}
let state = unsafe { &*(user_data.cast::<TestRetryingManagedSessionWakeState>()) };
let attempt = state.callback_count.fetch_add(1, Ordering::AcqRel) + 1;
if attempt != 1 {
return 0;
}
let diagnostic = b"forced host scheduler rejection";
let mut clone_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let clone_status = unsafe {
luaskills_ffi_buffer_clone(
diagnostic.as_ptr(),
diagnostic.len(),
error_out,
&mut clone_error,
)
};
if clone_status != 0 && !error_out.is_null() {
unsafe {
*error_out = clone_error;
}
} else {
unsafe { luaskills_ffi_buffer_free(clone_error) };
}
1
}
unsafe extern "C" fn record_managed_session_wake(
engine_id: u64,
user_data: *mut c_void,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
if !error_out.is_null() {
unsafe {
*error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
}
}
if user_data.is_null() {
return 1;
}
let state = unsafe { &*(user_data.cast::<TestManagedSessionWakeState>()) };
state.last_engine_id.store(engine_id, Ordering::Release);
state.callback_count.fetch_add(1, Ordering::AcqRel);
0
}
struct TestWakePollState {
callback_count: AtomicUsize,
poll_status: AtomicI32,
}
unsafe extern "C" fn poll_managed_session_events_from_wake(
engine_id: u64,
user_data: *mut c_void,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
if !error_out.is_null() {
unsafe {
*error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
}
}
if user_data.is_null() {
return 1;
}
let state = unsafe { &*(user_data.cast::<TestWakePollState>()) };
let mut result_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut poll_error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let status = unsafe {
luaskills_ffi_managed_session_events_poll(
engine_id,
1,
&mut result_out,
&mut poll_error_out,
)
};
state.poll_status.store(status, Ordering::Release);
state.callback_count.fetch_add(1, Ordering::AcqRel);
unsafe { luaskills_ffi_buffer_free(result_out) };
unsafe { luaskills_ffi_buffer_free(poll_error_out) };
0
}
struct TestBlockingWakeState {
engine_id: u64,
entered_tx: std::sync::mpsc::Sender<()>,
release: Arc<Barrier>,
should_poll: AtomicBool,
poll_status: AtomicI32,
}
unsafe extern "C" fn block_managed_session_wake_during_engine_free(
_engine_id: u64,
user_data: *mut c_void,
error_out: *mut FfiOwnedBuffer,
) -> i32 {
if !error_out.is_null() {
unsafe {
*error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
}
}
if user_data.is_null() {
return 1;
}
let state = unsafe { &*(user_data.cast::<TestBlockingWakeState>()) };
let _ = state.entered_tx.send(());
state.release.wait();
if state.should_poll.load(Ordering::Acquire) {
let mut result_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut poll_error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let status = unsafe {
luaskills_ffi_managed_session_events_poll(
state.engine_id,
1,
&mut result_out,
&mut poll_error_out,
)
};
state.poll_status.store(status, Ordering::Release);
unsafe { luaskills_ffi_buffer_free(result_out) };
unsafe { luaskills_ffi_buffer_free(poll_error_out) };
}
0
}
fn register_test_managed_session_event_source(
engine_id: u64,
managed_session_id: u64,
) -> (Arc<ManagedSessionEventCenter>, ManagedSessionEventToken) {
let event_center = super::clone_managed_session_event_center(engine_id)
.expect("clone managed session event center");
let token =
ManagedSessionEventToken::new("ffi-system-lease", "ffi-system-sid", 7, managed_session_id);
event_center
.register_session(token.clone())
.expect("register managed session event token");
(event_center, token)
}
fn borrowed_json_buffer(value: &CString) -> FfiBorrowedBuffer {
let bytes = value.as_bytes();
FfiBorrowedBuffer {
ptr: bytes.as_ptr(),
len: bytes.len(),
}
}
fn ffi_test_guard() -> MutexGuard<'static, ()> {
static TEST_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
match TEST_MUTEX.get_or_init(|| Mutex::new(())).lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
struct FailingJsonSerialize;
impl Serialize for FailingJsonSerialize {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Err(serde::ser::Error::custom("quoted \"serializer\" failure"))
}
}
#[test]
fn encode_json_buffer_serialization_failure_returns_escaped_error_envelope() {
let response = unsafe { decode_response_json(encode_json_buffer(&FailingJsonSerialize)) };
assert_eq!(response["ok"], false);
assert_eq!(
response["error"],
"Failed to serialize FFI response: quoted \"serializer\" failure"
);
assert!(response.get("result").is_none());
}
struct TestFfiEngineHandle {
engine_id: u64,
}
impl Drop for TestFfiEngineHandle {
fn drop(&mut self) {
drop(super::remove_ffi_engine_slot(self.engine_id));
}
}
fn register_test_engine() -> TestFfiEngineHandle {
let engine = LuaEngine::new(LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
crate::LuaRuntimeHostOptions::default(),
))
.expect("create ffi test engine");
let engine_id = FFI_ENGINE_COUNTER.fetch_add(1, Ordering::Relaxed);
lock_ffi_engine_registry().insert(engine_id, FfiEngineSlot::new(engine));
TestFfiEngineHandle { engine_id }
}
struct TestSystemFfiEngineHandle {
engine_id: u64,
runtime_root: PathBuf,
system_lua_lib_dir: PathBuf,
package_id: String,
package_root: PathBuf,
dependencies_file: PathBuf,
}
impl TestSystemFfiEngineHandle {
fn package_request_json(&self) -> serde_json::Value {
serde_json::json!({
"id": self.package_id.as_str(),
"root": render_host_visible_path(&self.package_root),
"dependencies_file": "dependencies.yaml"
})
}
fn expected_package_json(&self) -> serde_json::Value {
serde_json::json!({
"id": self.package_id.as_str(),
"root": render_host_visible_path(&self.package_root),
"dependencies_file": render_host_visible_path(&self.dependencies_file)
})
}
}
impl Drop for TestSystemFfiEngineHandle {
fn drop(&mut self) {
drop(super::remove_ffi_engine_slot(self.engine_id));
let _ = std::fs::remove_dir_all(&self.runtime_root);
}
}
fn register_system_test_engine(label: &str) -> TestSystemFfiEngineHandle {
let engine_id = FFI_ENGINE_COUNTER.fetch_add(1, Ordering::Relaxed);
let runtime_root = std::env::temp_dir().join(format!(
"luaskills-ffi-system-{label}-{}-{engine_id}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&runtime_root);
let system_lua_lib_dir = runtime_root.join("system_lua_lib");
let package_id = "vulcan-debug".to_string();
let package_root = system_lua_lib_dir.join(&package_id);
let dependencies_file = package_root.join("dependencies.yaml");
std::fs::create_dir_all(&package_root).expect("create strict System Plugin package root");
std::fs::write(&dependencies_file, "{}\n")
.expect("write strict System Plugin dependency manifest");
let canonical_runtime_root =
std::fs::canonicalize(&runtime_root).expect("canonicalize System FFI runtime root");
let canonical_system_lua_lib_dir =
std::fs::canonicalize(&system_lua_lib_dir).expect("canonicalize System FFI trust root");
let canonical_package_root =
std::fs::canonicalize(&package_root).expect("canonicalize System FFI package root");
let canonical_dependencies_file = std::fs::canonicalize(&dependencies_file)
.expect("canonicalize System FFI dependency manifest");
let engine = LuaEngine::new(LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
crate::LuaRuntimeHostOptions {
runtime_root: Some(canonical_runtime_root.clone()),
system_lua_lib_dir: Some(canonical_system_lua_lib_dir.clone()),
..Default::default()
},
))
.expect("create strict System FFI test engine");
lock_ffi_engine_registry().insert(engine_id, FfiEngineSlot::new(engine));
TestSystemFfiEngineHandle {
engine_id,
runtime_root: canonical_runtime_root,
system_lua_lib_dir: canonical_system_lua_lib_dir,
package_id,
package_root: canonical_package_root,
dependencies_file: canonical_dependencies_file,
}
}
fn invoke_system_create_json(request: serde_json::Value) -> serde_json::Value {
let request = CString::new(request.to_string()).expect("encode strict System create request");
unsafe {
decode_response_json(luaskills_ffi_system_runtime_lease_create_json(
borrowed_json_buffer(&request),
))
}
}
type StandardSystemLeaseCall =
unsafe extern "C" fn(u64, FfiBorrowedBuffer, *mut FfiOwnedBuffer, *mut FfiOwnedBuffer) -> i32;
fn invoke_standard_system_lease_call(
call: StandardSystemLeaseCall,
engine_id: u64,
request: serde_json::Value,
) -> serde_json::Value {
let request = CString::new(request.to_string()).expect("encode standard System lease request");
let mut result = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let status = unsafe {
call(
engine_id,
borrowed_json_buffer(&request),
&mut result,
&mut error,
)
};
let error_text = unsafe { take_owned_buffer_text(error) };
assert_eq!(status, 0, "standard System lease call failed: {error_text}");
assert!(error_text.is_empty(), "unexpected ABI error: {error_text}");
let result_text = unsafe { take_owned_buffer_text(result) };
serde_json::from_str(&result_text).expect("standard System lease result must be JSON")
}
fn run_standard_abi_managed_sidecar_integration(
runtime: crate::runtime::engine::tests::ManagedSessionTestRuntime,
) {
use crate::runtime::engine::tests::{
ManagedSessionSystemLayout, discover_host_managed_runtime, managed_session_open_lua,
};
let Some(host) = discover_host_managed_runtime(runtime) else {
return;
};
let runtime_label = match runtime {
crate::runtime::engine::tests::ManagedSessionTestRuntime::Python => "python",
crate::runtime::engine::tests::ManagedSessionTestRuntime::Node => "node",
};
let layout =
ManagedSessionSystemLayout::new(&format!("ffi-standard-{runtime_label}"), runtime, &host);
let engine = LuaEngine::new(LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
layout.host_options(),
))
.expect("create standard ABI managed-session engine");
let engine_id = FFI_ENGINE_COUNTER.fetch_add(1, Ordering::Relaxed);
lock_ffi_engine_registry().insert(engine_id, FfiEngineSlot::new(engine));
let _engine_handle = TestFfiEngineHandle { engine_id };
let created = invoke_standard_system_lease_call(
luaskills_ffi_system_runtime_lease_create,
engine_id,
layout.create_request(&format!("ffi-standard-{runtime_label}"), true),
);
assert_eq!(created["ok"], true);
let lease_id = created["lease_id"].as_str().expect("standard lease id");
let sid = created["sid"].as_str().expect("standard lease sid");
let generation = created["generation"]
.as_u64()
.expect("standard lease generation");
let opened = invoke_standard_system_lease_call(
luaskills_ffi_system_runtime_lease_eval,
engine_id,
serde_json::json!({
"lease_id": lease_id,
"sid": sid,
"generation": generation,
"code": managed_session_open_lua(&layout, "ffi_sidecar", 64 * 1024, &[])
}),
);
assert_eq!(opened["ok"], true);
assert!(opened["result"]["status"]["managed_session_id"].is_number());
let echoed = invoke_standard_system_lease_call(
luaskills_ffi_system_runtime_lease_eval,
engine_id,
serde_json::json!({
"lease_id": lease_id,
"sid": sid,
"generation": generation,
"code": r#"ffi_sidecar:write('{"action":"echo","value":"ffi-standard"}\n'); return ffi_sidecar:read({ timeout_ms = 5000, max_bytes = 65536, until_text = 'ffi-standard' })"#
}),
);
assert_eq!(echoed["ok"], true);
assert!(
echoed["result"]["stdout"]
.as_str()
.is_some_and(|stdout| stdout.contains("ffi-standard"))
);
let exited = invoke_standard_system_lease_call(
luaskills_ffi_system_runtime_lease_eval,
engine_id,
serde_json::json!({
"lease_id": lease_id,
"sid": sid,
"generation": generation,
"code": r#"ffi_sidecar:write('{"action":"exit"}\n'); local output = ffi_sidecar:read({ timeout_ms = 5000, max_bytes = 65536, until_text = '"event":"exit"' }); ffi_sidecar:close({ timeout_ms = 5000 }); return output"#
}),
);
assert_eq!(exited["ok"], true);
assert!(
exited["result"]["stdout"]
.as_str()
.is_some_and(|stdout| stdout.contains("\"event\":\"exit\""))
);
let closed = invoke_standard_system_lease_call(
luaskills_ffi_system_runtime_lease_close,
engine_id,
serde_json::json!({
"lease_id": lease_id,
"sid": sid,
"generation": generation
}),
);
assert_eq!(closed["closed"], true);
}
#[test]
fn ffi_standard_system_lease_executes_real_managed_python_sidecar() {
let _guard = ffi_test_guard();
run_standard_abi_managed_sidecar_integration(
crate::runtime::engine::tests::ManagedSessionTestRuntime::Python,
);
}
#[test]
fn ffi_standard_system_lease_executes_real_managed_node_sidecar() {
let _guard = ffi_test_guard();
run_standard_abi_managed_sidecar_integration(
crate::runtime::engine::tests::ManagedSessionTestRuntime::Node,
);
}
#[test]
fn ffi_runtime_session_json_preserves_vm_state() {
let _guard = ffi_test_guard();
let engine = register_test_engine();
let create_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"sid": "ffi-session-test",
"ttl_sec": 60
})
.to_string(),
)
.expect("create request");
let created = unsafe {
decode_response_json(luaskills_ffi_runtime_lease_create_json(
borrowed_json_buffer(&create_request),
))
};
assert_eq!(created["ok"], true);
assert_eq!(created["result"]["ok"], true);
let lease_id = created["result"]["lease_id"]
.as_str()
.expect("lease id")
.to_string();
let first_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"lease_id": lease_id,
"code": "counter = (counter or 0) + 1; return counter"
})
.to_string(),
)
.expect("first eval request");
let first = unsafe {
decode_response_json(luaskills_ffi_runtime_lease_eval_json(borrowed_json_buffer(
&first_request,
)))
};
assert_eq!(first["ok"], true);
assert_eq!(first["result"]["ok"], true);
assert_eq!(first["result"]["result"], serde_json::json!(1));
let second_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"lease_id": lease_id,
"code": "counter = (counter or 0) + 1; return counter"
})
.to_string(),
)
.expect("second eval request");
let second = unsafe {
decode_response_json(luaskills_ffi_runtime_lease_eval_json(borrowed_json_buffer(
&second_request,
)))
};
assert_eq!(second["ok"], true);
assert_eq!(second["result"]["result"], serde_json::json!(2));
let close_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"lease_id": lease_id
})
.to_string(),
)
.expect("close request");
let closed = unsafe {
decode_response_json(luaskills_ffi_runtime_lease_close_json(
borrowed_json_buffer(&close_request),
))
};
assert_eq!(closed["ok"], true);
assert_eq!(closed["result"]["closed"], true);
}
#[test]
fn ffi_runtime_session_json_lists_active_leases() {
let _guard = ffi_test_guard();
let engine = register_test_engine();
let alpha_create_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"sid": "ffi-alpha-session",
"ttl_sec": 60
})
.to_string(),
)
.expect("alpha create request");
let alpha_created = unsafe {
decode_response_json(luaskills_ffi_runtime_lease_create_json(
borrowed_json_buffer(&alpha_create_request),
))
};
assert_eq!(alpha_created["ok"], true);
let alpha_lease_id = alpha_created["result"]["lease_id"]
.as_str()
.expect("alpha lease id")
.to_string();
let beta_create_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"sid": "ffi-beta-session",
"ttl_sec": 60
})
.to_string(),
)
.expect("beta create request");
let beta_created = unsafe {
decode_response_json(luaskills_ffi_runtime_lease_create_json(
borrowed_json_buffer(&beta_create_request),
))
};
assert_eq!(beta_created["ok"], true);
let list_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id
})
.to_string(),
)
.expect("list request");
let listed = unsafe {
decode_response_json(luaskills_ffi_runtime_lease_list_json(borrowed_json_buffer(
&list_request,
)))
};
assert_eq!(listed["ok"], true);
assert_eq!(listed["result"]["ok"], true);
assert_eq!(listed["result"]["leases"].as_array().map(Vec::len), Some(2));
let close_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"lease_id": alpha_lease_id
})
.to_string(),
)
.expect("alpha close request");
let closed = unsafe {
decode_response_json(luaskills_ffi_runtime_lease_close_json(
borrowed_json_buffer(&close_request),
))
};
assert_eq!(closed["ok"], true);
let filtered_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"sid": "ffi-alpha-session"
})
.to_string(),
)
.expect("filtered list request");
let filtered = unsafe {
decode_response_json(luaskills_ffi_runtime_lease_list_json(borrowed_json_buffer(
&filtered_request,
)))
};
assert_eq!(filtered["ok"], true);
assert_eq!(
filtered["result"]["leases"].as_array().map(Vec::len),
Some(0)
);
}
#[test]
fn ffi_runtime_session_json_rejects_generation_mismatch() {
let _guard = ffi_test_guard();
let engine = register_test_engine();
let create_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"sid": "ffi-generation-session",
"ttl_sec": 60
})
.to_string(),
)
.expect("generation create request");
let created = unsafe {
decode_response_json(luaskills_ffi_runtime_lease_create_json(
borrowed_json_buffer(&create_request),
))
};
assert_eq!(created["ok"], true);
let lease_id = created["result"]["lease_id"]
.as_str()
.expect("generation lease id")
.to_string();
let sid = created["result"]["sid"]
.as_str()
.expect("generation sid")
.to_string();
let eval_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"lease_id": lease_id,
"sid": sid,
"generation": 999_u64,
"code": "return 1"
})
.to_string(),
)
.expect("generation eval request");
let eval = unsafe {
decode_response_json(luaskills_ffi_runtime_lease_eval_json(borrowed_json_buffer(
&eval_request,
)))
};
assert_eq!(eval["ok"], true);
assert_eq!(eval["result"]["ok"], false);
assert_eq!(
eval["result"]["error_code"],
serde_json::json!("lease_generation_mismatch")
);
}
#[test]
fn ffi_describe_json_lists_system_runtime_session_exports() {
let described = unsafe { decode_response_json(luaskills_ffi_describe_json()) };
assert_eq!(described["ok"], true);
let exported = described["result"]["exported_functions"]
.as_array()
.expect("exported_functions array");
let exported_names: Vec<&str> = exported
.iter()
.filter_map(serde_json::Value::as_str)
.collect();
assert!(exported_names.contains(&"luaskills_ffi_system_runtime_lease_create_json"));
assert!(exported_names.contains(&"luaskills_ffi_system_runtime_lease_eval_json"));
assert!(exported_names.contains(&"luaskills_ffi_system_runtime_lease_status_json"));
assert!(exported_names.contains(&"luaskills_ffi_system_runtime_lease_list_json"));
assert!(exported_names.contains(&"luaskills_ffi_system_runtime_lease_close_json"));
assert!(exported_names.contains(&"luaskills_ffi_managed_session_events_poll"));
assert!(exported_names.contains(&"luaskills_ffi_managed_session_events_wait"));
assert!(exported_names.contains(&"luaskills_ffi_set_managed_session_wake_callback"));
assert!(exported_names.contains(&"luaskills_ffi_managed_session_events_poll_json"));
assert!(exported_names.contains(&"luaskills_ffi_managed_session_events_wait_json"));
assert!(
exported_names
.contains(&"luaskills_ffi_system_private_install_skill_from_url_manifest_json")
);
assert!(
exported_names
.contains(&"luaskills_ffi_system_private_update_skill_from_url_manifest_json")
);
assert!(exported_names.contains(&"luaskills_ffi_engine_new_v3"));
assert!(exported_names.contains(&"luaskills_ffi_managed_runtime_resolve_json"));
}
#[test]
fn ffi_managed_runtime_resolve_json_returns_descriptor() {
let root = std::env::temp_dir().join(format!(
"luaskills_ffi_managed_runtime_resolve_test_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
let distribution_root = root.join("application assets").join("runtimes");
let install_root = distribution_root
.join("node")
.join("node-24.18.0-linux-x64");
let executable = install_root.join("bin").join("node");
std::fs::create_dir_all(executable.parent().expect("Node executable parent"))
.expect("create JSON resolver install root");
std::fs::write(&executable, b"node executable fixture")
.expect("write JSON resolver executable");
let manifest = serde_json::json!({
"schema_version": 1,
"runtime": "node",
"version": "24.18.0",
"platform": "linux-x64",
"executable": "bin/node"
});
std::fs::write(
install_root.join("runtime-manifest.json"),
serde_json::to_vec_pretty(&manifest).expect("encode JSON resolver manifest"),
)
.expect("write JSON resolver manifest");
let request = CString::new(
serde_json::json!({
"distribution_root": distribution_root,
"runtime": "node",
"version": "24.18.0",
"platform": "linux-x64"
})
.to_string(),
)
.expect("encode managed runtime resolver request");
let response = unsafe {
decode_response_json(luaskills_ffi_managed_runtime_resolve_json(
borrowed_json_buffer(&request),
))
};
assert_eq!(response["ok"], true);
assert_eq!(response["result"]["runtime"], "node");
assert_eq!(response["result"]["version"], "24.18.0");
assert_eq!(response["result"]["platform"], "linux-x64");
assert_eq!(
PathBuf::from(
response["result"]["install_root"]
.as_str()
.expect("descriptor install_root string")
),
std::fs::canonicalize(&install_root).expect("canonical JSON resolver install root")
);
assert_eq!(
response["result"]["manifest_hash"].as_str().map(str::len),
Some(64)
);
assert_eq!(
response["result"]["executable_hash"].as_str().map(str::len),
Some(64)
);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn ffi_engine_new_json_accepts_managed_runtime_roots() {
let _guard = ffi_test_guard();
let root = std::env::temp_dir().join(format!(
"luaskills_ffi_engine_managed_roots_test_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
let runtime_root = root.join("runtime data");
let distribution_root = root.join("application assets").join("runtimes");
let environment_root = root.join("user data").join("managed envs");
std::fs::create_dir_all(&runtime_root).expect("create JSON engine runtime root");
std::fs::create_dir_all(&distribution_root).expect("create JSON engine distribution root");
let mut host_options = LuaRuntimeHostOptions::with_runtime_root(&runtime_root);
host_options.managed_runtime_distribution_root = Some(distribution_root.clone());
host_options.managed_runtime_environment_root = Some(environment_root.clone());
let request = EngineNewJsonRequest {
options: LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
host_options,
),
};
let request = CString::new(
serde_json::to_string(&request).expect("encode JSON engine managed root request"),
)
.expect("build JSON engine managed root request");
let response = unsafe {
decode_response_json(luaskills_ffi_engine_new_json(borrowed_json_buffer(
&request,
)))
};
assert_eq!(response["ok"], true);
let engine_id = response["result"]["engine_id"]
.as_u64()
.expect("JSON managed root engine id");
assert!(environment_root.is_dir());
let free_request = CString::new(serde_json::json!({ "engine_id": engine_id }).to_string())
.expect("encode JSON managed root free request");
let free_response = unsafe {
decode_response_json(luaskills_ffi_engine_free_json(borrowed_json_buffer(
&free_request,
)))
};
assert_eq!(free_response["ok"], true);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn ffi_managed_session_events_json_are_strict_bounded_and_explicit() {
let _guard = ffi_test_guard();
let engine = register_test_engine();
let (event_center, token) = register_test_managed_session_event_source(engine.engine_id, 41);
event_center
.publish(&token, RuntimeManagedSessionEventKind::StdoutReadable)
.expect("publish stdout event");
event_center
.publish(&token, RuntimeManagedSessionEventKind::StderrReadable)
.expect("publish stderr event");
let poll_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"max_events": 1,
"authority": SkillManagementAuthority::System
})
.to_string(),
)
.expect("managed session poll request");
let polled = unsafe {
decode_response_json(luaskills_ffi_managed_session_events_poll_json(
borrowed_json_buffer(&poll_request),
))
};
assert_eq!(polled["ok"], true);
assert_eq!(polled["result"]["remaining"], 1);
assert_eq!(polled["result"]["timed_out"], false);
assert_eq!(polled["result"]["events"].as_array().map(Vec::len), Some(1));
assert_eq!(
polled["result"]["events"][0]["system_lease_id"],
"ffi-system-lease"
);
assert_eq!(polled["result"]["events"][0]["sid"], "ffi-system-sid");
assert_eq!(polled["result"]["events"][0]["generation"], 7);
assert_eq!(polled["result"]["events"][0]["managed_session_id"], 41);
assert_eq!(polled["result"]["events"][0]["kind"], "stdout_readable");
assert_eq!(polled["result"]["events"][0]["sequence"], 1);
let ready_wait_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"max_events": 4,
"timeout_ms": 0,
"authority": SkillManagementAuthority::DelegatedTool
})
.to_string(),
)
.expect("managed session ready wait request");
let ready_wait = unsafe {
decode_response_json(luaskills_ffi_managed_session_events_wait_json(
borrowed_json_buffer(&ready_wait_request),
))
};
assert_eq!(ready_wait["ok"], true);
assert_eq!(ready_wait["result"]["remaining"], 0);
assert_eq!(ready_wait["result"]["timed_out"], false);
assert_eq!(ready_wait["result"]["events"][0]["kind"], "stderr_readable");
assert_eq!(ready_wait["result"]["events"][0]["sequence"], 2);
let empty_wait = unsafe {
decode_response_json(luaskills_ffi_managed_session_events_wait_json(
borrowed_json_buffer(&ready_wait_request),
))
};
assert_eq!(empty_wait["ok"], true);
assert_eq!(empty_wait["result"]["events"], serde_json::json!([]));
assert_eq!(empty_wait["result"]["remaining"], 0);
assert_eq!(empty_wait["result"]["timed_out"], true);
let unknown_field_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"max_events": 1,
"authority": SkillManagementAuthority::System,
"unexpected": true
})
.to_string(),
)
.expect("managed session unknown-field request");
let unknown_field_response = unsafe {
decode_response_json(luaskills_ffi_managed_session_events_poll_json(
borrowed_json_buffer(&unknown_field_request),
))
};
assert_eq!(unknown_field_response["ok"], false);
assert!(
unknown_field_response["error"]
.as_str()
.expect("unknown-field error")
.contains("unknown field")
);
let missing_authority_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"max_events": 1
})
.to_string(),
)
.expect("managed session missing-authority request");
let missing_authority_response = unsafe {
decode_response_json(luaskills_ffi_managed_session_events_poll_json(
borrowed_json_buffer(&missing_authority_request),
))
};
assert_eq!(missing_authority_response["ok"], false);
assert!(
missing_authority_response["error"]
.as_str()
.expect("missing-authority error")
.contains("requires host-injected authority")
);
let invalid_authority_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"max_events": 1,
"authority": "invalid_authority"
})
.to_string(),
)
.expect("managed session invalid-authority request");
let invalid_authority_response = unsafe {
decode_response_json(luaskills_ffi_managed_session_events_poll_json(
borrowed_json_buffer(&invalid_authority_request),
))
};
assert_eq!(invalid_authority_response["ok"], false);
assert!(
invalid_authority_response["error"]
.as_str()
.expect("invalid-authority error")
.contains("invalid_authority")
);
event_center
.close()
.expect("close managed session event center");
let closed_response = unsafe {
decode_response_json(luaskills_ffi_managed_session_events_poll_json(
borrowed_json_buffer(&poll_request),
))
};
assert_eq!(closed_response["ok"], false);
assert!(
closed_response["error"]
.as_str()
.expect("closed event-center error")
.contains("managed session event center is closed")
);
let closed_wait_response = unsafe {
decode_response_json(luaskills_ffi_managed_session_events_wait_json(
borrowed_json_buffer(&ready_wait_request),
))
};
assert_eq!(closed_wait_response["ok"], false);
assert!(
closed_wait_response["error"]
.as_str()
.expect("closed wait event-center error")
.contains("managed session event center is closed")
);
}
#[test]
fn ffi_managed_session_events_standard_abi_and_wake_callback_work() {
let _guard = ffi_test_guard();
let engine = register_test_engine();
let (event_center, token) = register_test_managed_session_event_source(engine.engine_id, 73);
let callback_state = TestManagedSessionWakeState {
callback_count: AtomicUsize::new(0),
last_engine_id: AtomicU64::new(0),
};
let mut callback_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let set_status = unsafe {
luaskills_ffi_set_managed_session_wake_callback(
engine.engine_id,
Some(record_managed_session_wake),
(&callback_state as *const TestManagedSessionWakeState)
.cast_mut()
.cast::<c_void>(),
&mut callback_error,
)
};
assert_eq!(set_status, 0);
assert!(callback_error.ptr.is_null());
event_center
.publish(&token, RuntimeManagedSessionEventKind::Exited)
.expect("publish exited event");
let callback_deadline = std::time::Instant::now() + Duration::from_secs(2);
while callback_state.callback_count.load(Ordering::Acquire) < 1
&& std::time::Instant::now() < callback_deadline
{
std::thread::yield_now();
}
assert_eq!(callback_state.callback_count.load(Ordering::Acquire), 1);
assert_eq!(
callback_state.last_engine_id.load(Ordering::Acquire),
engine.engine_id
);
let mut poll_result = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut poll_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let poll_status = unsafe {
luaskills_ffi_managed_session_events_poll(
engine.engine_id,
1,
&mut poll_result,
&mut poll_error,
)
};
assert_eq!(poll_status, 0);
assert!(poll_error.ptr.is_null());
let polled = unsafe { decode_response_json(poll_result) };
assert_eq!(polled["events"][0]["kind"], "exited");
assert_eq!(polled["remaining"], 0);
assert_eq!(polled["timed_out"], false);
let clear_status = unsafe {
luaskills_ffi_set_managed_session_wake_callback(
engine.engine_id,
None,
ptr::null_mut(),
&mut callback_error,
)
};
assert_eq!(clear_status, 0);
assert!(callback_error.ptr.is_null());
event_center
.publish(&token, RuntimeManagedSessionEventKind::Failed)
.expect("publish failed event after callback clear");
assert_eq!(callback_state.callback_count.load(Ordering::Acquire), 1);
let mut wait_result = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut wait_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let wait_status = unsafe {
luaskills_ffi_managed_session_events_wait(
engine.engine_id,
4,
0,
&mut wait_result,
&mut wait_error,
)
};
assert_eq!(wait_status, 0);
assert!(wait_error.ptr.is_null());
let waited = unsafe { decode_response_json(wait_result) };
assert_eq!(waited["events"][0]["kind"], "failed");
assert_eq!(waited["timed_out"], false);
let mut invalid_result = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let mut invalid_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let invalid_status = unsafe {
luaskills_ffi_managed_session_events_poll(
engine.engine_id,
0,
&mut invalid_result,
&mut invalid_error,
)
};
assert_eq!(invalid_status, 1);
assert!(invalid_result.ptr.is_null());
let invalid_error_text = unsafe { take_owned_buffer_text(invalid_error) };
assert!(invalid_error_text.contains("max_events must be greater than 0"));
}
#[test]
fn ffi_managed_session_wake_callback_failure_retries_same_edge() {
let _guard = ffi_test_guard();
let engine = register_test_engine();
let (event_center, token) = register_test_managed_session_event_source(engine.engine_id, 74);
let callback_state = TestRetryingManagedSessionWakeState {
callback_count: AtomicUsize::new(0),
};
let mut callback_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let set_status = unsafe {
luaskills_ffi_set_managed_session_wake_callback(
engine.engine_id,
Some(reject_first_managed_session_wake),
(&callback_state as *const TestRetryingManagedSessionWakeState)
.cast_mut()
.cast::<c_void>(),
&mut callback_error,
)
};
assert_eq!(set_status, 0);
unsafe { luaskills_ffi_buffer_free(callback_error) };
event_center
.publish(&token, RuntimeManagedSessionEventKind::StdoutReadable)
.expect("publish one retryable wake edge");
let deadline = std::time::Instant::now() + Duration::from_secs(2);
while callback_state.callback_count.load(Ordering::Acquire) < 2
&& std::time::Instant::now() < deadline
{
std::thread::yield_now();
}
assert_eq!(callback_state.callback_count.load(Ordering::Acquire), 2);
assert_eq!(
event_center
.poll(1)
.expect("poll retried wake edge")
.events
.len(),
1
);
let mut clear_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let clear_status = unsafe {
luaskills_ffi_set_managed_session_wake_callback(
engine.engine_id,
None,
ptr::null_mut(),
&mut clear_error,
)
};
assert_eq!(clear_status, 0);
unsafe { luaskills_ffi_buffer_free(clear_error) };
}
#[test]
fn ffi_managed_session_wake_callback_poll_bypasses_engine_mutex() {
let _guard = ffi_test_guard();
let engine = register_test_engine();
let (event_center, token) = register_test_managed_session_event_source(engine.engine_id, 121);
let callback_state = Box::new(TestWakePollState {
callback_count: AtomicUsize::new(0),
poll_status: AtomicI32::new(-1),
});
let mut callback_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let set_status = unsafe {
luaskills_ffi_set_managed_session_wake_callback(
engine.engine_id,
Some(poll_managed_session_events_from_wake),
(&*callback_state as *const TestWakePollState)
.cast_mut()
.cast::<c_void>(),
&mut callback_error,
)
};
assert_eq!(set_status, 0);
assert!(callback_error.ptr.is_null());
let engine_handle = super::clone_engine_handle(engine.engine_id).expect("clone engine handle");
let engine_guard = engine_handle
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let publishing_center = Arc::clone(&event_center);
let publishing_token = token.clone();
let (publish_done_tx, publish_done_rx) = std::sync::mpsc::channel();
let publisher = thread::spawn(move || {
let publish_result = publishing_center.publish(
&publishing_token,
RuntimeManagedSessionEventKind::StdoutReadable,
);
let _ = publish_done_tx.send(publish_result);
});
let first_publish_result = publish_done_rx.recv_timeout(Duration::from_secs(2));
let callback_deadline = std::time::Instant::now() + Duration::from_secs(2);
while callback_state.callback_count.load(Ordering::Acquire) == 0
&& std::time::Instant::now() < callback_deadline
{
std::thread::yield_now();
}
let completed_while_engine_locked =
first_publish_result.is_ok() && callback_state.callback_count.load(Ordering::Acquire) == 1;
drop(engine_guard);
let publish_result = match first_publish_result {
Ok(publish_result) => publish_result,
Err(_) => publish_done_rx
.recv_timeout(Duration::from_secs(1))
.expect("publisher should finish after engine mutex release"),
};
publisher.join().expect("join wake callback publisher");
let clear_status = unsafe {
luaskills_ffi_set_managed_session_wake_callback(
engine.engine_id,
None,
ptr::null_mut(),
&mut callback_error,
)
};
assert_eq!(clear_status, 0);
assert!(callback_error.ptr.is_null());
assert!(
completed_while_engine_locked,
"wake callback poll unexpectedly waited for the engine mutex"
);
assert_eq!(publish_result, Ok(true));
assert_eq!(callback_state.callback_count.load(Ordering::Acquire), 1);
assert_eq!(callback_state.poll_status.load(Ordering::Acquire), 0);
}
#[test]
fn ffi_engine_free_releases_registry_before_callback_quiescence() {
let _guard = ffi_test_guard();
let engine = register_test_engine();
let (event_center, token) = register_test_managed_session_event_source(engine.engine_id, 122);
let (entered_tx, entered_rx) = std::sync::mpsc::channel();
let release = Arc::new(Barrier::new(2));
let callback_state = Box::new(TestBlockingWakeState {
engine_id: engine.engine_id,
entered_tx,
release: Arc::clone(&release),
should_poll: AtomicBool::new(false),
poll_status: AtomicI32::new(-1),
});
let mut callback_error = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let set_status = unsafe {
luaskills_ffi_set_managed_session_wake_callback(
engine.engine_id,
Some(block_managed_session_wake_during_engine_free),
(&*callback_state as *const TestBlockingWakeState)
.cast_mut()
.cast::<c_void>(),
&mut callback_error,
)
};
assert_eq!(set_status, 0);
assert!(callback_error.ptr.is_null());
let publishing_center = Arc::clone(&event_center);
let publishing_token = token.clone();
let publisher = thread::spawn(move || {
publishing_center.publish(
&publishing_token,
RuntimeManagedSessionEventKind::StderrReadable,
)
});
entered_rx
.recv_timeout(Duration::from_secs(1))
.expect("wake callback should enter before engine free");
let engine_id = engine.engine_id;
let free_thread = thread::spawn(move || {
let mut error_out = FfiOwnedBuffer {
ptr: ptr::null_mut(),
len: 0,
};
let status = unsafe { luaskills_ffi_engine_free(engine_id, &mut error_out) };
let error_text = unsafe { take_owned_buffer_text(error_out) };
(status, error_text)
});
let deadline = Instant::now() + Duration::from_secs(1);
let mut removed_while_callback_inflight = false;
while Instant::now() < deadline {
let entry_is_absent = match ffi_engine_registry().try_lock() {
Ok(registry) => !registry.contains_key(&engine_id),
Err(TryLockError::Poisoned(poisoned)) => {
!poisoned.into_inner().contains_key(&engine_id)
}
Err(TryLockError::WouldBlock) => false,
};
if entry_is_absent {
removed_while_callback_inflight = true;
break;
}
thread::sleep(Duration::from_millis(5));
}
callback_state
.should_poll
.store(removed_while_callback_inflight, Ordering::Release);
release.wait();
let publish_result = publisher.join().expect("join blocking callback publisher");
let (free_status, free_error_text) = free_thread.join().expect("join engine free thread");
assert!(
removed_while_callback_inflight,
"engine free retained the registry lock while awaiting callback quiescence"
);
assert_eq!(publish_result, Ok(true));
assert_eq!(
free_status, 0,
"unexpected engine free error: {free_error_text}"
);
assert!(free_error_text.is_empty());
assert_eq!(
callback_state.poll_status.load(Ordering::Acquire),
1,
"callback poll should observe the removed engine without blocking"
);
}
#[test]
fn ffi_managed_session_events_wait_releases_registry_and_engine_locks() {
let _guard = ffi_test_guard();
let engine = register_test_engine();
let (event_center, token) = register_test_managed_session_event_source(engine.engine_id, 99);
let engine_id = engine.engine_id;
let (started_tx, started_rx) = std::sync::mpsc::channel();
let (done_tx, done_rx) = std::sync::mpsc::channel();
let waiter = thread::spawn(move || {
let request = CString::new(
serde_json::json!({
"engine_id": engine_id,
"max_events": 1,
"timeout_ms": 2_000,
"authority": SkillManagementAuthority::System
})
.to_string(),
)
.expect("blocking managed session wait request");
started_tx.send(()).expect("signal wait start");
let response = unsafe {
decode_response_json(luaskills_ffi_managed_session_events_wait_json(
borrowed_json_buffer(&request),
))
};
done_tx.send(response).expect("send wait response");
});
started_rx
.recv_timeout(Duration::from_secs(1))
.expect("wait thread should start");
thread::sleep(Duration::from_millis(100));
match done_rx.try_recv() {
Err(std::sync::mpsc::TryRecvError::Empty) => {}
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
panic!("event wait thread disconnected before lock verification")
}
Ok(response) => panic!("event wait returned before publication: {response}"),
}
let registry_guard = match ffi_engine_registry().try_lock() {
Ok(registry_guard) => registry_guard,
Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
Err(TryLockError::WouldBlock) => {
panic!("blocking event wait must release registry lock")
}
};
drop(registry_guard);
let engine_handle = super::clone_engine_handle(engine_id).expect("clone engine handle");
let engine_guard = match engine_handle.try_lock() {
Ok(engine_guard) => engine_guard,
Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
Err(TryLockError::WouldBlock) => {
panic!("blocking event wait must release engine lock")
}
};
drop(engine_guard);
event_center
.publish(&token, RuntimeManagedSessionEventKind::StdoutReadable)
.expect("wake blocking event wait");
let response = done_rx
.recv_timeout(Duration::from_secs(1))
.expect("blocking event wait should return after publish");
assert_eq!(response["ok"], true);
assert_eq!(response["result"]["timed_out"], false);
assert_eq!(response["result"]["events"][0]["managed_session_id"], 99);
waiter.join().expect("join blocking event waiter");
}
#[test]
fn ffi_private_url_manifest_json_requires_system_authority() {
let _guard = ffi_test_guard();
let engine = register_test_engine();
let request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"skill_roots": [{
"name": "ROOT",
"skills_dir": "D:/tmp/luaskills-root"
}],
"skill_id": "internal-skill",
"manifest_url": "https://internal.example.com/skills/internal-skill.json",
"authority": "delegated_tool"
})
.to_string(),
)
.expect("private manifest request");
let response = unsafe {
decode_response_json(
luaskills_ffi_system_private_install_skill_from_url_manifest_json(
borrowed_json_buffer(&request),
),
)
};
assert_eq!(response["ok"], false);
assert!(
response["error"]
.as_str()
.expect("private manifest authority error")
.contains("requires system authority")
);
}
#[test]
fn ffi_system_runtime_session_json_requires_authority() {
let _guard = ffi_test_guard();
let engine = register_system_test_engine("authority");
let created = invoke_system_create_json(serde_json::json!({
"engine_id": engine.engine_id,
"sid": "ffi-system-session",
"ttl_sec": 60,
"system_package": engine.package_request_json()
}));
assert_eq!(created["ok"], false);
assert!(
created["error"]
.as_str()
.expect("system runtime session error")
.contains("requires host-injected authority")
);
}
#[test]
fn ffi_system_runtime_session_json_requires_system_package() {
let _guard = ffi_test_guard();
let engine = register_system_test_engine("missing-package");
let created = invoke_system_create_json(serde_json::json!({
"engine_id": engine.engine_id,
"sid": "ffi-system-missing-package",
"ttl_sec": 60,
"authority": SkillManagementAuthority::System
}));
assert_eq!(created["ok"], false);
let error = created["error"]
.as_str()
.expect("missing System package error");
assert!(error.contains("missing field"), "unexpected error: {error}");
assert!(
error.contains("system_package"),
"unexpected error: {error}"
);
}
#[test]
fn ffi_system_runtime_session_json_rejects_missing_package_field() {
let _guard = ffi_test_guard();
let engine = register_system_test_engine("missing-package-field");
let created = invoke_system_create_json(serde_json::json!({
"engine_id": engine.engine_id,
"sid": "ffi-system-missing-package-field",
"ttl_sec": 60,
"system_package": {
"id": engine.package_id.as_str(),
"root": render_host_visible_path(&engine.package_root)
},
"authority": SkillManagementAuthority::System
}));
assert_eq!(created["ok"], false);
let error = created["error"]
.as_str()
.expect("missing System package field error");
assert!(error.contains("missing field"), "unexpected error: {error}");
assert!(
error.contains("dependencies_file"),
"unexpected error: {error}"
);
}
#[test]
fn ffi_system_runtime_session_json_rejects_public_module_roots() {
let _guard = ffi_test_guard();
let engine = register_system_test_engine("module-roots");
let created = invoke_system_create_json(serde_json::json!({
"engine_id": engine.engine_id,
"sid": "ffi-system-module-roots",
"ttl_sec": 60,
"lua_roots": [render_host_visible_path(&engine.runtime_root)],
"c_roots": [render_host_visible_path(&engine.runtime_root)],
"system_package": engine.package_request_json(),
"authority": SkillManagementAuthority::System
}));
assert_eq!(created["ok"], false);
let error = created["error"]
.as_str()
.expect("unknown System module-root field error");
assert!(error.contains("unknown field"), "unexpected error: {error}");
assert!(
error.contains("lua_roots") || error.contains("c_roots"),
"unexpected error: {error}"
);
}
#[test]
fn ffi_system_runtime_session_json_rejects_package_root_escape() {
let _guard = ffi_test_guard();
let engine = register_system_test_engine("package-root-escape");
let outside_package_root = engine.runtime_root.join("outside-system-package");
std::fs::create_dir_all(&outside_package_root).expect("create outside System package root");
std::fs::write(outside_package_root.join("dependencies.yaml"), "{}\n")
.expect("write outside System package manifest");
let created = invoke_system_create_json(serde_json::json!({
"engine_id": engine.engine_id,
"sid": "ffi-system-package-root-escape",
"ttl_sec": 60,
"system_package": {
"id": "outside-system-package",
"root": render_host_visible_path(&outside_package_root),
"dependencies_file": "dependencies.yaml"
},
"authority": SkillManagementAuthority::System
}));
assert_eq!(created["ok"], false);
let error = created["error"]
.as_str()
.expect("System package root containment error");
assert!(
error.contains("must be a strict descendant of system_lua_lib root"),
"unexpected error: {error}"
);
assert!(
!outside_package_root.starts_with(&engine.system_lua_lib_dir),
"escape fixture must remain outside the trust root"
);
}
#[test]
fn ffi_system_runtime_session_json_supports_delegated_wrapper_flow() {
let _guard = ffi_test_guard();
let engine = register_system_test_engine("delegated-flow");
let create_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"sid": "ffi-system-wrapper-session",
"ttl_sec": 60,
"replace": true,
"system_package": engine.package_request_json(),
"authority": SkillManagementAuthority::DelegatedTool
})
.to_string(),
)
.expect("system create request");
let created = unsafe {
decode_response_json(luaskills_ffi_system_runtime_lease_create_json(
borrowed_json_buffer(&create_request),
))
};
assert_eq!(created["ok"], true);
assert_eq!(created["result"]["ok"], true);
assert_eq!(
created["result"]["system_package"],
engine.expected_package_json()
);
let lease_id = created["result"]["lease_id"]
.as_str()
.expect("system lease id")
.to_string();
let sid = created["result"]["sid"]
.as_str()
.expect("system sid")
.to_string();
let generation = created["result"]["generation"]
.as_u64()
.expect("system generation");
let first_eval_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"lease_id": lease_id,
"sid": sid,
"generation": generation,
"code": "counter = (counter or 0) + 1; return counter",
"authority": SkillManagementAuthority::DelegatedTool
})
.to_string(),
)
.expect("system first eval request");
let first = unsafe {
decode_response_json(luaskills_ffi_system_runtime_lease_eval_json(
borrowed_json_buffer(&first_eval_request),
))
};
assert_eq!(first["ok"], true);
assert_eq!(first["result"]["result"], serde_json::json!(1));
let second_eval_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"lease_id": created["result"]["lease_id"],
"sid": created["result"]["sid"],
"generation": created["result"]["generation"],
"code": "counter = (counter or 0) + 1; return counter",
"authority": SkillManagementAuthority::DelegatedTool
})
.to_string(),
)
.expect("system second eval request");
let second = unsafe {
decode_response_json(luaskills_ffi_system_runtime_lease_eval_json(
borrowed_json_buffer(&second_eval_request),
))
};
assert_eq!(second["ok"], true);
assert_eq!(second["result"]["result"], serde_json::json!(2));
let status_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"lease_id": created["result"]["lease_id"],
"sid": created["result"]["sid"],
"generation": created["result"]["generation"],
"authority": SkillManagementAuthority::DelegatedTool
})
.to_string(),
)
.expect("system status request");
let status = unsafe {
decode_response_json(luaskills_ffi_system_runtime_lease_status_json(
borrowed_json_buffer(&status_request),
))
};
assert_eq!(status["ok"], true);
assert_eq!(
status["result"]["system_package"],
engine.expected_package_json()
);
let list_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"sid": created["result"]["sid"],
"authority": SkillManagementAuthority::DelegatedTool
})
.to_string(),
)
.expect("system list request");
let listed = unsafe {
decode_response_json(luaskills_ffi_system_runtime_lease_list_json(
borrowed_json_buffer(&list_request),
))
};
assert_eq!(listed["ok"], true);
assert_eq!(listed["result"]["leases"].as_array().map(Vec::len), Some(1));
assert_eq!(
listed["result"]["leases"][0]["system_package"],
engine.expected_package_json()
);
let close_request = CString::new(
serde_json::json!({
"engine_id": engine.engine_id,
"lease_id": created["result"]["lease_id"],
"sid": created["result"]["sid"],
"generation": created["result"]["generation"],
"authority": SkillManagementAuthority::DelegatedTool
})
.to_string(),
)
.expect("system close request");
let closed = unsafe {
decode_response_json(luaskills_ffi_system_runtime_lease_close_json(
borrowed_json_buffer(&close_request),
))
};
assert_eq!(closed["ok"], true);
assert_eq!(closed["result"]["closed"], true);
}
fn write_query_test_skill(skill_root: &Path, skill_id: &str) -> PathBuf {
let skill_dir = skill_root.join(skill_id);
std::fs::create_dir_all(skill_dir.join("runtime")).expect("create query runtime dir");
std::fs::create_dir_all(skill_dir.join("help")).expect("create query help dir");
std::fs::write(
skill_dir.join("skill.yaml"),
format!(
"name: {skill_id}\nversion: 0.1.0\nenable: true\ndebug: false\nhelp:\n main:\n description: Main help.\n file: help/main.md\nentries:\n - name: ping\n description: Query ping entry.\n lua_entry: runtime/ping.lua\n lua_module: {skill_id}.ping\n"
),
)
.expect("write query skill yaml");
std::fs::write(
skill_dir.join("runtime").join("ping.lua"),
"return function(args)\n return 'query-ok'\nend\n",
)
.expect("write query runtime entry");
std::fs::write(
skill_dir.join("help").join("main.md"),
format!("# {skill_id}\n\nQuery help.\n"),
)
.expect("write query help file");
skill_dir
}
fn write_query_schema_test_skill(skill_root: &Path, skill_id: &str) -> PathBuf {
let skill_dir = skill_root.join(skill_id);
std::fs::create_dir_all(skill_dir.join("runtime")).expect("create query schema runtime dir");
std::fs::create_dir_all(skill_dir.join("help")).expect("create query schema help dir");
std::fs::create_dir_all(skill_dir.join("schemas")).expect("create query schema schema dir");
std::fs::write(
skill_dir.join("skill.yaml"),
format!(
"name: {skill_id}\nversion: 0.1.0\nenable: true\ndebug: false\nhelp:\n main:\n description: Main help.\n file: help/main.md\nentries:\n - name: inspect\n description: Query schema entry.\n lua_entry: runtime/inspect.lua\n lua_module: {skill_id}.inspect\n input_schema_file: schemas/inspect.input.schema.json\n"
),
)
.expect("write query schema skill yaml");
std::fs::write(
skill_dir.join("schemas").join("inspect.input.schema.json"),
serde_json::to_string_pretty(&serde_json::json!({
"type": "object",
"additionalProperties": false,
"properties": {
"nodes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file": { "type": "string" },
"structural_path": { "type": "string" }
},
"required": ["file", "structural_path"]
}
}
},
"required": ["nodes"]
}))
.expect("serialize query schema input schema"),
)
.expect("write query schema input schema");
std::fs::write(
skill_dir.join("runtime").join("inspect.lua"),
"return function(args)\n return 'schema-query-ok'\nend\n",
)
.expect("write query schema runtime entry");
std::fs::write(
skill_dir.join("help").join("main.md"),
format!("# {skill_id}\n\nQuery help.\n"),
)
.expect("write query schema help file");
skill_dir
}
#[test]
fn ffi_query_json_filters_root_for_delegated_authority() {
let _guard = ffi_test_guard();
let temp_root = std::env::temp_dir().join(format!(
"luaskills_ffi_query_authority_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let root_root = RuntimeSkillRoot {
name: " ROOT ".to_string(),
skills_dir: temp_root.join("root_skills"),
};
let user_root = RuntimeSkillRoot {
name: "USER".to_string(),
skills_dir: temp_root.join("user_skills"),
};
write_query_test_skill(&root_root.skills_dir, "vulcan-root-skill");
write_query_test_skill(&user_root.skills_dir, "vulcan-user-skill");
let mut engine = LuaEngine::new(LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
crate::LuaRuntimeHostOptions::default(),
))
.expect("create ffi query test engine");
engine
.load_from_roots(&[root_root, user_root])
.expect("load query test roots");
let engine_id = FFI_ENGINE_COUNTER.fetch_add(1, Ordering::Relaxed);
lock_ffi_engine_registry().insert(engine_id, FfiEngineSlot::new(engine));
let _handle = TestFfiEngineHandle { engine_id };
let system_entries_request = CString::new(
serde_json::json!({
"engine_id": engine_id,
"authority": SkillManagementAuthority::System
})
.to_string(),
)
.expect("system entries request");
let system_entries = unsafe {
decode_response_json(luaskills_ffi_list_entries_json(borrowed_json_buffer(
&system_entries_request,
)))
};
assert_eq!(system_entries["ok"], true);
assert!(
system_entries["result"]
.as_array()
.expect("system entries array")
.iter()
.any(|entry| entry["root_name"] == " ROOT ")
);
let delegated_entries_request = CString::new(
serde_json::json!({
"engine_id": engine_id,
"authority": SkillManagementAuthority::DelegatedTool
})
.to_string(),
)
.expect("delegated entries request");
let delegated_entries = unsafe {
decode_response_json(luaskills_ffi_list_entries_json(borrowed_json_buffer(
&delegated_entries_request,
)))
};
assert_eq!(delegated_entries["ok"], true);
assert!(
delegated_entries["result"]
.as_array()
.expect("delegated entries array")
.iter()
.all(|entry| entry["root_name"]
.as_str()
.map(|root_name| !root_name.trim().eq_ignore_ascii_case("ROOT"))
.unwrap_or(false))
);
let delegated_help = unsafe {
decode_response_json(luaskills_ffi_list_skill_help_json(borrowed_json_buffer(
&delegated_entries_request,
)))
};
assert_eq!(delegated_help["ok"], true);
assert!(
delegated_help["result"]
.as_array()
.expect("delegated help array")
.iter()
.all(|help| help["root_name"]
.as_str()
.map(|root_name| !root_name.trim().eq_ignore_ascii_case("ROOT"))
.unwrap_or(false))
);
let delegated_detail_request = CString::new(
serde_json::json!({
"engine_id": engine_id,
"authority": SkillManagementAuthority::DelegatedTool,
"skill_id": "vulcan-root-skill",
"flow_name": "main"
})
.to_string(),
)
.expect("delegated detail request");
let delegated_detail = unsafe {
decode_response_json(luaskills_ffi_render_skill_help_detail_json(
borrowed_json_buffer(&delegated_detail_request),
))
};
assert_eq!(delegated_detail["ok"], true);
assert!(delegated_detail["result"].is_null());
let delegated_is_skill_request = CString::new(
serde_json::json!({
"engine_id": engine_id,
"authority": SkillManagementAuthority::DelegatedTool,
"tool_name": "vulcan-root-skill-ping"
})
.to_string(),
)
.expect("delegated is_skill request");
let delegated_is_skill = unsafe {
decode_response_json(luaskills_ffi_is_skill_json(borrowed_json_buffer(
&delegated_is_skill_request,
)))
};
assert_eq!(delegated_is_skill["ok"], true);
assert_eq!(delegated_is_skill["result"]["value"], false);
let delegated_skill_name = unsafe {
decode_response_json(luaskills_ffi_skill_name_for_tool_json(
borrowed_json_buffer(&delegated_is_skill_request),
))
};
assert_eq!(delegated_skill_name["ok"], true);
assert!(delegated_skill_name["result"]["skill_id"].is_null());
let root_call_request = CString::new(
serde_json::json!({
"engine_id": engine_id,
"tool_name": "vulcan-root-skill-ping",
"args": {}
})
.to_string(),
)
.expect("root call request");
let root_call = unsafe {
decode_response_json(luaskills_ffi_call_skill_json(borrowed_json_buffer(
&root_call_request,
)))
};
assert_eq!(root_call["ok"], true);
assert_eq!(root_call["result"]["content"], "query-ok");
let root_run_lua_request = CString::new(
serde_json::json!({
"engine_id": engine_id,
"code": "return vulcan.call('vulcan-root-skill-ping', {})",
"args": {}
})
.to_string(),
)
.expect("root run_lua request");
let root_run_lua = unsafe {
decode_response_json(luaskills_ffi_run_lua_json(borrowed_json_buffer(
&root_run_lua_request,
)))
};
assert_eq!(root_run_lua["ok"], true);
assert_eq!(root_run_lua["result"], "query-ok");
let delegated_prompt_request = CString::new(
serde_json::json!({
"engine_id": engine_id,
"authority": SkillManagementAuthority::DelegatedTool,
"prompt_name": "demo",
"argument_name": "target"
})
.to_string(),
)
.expect("delegated prompt request");
let delegated_prompt = unsafe {
decode_response_json(luaskills_ffi_prompt_argument_completions_json(
borrowed_json_buffer(&delegated_prompt_request),
))
};
assert_eq!(delegated_prompt["ok"], true);
assert!(delegated_prompt["result"].is_null());
let missing_prompt_authority_request = CString::new(
serde_json::json!({
"engine_id": engine_id,
"prompt_name": "demo",
"argument_name": "target"
})
.to_string(),
)
.expect("missing prompt authority request");
let missing_prompt_authority = unsafe {
decode_response_json(luaskills_ffi_prompt_argument_completions_json(
borrowed_json_buffer(&missing_prompt_authority_request),
))
};
assert_eq!(missing_prompt_authority["ok"], false);
assert!(
missing_prompt_authority["error"]
.as_str()
.expect("missing prompt authority error")
.contains("requires host-injected authority")
);
let missing_authority_request = CString::new(
serde_json::json!({
"engine_id": engine_id
})
.to_string(),
)
.expect("missing authority request");
let missing_authority = unsafe {
decode_response_json(luaskills_ffi_list_entries_json(borrowed_json_buffer(
&missing_authority_request,
)))
};
assert_eq!(missing_authority["ok"], false);
assert!(
missing_authority["error"]
.as_str()
.expect("missing authority error")
.contains("requires host-injected authority")
);
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn ffi_list_entries_json_exposes_resolved_input_schema() {
let _guard = ffi_test_guard();
let temp_root = std::env::temp_dir().join(format!(
"luaskills_ffi_query_schema_test_{}",
std::process::id()
));
if temp_root.exists() {
let _ = std::fs::remove_dir_all(&temp_root);
}
let root_root = RuntimeSkillRoot {
name: "ROOT".to_string(),
skills_dir: temp_root.join("root_skills"),
};
write_query_schema_test_skill(&root_root.skills_dir, "vulcan-schema-skill");
let mut engine = LuaEngine::new(LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
crate::LuaRuntimeHostOptions {
runtime_root: None,
temp_dir: Some(temp_root.join("temp")),
resources_dir: Some(temp_root.join("resources")),
lua_packages_dir: Some(temp_root.join("lua_packages")),
host_provided_tool_root: Some(temp_root.join("bin").join("tools")),
host_provided_lua_root: Some(temp_root.join("lua_packages")),
host_provided_ffi_root: Some(temp_root.join("libs")),
download_cache_root: Some(temp_root.join("temp").join("downloads")),
..crate::LuaRuntimeHostOptions::default()
},
))
.expect("create FFI query schema engine");
engine
.load_from_roots(&[root_root])
.expect("load FFI query schema root");
let engine_id = FFI_ENGINE_COUNTER.fetch_add(1, Ordering::Relaxed);
lock_ffi_engine_registry().insert(engine_id, FfiEngineSlot::new(engine));
let list_request = CString::new(
serde_json::json!({
"engine_id": engine_id,
"authority": SkillManagementAuthority::System
})
.to_string(),
)
.expect("schema list request");
let listed = unsafe {
decode_response_json(luaskills_ffi_list_entries_json(borrowed_json_buffer(
&list_request,
)))
};
assert_eq!(listed["ok"], true);
let result = listed["result"]
.as_array()
.expect("schema list result array");
let entry = result
.iter()
.find(|item| item["local_name"] == "inspect")
.expect("inspect schema entry");
assert_eq!(entry["input_schema"]["type"], "object");
assert_eq!(
entry["input_schema"]["required"],
serde_json::json!(["nodes"])
);
assert_eq!(
entry["input_schema"]["properties"]["nodes"]["items"]["properties"]["file"]["type"],
"string"
);
assert_eq!(entry["parameters"][0]["name"], "nodes");
assert_eq!(entry["parameters"][0]["param_type"], "array");
drop(super::remove_ffi_engine_slot(engine_id));
let _ = std::fs::remove_dir_all(&temp_root);
}
#[test]
fn ffi_engine_registry_recovers_after_poisoned_lock_for_json_handles() {
let _guard = ffi_test_guard();
let poison_result = panic::catch_unwind(AssertUnwindSafe(|| {
let _registry_guard = ffi_engine_registry()
.lock()
.expect("initial ffi engine registry lock");
panic!("poison ffi engine registry for recovery test");
}));
assert!(poison_result.is_err());
let request = EngineNewJsonRequest {
options: LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
crate::LuaRuntimeHostOptions::default(),
),
};
let request_json =
CString::new(serde_json::to_string(&request).expect("engine new request json"))
.expect("engine new request cstring");
let created = unsafe {
decode_response_json(luaskills_ffi_engine_new_json(borrowed_json_buffer(
&request_json,
)))
};
assert_eq!(created["ok"], true);
let engine_id = created["result"]["engine_id"]
.as_u64()
.expect("created engine id");
let free_request = CString::new(
serde_json::to_string(&EngineIdJsonRequest { engine_id }).expect("free request json"),
)
.expect("free request cstring");
let freed = unsafe {
decode_response_json(luaskills_ffi_engine_free_json(borrowed_json_buffer(
&free_request,
)))
};
assert_eq!(freed["ok"], true);
}
#[test]
fn ffi_engine_new_and_free_roundtrip() {
let _guard = ffi_test_guard();
let temp_root =
std::env::temp_dir().join(format!("luaskills_ffi_engine_test_{}", std::process::id()));
let request = EngineNewJsonRequest {
options: LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
crate::LuaRuntimeHostOptions {
runtime_root: None,
managed_runtime_distribution_root: None,
managed_runtime_environment_root: None,
managed_runtime_config: Default::default(),
temp_dir: Some(temp_root.join("temp")),
resources_dir: Some(temp_root.join("resources")),
lua_packages_dir: Some(temp_root.join("lua_packages")),
host_provided_tool_root: Some(temp_root.join("bin").join("tools")),
host_provided_lua_root: Some(temp_root.join("lua_packages")),
host_provided_ffi_root: Some(temp_root.join("libs")),
system_lua_lib_dir: None,
download_cache_root: Some(temp_root.join("temp").join("downloads")),
dependency_dir_name: "dependencies".to_string(),
state_dir_name: "state".to_string(),
database_dir_name: "databases".to_string(),
skill_config_file_path: None,
allow_network_download: false,
github_base_url: None,
github_api_base_url: None,
official_skill_hub_base_url: None,
enable_private_url_skill_install: false,
private_skill_source_allowlist: Vec::new(),
default_text_encoding: None,
sqlite_library_path: None,
sqlite_provider_mode: crate::LuaRuntimeDatabaseProviderMode::DynamicLibrary,
sqlite_callback_mode: crate::LuaRuntimeDatabaseCallbackMode::Standard,
lancedb_library_path: None,
lancedb_provider_mode: crate::LuaRuntimeDatabaseProviderMode::DynamicLibrary,
lancedb_callback_mode: crate::LuaRuntimeDatabaseCallbackMode::Standard,
space_controller: crate::LuaRuntimeSpaceControllerOptions::default(),
cache_config: None,
runlua_pool_config: None,
reserved_entry_names: Vec::new(),
ignored_skill_ids: Vec::new(),
capabilities: Default::default(),
},
),
};
let input = CString::new(serde_json::to_string(&request).expect("request json"))
.expect("request cstring");
let response = unsafe {
decode_response_json(luaskills_ffi_engine_new_json(borrowed_json_buffer(&input)))
};
assert_eq!(response["ok"], true);
let result: EngineHandleJsonResult =
serde_json::from_value(response["result"].clone()).expect("engine result should parse");
let free_request = CString::new(
serde_json::to_string(&EngineIdJsonRequest {
engine_id: result.engine_id,
})
.expect("free request json"),
)
.expect("free request cstring");
let free_response = unsafe {
decode_response_json(luaskills_ffi_engine_free_json(borrowed_json_buffer(
&free_request,
)))
};
assert_eq!(free_response["ok"], true);
}
#[test]
fn ffi_engine_new_rejects_invalid_managed_runtime_config() {
let _guard = ffi_test_guard();
let mut host_options = crate::LuaRuntimeHostOptions::default();
host_options
.managed_runtime_config
.persistent_session_limit_per_engine = 0;
let request = EngineNewJsonRequest {
options: LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
host_options,
),
};
let input = CString::new(serde_json::to_string(&request).expect("request json"))
.expect("request cstring");
let response = unsafe {
decode_response_json(luaskills_ffi_engine_new_json(borrowed_json_buffer(&input)))
};
assert_eq!(response["ok"], false);
assert!(
response["error"]
.as_str()
.is_some_and(|error| error.contains("persistent_session_limit_per_engine"))
);
}
#[test]
fn ffi_skill_config_json_roundtrip() {
let _guard = ffi_test_guard();
let temp_root = std::env::temp_dir().join(format!(
"luaskills_ffi_skill_config_json_test_{}",
std::process::id()
));
let request = EngineNewJsonRequest {
options: LuaEngineOptions::new(
LuaVmPoolConfig {
min_size: 1,
max_size: 1,
idle_ttl_secs: 30,
},
crate::LuaRuntimeHostOptions {
runtime_root: None,
managed_runtime_distribution_root: None,
managed_runtime_environment_root: None,
managed_runtime_config: Default::default(),
temp_dir: Some(temp_root.join("temp")),
resources_dir: Some(temp_root.join("resources")),
lua_packages_dir: Some(temp_root.join("lua_packages")),
host_provided_tool_root: Some(temp_root.join("bin").join("tools")),
host_provided_lua_root: Some(temp_root.join("lua_packages")),
host_provided_ffi_root: Some(temp_root.join("libs")),
system_lua_lib_dir: None,
download_cache_root: Some(temp_root.join("temp").join("downloads")),
dependency_dir_name: "dependencies".to_string(),
state_dir_name: "state".to_string(),
database_dir_name: "databases".to_string(),
skill_config_file_path: Some(temp_root.join("config").join("skill_config.json")),
allow_network_download: false,
github_base_url: None,
github_api_base_url: None,
official_skill_hub_base_url: None,
enable_private_url_skill_install: false,
private_skill_source_allowlist: Vec::new(),
default_text_encoding: None,
sqlite_library_path: None,
sqlite_provider_mode: crate::LuaRuntimeDatabaseProviderMode::DynamicLibrary,
sqlite_callback_mode: crate::LuaRuntimeDatabaseCallbackMode::Standard,
lancedb_library_path: None,
lancedb_provider_mode: crate::LuaRuntimeDatabaseProviderMode::DynamicLibrary,
lancedb_callback_mode: crate::LuaRuntimeDatabaseCallbackMode::Standard,
space_controller: crate::LuaRuntimeSpaceControllerOptions::default(),
cache_config: None,
runlua_pool_config: None,
reserved_entry_names: Vec::new(),
ignored_skill_ids: Vec::new(),
capabilities: Default::default(),
},
),
};
let input = CString::new(serde_json::to_string(&request).expect("request json"))
.expect("request cstring");
let response = unsafe {
decode_response_json(luaskills_ffi_engine_new_json(borrowed_json_buffer(&input)))
};
assert_eq!(response["ok"], true);
let result: EngineHandleJsonResult =
serde_json::from_value(response["result"].clone()).expect("engine result should parse");
let set_request = CString::new(
serde_json::to_string(&SkillConfigSetJsonRequest {
engine_id: result.engine_id,
skill_id: "demo-skill".to_string(),
key: "api_token".to_string(),
value: "sk-json-ffi".to_string(),
})
.expect("set request json"),
)
.expect("set request cstring");
let set_response = unsafe {
decode_response_json(luaskills_ffi_skill_config_set_json(borrowed_json_buffer(
&set_request,
)))
};
assert_eq!(set_response["ok"], true);
assert_eq!(set_response["result"]["action"], "set");
assert_eq!(set_response["result"]["skill_id"], "demo-skill");
assert_eq!(set_response["result"]["key"], "api_token");
assert_eq!(set_response["result"]["value"], "sk-json-ffi");
let get_request = CString::new(
serde_json::to_string(&SkillConfigGetJsonRequest {
engine_id: result.engine_id,
skill_id: "demo-skill".to_string(),
key: "api_token".to_string(),
})
.expect("get request json"),
)
.expect("get request cstring");
let get_response = unsafe {
decode_response_json(luaskills_ffi_skill_config_get_json(borrowed_json_buffer(
&get_request,
)))
};
assert_eq!(get_response["ok"], true);
assert_eq!(get_response["result"]["found"], true);
assert_eq!(get_response["result"]["value"], "sk-json-ffi");
let list_request = CString::new(
serde_json::to_string(&SkillConfigListJsonRequest {
engine_id: result.engine_id,
skill_id: Some("demo-skill".to_string()),
})
.expect("list request json"),
)
.expect("list request cstring");
let list_response = unsafe {
decode_response_json(luaskills_ffi_skill_config_list_json(borrowed_json_buffer(
&list_request,
)))
};
assert_eq!(list_response["ok"], true);
assert_eq!(list_response["result"].as_array().map(Vec::len), Some(1));
assert_eq!(list_response["result"][0]["skill_id"], "demo-skill");
assert_eq!(list_response["result"][0]["key"], "api_token");
assert_eq!(list_response["result"][0]["value"], "sk-json-ffi");
let delete_request = CString::new(
serde_json::to_string(&SkillConfigGetJsonRequest {
engine_id: result.engine_id,
skill_id: "demo-skill".to_string(),
key: "api_token".to_string(),
})
.expect("delete request json"),
)
.expect("delete request cstring");
let delete_response = unsafe {
decode_response_json(luaskills_ffi_skill_config_delete_json(
borrowed_json_buffer(&delete_request),
))
};
assert_eq!(delete_response["ok"], true);
assert_eq!(delete_response["result"]["action"], "delete");
assert_eq!(delete_response["result"]["deleted"], true);
let free_request = CString::new(
serde_json::to_string(&EngineIdJsonRequest {
engine_id: result.engine_id,
})
.expect("free request json"),
)
.expect("free request cstring");
let free_response = unsafe {
decode_response_json(luaskills_ffi_engine_free_json(borrowed_json_buffer(
&free_request,
)))
};
assert_eq!(free_response["ok"], true);
}
#[test]
fn with_engine_releases_registry_lock_before_operation() {
let _guard = ffi_test_guard();
let handle = register_test_engine();
let result = with_engine(handle.engine_id, |_engine| {
let registry_available = match ffi_engine_registry().try_lock() {
Ok(_guard) => true,
Err(TryLockError::Poisoned(_poisoned)) => true,
Err(TryLockError::WouldBlock) => false,
};
assert!(
registry_available,
"registry lock should be acquirable while engine operation is running"
);
Ok(())
});
assert!(result.is_ok());
}
#[test]
fn with_engine_recovers_after_engine_handle_lock_poisoned() {
let _guard = ffi_test_guard();
let handle = register_test_engine();
let engine_handle =
super::clone_engine_handle(handle.engine_id).expect("clone ffi engine handle");
let poison_result = panic::catch_unwind(AssertUnwindSafe(|| {
let _engine = engine_handle
.lock()
.expect("initial ffi engine handle lock");
panic!("poison ffi engine handle for recovery test");
}));
assert!(poison_result.is_err());
let read_result = with_engine(handle.engine_id, |_engine| Ok(()));
assert!(read_result.is_ok());
let write_result = with_engine_mut(handle.engine_id, |_engine| Ok(()));
assert!(write_result.is_ok());
}
#[test]
fn with_engine_rejects_same_thread_reentry() {
let _guard = ffi_test_guard();
let handle = register_test_engine();
let outer_result = with_engine(handle.engine_id, |_engine| {
let nested_result = with_engine(handle.engine_id, |_nested| Ok(()));
let nested_error = nested_result.expect_err("same-thread reentry should fail");
assert!(nested_error.contains("reentrant access"));
Ok(())
});
assert!(outer_result.is_ok());
}