use super::{
EngineHandleJsonResult, EngineIdJsonRequest, EngineNewJsonRequest, FFI_ENGINE_COUNTER,
FfiEngineSlot, SkillConfigGetJsonRequest, SkillConfigListJsonRequest,
SkillConfigSetJsonRequest, 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_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_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, with_engine,
};
use crate::ffi_standard::{FfiBorrowedBuffer, FfiOwnedBuffer, luaskills_ffi_buffer_free};
use crate::{
LuaEngine, LuaEngineOptions, LuaVmPoolConfig, RuntimeSkillRoot, SkillManagementAuthority,
};
use std::ffi::CString;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
use std::sync::{Mutex, MutexGuard, OnceLock};
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
}
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 TestFfiEngineHandle {
engine_id: u64,
}
impl Drop for TestFfiEngineHandle {
fn drop(&mut self) {
if let Ok(mut registry) = ffi_engine_registry().lock() {
registry.remove(&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);
ffi_engine_registry()
.lock()
.expect("lock ffi engine registry")
.insert(engine_id, FfiEngineSlot::new(engine));
TestFfiEngineHandle { engine_id }
}
#[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"));
}
#[test]
fn ffi_system_runtime_session_json_requires_authority() {
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-system-session",
"ttl_sec": 60
})
.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"], false);
assert!(
created["error"]
.as_str()
.expect("system runtime session error")
.contains("requires host-injected authority")
);
}
#[test]
fn ffi_system_runtime_session_json_supports_delegated_wrapper_flow() {
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-system-wrapper-session",
"ttl_sec": 60,
"replace": true,
"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);
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 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));
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
}
#[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);
ffi_engine_registry()
.lock()
.expect("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().to_ascii_uppercase() != "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().to_ascii_uppercase() != "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_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 {
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,
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_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 {
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,
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_lock = ffi_engine_registry().try_lock();
assert!(
registry_lock.is_ok(),
"registry lock should be acquirable while engine operation is running"
);
Ok(())
});
assert!(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());
}