#[path = "config_auth_app_state_connectivity_e2e.rs"] mod base_coverage; use std::path::{Path, PathBuf}; use serde_json::{json, Value}; use tempfile::{tempdir, TempDir}; use openhuman_core::openhuman::app_state::{snapshot, update_local_state, StoredAppStatePatch}; use openhuman_core::openhuman::config::rpc as config_rpc; use openhuman_core::openhuman::credentials::{ auth_get_session_token_json, clear_session, list_provider_credentials, remove_provider_credentials, store_provider_credentials, store_session, AuthService, }; use tokio::io::{AsyncReadExt, AsyncWriteExt}; struct Round13EnvVarGuard { key: &'static str, old: Option, } impl Round13EnvVarGuard { fn set(key: &'static str, value: &str) -> Self { let old = std::env::var(key).ok(); std::env::set_var(key, value); Self { key, old } } fn set_to_path(key: &'static str, path: &Path) -> Self { let old = std::env::var(key).ok(); std::env::set_var(key, path.as_os_str()); Self { key, old } } fn unset(key: &'static str) -> Self { let old = std::env::var(key).ok(); std::env::remove_var(key); Self { key, old } } } impl Drop for Round13EnvVarGuard { fn drop(&mut self) { match &self.old { Some(value) => std::env::set_var(self.key, value), None => std::env::remove_var(self.key), } } } struct Round13Harness { _tmp: TempDir, workspace_override: PathBuf, _guards: Vec, } impl Round13Harness { async fn config(&self) -> openhuman_core::openhuman::config::Config { config_rpc::load_config_with_timeout() .await .expect("isolated config should load") } fn app_state_file(&self) -> PathBuf { self.workspace_override .join("workspace/state/app-state.json") } } fn round13_env_lock() -> std::sync::MutexGuard<'static, ()> { // Delegate to the lock owned by the included `base_coverage` (config_auth) // module so round13 env mutations serialize against every other // OPENHUMAN_WORKSPACE/BACKEND_URL-mutating test in this combined binary. // Two separate mutexes let the two groups race and quarantine the wrong // workspace (flaky `!state_file.exists()` / spurious JSON-RPC errors). base_coverage::env_lock() } fn write_round13_min_config(openhuman_dir: &Path) { std::fs::create_dir_all(openhuman_dir).expect("create openhuman config dir"); let cfg = r#"api_url = "http://127.0.0.1:9" default_model = "round13-raw-coverage-model" default_temperature = 0.2 onboarding_completed = false chat_onboarding_completed = false [secrets] encrypt = false [local_ai] enabled = false runtime_enabled = false opt_in_confirmed = false [memory] provider = "none" embedding_provider = "none" embedding_model = "none" embedding_dimensions = 0 auto_save = false [memory_tree] embedding_strict = false "#; std::fs::write(openhuman_dir.join("config.toml"), cfg).expect("write config.toml"); let _: openhuman_core::openhuman::config::Config = toml::from_str(cfg).expect("test config must match schema"); } fn round13_setup() -> Round13Harness { let tmp = tempdir().expect("tempdir"); let workspace_override = tmp.path().join("openhuman"); write_round13_min_config(&workspace_override); let guards = vec![ Round13EnvVarGuard::set_to_path("OPENHUMAN_WORKSPACE", &workspace_override), Round13EnvVarGuard::set_to_path("HOME", tmp.path()), Round13EnvVarGuard::unset("BACKEND_URL"), Round13EnvVarGuard::unset("VITE_BACKEND_URL"), Round13EnvVarGuard::unset("OPENHUMAN_API_URL"), Round13EnvVarGuard::unset("OPENHUMAN_CORE_RPC_URL"), Round13EnvVarGuard::unset("OPENHUMAN_CORE_PORT"), Round13EnvVarGuard::set("OPENHUMAN_KEYRING_BACKEND", "file"), Round13EnvVarGuard::set("OPENHUMAN_MEMORY_EMBED_STRICT", "false"), Round13EnvVarGuard::set("OPENHUMAN_MEMORY_EMBED_ENDPOINT", ""), Round13EnvVarGuard::set("OPENHUMAN_MEMORY_EMBED_MODEL", ""), Round13EnvVarGuard::set("OPENHUMAN_BROWSER_ALLOW_ALL_RPC_ENABLE", ""), ]; Round13Harness { _tmp: tmp, workspace_override, _guards: guards, } } #[tokio::test] async fn raw_round13_provider_field_fallbacks_default_profile_and_non_active_listing() { let _lock = round13_env_lock(); let harness = round13_setup(); let config = harness.config().await; let empty = store_provider_credentials(&config, " ", None, None, None, None).await; assert_eq!(empty.unwrap_err(), "provider is required"); let invalid_fields = store_provider_credentials( &config, "raw-provider", None, None, Some(json!("bad")), None, ) .await; assert!(invalid_fields .unwrap_err() .contains("fields must be a JSON object")); let missing = store_provider_credentials(&config, "raw-provider", None, None, Some(json!({})), None) .await; assert!(missing .unwrap_err() .contains("provide at least one credential")); let stored_from_token_field = store_provider_credentials( &config, "raw-provider", None, None, Some(json!({ "token": "field-token", "region": "us-test-1" })), Some(true), ) .await .expect("store token field credential"); assert_eq!(stored_from_token_field.value.provider, "raw-provider"); assert_eq!(stored_from_token_field.value.profile_name, "default"); assert!(stored_from_token_field.value.has_token); let auth = AuthService::from_config(&config); assert_eq!( auth.get_provider_bearer_token("raw-provider", None) .expect("read active bearer") .as_deref(), Some("field-token") ); let stored_from_api_key = store_provider_credentials( &config, "raw-provider", Some("secondary"), None, Some(json!({ "api_key": "api-key-token", "label": "kept as metadata" })), Some(false), ) .await .expect("store api_key field credential"); assert_eq!(stored_from_api_key.value.profile_name, "secondary"); let listed = list_provider_credentials(&config, Some("raw-provider".to_string())) .await .expect("list raw provider credentials") .value; let names = listed .iter() .map(|profile| profile.profile_name.as_str()) .collect::>(); assert_eq!(names, vec!["default", "secondary"]); let removed_default = remove_provider_credentials(&config, "raw-provider", None) .await .expect("remove default profile"); assert_eq!(removed_default.value["removed"], true); assert_eq!(removed_default.value["profile"], "default"); let removed_missing = remove_provider_credentials(&config, "raw-provider", Some("missing")) .await .expect("remove missing profile is non-fatal"); assert_eq!(removed_missing.value["removed"], false); } #[tokio::test] async fn raw_round13_local_session_string_payload_and_double_clear_are_offline() { let _lock = round13_env_lock(); let harness = round13_setup(); let config = harness.config().await; let empty_token = store_session(&config, " ", None, None).await; assert_eq!(empty_token.unwrap_err(), "token is required"); let local_without_user = store_session(&config, "header.payload.local", None, None).await; assert_eq!( local_without_user.unwrap_err(), "local session requires a user payload" ); let stored = store_session( &config, "header.payload.local", Some("ignored-user-hint".to_string()), Some(json!("string user payload is preserved")), ) .await .expect("store local session with non-object user payload"); assert_eq!(stored.value.provider, "app-session"); assert!(stored .logs .iter() .any(|log| log == "local session accepted without backend validation")); let effective_config = config_rpc::load_config_with_timeout() .await .expect("reload active local user config"); let token = auth_get_session_token_json(&effective_config) .await .expect("session token") .value; assert_eq!(token["token"], "header.payload.local"); let first_clear = clear_session(&effective_config) .await .expect("clear stored session"); assert_eq!(first_clear.value["removed"], true); let signed_out_config = config_rpc::load_config_with_timeout() .await .expect("reload signed-out config"); let second_clear = clear_session(&signed_out_config) .await .expect("clear missing session is idempotent"); assert_eq!(second_clear.value["removed"], false); } #[tokio::test] async fn raw_round13_app_state_update_trims_clears_and_preserves_optional_local_state() { let _lock = round13_env_lock(); let harness = round13_setup(); let updated = update_local_state(StoredAppStatePatch { keyring_consent: None, encryption_key: Some(Some(" raw-key ".to_string())), onboarding_tasks: Some(Some(Default::default())), }) .await .expect("update local app state") .value; assert_eq!(updated.encryption_key.as_deref(), Some("raw-key")); assert!(updated.onboarding_tasks.is_some()); let cleared_key = update_local_state(StoredAppStatePatch { keyring_consent: None, encryption_key: Some(Some(" ".to_string())), onboarding_tasks: None, }) .await .expect("blank key clears encryption key") .value; assert!(cleared_key.encryption_key.is_none()); assert!(cleared_key.onboarding_tasks.is_some()); let cleared_tasks = update_local_state(StoredAppStatePatch { keyring_consent: None, encryption_key: None, onboarding_tasks: Some(None), }) .await .expect("null tasks clear onboarding tasks") .value; assert!(cleared_tasks.encryption_key.is_none()); assert!(cleared_tasks.onboarding_tasks.is_none()); let unchanged = update_local_state(StoredAppStatePatch { keyring_consent: None, encryption_key: None, onboarding_tasks: None, }) .await .expect("empty patch preserves cleared state") .value; assert!(unchanged.encryption_key.is_none()); assert!(unchanged.onboarding_tasks.is_none()); let raw = std::fs::read_to_string(harness.app_state_file()).expect("app state persisted"); assert_eq!( serde_json::from_str::(&raw).expect("valid app state json"), json!({}) ); } #[tokio::test] async fn raw_round13_app_state_snapshot_quarantines_null_and_malformed_local_state_files() { let _lock = round13_env_lock(); let harness = round13_setup(); let state_file = harness.app_state_file(); let state_dir = state_file.parent().expect("state dir"); std::fs::create_dir_all(state_dir).expect("create state dir"); std::fs::write(&state_file, "null").expect("write semantically invalid app state"); let null_snapshot = snapshot().await.expect("snapshot with null state").value; assert!(null_snapshot.local_state.encryption_key.is_none()); assert!(null_snapshot.local_state.onboarding_tasks.is_none()); assert!( !state_file.exists(), "invalid app-state.json should be quarantined or removed" ); assert!( std::fs::read_dir(state_dir) .expect("state dir entries") .filter_map(Result::ok) .any(|entry| entry .file_name() .to_string_lossy() .starts_with("app-state.json.corrupted.")), "null app-state file should leave a quarantine artifact" ); std::fs::write(&state_file, "{not-json").expect("write malformed app state"); let malformed_snapshot = snapshot() .await .expect("snapshot with malformed state") .value; assert!(malformed_snapshot.local_state.encryption_key.is_none()); assert!(malformed_snapshot.local_state.onboarding_tasks.is_none()); assert!(!state_file.exists()); } async fn spawn_probe_listener( host: &str, status: &str, body: &'static str, ) -> Option<( u16, tokio::task::JoinHandle<()>, tokio::sync::oneshot::Sender<()>, )> { let listener = match tokio::net::TcpListener::bind((host, 0)).await { Ok(listener) => listener, Err(_) => return None, }; let port = listener.local_addr().expect("probe listener addr").port(); let status = status.to_string(); let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>(); let task = tokio::spawn(async move { loop { tokio::select! { _ = &mut shutdown_rx => break, accepted = listener.accept() => { let Ok((mut stream, _addr)) = accepted else { break; }; let mut req_buf = [0_u8; 1024]; let _ = stream.read(&mut req_buf).await; let response = format!( "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", body.len(), body ); let _ = stream.write_all(response.as_bytes()).await; let _ = stream.shutdown().await; } } } }); Some((port, task, shutdown_tx)) } #[tokio::test] async fn raw_round13_connectivity_picker_identifies_openhuman_probe_listener() { let Some((preferred, task, shutdown_tx)) = spawn_probe_listener("127.0.0.1", "200 OK", r#"{"name":"openhuman","ok":true}"#).await else { return; }; let result = openhuman_core::openhuman::connectivity::rpc::pick_listen_port_for_host( "127.0.0.1", preferred, ) .await; let err = result.expect_err("openhuman probe listener should request takeover"); assert!( matches!( err, openhuman_core::openhuman::connectivity::rpc::PickListenPortError::WouldTakeOver { preferred: p, ref fingerprint } if p == preferred && fingerprint == "openhuman-core" ), "unexpected picker error: {err:?}" ); let _ = shutdown_tx.send(()); let _ = task.await; } #[tokio::test] async fn raw_round13_connectivity_picker_falls_back_for_non_success_probe_status() { let Some((preferred, task, shutdown_tx)) = spawn_probe_listener( "127.0.0.1", "503 Service Unavailable", r#"{"name":"openhuman"}"#, ) .await else { return; }; let picked = openhuman_core::openhuman::connectivity::rpc::pick_listen_port_for_host( "127.0.0.1", preferred, ) .await .expect("non-openhuman status should fall back"); assert_ne!(picked.port, preferred); assert_eq!(picked.fallback_from, Some(preferred)); drop(picked.listener); let _ = shutdown_tx.send(()); let _ = task.await; } #[tokio::test] async fn raw_round13_connectivity_picker_falls_back_for_non_identifying_probe_body() { let Some((preferred, task, shutdown_tx)) = spawn_probe_listener("127.0.0.1", "200 OK", r#"{"name":"someone-else"}"#).await else { return; }; let picked = openhuman_core::openhuman::connectivity::rpc::pick_listen_port_for_host( "127.0.0.1", preferred, ) .await .expect("non-identifying body should fall back"); assert_ne!(picked.port, preferred); assert_eq!(picked.fallback_from, Some(preferred)); drop(picked.listener); let _ = shutdown_tx.send(()); let _ = task.await; }