//! Round 20 near-90 raw integration coverage closures. //! //! All fixtures are local and deterministic: temp workspaces, loopback HTTP, //! and a fake `gh` binary. Run with `--test-threads=1`; several covered //! surfaces resolve config/workspace through process environment. use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::{Mutex, OnceLock}; use std::time::Duration as StdDuration; use chrono::Utc; use openhuman_core::openhuman::app_state::{ snapshot, update_local_state, StoredAppStatePatch, StoredOnboardingTasks, }; use openhuman_core::openhuman::config::rpc as config_rpc; use openhuman_core::openhuman::credentials::profiles::{ AuthProfile, AuthProfileKind, AuthProfilesStore, }; use openhuman_core::openhuman::credentials::{ AuthService, APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME, }; use openhuman_core::openhuman::memory::{ ai_list_memory_files, ai_read_memory_file, ai_write_memory_file, clear_namespace, context_query, context_recall, doc_delete, doc_list, doc_put, memory_delete_document, memory_init, memory_list_documents, memory_list_namespaces, memory_query_namespace, memory_recall_context, memory_recall_memories, namespace_list, ClearNamespaceParams, DeleteDocParams, EmptyRequest, ListDocumentsRequest, ListMemoryFilesRequest, MemoryInitRequest, PutDocParams, QueryNamespaceParams, QueryNamespaceRequest, ReadMemoryFileRequest, RecallContextRequest, RecallMemoriesRequest, RecallNamespaceParams, WriteMemoryFileRequest, }; use openhuman_core::openhuman::memory_sources::readers::SourceReader; use openhuman_core::openhuman::memory_sources::sync::sync_source; use openhuman_core::openhuman::memory_sources::{ContentType, MemorySourceEntry, SourceKind}; use openhuman_core::openhuman::threads::ops as thread_ops; use openhuman_core::openhuman::threads::welcome_migration::migrate_welcome_agent_artifacts; use serde_json::{json, Value}; use tempfile::{Builder, TempDir}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; static ROUND20_ENV_LOCK: OnceLock> = OnceLock::new(); struct EnvGuard { key: &'static str, old: Option, } impl EnvGuard { fn set(key: &'static str, value: impl Into) -> Self { let old = std::env::var(key).ok(); unsafe { std::env::set_var(key, value.into()) }; Self { key, old } } fn set_path(key: &'static str, path: &Path) -> Self { Self::set(key, path.to_string_lossy().into_owned()) } fn unset(key: &'static str) -> Self { let old = std::env::var(key).ok(); unsafe { std::env::remove_var(key) }; Self { key, old } } } impl Drop for EnvGuard { fn drop(&mut self) { match &self.old { Some(value) => unsafe { std::env::set_var(self.key, value) }, None => unsafe { std::env::remove_var(self.key) }, } } } struct Harness { _tmp: TempDir, root: PathBuf, _guards: Vec, } impl Harness { async fn config(&self) -> openhuman_core::openhuman::config::Config { config_rpc::load_config_with_timeout() .await .expect("isolated config should load") } fn workspace_dir(&self) -> PathBuf { self.root.join("workspace") } fn state_dir(&self) -> PathBuf { self.workspace_dir().join("state") } fn app_state_file(&self) -> PathBuf { self.state_dir().join("app-state.json") } } fn env_lock() -> std::sync::MutexGuard<'static, ()> { ROUND20_ENV_LOCK .get_or_init(|| Mutex::new(())) .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) } fn tempdir() -> TempDir { std::fs::create_dir_all("target").expect("create target"); Builder::new() .prefix("near90-closure-round20-") .tempdir_in("target") .expect("round20 tempdir") } fn write_min_config(root: &Path, api_url: &str) { std::fs::create_dir_all(root).expect("create config root"); let cfg = format!( r#"api_url = "{api_url}" default_model = "round20-coverage-model" default_temperature = 0.2 onboarding_completed = true chat_onboarding_completed = false [observability] analytics_enabled = false [secrets] encrypt = false [meet] auto_orchestrator_handoff = 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(root.join("config.toml"), &cfg).expect("write config.toml"); let _: openhuman_core::openhuman::config::Config = toml::from_str(&cfg).expect("round20 config must match schema"); } fn setup(api_url: &str) -> Harness { let tmp = tempdir(); let root = tmp.path().join("openhuman"); write_min_config(&root, api_url); let guards = vec![ EnvGuard::set_path("OPENHUMAN_WORKSPACE", &root), EnvGuard::set_path("HOME", tmp.path()), EnvGuard::unset("BACKEND_URL"), EnvGuard::unset("VITE_BACKEND_URL"), EnvGuard::unset("OPENHUMAN_API_URL"), EnvGuard::unset("OPENHUMAN_CORE_RPC_URL"), EnvGuard::unset("OPENHUMAN_CORE_PORT"), EnvGuard::set("OPENHUMAN_KEYRING_BACKEND", "file"), EnvGuard::set("OPENHUMAN_MEMORY_EMBED_STRICT", "false"), EnvGuard::set("OPENHUMAN_MEMORY_EMBED_ENDPOINT", ""), EnvGuard::set("OPENHUMAN_MEMORY_EMBED_MODEL", ""), ]; Harness { _tmp: tmp, root, _guards: guards, } } fn source_entry(id: &str, kind: SourceKind) -> MemorySourceEntry { MemorySourceEntry { id: id.to_string(), kind, label: format!("{id} label"), enabled: true, toolkit: None, connection_id: None, path: None, glob: None, url: None, branch: None, paths: Vec::new(), query: None, since_days: None, max_items: None, max_commits: None, max_issues: None, max_prs: None, selector: None, max_tokens_per_sync: None, max_cost_per_sync_usd: None, sync_depth_days: None, } } async fn auth_me_server( status: &'static str, body: &'static str, ) -> (String, tokio::task::JoinHandle<()>) { let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) .await .expect("bind auth fixture"); let url = format!("http://{}", listener.local_addr().expect("listener addr")); let task = tokio::spawn(async move { while let Ok((mut stream, _)) = listener.accept().await { let mut req = [0_u8; 2048]; let _ = stream.read(&mut req).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; } }); (url, task) } #[tokio::test] async fn round20_app_state_quarantines_directory_state_and_uses_stored_user_on_http_error() { let _lock = env_lock(); let (api_url, server) = auth_me_server("503 Service Unavailable", r#"{"error":"down"}"#).await; let harness = setup(&api_url); let config = harness.config().await; AuthService::from_config(&config) .store_provider_token( APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME, "round20.remote.token", HashMap::from([ ("user_id".to_string(), "stored-round20".to_string()), ( "user_json".to_string(), json!({"id":"stored-round20","fullName":"Stored Round20"}).to_string(), ), ]), true, ) .expect("seed app session"); let first = update_local_state(StoredAppStatePatch { keyring_consent: None, encryption_key: None, onboarding_tasks: Some(Some(StoredOnboardingTasks { accessibility_permission_granted: false, local_model_consent_given: true, local_model_download_started: false, enabled_tools: vec!["github".to_string()], connected_sources: vec!["rss".to_string()], updated_at_ms: Some(20), })), }) .await .expect("write tasks") .value; assert!(first.encryption_key.is_none()); assert_eq!( first.onboarding_tasks.expect("tasks").enabled_tools, vec!["github"] ); let snap = snapshot().await.expect("snapshot").value; assert_eq!( snap.current_user.as_ref().and_then(|v| v.get("id")), Some(&json!("stored-round20")) ); assert_eq!(snap.session_token.as_deref(), Some("round20.remote.token")); assert!(!snap.analytics_enabled); assert!(!snap.meet_auto_orchestrator_handoff); std::fs::remove_file(harness.app_state_file()).expect("remove app-state file"); std::fs::create_dir_all(harness.app_state_file()).expect("directory at app-state path"); let recovered = snapshot() .await .expect("snapshot should quarantine unreadable app-state path") .value; assert!(recovered.local_state.onboarding_tasks.is_none()); assert!(!harness.app_state_file().exists()); assert!( std::fs::read_dir(harness.state_dir()) .expect("state dir") .filter_map(Result::ok) .any(|entry| entry .file_name() .to_string_lossy() .contains("json.corrupted")), "directory app-state should be moved aside" ); server.abort(); } #[test] fn round20_credentials_profiles_cover_legacy_plaintext_errors_and_active_edges() { let _lock = env_lock(); let harness = setup("http://127.0.0.1:9"); let state_dir = harness.root.join("profile-store"); let store = AuthProfilesStore::new(&state_dir, false); let token = AuthProfile::new_token("linear", "team", "lin-round20".to_string()); store .upsert_profile(token.clone(), false) .expect("insert token"); let inactive = store.load().expect("load inactive token"); assert!(!inactive.active_profiles.contains_key("linear")); store .set_active_profile("linear", &token.id) .expect("activate token"); assert_eq!( store .load() .expect("load active") .active_profiles .get("linear"), Some(&token.id) ); let path = store.path().to_path_buf(); let now = Utc::now().to_rfc3339(); std::fs::write( &path, serde_json::to_string_pretty(&json!({ "schema_version": 1, "updated_at": now, "active_profiles": { "github": "legacy-oauth", "token": "legacy-token" }, "profiles": { "legacy-token": { "provider": "token", "profile_name": "plain", "kind": "token", "token": "plain-secret", "metadata": { "k": "v" }, "created_at": "bad-created", "updated_at": "bad-updated" }, "legacy-oauth": { "provider": "github", "profile_name": "oauth", "kind": "oauth", "access_token": "plain-access", "refresh_token": "plain-refresh", "id_token": "", "expires_at": null, "token_type": "Bearer", "scope": "repo", "metadata": {}, "created_at": "2026-05-29T00:00:00Z", "updated_at": "2026-05-29T00:00:00Z" } } })) .expect("profile json"), ) .expect("write legacy plaintext store"); let loaded = store.load().expect("load plaintext legacy profiles"); assert_eq!( loaded .profiles .get("legacy-token") .and_then(|profile| profile.token.as_deref()), Some("plain-secret") ); let oauth = loaded.profiles.get("legacy-oauth").expect("oauth loaded"); assert_eq!(oauth.kind, AuthProfileKind::OAuth); assert_eq!( oauth.token_set.as_ref().map(|tokens| ( tokens.access_token.as_str(), tokens.refresh_token.as_deref(), tokens.expires_at )), Some(("plain-access", Some("plain-refresh"), None)) ); let set_active_err = store .set_active_profile("missing", "not-there") .expect_err("missing active profile") .to_string(); assert!(set_active_err.contains("Auth profile not found")); } #[tokio::test] async fn round20_memory_sources_readers_and_sync_cover_error_edges_without_network() { let _lock = env_lock(); let harness = setup("http://127.0.0.1:9"); let config = harness.config().await; let rss = openhuman_core::openhuman::memory_sources::readers::rss::RssReader; let mut missing_url = source_entry("rss-missing-url", SourceKind::RssFeed); assert_eq!( rss.list_items(&missing_url, &config) .await .expect_err("rss url required"), "rss source requires a url" ); let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) .await .expect("bind feed fixture"); let feed_url = format!("http://{}", listener.local_addr().expect("addr")); let server = tokio::spawn(async move { if let Ok((mut stream, _)) = listener.accept().await { let mut req = [0_u8; 1024]; let _ = stream.read(&mut req).await; let response = "HTTP/1.1 200 OK\r\ncontent-type: text/xml\r\ncontent-length: 10\r\n\r\nnot-a-feed"; let _ = stream.write_all(response.as_bytes()).await; let _ = stream.shutdown().await; } }); missing_url.url = Some(feed_url); let feed_err = rss .list_items(&missing_url, &config) .await .expect_err("unrecognized feed rejected"); assert!(feed_err.contains("unrecognized feed format")); let _ = server.await; // GitHub reader portion requires a real `gh` on PATH to shadow with our // fake. Skip on CI containers that lack `gh` — without it the reader // falls through to the real GitHub API and rate-limits. let gh_available = std::process::Command::new("gh") .arg("--version") .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .status() .map(|s| s.success()) .unwrap_or(false); let tmp = tempdir(); let bin = tmp.path().join("bin"); std::fs::create_dir_all(&bin).expect("bin dir"); let script = bin.join("gh"); write_fake_gh_round20(&script); let git_stub = bin.join("git"); std::fs::write(&git_stub, "#!/usr/bin/env bash\nexit 1\n").expect("write fake git"); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = std::fs::metadata(&git_stub) .expect("metadata") .permissions(); perms.set_mode(0o755); std::fs::set_permissions(&git_stub, perms).expect("chmod fake git"); } let old_path = std::env::var("PATH").unwrap_or_default(); let _path = EnvGuard::set("PATH", format!("{}:{old_path}", bin.display())); let github = openhuman_core::openhuman::memory_sources::readers::github::GithubReader; let mut entry = source_entry("github-round20", SourceKind::GithubRepo); entry.url = Some("git@github.com:tinyhumansai/openhuman.git".to_string()); if !gh_available { eprintln!("skipping github reader assertions: gh CLI not available"); } else { let items = github .list_items(&entry, &config) .await .expect("github list via fake gh"); assert!(items.iter().any(|item| item.id == "commit:def456")); assert!(items.iter().any(|item| item.id == "issue:20")); let pr = github .read_item(&entry, "pr:21", &config) .await .expect("read merged pr"); assert_eq!(pr.content_type, ContentType::Markdown); assert!(pr.body.contains("merged at 2026-05-29T01:00:00Z")); assert_eq!( pr.metadata.get("merged").and_then(Value::as_bool), Some(true) ); let bad_issue = github .read_item(&entry, "issue:not-a-number", &config) .await .expect_err("bad issue number"); assert!(bad_issue.contains("invalid issue number")); } let mut disabled = source_entry("disabled-twitter", SourceKind::TwitterQuery); disabled.enabled = false; let disabled_err = sync_source(disabled, config.clone()) .await .expect_err("disabled sync rejected"); assert!(disabled_err.contains("is disabled")); let twitter = source_entry("twitter-round20", SourceKind::TwitterQuery); sync_source(twitter, config) .await .expect("twitter placeholder is reported by background task"); tokio::time::sleep(StdDuration::from_millis(25)).await; } #[tokio::test] async fn round20_memory_documents_files_and_envelopes_cover_success_and_failure_paths() { let _lock = env_lock(); let harness = setup("http://127.0.0.1:9"); let init = memory_init(MemoryInitRequest { jwt_token: Some("ignored-round20".to_string()), }) .await .expect("memory init") .value .data .expect("init data"); assert!(init.initialized); assert!(init.memory_dir.ends_with("/memory")); ai_write_memory_file(WriteMemoryFileRequest { relative_path: "notes/round20.md".to_string(), content: "Round20 local file memory".to_string(), }) .await .expect("write memory file"); let read = ai_read_memory_file(ReadMemoryFileRequest { relative_path: "notes/round20.md".to_string(), }) .await .expect("read memory file") .value .data .expect("read data"); assert!(read.content.contains("local file memory")); let traversal = ai_write_memory_file(WriteMemoryFileRequest { relative_path: "../escape.md".to_string(), content: "no".to_string(), }) .await .expect_err("traversal rejected"); assert!(traversal.contains("path traversal")); let listed = ai_list_memory_files(ListMemoryFilesRequest { relative_dir: "notes".to_string(), }) .await .expect("list notes") .value .data .expect("list data"); assert_eq!(listed.files, vec!["round20.md"]); // Use a deterministic namespace — random UUIDs can trigger PII detection. let namespace = "round20-cov-test-ns".to_string(); let put = doc_put(PutDocParams { namespace: namespace.clone(), key: "launch-note".to_string(), title: "Launch Note".to_string(), content: "The Calypso launch depends on QA, design, and release owners.".to_string(), source_type: "note".to_string(), priority: "high".to_string(), tags: vec!["round20".to_string()], metadata: json!({"round": 20}), category: "core".to_string(), session_id: Some("session-round20".to_string()), document_id: Some("doc-round20".to_string()), }) .await .expect("put doc") .value; assert_eq!(put.document_id, "doc-round20"); let docs = doc_list(None) .await .expect("doc list all") .value .get("documents") .and_then(Value::as_array) .cloned() .expect("documents"); assert!(docs.iter().any(|doc| doc["documentId"] == "doc-round20")); let namespaces = namespace_list().await.expect("namespace list").value; assert!(namespaces.contains(&namespace)); let query = context_query(QueryNamespaceParams { namespace: namespace.clone(), query: "Calypso QA".to_string(), limit: Some(3), }) .await .expect("context query") .value; assert!(query.contains("Calypso") || query.contains("launch")); let recall = context_recall(RecallNamespaceParams { namespace: namespace.clone(), limit: Some(3), }) .await .expect("context recall") .value; assert!(recall.as_deref().unwrap_or_default().contains("Calypso")); let envelope_docs = memory_list_documents(ListDocumentsRequest { namespace: Some(namespace.clone()), }) .await .expect("memory list documents") .value .data .expect("list documents data"); assert_eq!(envelope_docs.count, 1); let envelope_namespaces = memory_list_namespaces(EmptyRequest {}) .await .expect("memory namespaces") .value .data .expect("namespaces data"); assert!(envelope_namespaces.namespaces.contains(&namespace)); let hidden_context = memory_query_namespace(QueryNamespaceRequest { namespace: namespace.clone(), query: "Calypso".to_string(), include_references: Some(false), document_ids: Some(vec!["doc-round20".to_string()]), limit: Some(1), max_chunks: Some(2), }) .await .expect("query namespace envelope") .value .data .expect("query namespace data"); assert!(hidden_context.context.is_none()); assert!(hidden_context.llm_context_message.is_some()); let recall_context = memory_recall_context(RecallContextRequest { namespace: namespace.clone(), include_references: Some(true), limit: None, max_chunks: Some(1), }) .await .expect("recall context envelope") .value .data .expect("recall context data"); assert!(recall_context.llm_context_message.is_some()); let memories = memory_recall_memories(RecallMemoriesRequest { namespace: namespace.clone(), min_retention: Some(0.0), as_of: Some(0.0), limit: Some(1), max_chunks: None, top_k: Some(1), }) .await .expect("recall memories") .value .data .expect("memories data"); assert!(!memories.memories.is_empty()); let deleted = memory_delete_document(openhuman_core::openhuman::memory::DeleteDocumentRequest { namespace: namespace.clone(), document_id: "doc-round20".to_string(), }) .await .expect("delete document envelope") .value .data .expect("delete data"); assert!(deleted.deleted); let second_delete = doc_delete(DeleteDocParams { namespace: namespace.clone(), document_id: "doc-round20".to_string(), }) .await .expect("direct delete missing") .value; assert_eq!(second_delete["deleted"], false); let cleared = clear_namespace(ClearNamespaceParams { namespace }) .await .expect("clear namespace") .value; assert!(cleared.cleared); let _ = harness; } #[tokio::test] async fn round20_threads_fallback_title_delete_missing_and_welcome_noop_paths() { let _lock = env_lock(); let harness = setup("http://127.0.0.1:9"); let created = thread_ops::thread_create_new( openhuman_core::openhuman::memory::CreateConversationThreadRequest { labels: None, personality_id: None, }, ) .await .expect("create thread") .value .data .expect("created data"); let user_message = openhuman_core::openhuman::memory::ConversationMessageRecord { id: "round20-user-msg".to_string(), content: "Please map the onboarding telemetry rollout across product analytics and QA." .to_string(), message_type: "text".to_string(), extra_metadata: Value::Null, sender: "user".to_string(), created_at: Utc::now().to_rfc3339(), }; thread_ops::message_append( openhuman_core::openhuman::memory::AppendConversationMessageRequest { thread_id: created.id.clone(), message: user_message, }, ) .await .expect("append user"); let fallback = thread_ops::thread_generate_title( openhuman_core::openhuman::memory::GenerateConversationThreadTitleRequest { thread_id: created.id.clone(), assistant_message: None, }, ) .await .expect("fallback title") .value .data .expect("fallback data"); assert!(!fallback.title.trim().is_empty()); assert_ne!(fallback.title, created.title); let missing_update = thread_ops::message_update( openhuman_core::openhuman::memory::UpdateConversationMessageRequest { thread_id: created.id.clone(), message_id: "missing-message".to_string(), extra_metadata: Some(json!({"x": true})), }, ) .await .expect_err("missing message update fails"); assert!(missing_update.contains("message") || missing_update.contains("not found")); let missing_delete = thread_ops::thread_delete( openhuman_core::openhuman::memory::DeleteConversationThreadRequest { thread_id: "missing-thread-round20".to_string(), deleted_at: Utc::now().to_rfc3339(), }, ) .await .expect("missing thread delete is idempotent") .value .data .expect("delete data"); assert!(!missing_delete.deleted); std::fs::create_dir_all(harness.workspace_dir().join("session_raw")).expect("raw dir"); std::fs::write( harness.workspace_dir().join("session_raw/ignore.jsonl"), "{\"_meta\":{\"agent\":\"orchestrator\",\"thread_id\":\"t\"}}\n", ) .expect("write non-welcome transcript"); let migration = migrate_welcome_agent_artifacts(&harness.workspace_dir()).expect("noop migration"); assert_eq!(migration.threads_updated, 0); assert_eq!(migration.transcripts_updated, 0); } fn write_fake_gh_round20(path: &PathBuf) { let script = r#"#!/usr/bin/env bash set -euo pipefail if [[ "${1:-}" == "--version" ]]; then echo "gh version 2.0.0" exit 0 fi if [[ "${1:-}" != "api" ]]; then echo "unsupported gh command" >&2 exit 2 fi case "${2:-}" in repos/tinyhumansai/openhuman/commits\?*) cat <<'JSON' [{"sha":"def456","commit":{"message":"Round20 commit fixture","author":{"name":"Ada","email":"ada@example.test","date":"2026-05-29T00:00:00Z"},"committer":{"name":"Ada","email":"ada@example.test","date":"2026-05-29T00:00:00Z"}}}] JSON ;; repos/tinyhumansai/openhuman/issues\?*) cat <<'JSON' [{"number":20,"title":"Round20 issue","body":null,"state":"closed","user":null,"labels":[],"created_at":null,"updated_at":"2026-05-29T00:30:00Z","pull_request":null}] JSON ;; repos/tinyhumansai/openhuman/pulls\?*) cat <<'JSON' [{"number":21,"title":"Round20 merged PR","body":null,"state":"closed","user":null,"labels":[],"created_at":null,"updated_at":"2026-05-29T01:00:00Z","merged_at":"2026-05-29T01:00:00Z","comments":0}] JSON ;; repos/tinyhumansai/openhuman/pulls/21) cat <<'JSON' {"number":21,"title":"Round20 merged PR","body":null,"state":"closed","user":null,"labels":[],"created_at":null,"updated_at":"2026-05-29T01:00:00Z","merged_at":"2026-05-29T01:00:00Z","comments":0} JSON ;; repos/tinyhumansai/openhuman/issues/21/comments\?*) cat <<'JSON' [] JSON ;; *) echo "unexpected gh api path: ${2:-}" >&2 exit 3 ;; esac "#; std::fs::write(path, script).expect("write fake gh"); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = std::fs::metadata(path) .expect("fake gh metadata") .permissions(); perms.set_mode(0o755); std::fs::set_permissions(path, perms).expect("chmod fake gh"); } }