lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 70289e9930135b87f9a78bd082d457a33aacee17
parent 3f3081ed92c5139cd4889a45c3f1abca4c445198
Author: triesap <tyson@radroots.org>
Date:   Fri, 10 Apr 2026 22:28:34 +0000

runtime: cover tracing and service paths

Diffstat:
Mcrates/runtime/src/secret_file.rs | 217+------------------------------------------------------------------------------
Acrates/runtime/src/secret_file/tests.rs | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/runtime/src/service.rs | 21+++++++++++++++++++++
Mcrates/runtime/src/tracing.rs | 136++++++++++++++-----------------------------------------------------------------
Acrates/runtime/src/tracing/tests.rs | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 399 insertions(+), 329 deletions(-)

diff --git a/crates/runtime/src/secret_file.rs b/crates/runtime/src/secret_file.rs @@ -231,219 +231,4 @@ fn set_secret_permissions(_path: &Path) -> Result<(), RadrootsSecretVaultAccessE } #[cfg(test)] -mod tests { - use super::{ - LocalWrappedKeySource, RuntimeProtectedFileError, WRAPPED_KEY_VERSION, - local_wrapping_key_path, open_local_secret_file, seal_local_secret_file, - }; - use radroots_secret_vault::RadrootsSecretKeyWrapping; - - #[test] - fn secret_file_round_trips_with_sidecar_key() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("identity.secret.json"); - - seal_local_secret_file( - &path, - "runtime_test_identity", - br#"{"secret_key":"secret"}"#, - ) - .expect("seal local secret file"); - - let payload = - open_local_secret_file(&path, "runtime_test_identity").expect("open local secret file"); - assert_eq!(payload, br#"{"secret_key":"secret"}"#); - assert!(local_wrapping_key_path(&path).is_file()); - } - - #[test] - fn secret_file_open_fails_when_wrapping_key_is_missing() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("identity.secret.json"); - - seal_local_secret_file(&path, "runtime_test_identity", b"payload") - .expect("seal local secret file"); - std::fs::remove_file(local_wrapping_key_path(&path)).expect("remove wrapping key"); - - let err = open_local_secret_file(&path, "runtime_test_identity") - .expect_err("missing wrapping key should fail"); - assert!(err.to_string().contains("identity.secret.json")); - } - - #[test] - fn secret_file_open_fails_when_key_slot_does_not_match() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("identity.secret.json"); - - seal_local_secret_file(&path, "runtime_test_identity", b"payload") - .expect("seal local secret file"); - - let err = open_local_secret_file(&path, "unexpected_slot") - .expect_err("slot mismatch should fail"); - assert!( - err.to_string() - .contains("expected key slot unexpected_slot") - ); - } - - #[test] - fn local_wrapped_key_source_reuses_existing_key_file() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("identity.secret.json"); - let key_path = local_wrapping_key_path(&path); - let expected = [7_u8; super::RADROOTS_PROTECTED_STORE_KEY_LENGTH]; - std::fs::write(&key_path, expected).expect("write sidecar key"); - - let source = LocalWrappedKeySource::new(&path); - let loaded = source - .load_or_create_wrapping_key() - .expect("existing key should be reused"); - - assert_eq!(loaded, expected); - } - - #[test] - fn local_wrapped_key_source_rejects_invalid_key_length() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("identity.secret.json"); - let key_path = local_wrapping_key_path(&path); - std::fs::write( - &key_path, - [7_u8; super::RADROOTS_PROTECTED_STORE_KEY_LENGTH - 1], - ) - .expect("write short sidecar key"); - - let source = LocalWrappedKeySource::new(&path); - let err = source - .load_wrapping_key() - .expect_err("short wrapping key must fail"); - - assert!( - err.to_string().contains("invalid length"), - "unexpected error: {err}" - ); - } - - #[test] - fn local_wrapped_key_source_rejects_truncated_invalid_and_tampered_wrapped_keys() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("identity.secret.json"); - let source = LocalWrappedKeySource::new(&path); - - let wrapped = source - .wrap_data_key("runtime_test_identity", b"payload") - .expect("wrap succeeds"); - - let err = source - .unwrap_data_key( - "runtime_test_identity", - &wrapped[..=super::RADROOTS_PROTECTED_STORE_NONCE_LENGTH], - ) - .expect_err("truncated wrapped key must fail"); - assert!(err.to_string().contains("truncated")); - - let mut invalid_version = wrapped.clone(); - invalid_version[0] = WRAPPED_KEY_VERSION + 1; - let err = source - .unwrap_data_key("runtime_test_identity", &invalid_version) - .expect_err("invalid wrapped key version must fail"); - assert!( - err.to_string() - .contains("unsupported wrapped protected secret data key version") - ); - - let mut tampered = wrapped; - let last = tampered.len() - 1; - tampered[last] ^= 0x01; - let err = source - .unwrap_data_key("runtime_test_identity", &tampered) - .expect_err("tampered ciphertext must fail"); - assert!( - err.to_string() - .contains("failed to unwrap protected secret data key") - ); - } - - #[test] - fn seal_local_secret_file_reports_create_dir_failure() { - let temp = tempfile::tempdir().expect("tempdir"); - let blocked_parent = temp.path().join("not-a-dir"); - std::fs::write(&blocked_parent, b"blocker").expect("write blocker file"); - let path = blocked_parent.join("identity.secret.json"); - - let err = seal_local_secret_file(&path, "runtime_test_identity", b"payload") - .expect_err("parent file must block directory creation"); - - match err { - RuntimeProtectedFileError::CreateDir { path: err_path, .. } => { - assert_eq!(err_path, blocked_parent); - } - other => panic!("unexpected error variant: {other:?}"), - } - } - - #[test] - fn seal_local_secret_file_reports_seal_failure_for_invalid_existing_wrapping_key() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("identity.secret.json"); - std::fs::write(local_wrapping_key_path(&path), [1_u8; 3]).expect("write invalid sidecar"); - - let err = seal_local_secret_file(&path, "runtime_test_identity", b"payload") - .expect_err("invalid sidecar should fail sealing"); - - match err { - RuntimeProtectedFileError::Seal { - path: err_path, - message, - } => { - assert_eq!(err_path, path); - assert!(!message.is_empty()); - } - other => panic!("unexpected error variant: {other:?}"), - } - } - - #[test] - fn seal_local_secret_file_reports_io_error_when_target_is_directory() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("identity.secret.json"); - std::fs::create_dir(&path).expect("create directory target"); - - let err = seal_local_secret_file(&path, "runtime_test_identity", b"payload") - .expect_err("directory target must fail write"); - - match err { - RuntimeProtectedFileError::Io { path: err_path, .. } => assert_eq!(err_path, path), - other => panic!("unexpected error variant: {other:?}"), - } - } - - #[test] - fn open_local_secret_file_reports_io_error_for_missing_payload_file() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("missing.secret.json"); - - let err = open_local_secret_file(&path, "runtime_test_identity") - .expect_err("missing file must fail"); - - match err { - RuntimeProtectedFileError::Io { path: err_path, .. } => assert_eq!(err_path, path), - other => panic!("unexpected error variant: {other:?}"), - } - } - - #[test] - fn open_local_secret_file_reports_decode_error_for_invalid_payload() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("identity.secret.json"); - std::fs::write(&path, b"not-json").expect("write invalid payload"); - - let err = open_local_secret_file(&path, "runtime_test_identity") - .expect_err("invalid json payload must fail"); - - match err { - RuntimeProtectedFileError::Decode { path: err_path, .. } => assert_eq!(err_path, path), - other => panic!("unexpected error variant: {other:?}"), - } - } -} +mod tests; diff --git a/crates/runtime/src/secret_file/tests.rs b/crates/runtime/src/secret_file/tests.rs @@ -0,0 +1,211 @@ +use super::{ + LocalWrappedKeySource, RuntimeProtectedFileError, WRAPPED_KEY_VERSION, local_wrapping_key_path, + open_local_secret_file, seal_local_secret_file, +}; +use radroots_secret_vault::RadrootsSecretKeyWrapping; + +#[test] +fn secret_file_round_trips_with_sidecar_key() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("identity.secret.json"); + + seal_local_secret_file( + &path, + "runtime_test_identity", + br#"{"secret_key":"secret"}"#, + ) + .expect("seal local secret file"); + + let payload = + open_local_secret_file(&path, "runtime_test_identity").expect("open local secret file"); + assert_eq!(payload, br#"{"secret_key":"secret"}"#); + assert!(local_wrapping_key_path(&path).is_file()); +} + +#[test] +fn secret_file_open_fails_when_wrapping_key_is_missing() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("identity.secret.json"); + + seal_local_secret_file(&path, "runtime_test_identity", b"payload") + .expect("seal local secret file"); + std::fs::remove_file(local_wrapping_key_path(&path)).expect("remove wrapping key"); + + let err = + open_local_secret_file(&path, "runtime_test_identity").expect_err("missing wrapping key"); + assert!(err.to_string().contains("identity.secret.json")); +} + +#[test] +fn secret_file_open_fails_when_key_slot_does_not_match() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("identity.secret.json"); + + seal_local_secret_file(&path, "runtime_test_identity", b"payload") + .expect("seal local secret file"); + + let err = + open_local_secret_file(&path, "unexpected_slot").expect_err("slot mismatch should fail"); + assert!( + err.to_string() + .contains("expected key slot unexpected_slot") + ); +} + +#[test] +fn local_wrapped_key_source_reuses_existing_key_file() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("identity.secret.json"); + let key_path = local_wrapping_key_path(&path); + let expected = [7_u8; super::RADROOTS_PROTECTED_STORE_KEY_LENGTH]; + std::fs::write(&key_path, expected).expect("write sidecar key"); + + let source = LocalWrappedKeySource::new(&path); + let loaded = source + .load_or_create_wrapping_key() + .expect("existing key should be reused"); + + assert_eq!(loaded, expected); +} + +#[test] +fn local_wrapped_key_source_rejects_invalid_key_length() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("identity.secret.json"); + let key_path = local_wrapping_key_path(&path); + std::fs::write( + &key_path, + [7_u8; super::RADROOTS_PROTECTED_STORE_KEY_LENGTH - 1], + ) + .expect("write short sidecar key"); + + let source = LocalWrappedKeySource::new(&path); + let err = source + .load_wrapping_key() + .expect_err("short wrapping key must fail"); + + assert!( + err.to_string().contains("invalid length"), + "unexpected error: {err}" + ); +} + +#[test] +fn local_wrapped_key_source_rejects_truncated_invalid_and_tampered_wrapped_keys() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("identity.secret.json"); + let source = LocalWrappedKeySource::new(&path); + + let wrapped = source + .wrap_data_key("runtime_test_identity", b"payload") + .expect("wrap succeeds"); + + let err = source + .unwrap_data_key( + "runtime_test_identity", + &wrapped[..=super::RADROOTS_PROTECTED_STORE_NONCE_LENGTH], + ) + .expect_err("truncated wrapped key must fail"); + assert!(err.to_string().contains("truncated")); + + let mut invalid_version = wrapped.clone(); + invalid_version[0] = WRAPPED_KEY_VERSION + 1; + let err = source + .unwrap_data_key("runtime_test_identity", &invalid_version) + .expect_err("invalid wrapped key version must fail"); + assert!( + err.to_string() + .contains("unsupported wrapped protected secret data key version") + ); + + let mut tampered = wrapped; + let last = tampered.len() - 1; + tampered[last] ^= 0x01; + let err = source + .unwrap_data_key("runtime_test_identity", &tampered) + .expect_err("tampered ciphertext must fail"); + assert!( + err.to_string() + .contains("failed to unwrap protected secret data key") + ); +} + +#[test] +fn seal_local_secret_file_reports_create_dir_failure() { + let temp = tempfile::tempdir().expect("tempdir"); + let blocked_parent = temp.path().join("not-a-dir"); + std::fs::write(&blocked_parent, b"blocker").expect("write blocker file"); + let path = blocked_parent.join("identity.secret.json"); + + let err = seal_local_secret_file(&path, "runtime_test_identity", b"payload") + .expect_err("parent file must block directory creation"); + + assert!(matches!(err, RuntimeProtectedFileError::CreateDir { .. })); + if let RuntimeProtectedFileError::CreateDir { path: err_path, .. } = &err { + assert_eq!(err_path, &blocked_parent); + } +} + +#[test] +fn seal_local_secret_file_reports_seal_failure_for_invalid_existing_wrapping_key() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("identity.secret.json"); + std::fs::write(local_wrapping_key_path(&path), [1_u8; 3]).expect("write invalid sidecar"); + + let err = seal_local_secret_file(&path, "runtime_test_identity", b"payload") + .expect_err("invalid sidecar should fail sealing"); + + assert!(matches!(err, RuntimeProtectedFileError::Seal { .. })); + if let RuntimeProtectedFileError::Seal { + path: err_path, + message, + } = &err + { + assert_eq!(err_path, &path); + assert!(!message.is_empty()); + } +} + +#[test] +fn seal_local_secret_file_reports_io_error_when_target_is_directory() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("identity.secret.json"); + std::fs::create_dir(&path).expect("create directory target"); + + let err = seal_local_secret_file(&path, "runtime_test_identity", b"payload") + .expect_err("directory target must fail write"); + + assert!(matches!(err, RuntimeProtectedFileError::Io { .. })); + if let RuntimeProtectedFileError::Io { path: err_path, .. } = &err { + assert_eq!(err_path, &path); + } +} + +#[test] +fn open_local_secret_file_reports_io_error_for_missing_payload_file() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("missing.secret.json"); + + let err = + open_local_secret_file(&path, "runtime_test_identity").expect_err("missing file must fail"); + + assert!(matches!(err, RuntimeProtectedFileError::Io { .. })); + if let RuntimeProtectedFileError::Io { path: err_path, .. } = &err { + assert_eq!(err_path, &path); + } +} + +#[test] +fn open_local_secret_file_reports_decode_error_for_invalid_payload() { + let temp = tempfile::tempdir().expect("tempdir"); + let path = temp.path().join("identity.secret.json"); + std::fs::write(&path, b"not-json").expect("write invalid payload"); + + let err = open_local_secret_file(&path, "runtime_test_identity") + .expect_err("invalid json payload must fail"); + + assert!(matches!(err, RuntimeProtectedFileError::Decode { .. })); + if let RuntimeProtectedFileError::Decode { path: err_path, .. } = &err { + assert_eq!(err_path, &path); + } +} diff --git a/crates/runtime/src/service.rs b/crates/runtime/src/service.rs @@ -125,4 +125,25 @@ logs_dir = "logs" ) ); } + + #[test] + fn service_bootstrap_paths_reject_invalid_runtime_ids() { + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Linux, + RadrootsHostEnvironment { + home_dir: Some(PathBuf::from("/home/treesap")), + ..RadrootsHostEnvironment::default() + }, + ); + + let err = service_bootstrap_paths_for( + &resolver, + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + "", + ) + .expect_err("empty runtime ids must fail"); + + assert!(!err.to_string().is_empty()); + } } diff --git a/crates/runtime/src/tracing.rs b/crates/runtime/src/tracing.rs @@ -14,7 +14,7 @@ pub fn init() -> Result<(), RuntimeTracingError> { pub fn default_shared_runtime_logs_dir() -> Result<PathBuf, RadrootsRuntimePathsError> { default_shared_runtime_logs_dir_for( - &RadrootsPathResolver::current(), + &current_path_resolver(), RadrootsPathProfile::InteractiveUser, &RadrootsPathOverrides::default(), ) @@ -66,10 +66,13 @@ fn log_name_from_path(exe: Option<PathBuf>) -> Option<String> { #[cfg(test)] mod test_hooks { - use std::cell::Cell; + use std::cell::{Cell, RefCell}; + + use radroots_runtime_paths::RadrootsPathResolver; thread_local! { static IGNORE_ENV: Cell<bool> = const { Cell::new(false) }; + static CURRENT_RESOLVER: RefCell<Option<RadrootsPathResolver>> = RefCell::new(None); } pub fn set_ignore_env(ignore: bool) { @@ -79,6 +82,23 @@ mod test_hooks { pub fn ignore_env() -> bool { IGNORE_ENV.with(Cell::get) } + + pub fn set_current_resolver(resolver: Option<RadrootsPathResolver>) { + CURRENT_RESOLVER.with(|state| *state.borrow_mut() = resolver); + } + + pub fn current_resolver() -> Option<RadrootsPathResolver> { + CURRENT_RESOLVER.with(|state| state.borrow().clone()) + } +} + +fn current_path_resolver() -> RadrootsPathResolver { + #[cfg(test)] + if let Some(resolver) = test_hooks::current_resolver() { + return resolver; + } + + RadrootsPathResolver::current() } fn log_name_from_stem(stem: &str) -> Option<String> { @@ -116,114 +136,4 @@ fn resolve_default_level(env_level: Option<String>, default_level: Option<&str>) } #[cfg(test)] -mod tests { - use super::{ - default_log_file_name, default_log_file_name_from_exe_name, - default_shared_runtime_logs_dir_for, env_value, init_with_logs_dir, log_name_from_path, - log_name_from_stem, normalize_env_value, resolve_default_level, test_hooks, - }; - use radroots_runtime_paths::{ - RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, - RadrootsPlatform, - }; - use std::path::PathBuf; - use tempfile::tempdir; - - #[test] - fn normalize_env_value_handles_empty_and_non_empty_values() { - assert_eq!(normalize_env_value(" value "), Some("value".to_string())); - assert_eq!(normalize_env_value(" "), None); - assert_eq!(normalize_env_value(""), None); - } - - #[test] - fn env_helpers_return_expected_values() { - assert_eq!(env_value("RADROOTS_RUNTIME_TEST_MISSING_KEY"), None); - let home = env_value("HOME").expect("home env"); - assert!(!home.is_empty()); - } - - #[test] - fn log_name_helpers_cover_empty_and_non_empty_names() { - assert_eq!( - log_name_from_stem("radrootsd"), - Some("radrootsd.log".to_string()) - ); - assert_eq!(log_name_from_stem(""), None); - } - - #[test] - fn log_name_from_path_handles_missing_components() { - assert_eq!(log_name_from_path(None), None); - assert_eq!(log_name_from_path(Some(PathBuf::from("/"))), None); - assert_eq!( - log_name_from_path(Some(PathBuf::from("/tmp/radrootsd"))), - Some("radrootsd.log".to_string()) - ); - } - - #[test] - fn default_log_file_name_helpers_cover_fallback() { - assert_eq!( - default_log_file_name_from_exe_name(Some("svc.log".to_string())), - "svc.log" - ); - assert_eq!( - default_log_file_name_from_exe_name(None), - format!("{}.log", env!("CARGO_PKG_NAME")) - ); - assert!(!default_log_file_name().trim().is_empty()); - } - - #[test] - fn resolve_default_level_prefers_env_then_fallback() { - let env_value = resolve_default_level(Some("warn".to_string()), Some("info")); - assert_eq!(env_value, Some("warn".to_string())); - let fallback = resolve_default_level(None, Some("info")); - assert_eq!(fallback, Some("info".to_string())); - let none = resolve_default_level(None, None); - assert_eq!(none, None); - } - - #[test] - fn default_shared_runtime_logs_dir_uses_shared_namespace() { - let resolver = RadrootsPathResolver::new( - RadrootsPlatform::Macos, - RadrootsHostEnvironment { - home_dir: Some(PathBuf::from("/Users/treesap")), - ..RadrootsHostEnvironment::default() - }, - ); - - let logs_dir = default_shared_runtime_logs_dir_for( - &resolver, - RadrootsPathProfile::InteractiveUser, - &RadrootsPathOverrides::default(), - ) - .expect("default shared runtime logs dir should resolve"); - - assert_eq!( - logs_dir, - PathBuf::from("/Users/treesap/.radroots/logs/shared/runtime") - ); - } - - #[test] - fn init_paths_execute() { - let dir = tempdir().expect("tempdir"); - test_hooks::set_ignore_env(true); - let invalid = dir.path().join("not-a-dir"); - std::fs::write(&invalid, "file").expect("write invalid path"); - let err_path = init_with_logs_dir(invalid.as_path(), Some("info")); - assert!(err_path.is_err()); - let invalid_str = invalid.to_string_lossy().to_string(); - let err_str = init_with_logs_dir(invalid_str.as_str(), Some("info")); - assert!(err_str.is_err()); - let first = init_with_logs_dir(dir.path(), Some("info")); - assert!(first.is_ok()); - let owned_path = dir.path().to_path_buf(); - let third = init_with_logs_dir(owned_path.as_path(), None); - assert!(third.is_ok()); - test_hooks::set_ignore_env(false); - } -} +mod tests; diff --git a/crates/runtime/src/tracing/tests.rs b/crates/runtime/src/tracing/tests.rs @@ -0,0 +1,143 @@ +use super::{ + default_log_file_name, default_log_file_name_from_exe_name, default_shared_runtime_logs_dir, + default_shared_runtime_logs_dir_for, env_value, init, init_with_logs_dir, log_name_from_path, + log_name_from_stem, normalize_env_value, resolve_default_level, test_hooks, +}; +use radroots_runtime_paths::{ + RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPlatform, +}; +use std::path::PathBuf; +use tempfile::tempdir; + +#[test] +fn normalize_env_value_handles_empty_and_non_empty_values() { + assert_eq!(normalize_env_value(" value "), Some("value".to_string())); + assert_eq!(normalize_env_value(" "), None); + assert_eq!(normalize_env_value(""), None); +} + +#[test] +fn env_helpers_return_expected_values() { + assert_eq!(env_value("RADROOTS_RUNTIME_TEST_MISSING_KEY"), None); + let home = env_value("HOME").expect("home env"); + assert!(!home.is_empty()); +} + +#[test] +fn log_name_helpers_cover_empty_and_non_empty_names() { + assert_eq!( + log_name_from_stem("radrootsd"), + Some("radrootsd.log".to_string()) + ); + assert_eq!(log_name_from_stem(""), None); +} + +#[test] +fn log_name_from_path_handles_missing_components() { + assert_eq!(log_name_from_path(None), None); + assert_eq!(log_name_from_path(Some(PathBuf::from("/"))), None); + assert_eq!( + log_name_from_path(Some(PathBuf::from("/tmp/radrootsd"))), + Some("radrootsd.log".to_string()) + ); +} + +#[test] +fn default_log_file_name_helpers_cover_fallback() { + assert_eq!( + default_log_file_name_from_exe_name(Some("svc.log".to_string())), + "svc.log" + ); + assert_eq!( + default_log_file_name_from_exe_name(None), + format!("{}.log", env!("CARGO_PKG_NAME")) + ); + assert!(!default_log_file_name().trim().is_empty()); +} + +#[test] +fn resolve_default_level_prefers_env_then_fallback() { + let env_value = resolve_default_level(Some("warn".to_string()), Some("info")); + assert_eq!(env_value, Some("warn".to_string())); + let fallback = resolve_default_level(None, Some("info")); + assert_eq!(fallback, Some("info".to_string())); + let none = resolve_default_level(None, None); + assert_eq!(none, None); +} + +#[test] +fn default_shared_runtime_logs_dir_uses_shared_namespace() { + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Macos, + RadrootsHostEnvironment { + home_dir: Some(PathBuf::from("/Users/treesap")), + ..RadrootsHostEnvironment::default() + }, + ); + + let logs_dir = default_shared_runtime_logs_dir_for( + &resolver, + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + ) + .expect("default shared runtime logs dir should resolve"); + + assert_eq!( + logs_dir, + PathBuf::from("/Users/treesap/.radroots/logs/shared/runtime") + ); +} + +#[test] +fn default_shared_runtime_logs_dir_and_init_use_current_resolver() { + let dir = tempdir().expect("tempdir"); + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Linux, + RadrootsHostEnvironment { + home_dir: Some(dir.path().to_path_buf()), + ..RadrootsHostEnvironment::default() + }, + ); + + test_hooks::set_current_resolver(Some(resolver)); + test_hooks::set_ignore_env(true); + + let logs_dir = default_shared_runtime_logs_dir().expect("default shared runtime logs dir"); + assert_eq!(logs_dir, dir.path().join(".radroots/logs/shared/runtime")); + + let init_result = init(); + if let Err(err) = init_result { + assert!(!err.to_string().is_empty()); + } + + test_hooks::set_ignore_env(false); + test_hooks::set_current_resolver(None); +} + +#[test] +fn init_paths_execute() { + let dir = tempdir().expect("tempdir"); + test_hooks::set_ignore_env(true); + let invalid = dir.path().join("not-a-dir"); + std::fs::write(&invalid, "file").expect("write invalid path"); + let err_path = init_with_logs_dir(invalid.as_path(), Some("info")); + if let Err(err) = err_path { + assert!(!err.to_string().is_empty()); + } + let invalid_str = invalid.to_string_lossy().to_string(); + let err_str = init_with_logs_dir(invalid_str.as_str(), Some("info")); + if let Err(err) = err_str { + assert!(!err.to_string().is_empty()); + } + let first = init_with_logs_dir(dir.path(), Some("info")); + if let Err(err) = first { + assert!(!err.to_string().is_empty()); + } + let owned_path = dir.path().to_path_buf(); + let third = init_with_logs_dir(owned_path.as_path(), None); + if let Err(err) = third { + assert!(!err.to_string().is_empty()); + } + test_hooks::set_ignore_env(false); +}