diff --git a/src/tokenless/crates/tokenless-ccr/src/tests/store_tests.rs b/src/tokenless/crates/tokenless-ccr/src/tests/store_tests.rs index bc3cf7a2a..eccd343a7 100644 --- a/src/tokenless/crates/tokenless-ccr/src/tests/store_tests.rs +++ b/src/tokenless/crates/tokenless-ccr/src/tests/store_tests.rs @@ -13,3 +13,25 @@ fn stash_error_display() { let e = StashError::Backend("test error".to_string()); assert!(format!("{}", e).contains("test error")); } + +#[test] +fn default_trait_creates_working_store() { + let store = InMemoryStore::default(); + assert!(store.is_empty()); + let key = store.stash("payload").unwrap(); + assert!(!store.is_empty()); + let retrieved = store.retrieve(&key).unwrap(); + assert_eq!(retrieved, Some("payload".to_string())); +} + +#[cfg(feature = "sqlite")] +#[test] +fn stash_error_from_rusqlite() { + let rusqlite_err = rusqlite::Error::SqliteFailure( + rusqlite::ffi::Error::new(1), + Some("test error".to_string()), + ); + let stash_err: StashError = StashError::from(rusqlite_err); + let msg = format!("{}", stash_err); + assert!(msg.contains("test error")); +} diff --git a/src/tokenless/crates/tokenless-cli/src/main.rs b/src/tokenless/crates/tokenless-cli/src/main.rs index 0bfac8f08..3219aa237 100644 --- a/src/tokenless/crates/tokenless-cli/src/main.rs +++ b/src/tokenless/crates/tokenless-cli/src/main.rs @@ -399,8 +399,11 @@ fn open_stash_store(override_path: Option<&str>) -> Option> fn run() -> Result<(), (String, i32)> { let cli = Cli::parse(); + run_command(cli.command) +} - match cli.command { +fn run_command(command: Commands) -> Result<(), (String, i32)> { + match command { Commands::CompressSchema { file, batch, diff --git a/src/tokenless/crates/tokenless-cli/src/tests/env_check_tests.rs b/src/tokenless/crates/tokenless-cli/src/tests/env_check_tests.rs index c00a18d3a..d81b83388 100644 --- a/src/tokenless/crates/tokenless-cli/src/tests/env_check_tests.rs +++ b/src/tokenless/crates/tokenless-cli/src/tests/env_check_tests.rs @@ -1,5 +1,9 @@ use serde_json::json; +use std::sync::Mutex; + +static ENV_MUTEX: Mutex<()> = Mutex::new(()); + #[test] fn normalize_dep_simple_string() { let dep = normalize_dep(&json!("jq")); @@ -703,6 +707,8 @@ fn check_dep_missing_binary() { #[test] fn find_spec_path_error_when_none_exists() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + // Override env to a nonexistent path and clear defaults unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", "/nonexistent/spec.json") }; let result = find_spec_path(); @@ -713,6 +719,8 @@ fn find_spec_path_error_when_none_exists() { #[test] fn detect_system_manager_env_override() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + unsafe { std::env::set_var("TOKENLESS_PACKAGE_MANAGER", "test-mgr") }; let mgr = detect_system_manager(); unsafe { std::env::remove_var("TOKENLESS_PACKAGE_MANAGER") }; @@ -834,6 +842,7 @@ fn format_status_all_variants() { } #[test] +#[ignore] fn check_network_https_outbound() { // Just exercise the path — may or may not succeed depending on network let _ = check_network("https_outbound"); @@ -928,3 +937,519 @@ fn check_config_file_expanded_tilde() { let result = check_config_file("~/.tokenless-nonexistent-cfg-xyz"); assert!(!result); } + +fn write_test_spec(dir: &std::path::Path) -> std::path::PathBuf { + let spec_path = dir.join("test-spec.json"); + let spec = serde_json::json!({ + "TestTool": { + "required": ["ls"], + "recommended": ["cat"], + "config_files": [], + "permissions": [], + "network": [], + "aliases": ["test-tool", "tt"] + }, + "MissingTool": { + "required": ["nonexistent_binary_xyz_99"], + "recommended": [], + "config_files": ["~/.nonexistent_config_xyz"], + "permissions": ["nonexistent_perm_xyz"], + "network": [] + } + }); + std::fs::write(&spec_path, serde_json::to_string(&spec).unwrap()).unwrap(); + spec_path +} + +fn write_versioned_spec(dir: &std::path::Path) -> std::path::PathBuf { + let spec_path = dir.join("versioned-spec.json"); + let spec = serde_json::json!({ + "VersionedTool": { + "required": [ + {"binary": "bash", "version": ">=1.0", "package": "bash", "manager": "rpm"} + ], + "recommended": [ + {"binary": "cat", "version": ">=0.1", "package": "coreutils", "manager": "rpm"} + ], + "config_files": ["~/.bashrc"], + "permissions": [], + "network": [] + }, + "_comment": "This is a comment key that should be skipped" + }); + std::fs::write(&spec_path, serde_json::to_string(&spec).unwrap()).unwrap(); + spec_path +} + +#[test] +fn check_tool_all_available() { + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + let specs = load_spec(&spec_path).unwrap(); + let result = check_tool("TestTool", specs.get("TestTool").unwrap()); + assert_eq!(result.status, ReadyStatus::Ready); +} + +#[test] +fn check_tool_with_missing_dep() { + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + let specs = load_spec(&spec_path).unwrap(); + let result = check_tool("MissingTool", specs.get("MissingTool").unwrap()); + assert_eq!(result.status, ReadyStatus::NotReady); + assert!(!result.required_results.is_empty()); +} + +#[test] +fn check_tool_versioned_available() { + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_versioned_spec(dir.path()); + let specs = load_spec(&spec_path).unwrap(); + let result = check_tool("VersionedTool", specs.get("VersionedTool").unwrap()); + // bash >= 1.0 should be available on any Linux + assert_eq!(result.status, ReadyStatus::Ready); + assert!(!result.config_results.is_empty()); +} + +#[test] +fn generate_checklist_multiple_tools() { + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + let specs = load_spec(&spec_path).unwrap(); + let results: Vec = specs + .keys() + .map(|name| check_tool(name, specs.get(name).unwrap())) + .collect(); + let checklist = generate_checklist(&results); + assert!(checklist.contains("TestTool") || checklist.contains("MissingTool")); +} + +#[test] +fn format_dep_status_and_label_coverage() { + let avail = format_dep_status(&DepStatus::Available); + assert!(!avail.is_empty()); + let missing = format_dep_status(&DepStatus::Missing); + assert!(!missing.is_empty()); + let low = format_dep_status(&DepStatus::VersionLow { + installed: "1.0".to_string(), + required: "2.0".to_string(), + }); + assert!(low.contains("1.0")); +} + +#[test] +fn build_json_result_not_ready_diagnostic() { + let result = build_json_result( + "TestTool", + &ReadyStatus::NotReady, + &[], + &["dep1".to_string(), "dep2".to_string()], + ); + assert!(result["diagnostic"].as_str().unwrap().contains("NOT_READY")); + assert!(result["diagnostic"].as_str().unwrap().contains("dep1")); +} + +#[test] +#[ignore] +fn check_network_https_resolves() { + let result = check_network("https://httpbin.org/status/200"); + let _ = result; +} + +#[test] +fn run_all_text_output() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(None, true, false, false, false); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_all_json_output() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(None, true, false, false, true); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_checklist_output() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(None, false, false, true, false); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_specific_tool_text() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(Some("TestTool"), false, false, false, false); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_specific_tool_json_output() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(Some("TestTool"), false, false, false, true); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_alias_lookup_text() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(Some("tt"), false, false, false, false); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_case_insensitive_tool() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(Some("testtool"), false, false, false, false); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_unknown_tool_text() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(Some("UnknownTool"), false, false, false, false); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_unknown_tool_json_output() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(Some("UnknownTool"), false, false, false, true); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_no_tool_no_all_errors() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(None, false, false, false, false); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_missing_tool_text() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(Some("MissingTool"), false, false, false, false); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_missing_tool_json_output() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(Some("MissingTool"), false, false, false, true); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_versioned_all_text() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_versioned_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(None, true, false, false, false); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_versioned_all_json() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_versioned_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(None, true, false, false, true); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn run_versioned_checklist() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_versioned_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let _ = run(None, false, false, true, false); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; +} + +#[test] +fn load_spec_valid_file() { + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_test_spec(dir.path()); + let specs = load_spec(&spec_path).unwrap(); + assert!(specs.contains_key("TestTool")); + assert!(specs.contains_key("MissingTool")); + let test_tool = &specs["TestTool"]; + assert_eq!(test_tool.aliases, vec!["test-tool", "tt"]); + assert_eq!(test_tool.required.len(), 1); + assert_eq!(test_tool.required[0].binary, "ls"); +} + +#[test] +fn build_json_result_ready_with_diagnostic() { + let result = build_json_result( + "TestTool", + &ReadyStatus::NotReady, + &[], + &["missing-dep".to_string()], + ); + assert_eq!(result["tool"], "TestTool"); + assert!(result["diagnostic"].as_str().unwrap().contains("NOT_READY")); + assert!(result["missing"].as_array().unwrap().len() == 1); +} + +#[test] +fn build_json_result_with_fixed_and_missing() { + let result = build_json_result( + "TestTool", + &ReadyStatus::Ready, + &["fixed-dep".to_string()], + &["still-missing".to_string()], + ); + assert!(result["fixed"].as_array().unwrap().len() == 1); + assert!(result["missing"].as_array().unwrap().len() == 1); +} + +#[test] +fn is_trusted_path_system_usr_share() { + let path = std::path::Path::new("/usr/share/doc"); + if path.exists() { + assert!(is_trusted_path(path)); + } +} + +#[test] +fn is_trusted_path_system_usr_local_share() { + let path = std::path::Path::new("/usr/local/share"); + if path.exists() { + assert!(is_trusted_path(path)); + } +} + +#[test] +fn is_trusted_path_nonexistent() { + let path = std::path::Path::new("/nonexistent/path/xyz"); + assert!(!is_trusted_path(path)); +} + +#[test] +fn is_trusted_path_owned_file() { + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("test_file"); + std::fs::write(&file, "test").unwrap(); + let result = is_trusted_path(&file); + assert!(result); +} + +#[test] +fn is_trusted_path_symlink_to_owned_file() { + let dir = tempfile::tempdir().unwrap(); + let target = dir.path().join("target_file"); + std::fs::write(&target, "data").unwrap(); + let link = dir.path().join("link_to_file"); + std::os::unix::fs::symlink(&target, &link).unwrap(); + let result = is_trusted_path(&link); + assert!(result); +} + +#[test] +fn is_trusted_path_symlink_to_system_path() { + let dir = tempfile::tempdir().unwrap(); + let link = dir.path().join("link_to_usr_share"); + if std::path::Path::new("/usr/share/doc").exists() { + std::os::unix::fs::symlink("/usr/share/doc", &link).unwrap(); + let result = is_trusted_path(&link); + assert!(result); + } +} + +#[test] +fn is_trusted_path_broken_symlink() { + let dir = tempfile::tempdir().unwrap(); + let link = dir.path().join("broken_link"); + std::os::unix::fs::symlink("/nonexistent/target/xyz", &link).unwrap(); + let result = is_trusted_path(&link); + assert!(!result); +} + +#[test] +fn is_trusted_path_symlink_in_different_dir() { + let dir1 = tempfile::tempdir().unwrap(); + let dir2 = tempfile::tempdir().unwrap(); + let target = dir1.path().join("target"); + std::fs::write(&target, "data").unwrap(); + let link = dir2.path().join("cross_dir_link"); + std::os::unix::fs::symlink(&target, &link).unwrap(); + let result = is_trusted_path(&link); + assert!(result); +} + +#[test] +fn check_dep_available_ls() { + let dep = make_dep("ls"); + let status = check_dep(&dep); + assert_eq!(status, DepStatus::Available); +} + +#[test] +fn check_dep_with_version_ls() { + let mut dep = make_dep("ls"); + dep.version = Some(">=0.1".to_string()); + let status = check_dep(&dep); + // ls doesn't have a useful --version, so it may report VersionLow + let _ = status; +} + +#[test] +fn check_dep_with_version_bash() { + let mut dep = make_dep("bash"); + dep.version = Some(">=1.0".to_string()); + let status = check_dep(&dep); + assert_eq!(status, DepStatus::Available); +} + +#[test] +fn check_dep_version_low() { + let mut dep = make_dep("bash"); + dep.version = Some(">=999.0".to_string()); + let status = check_dep(&dep); + match status { + DepStatus::VersionLow { installed, required } => { + assert_eq!(required, "999.0"); + assert!(!installed.is_empty()); + } + _ => panic!("Expected VersionLow"), + } +} + +#[test] +fn run_versioned_tool_all() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_versioned_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let result = run(None, true, false, false, false); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; + assert!(result.is_ok()); +} + +#[test] +fn run_versioned_tool_json() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + + let dir = tempfile::tempdir().unwrap(); + let spec_path = write_versioned_spec(dir.path()); + unsafe { std::env::set_var("TOKENLESS_TOOL_READY_SPEC", spec_path.to_str().unwrap()) }; + let result = run(None, true, false, false, true); + unsafe { std::env::remove_var("TOKENLESS_TOOL_READY_SPEC") }; + assert!(result.is_ok()); +} + +#[test] +fn resolve_package_with_apt_override() { + let mut dep = make_dep("curl"); + dep.apt_package = Some("libcurl4".to_string()); + let pkg = resolve_package(&dep); + // Will resolve based on detected system manager + assert!(!pkg.is_empty()); +} + + +#[test] +fn format_dep_status_label_all_variants() { + let avail = format_dep_status_label(&DepStatus::Available); + let missing = format_dep_status_label(&DepStatus::Missing); + let low = format_dep_status_label(&DepStatus::VersionLow { + installed: "1.0".to_string(), + required: "2.0".to_string(), + }); + assert!(!avail.is_empty()); + assert!(!missing.is_empty()); + assert!(!low.is_empty()); +} + +#[test] +fn resolve_manager_non_rpm() { + let result = resolve_manager("pip"); + assert_eq!(result, "pip"); +} + +#[test] +fn resolve_manager_rpm() { + let result = resolve_manager("rpm"); + assert!(!result.is_empty()); +} + +#[test] +fn extract_required_version_variations() { + assert_eq!(extract_required_version(">=1.2"), "1.2"); + assert_eq!(extract_required_version(">1.2"), "1.2"); + assert_eq!(extract_required_version("1.2"), "1.2"); +} + +#[test] +fn normalize_dep_with_fallback() { + let dep = normalize_dep(&serde_json::json!({ + "binary": "rtk", + "package": "rtk", + "manager": "pip", + "fallback": [ + {"binary": "rtk-fallback", "url": "https://example.com/rtk"}, + "not_an_object" + ] + })); + assert_eq!(dep.binary, "rtk"); + assert_eq!(dep.manager, "pip"); + assert_eq!(dep.fallback.len(), 1); + assert_eq!(dep.fallback[0].binary.as_deref(), Some("rtk-fallback")); +} diff --git a/src/tokenless/crates/tokenless-cli/src/tests/main_tests.rs b/src/tokenless/crates/tokenless-cli/src/tests/main_tests.rs index f4dfd20e4..546a84fba 100644 --- a/src/tokenless/crates/tokenless-cli/src/tests/main_tests.rs +++ b/src/tokenless/crates/tokenless-cli/src/tests/main_tests.rs @@ -1,3 +1,46 @@ +use std::sync::Mutex; + +static ENV_MUTEX: Mutex<()> = Mutex::new(()); + +struct TempDbGuard { + _lock: std::sync::MutexGuard<'static, ()>, + test_dir: String, +} + +impl TempDbGuard { + fn new() -> Option { + let lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + let home = get_home_dir(); + if home.is_empty() { + return None; + } + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let test_dir = format!("{}/.tokenless-test-{}", home, nanos); + std::fs::create_dir_all(&test_dir).unwrap(); + unsafe { + std::env::set_var("TOKENLESS_STATS_DB", format!("{}/stats.db", test_dir)); + std::env::set_var("TOKENLESS_STASH_DB", format!("{}/stash.db", test_dir)); + } + Some(TempDbGuard { + _lock: lock, + test_dir, + }) + } +} + +impl Drop for TempDbGuard { + fn drop(&mut self) { + unsafe { + std::env::remove_var("TOKENLESS_STATS_DB"); + std::env::remove_var("TOKENLESS_STASH_DB"); + } + std::fs::remove_dir_all(&self.test_dir).ok(); + } +} + fn temp_subdir(label: &str) -> std::path::PathBuf { use std::time::{SystemTime, UNIX_EPOCH}; let nanos = SystemTime::now() @@ -168,6 +211,7 @@ fn warn_mode_mismatch_no_warning_when_matching() { #[test] fn get_stash_db_path_default() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); let home = get_home_dir(); if home.is_empty() { return; @@ -179,30 +223,22 @@ fn get_stash_db_path_default() { #[test] fn get_stash_db_path_with_valid_override() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); let home = get_home_dir(); if home.is_empty() { return; } - let dir = temp_subdir("stash-override"); - let canon = std::fs::canonicalize(&dir).unwrap(); - let canon_home = std::fs::canonicalize(std::path::Path::new(&home)).unwrap(); - // Only test when the temp dir happens to be under home - if !canon.starts_with(&canon_home) { - std::fs::remove_dir_all(&dir).ok(); - return; - } - let db_path = canon.join("test.db"); - let result = get_stash_db_path(Some(db_path.to_str().unwrap())); - assert_eq!(result, Some(db_path.to_str().unwrap().to_string())); - std::fs::remove_dir_all(&dir).ok(); + let dir = format!("{}/.tokenless", home); + std::fs::create_dir_all(&dir).unwrap(); + let db_path = format!("{}/override_test.db", dir); + let result = get_stash_db_path(Some(&db_path)); + assert!(result.is_some()); + assert_eq!(result.unwrap(), db_path); } #[test] fn open_stash_store_falls_back_on_bad_override() { - let home = get_home_dir(); - if home.is_empty() { - return; - } + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; // Bad override is rejected but falls back to the default path. // Result may be None in CI if the DB is locked by another test. let _result = open_stash_store(Some("/nonexistent/deep/dir/stash.db")); @@ -210,10 +246,7 @@ fn open_stash_store_falls_back_on_bad_override() { #[test] fn open_stash_store_or_err_falls_back_on_bad_override() { - let home = get_home_dir(); - if home.is_empty() { - return; - } + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; let result = open_stash_store_or_err(Some("/nonexistent/deep/dir/stash.db")); // Bad override path is rejected and falls back to default; result is Ok assert!(result.is_ok()); @@ -221,6 +254,8 @@ fn open_stash_store_or_err_falls_back_on_bad_override() { #[test] fn ensure_db_dir_creates_parent() { + let _guard = TempDbGuard::new(); + // ensure_db_dir is idempotent — calling it when the dir already exists // (which it does for most test envs) succeeds. let result = ensure_db_dir(); @@ -310,20 +345,14 @@ fn record_compression_stats_records_dryrun_mode() { #[test] fn get_db_path_returns_valid_path() { - let home = get_home_dir(); - if home.is_empty() { - return; - } + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; let db_path = get_db_path(); - assert!(db_path.contains(".tokenless/stats.db")); + assert!(db_path.contains("stats.db")); } #[test] fn open_recorder_exercises_path() { - let home = get_home_dir(); - if home.is_empty() { - return; - } + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; let result = open_recorder(); // May succeed or fail depending on filesystem permissions let _ = result; @@ -339,3 +368,656 @@ fn read_input_oversized_file() { // File doesn't exist, so should error assert!(result.is_err()); } + +#[test] +fn run_command_compress_schema_from_file() { + let _guard = TempDbGuard::new(); + + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("schema.json"); + std::fs::write( + &f, + r#"{"function":{"name":"test","description":"A test function with a long description that might get compressed by the schema compressor depending on settings","parameters":{"type":"object","properties":{"x":{"type":"string","title":"Remove Me","examples":["ex1"]}}}}}"#, + ) + .unwrap(); + let result = run_command(Commands::CompressSchema { + file: Some(f.to_str().unwrap().to_string()), + batch: false, + agent_id: Some("test-agent".to_string()), + session_id: Some("test-session".to_string()), + tool_use_id: Some("tool-1".to_string()), + no_stash: true, + stash_db: None, + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_compress_schema_batch() { + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("schemas.json"); + std::fs::write( + &f, + r#"[{"function":{"name":"a","parameters":{"type":"object","properties":{}}}},{"function":{"name":"b","parameters":{"type":"object","properties":{}}}}]"#, + ) + .unwrap(); + let result = run_command(Commands::CompressSchema { + file: Some(f.to_str().unwrap().to_string()), + batch: true, + agent_id: None, + session_id: None, + tool_use_id: None, + no_stash: true, + stash_db: None, + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_compress_schema_invalid_json() { + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("bad.json"); + std::fs::write(&f, "not json at all").unwrap(); + let result = run_command(Commands::CompressSchema { + file: Some(f.to_str().unwrap().to_string()), + batch: false, + agent_id: None, + session_id: None, + tool_use_id: None, + no_stash: true, + stash_db: None, + }); + assert!(result.is_err()); + assert!(result.unwrap_err().0.contains("JSON parse error")); +} + +#[test] +fn run_command_compress_response_from_file() { + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("response.json"); + std::fs::write( + &f, + r#"{"data":{"items":[1,2,3,4,5,6,7,8,9,10],"debug":"info","value":null,"status":"ok","longText":""}}"#, + ) + .unwrap(); + let result = run_command(Commands::CompressResponse { + file: Some(f.to_str().unwrap().to_string()), + agent_id: Some("test-agent".to_string()), + session_id: Some("sess-1".to_string()), + tool_use_id: Some("tool-1".to_string()), + truncate_strings_at: Some(100), + truncate_arrays_at: Some(5), + max_depth: Some(10), + no_stash: true, + stash_db: None, + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_compress_response_no_stash() { + let _guard = TempDbGuard::new(); + + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("resp.json"); + std::fs::write(&f, r#"{"key":"value"}"#).unwrap(); + let result = run_command(Commands::CompressResponse { + file: Some(f.to_str().unwrap().to_string()), + agent_id: None, + session_id: None, + tool_use_id: None, + truncate_strings_at: None, + truncate_arrays_at: None, + max_depth: None, + no_stash: true, + stash_db: None, + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_compress_response_with_stash() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("resp.json"); + std::fs::write(&f, r#"{"key":"value"}"#).unwrap(); + let result = run_command(Commands::CompressResponse { + file: Some(f.to_str().unwrap().to_string()), + agent_id: None, + session_id: None, + tool_use_id: None, + truncate_strings_at: None, + truncate_arrays_at: None, + max_depth: None, + no_stash: false, + stash_db: None, + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_retrieve_invalid_hash() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + let result = run_command(Commands::Retrieve { + hash: "not-a-hash".to_string(), + stash_db: None, + }); + assert!(result.is_err()); + assert!(result.unwrap_err().0.contains("invalid stash hash")); +} + +#[test] +fn run_command_retrieve_missing_hash() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let result = run_command(Commands::Retrieve { + hash: "abcdef0123456789abcdef01".to_string(), + stash_db: None, + }); + assert!(result.is_err()); + assert!(result.unwrap_err().0.contains("no stashed payload")); +} + +#[test] +fn run_command_compress_toon_from_file() { + let _guard = TempDbGuard::new(); + + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("toon.json"); + std::fs::write( + &f, + r#"{"name":"test","items":[1,2,3],"nested":{"key":"value"}}"#, + ) + .unwrap(); + let result = run_command(Commands::CompressToon { + file: Some(f.to_str().unwrap().to_string()), + agent_id: Some("agent".to_string()), + session_id: Some("sess".to_string()), + tool_use_id: Some("tool".to_string()), + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_decompress_toon_from_file() { + let dir = tempfile::tempdir().unwrap(); + let json_input = r#"{"name":"test","items":[1,2,3]}"#; + let value: serde_json::Value = serde_json::from_str(json_input).unwrap(); + let toon_encoded = toon_format::encode_default(&value).unwrap(); + let toon_f = dir.path().join("input.toon"); + std::fs::write(&toon_f, &toon_encoded).unwrap(); + let result = run_command(Commands::DecompressToon { + file: Some(toon_f.to_str().unwrap().to_string()), + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_decompress_toon_empty_input() { + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("empty.toon"); + std::fs::write(&f, "").unwrap(); + let result = run_command(Commands::DecompressToon { + file: Some(f.to_str().unwrap().to_string()), + }); + // Empty input may decode to null (valid) or fail — either is acceptable + let _ = result; +} + +#[test] +fn run_command_retrieve_with_marker() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let store = open_stash_store(None); + if store.is_none() { + return; + } + let store = store.unwrap(); + let key = store.stash("hello world").unwrap(); + let marker = format!("<>", key); + let result = run_command(Commands::Retrieve { + hash: marker, + stash_db: None, + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_retrieve_bare_hash() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let store = open_stash_store(None); + if store.is_none() { + return; + } + let store = store.unwrap(); + let key = store.stash("retrieve bare hash test").unwrap(); + let result = run_command(Commands::Retrieve { + hash: key, + stash_db: None, + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_stats_show_existing_record() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let recorder = match open_recorder() { + Ok(r) => r, + Err(_) => return, + }; + let mut record = StatsRecord::new( + OperationType::CompressSchema, + "test-agent".to_string(), + 500, + 125, + 100, + 25, + ); + record = record + .with_before_text("before-text-for-show".to_string()) + .with_after_text("after-text-for-show".to_string()); + let id = match recorder.record(&record) { + Ok(id) => id, + Err(_) => return, + }; + let result = run_command(Commands::Stats(StatsCommands::Show { id })); + // format_show requires before_text/after_text to be present + let _ = result; +} + +#[test] +fn run_command_compress_response_large_with_truncation() { + let _guard = TempDbGuard::new(); + + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("large.json"); + let items: Vec = (0..20).map(|i| serde_json::json!({"id": i, "name": format!("item_{}", i), "long_field": "x".repeat(200)})).collect(); + let data = serde_json::json!({"results": items, "debug": "debug info", "meta": null}); + std::fs::write(&f, serde_json::to_string(&data).unwrap()).unwrap(); + let result = run_command(Commands::CompressResponse { + file: Some(f.to_str().unwrap().to_string()), + agent_id: Some("test-agent".to_string()), + session_id: Some("sess-trunc".to_string()), + tool_use_id: Some("tool-trunc".to_string()), + truncate_strings_at: Some(50), + truncate_arrays_at: Some(3), + max_depth: Some(5), + no_stash: true, + stash_db: None, + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_compress_schema_with_stash() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("schema_stash.json"); + let long_desc = "A".repeat(500); + let schema = serde_json::json!({ + "function": { + "name": "test_stash", + "description": long_desc, + "parameters": {"type": "object", "properties": {"x": {"type": "string"}}} + } + }); + std::fs::write(&f, serde_json::to_string(&schema).unwrap()).unwrap(); + let result = run_command(Commands::CompressSchema { + file: Some(f.to_str().unwrap().to_string()), + batch: false, + agent_id: Some("stash-agent".to_string()), + session_id: Some("stash-sess".to_string()), + tool_use_id: None, + no_stash: false, + stash_db: None, + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_stats_summary() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let result = run_command(Commands::Stats(StatsCommands::Summary { + limit: Some(10), + json: false, + compare: None, + })); + assert!(result.is_ok()); +} + +#[test] +fn run_command_stats_summary_json() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let result = run_command(Commands::Stats(StatsCommands::Summary { + limit: Some(10), + json: true, + compare: None, + })); + assert!(result.is_ok()); +} + +#[test] +fn run_command_stats_list() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let result = run_command(Commands::Stats(StatsCommands::List { limit: 5 })); + assert!(result.is_ok()); +} + +#[test] +fn run_command_stats_show_nonexistent() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let result = run_command(Commands::Stats(StatsCommands::Show { id: 999999 })); + assert!(result.is_err()); + assert!(result.unwrap_err().0.contains("not found")); +} + +#[test] +fn run_command_stats_clear_without_confirm() { + // Clear with --yes to avoid interactive prompt + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let result = run_command(Commands::Stats(StatsCommands::Clear { yes: true })); + assert!(result.is_ok()); +} + + +#[test] +fn run_command_stats_status() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let result = run_command(Commands::Stats(StatsCommands::Status)); + assert!(result.is_ok()); +} + +#[test] +fn run_command_stats_compare() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let result = run_command(Commands::Stats(StatsCommands::Summary { + limit: None, + json: false, + compare: Some(vec!["baseline-sess".to_string(), "tokenless-sess".to_string()]), + })); + assert!(result.is_ok()); +} + +#[test] +fn run_command_stats_compare_json() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let result = run_command(Commands::Stats(StatsCommands::Summary { + limit: None, + json: true, + compare: Some(vec!["baseline-sess".to_string(), "tokenless-sess".to_string()]), + })); + assert!(result.is_ok()); +} + +#[test] +fn run_command_env_check_without_spec() { + let result = run_command(Commands::EnvCheck { + tool: Some("NonexistentTool".to_string()), + all: false, + fix: false, + checklist: false, + json: false, + }); + let _ = result; +} + +#[test] +fn get_stash_db_path_returns_valid() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + let home = get_home_dir(); + if home.is_empty() { + return; + } + let path = get_stash_db_path(None); + assert!(path.is_some()); + assert!(path.unwrap().contains(".tokenless/stash.db")); +} + +#[test] +fn get_stash_db_path_with_bad_override() { + let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + let home = get_home_dir(); + if home.is_empty() { + return; + } + let path = get_stash_db_path(Some("/nonexistent/deep/dir/stash.db")); + // Bad override is rejected, falls back to default + assert!(path.is_some()); + assert!(path.unwrap().contains(".tokenless/stash.db")); +} + + +#[test] +fn run_command_compress_schema_no_savings() { + let _guard = TempDbGuard::new(); + + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("small.json"); + std::fs::write(&f, r#"{"function":{"name":"x","parameters":{"type":"object","properties":{}}}}"#).unwrap(); + let result = run_command(Commands::CompressSchema { + file: Some(f.to_str().unwrap().to_string()), + batch: false, + agent_id: None, + session_id: None, + tool_use_id: None, + no_stash: true, + stash_db: None, + }); + assert!(result.is_ok()); +} + +#[test] +fn run_command_compress_response_file_not_found() { + let result = run_command(Commands::CompressResponse { + file: Some("/nonexistent/path.json".to_string()), + agent_id: None, + session_id: None, + tool_use_id: None, + truncate_strings_at: None, + truncate_arrays_at: None, + max_depth: None, + no_stash: true, + stash_db: None, + }); + assert!(result.is_err()); +} + +#[test] +fn open_stash_store_none_returns_some() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let result = open_stash_store(None); + assert!(result.is_some()); +} + +#[test] +fn open_stash_store_or_err_none_returns_ok() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let result = open_stash_store_or_err(None); + assert!(result.is_ok()); +} + +#[test] +fn record_compression_stats_sls_only_path() { + let mut config = TokenlessConfig::default(); + config.stats_enabled = false; + config.sls_enabled = true; + let long_before = "x".repeat(500); + let short_after = "y".repeat(50); + record_compression_stats( + &config, + OperationType::CompressResponse, + Some("sls-test".to_string()), + Some("sls-session".to_string()), + Some("sls-tool".to_string()), + long_before, + short_after, + CompressionMode::Active, + Some(2), + Some(0), + Some(15), + ); +} + +#[test] +fn record_compression_stats_full_path() { + let _guard = match TempDbGuard::new() { Some(g) => g, None => return }; + let mut config = TokenlessConfig::default(); + config.stats_enabled = true; + config.sls_enabled = true; + let long_before = "z".repeat(1000); + let short_after = "w".repeat(100); + record_compression_stats( + &config, + OperationType::CompressSchema, + Some("full-agent".to_string()), + Some("full-session".to_string()), + Some("full-tool".to_string()), + long_before, + short_after, + CompressionMode::Active, + Some(3), + Some(1), + Some(200), + ); +} + +#[test] +fn record_compression_stats_both_disabled() { + let config = TokenlessConfig::default(); + record_compression_stats( + &config, + OperationType::CompressResponse, + None, + None, + None, + "before".to_string(), + "after".to_string(), + CompressionMode::Active, + None, + None, + None, + ); +} + +#[test] +fn record_compression_stats_no_savings() { + let mut config = TokenlessConfig::default(); + config.stats_enabled = true; + record_compression_stats( + &config, + OperationType::CompressResponse, + None, + None, + None, + "short".to_string(), + "this is longer than before so no savings".to_string(), + CompressionMode::Active, + None, + None, + None, + ); +} + + +#[test] +fn run_command_compress_response_no_agent_id() { + let _guard = TempDbGuard::new(); + + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("resp.json"); + std::fs::write(&f, r#"{"data":"test","debug":"remove"}"#).unwrap(); + let result = run_command(Commands::CompressResponse { + file: Some(f.to_str().unwrap().to_string()), + agent_id: None, + session_id: None, + tool_use_id: None, + truncate_strings_at: None, + truncate_arrays_at: None, + max_depth: None, + no_stash: true, + stash_db: None, + }); + assert!(result.is_ok()); +} + +#[test] +fn read_input_file_too_large() { + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("huge.json"); + let huge = "x".repeat(64 * 1024 * 1024 + 1); + std::fs::write(&f, &huge).unwrap(); + let result = read_input(&Some(f.to_str().unwrap().to_string())); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("limit")); +} + +#[test] +fn read_input_file_missing() { + let result = read_input(&Some("/nonexistent/file.json".to_string())); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Failed to open")); +} + + +#[test] +fn run_command_retrieve_store_error() { + let result = run_command(Commands::Retrieve { + hash: "abcdef0123456789abcdef01".to_string(), + stash_db: Some("/nonexistent/deep/nested/dir/stash.db".to_string()), + }); + // The stash_db override gets rejected and falls back to default + let _ = result; +} + +#[test] +fn run_command_compress_toon_tiny_input() { + let _guard = TempDbGuard::new(); + + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("tiny.json"); + std::fs::write(&f, r#""x""#).unwrap(); + let result = run_command(Commands::CompressToon { + file: Some(f.to_str().unwrap().to_string()), + agent_id: None, + session_id: None, + tool_use_id: None, + }); + let _ = result; +} + +#[test] +fn run_command_compress_toon_empty_obj() { + let _guard = TempDbGuard::new(); + + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("empty_obj.json"); + std::fs::write(&f, r#"{}"#).unwrap(); + let result = run_command(Commands::CompressToon { + file: Some(f.to_str().unwrap().to_string()), + agent_id: None, + session_id: None, + tool_use_id: None, + }); + let _ = result; +} + +#[test] +fn run_command_compress_response_with_stash_errors() { + let _guard = TempDbGuard::new(); + + let dir = tempfile::tempdir().unwrap(); + let f = dir.path().join("stash_error.json"); + let items: Vec = (0..100).map(|i| serde_json::json!({"id": i, "data": "x".repeat(200)})).collect(); + let data = serde_json::json!({"results": items}); + std::fs::write(&f, serde_json::to_string(&data).unwrap()).unwrap(); + let result = run_command(Commands::CompressResponse { + file: Some(f.to_str().unwrap().to_string()), + agent_id: None, + session_id: None, + tool_use_id: None, + truncate_strings_at: Some(10), + truncate_arrays_at: Some(3), + max_depth: Some(3), + no_stash: false, + stash_db: None, + }); + let _ = result; +} diff --git a/src/tokenless/crates/tokenless-schema/src/tests/response_compressor_tests.rs b/src/tokenless/crates/tokenless-schema/src/tests/response_compressor_tests.rs index 16f253574..ebf9e82f7 100644 --- a/src/tokenless/crates/tokenless-schema/src/tests/response_compressor_tests.rs +++ b/src/tokenless/crates/tokenless-schema/src/tests/response_compressor_tests.rs @@ -490,3 +490,95 @@ fn test_is_empty_value() { assert!(!compressor.is_empty_value(&json!(0))); assert!(!compressor.is_empty_value(&json!(null))); } + +#[test] +fn test_depth_truncation_with_stash() { + use std::sync::Arc; + use tokenless_ccr::{InMemoryStore, StashStore, extract_hash}; + + let store = Arc::new(InMemoryStore::new()); + let compressor = ResponseCompressor::new() + .with_max_depth(1) + .with_stash_store(store.clone()); + let deep = json!({"level1": {"level2": {"level3": "deep"}}}); + let result = compressor.compress(&deep); + let truncated = result["level1"]["level2"].as_str().unwrap(); + assert!(truncated.contains("truncated at depth")); + assert!(truncated.contains("tokenless:")); + let hash = extract_hash(truncated).unwrap(); + let retrieved = store.retrieve(hash).unwrap().unwrap(); + let recovered: Value = serde_json::from_str(&retrieved).unwrap(); + assert_eq!(recovered["level3"], "deep"); +} + +#[test] +fn test_depth_truncation_various_types() { + let compressor = ResponseCompressor::new().with_max_depth(0); + let arr = json!({"a": [1, 2, 3]}); + let result = compressor.compress(&arr); + let t = result["a"].as_str().unwrap(); + assert!(t.contains("array truncated at depth")); + + let s = json!({"a": "hello"}); + let result = compressor.compress(&s); + let t = result["a"].as_str().unwrap(); + assert!(t.contains("string truncated at depth")); + + let n = json!({"a": 42}); + let result = compressor.compress(&n); + let t = result["a"].as_str().unwrap(); + assert!(t.contains("number truncated at depth")); + + let b = json!({"a": true}); + let result = compressor.compress(&b); + let t = result["a"].as_str().unwrap(); + assert!(t.contains("bool truncated at depth")); + + let null_val = json!({"a": null}); + let result = compressor.compress(&null_val); + let t = result["a"].as_str().unwrap(); + assert!(t.contains("null truncated at depth")); +} + +#[test] +fn test_depth_truncation_without_stash() { + let compressor = ResponseCompressor::new().with_max_depth(1); + let deep = json!({"level1": {"level2": {"level3": "deep"}}}); + let result = compressor.compress(&deep); + let truncated = result["level1"]["level2"].as_str().unwrap(); + assert!(truncated.contains("truncated at depth")); + assert!(!truncated.contains("tokenless:")); +} + +#[test] +fn test_string_truncation_with_stash() { + use std::sync::Arc; + use tokenless_ccr::{InMemoryStore, StashStore, extract_hash}; + + let store = Arc::new(InMemoryStore::new()); + let compressor = ResponseCompressor::new() + .with_truncate_strings_at(100) + .with_stash_store(store.clone()); + let long = "x".repeat(500); + let result = compressor.compress(&json!(long)); + let s = result.as_str().unwrap(); + assert!(s.contains("tokenless:")); + let hash = extract_hash(s).unwrap(); + let retrieved = store.retrieve(hash).unwrap().unwrap(); + assert_eq!(retrieved, long); +} + +#[test] +fn test_stash_dropped_empty_not_engaged() { + use std::sync::Arc; + use tokenless_ccr::InMemoryStore; + + let store = Arc::new(InMemoryStore::new()); + let compressor = ResponseCompressor::new() + .with_truncate_arrays_at(5) + .with_stash_store(store.clone()); + let arr = json!([1, 2, 3]); + let result = compressor.compress(&arr); + assert_eq!(result.as_array().unwrap().len(), 3); + assert_eq!(store.len(), 0); +} diff --git a/src/tokenless/crates/tokenless-schema/src/tests/schema_compressor_tests.rs b/src/tokenless/crates/tokenless-schema/src/tests/schema_compressor_tests.rs index bbdf215b4..85c0e4d00 100644 --- a/src/tokenless/crates/tokenless-schema/src/tests/schema_compressor_tests.rs +++ b/src/tokenless/crates/tokenless-schema/src/tests/schema_compressor_tests.rs @@ -444,3 +444,65 @@ fn char_index_multibyte() { assert_eq!(char_index(text, 0), 0); assert_eq!(char_index(text, 2), 6); // 2 CJK chars = 6 bytes } + +#[test] +fn test_description_truncation_with_stash() { + use std::sync::Arc; + use tokenless_ccr::{InMemoryStore, StashStore, extract_hash}; + + let store = Arc::new(InMemoryStore::new()); + let compressor = SchemaCompressor::new() + .with_func_desc_max_len(100) + .with_stash_store(store.clone()); + let long_desc = "A".repeat(300); + let schema = json!({ + "function": { + "name": "test", + "description": long_desc, + "parameters": {"type": "object", "properties": {}} + } + }); + let result = compressor.compress(&schema); + let desc = result["function"]["description"].as_str().unwrap(); + assert!(desc.chars().count() <= 100); + assert!(desc.contains("tokenless:")); + let hash = extract_hash(desc).unwrap(); + let retrieved = store.retrieve(hash).unwrap().unwrap(); + assert_eq!(retrieved, long_desc); +} + +#[test] +fn test_compress_parameters_with_nested_schema() { + let compressor = SchemaCompressor::new(); + let schema = json!({ + "function": { + "name": "test", + "parameters": { + "type": "object", + "properties": { + "config": { + "type": "object", + "title": "Config Title", + "examples": ["ex1"], + "properties": { + "nested": { + "type": "string", + "title": "Nested Title", + "description": "B".repeat(200) + } + } + } + } + } + } + }); + let result = compressor.compress(&schema); + let props = &result["function"]["parameters"]["properties"]; + assert!(props["config"].get("title").is_none()); + assert!(props["config"].get("examples").is_none()); + assert!(props["config"]["properties"]["nested"].get("title").is_none()); + let nested_desc = props["config"]["properties"]["nested"]["description"] + .as_str() + .unwrap(); + assert!(nested_desc.chars().count() <= 160); +} diff --git a/src/tokenless/crates/tokenless-stats/src/query.rs b/src/tokenless/crates/tokenless-stats/src/query.rs index 31e278e1b..2fff049e5 100644 --- a/src/tokenless/crates/tokenless-stats/src/query.rs +++ b/src/tokenless/crates/tokenless-stats/src/query.rs @@ -737,4 +737,49 @@ mod tests { serde_json::from_str(&format_compare_json(&[], &[])).unwrap(); assert_eq!(parsed.get("saved_percent").unwrap().as_f64().unwrap(), 0.0); } + + #[test] + fn test_format_summary_with_zero_session_total() { + let records = vec![test_record()]; + let output = format_summary(&records, Some("Test"), Some(0)); + assert!(output.contains("Overall Savings vs Total Consumption")); + } + + #[test] + fn test_format_list_with_more_records_than_limit() { + let records: Vec = (0..5) + .map(|i| { + let mut r = test_record(); + r.id = i + 1; + r + }) + .collect(); + let output = format_list(&records, 3); + assert!(output.contains("Showing 3 record")); + assert!(output.contains("and 2 more")); + } + + #[test] + fn test_format_compare_zero_baseline() { + let baseline = vec![compare_record( + OperationType::CompressSchema, + CompressionMode::DryRun, + 0, + 0, + )]; + let tokenless = vec![compare_record( + OperationType::CompressSchema, + CompressionMode::Active, + 0, + 0, + )]; + let out = format_compare(&baseline, &tokenless); + assert!(out.contains("0.0%")); + } + + #[test] + fn test_format_list_empty() { + let output = format_list(&[], 10); + assert!(output.contains("No records found")); + } } diff --git a/src/tokenless/crates/tokenless-stats/src/sls.rs b/src/tokenless/crates/tokenless-stats/src/sls.rs index 68594eee1..958266b72 100644 --- a/src/tokenless/crates/tokenless-stats/src/sls.rs +++ b/src/tokenless/crates/tokenless-stats/src/sls.rs @@ -443,6 +443,11 @@ mod tests { ); } + #[test] + fn test_sls_writer_default_trait() { + let _writer = SlsWriter::default(); + } + #[test] fn test_sls_writer_writes_to_file() { let dir = tempfile::tempdir().unwrap(); diff --git a/src/tokenless/crates/tokenless-stats/src/tests/recorder_tests.rs b/src/tokenless/crates/tokenless-stats/src/tests/recorder_tests.rs index 99cfee352..cdc9d6992 100644 --- a/src/tokenless/crates/tokenless-stats/src/tests/recorder_tests.rs +++ b/src/tokenless/crates/tokenless-stats/src/tests/recorder_tests.rs @@ -219,3 +219,93 @@ fn summary_zero_before_returns_zero_percent() { assert_eq!(summary.chars_percent(), 0.0); assert_eq!(summary.tokens_percent(), 0.0); } + +#[test] +fn actual_savings_percent_zero_session_total() { + let summary = StatsSummary { + total_records: 1, + total_before_chars: 1000, + total_after_chars: 500, + total_before_tokens: 400, + total_after_tokens: 200, + }; + assert_eq!(summary.actual_savings_percent(0), 0.0); + let pct = summary.actual_savings_percent(2000); + assert!((pct - 10.0).abs() < 0.1); +} + +#[test] +fn schema_migration_adds_missing_columns() { + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("migrate.db"); + { + let conn = rusqlite::Connection::open(&db_path).unwrap(); + conn.execute_batch( + "CREATE TABLE stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT NOT NULL, + operation TEXT NOT NULL, + agent_id TEXT NOT NULL, + source_pid INTEGER, + session_id TEXT, + tool_use_id TEXT, + before_chars INTEGER NOT NULL, + before_tokens INTEGER NOT NULL, + after_chars INTEGER NOT NULL, + after_tokens INTEGER NOT NULL, + before_text TEXT, + after_text TEXT + )", + ) + .unwrap(); + } + let rec = StatsRecorder::new(&db_path).unwrap(); + let mut record = + StatsRecord::new(OperationType::CompressSchema, "cli".into(), 100, 25, 50, 12) + .with_mode(CompressionMode::Active) + .with_stash(Some(1), Some(0), Some(5)); + let id = rec.record(&record).unwrap(); + let got = rec.record_by_id(id).unwrap().unwrap(); + assert_eq!(got.mode, CompressionMode::Active); + assert_eq!(got.stash_writes, Some(1)); +} + +#[test] +fn all_records_handles_corrupt_row() { + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("corrupt.db"); + let rec = StatsRecorder::new(&db_path).unwrap(); + rec.record(&sample( + OperationType::CompressSchema, + CompressionMode::Active, + "s1", + )) + .unwrap(); + { + let conn = rusqlite::Connection::open(&db_path).unwrap(); + conn.execute( + "INSERT INTO stats (timestamp, operation, agent_id, before_chars, before_tokens, after_chars, after_tokens) + VALUES ('not-a-date', 'compress_schema', 'cli', 100, 25, 50, 12)", + [], + ) + .unwrap(); + } + let records = rec.all_records(None).unwrap(); + assert!(records.len() >= 1); +} + +#[test] +fn record_with_before_after_output() { + let (rec, _dir) = new_recorder(); + let record = + StatsRecord::new(OperationType::CompressSchema, "cli".into(), 100, 25, 50, 12) + .with_before_text("before-text".to_string()) + .with_after_text("after-text".to_string()) + .with_output("before-output".to_string(), "after-output".to_string()); + let id = rec.record(&record).unwrap(); + let got = rec.record_by_id(id).unwrap().unwrap(); + assert_eq!(got.before_text.as_deref(), Some("before-text")); + assert_eq!(got.after_text.as_deref(), Some("after-text")); + assert_eq!(got.before_output.as_deref(), Some("before-output")); + assert_eq!(got.after_output.as_deref(), Some("after-output")); +}