Files
openhuman/tests/app_credentials_threads_sources_round26_raw_coverage_e2e.rs
T

312 lines
10 KiB
Rust

//! Round26 closure coverage for near-threshold app, credentials, threads,
//! and memory_sources paths.
//!
//! Uses only temp workspaces, local files, and intentionally unreachable
//! loopback endpoints.
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, SystemTime};
use filetime::FileTime;
use serde_json::Value;
use tempfile::{Builder, TempDir};
use openhuman_core::openhuman::app_state::{snapshot, update_local_state, StoredAppStatePatch};
use openhuman_core::openhuman::config::Config;
use openhuman_core::openhuman::credentials::profiles::{AuthProfile, AuthProfilesStore, TokenSet};
use openhuman_core::openhuman::memory::{
AppendConversationMessageRequest, ConversationMessageRecord, CreateConversationThreadRequest,
GenerateConversationThreadTitleRequest, UpsertConversationThreadRequest,
};
use openhuman_core::openhuman::memory_sources::reconcile::ensure_composio_sources;
use openhuman_core::openhuman::threads::ops::{
message_append, thread_create_new, thread_generate_title, thread_upsert,
};
use openhuman_core::openhuman::threads::welcome_migration::migrate_welcome_agent_artifacts;
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
struct EnvGuard {
key: &'static str,
old: Option<String>,
}
impl EnvGuard {
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_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 EnvGuard {
fn drop(&mut self) {
match &self.old {
Some(value) => std::env::set_var(self.key, value),
None => std::env::remove_var(self.key),
}
}
}
struct Harness {
_tmp: TempDir,
root: PathBuf,
workspace: PathBuf,
_guards: Vec<EnvGuard>,
}
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
async fn setup(prefix: &str) -> Harness {
std::fs::create_dir_all("target").expect("target dir");
let tmp = Builder::new()
.prefix(prefix)
.tempdir_in("target")
.expect("tempdir");
let root = tmp.path().join("openhuman");
let workspace = root.join("workspace");
std::fs::create_dir_all(&workspace).expect("workspace");
let guards = vec![
EnvGuard::set_path("OPENHUMAN_WORKSPACE", &root),
EnvGuard::set_path("HOME", tmp.path()),
EnvGuard::set("OPENHUMAN_KEYRING_BACKEND", "file"),
EnvGuard::set("OPENHUMAN_MEMORY_EMBED_STRICT", "false"),
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"),
];
let mut config = Config {
workspace_dir: workspace.clone(),
config_path: root.join("config.toml"),
api_url: Some("http://127.0.0.1:9".to_string()),
onboarding_completed: false,
chat_onboarding_completed: true,
..Config::default()
};
config.secrets.encrypt = false;
config.composio.mode = "backend".to_string();
config.save().await.expect("save config");
Harness {
_tmp: tmp,
root,
workspace,
_guards: guards,
}
}
fn write_transcript(path: &Path, agent: &str) {
let body = format!(
"{{\"_meta\":{{\"agent\":\"{agent}\",\"thread_id\":\"round26-thread\",\"dispatcher\":\"native\"}}}}\n{{\"role\":\"user\",\"content\":\"hello\"}}\n"
);
std::fs::create_dir_all(path.parent().unwrap()).expect("transcript dir");
std::fs::write(path, body).expect("write transcript");
}
#[tokio::test]
async fn round26_app_state_quarantines_corrupt_local_file_and_preserves_patch_noops() {
let _lock = env_lock();
let harness = setup("round26-app-state-").await;
let state_dir = harness.workspace.join("state");
std::fs::create_dir_all(&state_dir).expect("state dir");
let app_state_path = state_dir.join("app-state.json");
std::fs::write(&app_state_path, b"{ not valid json").expect("corrupt app state");
let snapshot = snapshot().await.expect("snapshot with corrupt state").value;
assert!(!snapshot.onboarding_completed);
assert!(snapshot.chat_onboarding_completed);
assert!(snapshot.local_state.encryption_key.is_none());
assert!(snapshot.local_state.onboarding_tasks.is_none());
assert!(
!app_state_path.exists(),
"corrupt file should be quarantined"
);
assert!(
std::fs::read_dir(&state_dir)
.expect("state listing")
.flatten()
.any(|entry| entry
.file_name()
.to_string_lossy()
.contains("json.corrupted")),
"quarantine file should remain for diagnostics"
);
let unchanged = update_local_state(StoredAppStatePatch {
keyring_consent: None,
encryption_key: None,
onboarding_tasks: None,
})
.await
.expect("noop patch")
.value;
assert!(unchanged.encryption_key.is_none());
assert!(unchanged.onboarding_tasks.is_none());
}
#[test]
fn round26_credentials_profiles_reclaim_stale_locks_and_reject_bad_active_profile() {
let _lock = env_lock();
let runtime = tokio::runtime::Runtime::new().expect("runtime");
let harness = runtime.block_on(setup("round26-credentials-"));
let store = AuthProfilesStore::new(&harness.root, false);
let lock_path = harness.root.join("auth-profiles.lock");
std::fs::write(&lock_path, "pid=4294967295\n").expect("stale pid lock");
let empty = store.load().expect("stale pid lock should be reclaimed");
assert!(empty.profiles.is_empty());
assert!(!lock_path.exists());
std::fs::write(&lock_path, "not-a-pid\n").expect("malformed lock");
let old = FileTime::from_system_time(SystemTime::now() - Duration::from_secs(3));
filetime::set_file_mtime(&lock_path, old).expect("old lock mtime");
store
.upsert_profile(
AuthProfile::new_token("anthropic", "default", "sk-round26".to_string()),
false,
)
.expect("malformed old lock should be reclaimed");
assert!(!lock_path.exists());
let missing_active = store
.set_active_profile("anthropic", "anthropic:missing")
.expect_err("missing active profile");
assert!(missing_active
.to_string()
.contains("Auth profile not found"));
let oauth = AuthProfile::new_oauth(
"gmail",
"round26",
TokenSet {
access_token: "access-round26".to_string(),
refresh_token: None,
id_token: None,
expires_at: None,
token_type: None,
scope: None,
},
);
let oauth_id = oauth.id.clone();
store.upsert_profile(oauth, true).expect("upsert oauth");
let loaded = store.load().expect("load oauth");
assert_eq!(loaded.active_profiles.get("gmail"), Some(&oauth_id));
assert_eq!(
loaded
.profiles
.get(&oauth_id)
.and_then(|profile| profile.token_set.as_ref())
.map(|tokens| tokens.access_token.as_str()),
Some("access-round26")
);
}
#[tokio::test]
async fn round26_threads_generate_fallback_titles_and_migrate_in_place_transcripts() {
let _lock = env_lock();
let harness = setup("round26-threads-").await;
let created = thread_create_new(CreateConversationThreadRequest {
labels: Some(vec!["round26".to_string()]),
personality_id: None,
})
.await
.expect("create thread")
.value
.data
.expect("created thread");
message_append(AppendConversationMessageRequest {
thread_id: created.id.clone(),
message: ConversationMessageRecord {
id: "msg-round26-user".to_string(),
content: "/please summarize this long launch checklist for me".to_string(),
message_type: "text".to_string(),
extra_metadata: Value::Null,
sender: "user".to_string(),
created_at: "2026-05-30T00:00:00Z".to_string(),
},
})
.await
.expect("append user message");
let titled = thread_generate_title(GenerateConversationThreadTitleRequest {
thread_id: created.id.clone(),
assistant_message: None,
})
.await
.expect("fallback title")
.value
.data
.expect("titled thread");
assert_eq!(
titled.title,
"please summarize this long launch checklist for me"
);
let custom = thread_upsert(UpsertConversationThreadRequest {
id: "round26-custom-title".to_string(),
title: "Already Named Thread".to_string(),
created_at: "2026-05-30T00:00:01Z".to_string(),
parent_thread_id: None,
labels: Some(vec!["work".to_string()]),
personality_id: Some("p1".to_string()),
})
.await
.expect("upsert custom")
.value
.data
.expect("custom thread");
let skipped = thread_generate_title(GenerateConversationThreadTitleRequest {
thread_id: custom.id,
assistant_message: Some("assistant text".to_string()),
})
.await
.expect("skip custom title")
.value
.data
.expect("skipped title");
assert_eq!(skipped.title, "Already Named Thread");
let transcript = harness.workspace.join("session_raw/round26-chat.jsonl");
write_transcript(&transcript, "welcome");
let result = migrate_welcome_agent_artifacts(&harness.workspace).expect("migration");
assert_eq!(result.transcripts_updated, 1);
assert_eq!(result.transcript_files_renamed, 0);
assert!(transcript.exists());
let rewritten = std::fs::read_to_string(&transcript).expect("rewritten transcript");
assert!(rewritten.contains("\"agent\":\"orchestrator\""));
}
#[tokio::test]
async fn round26_memory_sources_reconcile_handles_unavailable_composio_without_registry_changes() {
let _lock = env_lock();
let harness = setup("round26-memory-sources-").await;
ensure_composio_sources().await;
let config = Config::load_or_init().await.expect("reload config");
assert_eq!(config.config_path, harness.root.join("config.toml"));
assert!(config.memory_sources.is_empty());
}