use super::{
LoadedSqliteApi, SkillSqliteMeta, SqliteBindingMode, SqliteSkillBinding, SqliteSkillHost,
parse_single_sql_params, require_present_string_field, require_sqlite_dynamic_string,
validate_sqlite_library_info,
};
use crate::LuaRuntimeHostOptions;
use crate::host::database::{
LuaRuntimeDatabaseCallbackMode, LuaRuntimeDatabaseProviderMode, RuntimeDatabaseBindingContext,
RuntimeDatabaseBindingContextSpec, RuntimeDatabaseKind, RuntimeDatabaseProviderCallbacks,
};
use crate::runtime::path::render_host_visible_path;
use serde_json::json;
use std::collections::HashMap;
use std::panic::{self, AssertUnwindSafe};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
#[test]
fn parse_single_sql_params_rejects_legacy_params_json() {
let error = match parse_single_sql_params(&json!({
"params_json": "[1, 2, 3]"
})) {
Ok(_) => panic!("legacy params_json input should be rejected"),
Err(error) => error,
};
assert_eq!(error, "params_json is no longer supported; use params");
}
#[test]
fn require_sqlite_dynamic_string_rejects_missing_value() {
let present = require_sqlite_dynamic_string(Some("jieba".to_string()), "custom_word.word")
.expect("present dynamic string should pass");
let missing_error = require_sqlite_dynamic_string(None, "custom_word.word")
.expect_err("missing dynamic string should fail");
assert_eq!(present, "jieba");
assert_eq!(
missing_error,
"SQLite dynamic result field `custom_word.word` is missing"
);
}
#[test]
fn require_sqlite_dynamic_string_labels_missing_fts_hit_field() {
let missing_error = require_sqlite_dynamic_string(None, "fts_hit.title_highlight")
.expect_err("missing FTS hit string should fail");
assert_eq!(
missing_error,
"SQLite dynamic result field `fts_hit.title_highlight` is missing"
);
}
#[test]
fn require_sqlite_dynamic_string_labels_missing_execute_message() {
let missing_error = require_sqlite_dynamic_string(None, "execute_result.message")
.expect_err("missing execute message should fail");
assert_eq!(
missing_error,
"SQLite dynamic result field `execute_result.message` is missing"
);
}
#[test]
fn require_present_string_field_preserves_empty_text() {
let input = json!({ "title": "" });
let title =
require_present_string_field(&input, "title").expect("explicit empty title should pass");
let missing_error = require_present_string_field(&input, "content")
.expect_err("missing content field should fail");
assert_eq!(title, "");
assert_eq!(missing_error, "missing or non-string field `content`");
}
#[test]
fn sqlite_missing_library_error_uses_host_visible_path() {
let library_path = std::env::temp_dir().join(format!(
"luaskills-missing-sqlite-provider-{}.dll",
std::process::id()
));
let error = match LoadedSqliteApi::load(&library_path) {
Ok(_) => panic!("missing SQLite library should fail"),
Err(error) => error,
};
let expected_error = format!(
"SQLite dynamic library path does not exist: {}",
render_host_visible_path(&library_path)
);
assert_eq!(error, expected_error);
}
fn sample_sqlite_binding_context() -> RuntimeDatabaseBindingContext {
RuntimeDatabaseBindingContext::new(RuntimeDatabaseBindingContextSpec {
space_label: "ROOT".to_string(),
skill_id: "sqlite-api-state-skill".to_string(),
root_name: "ROOT".to_string(),
space_root: "D:/runtime-test-root/databases".to_string(),
skill_dir: "D:/runtime-test-root/skills/sqlite-api-state-skill".to_string(),
skill_dir_name: "sqlite-api-state-skill".to_string(),
database_kind: RuntimeDatabaseKind::Sqlite,
default_database_path: "D:/runtime-test-root/databases/sqlite-api-state-skill.sqlite3"
.to_string(),
})
}
fn dynamic_sqlite_binding_without_api() -> SqliteSkillBinding {
SqliteSkillBinding {
api: None,
skill_name: "sqlite-api-state-skill".to_string(),
skill_dir_name: "sqlite-api-state-skill".to_string(),
database_path: "D:/runtime-test-root/databases/sqlite-api-state-skill.sqlite3".to_string(),
config: SkillSqliteMeta::default(),
provider_mode: SqliteBindingMode::DynamicLibrary,
callback_mode: LuaRuntimeDatabaseCallbackMode::Standard,
handles: None,
controller: None,
provider_callbacks: Arc::new(RuntimeDatabaseProviderCallbacks::default()),
provider_binding: sample_sqlite_binding_context(),
}
}
fn space_controller_sqlite_binding_without_controller() -> SqliteSkillBinding {
SqliteSkillBinding {
api: None,
skill_name: "sqlite-space-controller-validation-skill".to_string(),
skill_dir_name: "sqlite-space-controller-validation-skill".to_string(),
database_path:
"D:/runtime-test-root/databases/sqlite-space-controller-validation-skill.sqlite3"
.to_string(),
config: SkillSqliteMeta::default(),
provider_mode: SqliteBindingMode::SpaceController,
callback_mode: LuaRuntimeDatabaseCallbackMode::Standard,
handles: None,
controller: None,
provider_callbacks: Arc::new(RuntimeDatabaseProviderCallbacks::default()),
provider_binding: sample_sqlite_binding_context(),
}
}
#[test]
fn sqlite_dynamic_binding_without_api_returns_error() {
let binding = dynamic_sqlite_binding_without_api();
let error = binding
.execute_script(&json!({ "sql": "select 1", "params": [] }))
.expect_err("missing dynamic API should return an error");
assert_eq!(
error,
"SQLite dynamic-library API is unavailable for dynamic_library binding"
);
}
#[test]
fn sqlite_dynamic_status_without_api_returns_error() {
let binding = dynamic_sqlite_binding_without_api();
let error = binding
.status_json()
.expect_err("missing dynamic API should make status fail");
assert_eq!(
error,
"SQLite dynamic-library API is unavailable for dynamic_library binding"
);
}
#[test]
fn sqlite_library_info_requires_protocol_fields() {
let non_object_error = validate_sqlite_library_info(&json!("invalid"), "library_info_json")
.expect_err("non-object metadata should fail metadata validation");
let missing_version_error = validate_sqlite_library_info(
&json!({
"name": "vldb-sqlite",
"ffi_stage": "stable",
"capabilities": []
}),
"library_info_json",
)
.expect_err("missing version should fail metadata validation");
let invalid_capabilities_error = validate_sqlite_library_info(
&json!({
"name": "vldb-sqlite",
"version": "1",
"ffi_stage": "stable",
"capabilities": "fts"
}),
"library_info_json",
)
.expect_err("scalar capabilities should fail metadata validation");
assert_eq!(
non_object_error,
"SQLite dynamic library_info_json must be a JSON object"
);
assert_eq!(
missing_version_error,
"SQLite dynamic library_info_json field `version` is missing or empty"
);
assert_eq!(
invalid_capabilities_error,
"SQLite dynamic library_info_json field `capabilities` must be an array"
);
}
#[test]
fn upsert_fts_document_requires_explicit_title_and_content() {
let binding = space_controller_sqlite_binding_without_controller();
let missing_title_error = binding
.upsert_fts_document_json(&json!({
"index_name": "documents",
"id": "doc-1",
"file_path": "docs/doc-1.md",
"content": "body"
}))
.expect_err("missing title should fail before controller access");
let missing_content_error = binding
.upsert_fts_document_json(&json!({
"index_name": "documents",
"id": "doc-1",
"file_path": "docs/doc-1.md",
"title": ""
}))
.expect_err("missing content should fail before controller access");
assert_eq!(missing_title_error, "missing or non-string field `title`");
assert_eq!(
missing_content_error,
"missing or non-string field `content`"
);
}
#[test]
fn sqlite_skill_binding_registry_recovers_after_poisoned_lock() {
let host_options = LuaRuntimeHostOptions {
sqlite_provider_mode: LuaRuntimeDatabaseProviderMode::HostCallback,
database_dir_name: "databases".to_string(),
..LuaRuntimeHostOptions::default()
};
let host = SqliteSkillHost {
api: None,
controller: None,
skills: Mutex::new(HashMap::new()),
provider_callbacks: Arc::new(RuntimeDatabaseProviderCallbacks::default()),
host_options,
};
let poison_result = panic::catch_unwind(AssertUnwindSafe(|| {
let _registry_guard = host
.skills
.lock()
.expect("initial sqlite skill binding registry lock");
panic!("poison sqlite skill binding registry for recovery test");
}));
assert!(poison_result.is_err());
let skill_dir = PathBuf::from("D:/runtime-test-root/skills/sqlite-poison-skill");
let binding = host
.register_skill(
"ROOT",
"sqlite-poison-skill",
&skill_dir,
SkillSqliteMeta::default(),
)
.expect("register sqlite binding after poison");
let fetched = host
.binding_for_skill("sqlite-poison-skill")
.expect("fetch sqlite binding after poison")
.expect("sqlite binding should exist");
assert!(Arc::ptr_eq(&binding, &fetched));
}