Files
openhuman/tests/app_credentials_threads_round24_raw_coverage_e2e.rs
T

384 lines
12 KiB
Rust

//! Round24 focused raw coverage for app_state, credentials profiles, and
//! threads public operations.
//!
//! Uses temp workspaces and local filesystem state only. No real backend,
//! keychain service, or non-loopback network access is required.
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use chrono::{Duration as ChronoDuration, Utc};
use serde_json::{json, Value};
use tempfile::{Builder, TempDir};
use openhuman_core::openhuman::app_state::{
snapshot, update_local_state, StoredAppStatePatch, StoredOnboardingTasks,
};
use openhuman_core::openhuman::config::Config;
use openhuman_core::openhuman::credentials::profiles::{AuthProfile, AuthProfilesStore, TokenSet};
use openhuman_core::openhuman::memory::{
AppendConversationMessageRequest, ConversationMessageRecord, ConversationMessagesRequest,
CreateConversationThreadRequest, DeleteConversationThreadRequest, EmptyRequest,
UpdateConversationMessageRequest, UpdateConversationThreadLabelsRequest,
UpdateConversationThreadTitleRequest,
};
use openhuman_core::openhuman::threads::ops::{
message_append, message_update, messages_list, thread_create_new, thread_delete,
thread_update_labels, thread_update_title, threads_list, threads_purge,
};
static ROUND24_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_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 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,
_guards: Vec<EnvGuard>,
}
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
ROUND24_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("app-credentials-threads-round24-")
.tempdir_in("target")
.expect("round24 tempdir")
}
async fn setup() -> Harness {
let tmp = 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_to_path("OPENHUMAN_WORKSPACE", &root),
EnvGuard::set_to_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", ""),
];
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: true,
chat_onboarding_completed: false,
..Config::default()
};
config.observability.analytics_enabled = false;
config.secrets.encrypt = false;
config.save().await.expect("save config");
Harness {
_tmp: tmp,
root,
_guards: guards,
}
}
fn profile_store(harness: &Harness) -> AuthProfilesStore {
AuthProfilesStore::new(&harness.root, false)
}
#[tokio::test]
async fn round24_app_state_update_and_snapshot_preserve_local_state() {
let _lock = env_lock();
let _harness = setup().await;
let tasks = StoredOnboardingTasks {
accessibility_permission_granted: true,
local_model_consent_given: true,
local_model_download_started: true,
enabled_tools: vec!["search".into(), "memory".into()],
connected_sources: vec!["slack".into()],
updated_at_ms: Some(4242),
};
let updated = update_local_state(StoredAppStatePatch {
keyring_consent: None,
encryption_key: Some(Some(" round24-key ".into())),
onboarding_tasks: Some(Some(tasks.clone())),
})
.await
.expect("update local state")
.value;
assert_eq!(updated.encryption_key.as_deref(), Some("round24-key"));
assert_eq!(
updated.onboarding_tasks.as_ref().unwrap().connected_sources,
tasks.connected_sources
);
let snapshot = snapshot().await.expect("snapshot").value;
assert!(snapshot.onboarding_completed);
assert!(!snapshot.chat_onboarding_completed);
assert!(!snapshot.analytics_enabled);
assert_eq!(
snapshot.local_state.encryption_key.as_deref(),
Some("round24-key")
);
assert!(snapshot.current_user.is_none());
assert!(snapshot.session_token.is_none());
let cleared = update_local_state(StoredAppStatePatch {
keyring_consent: None,
encryption_key: Some(Some(" ".into())),
onboarding_tasks: Some(None),
})
.await
.expect("clear local state")
.value;
assert!(cleared.encryption_key.is_none());
assert!(cleared.onboarding_tasks.is_none());
}
#[test]
fn round24_credentials_profiles_cover_schema_and_mutation_edges() {
let _lock = env_lock();
let runtime = tokio::runtime::Runtime::new().expect("runtime");
let harness = runtime.block_on(setup());
let store = profile_store(&harness);
std::fs::create_dir_all(&harness.root).expect("state dir");
std::fs::write(store.path(), "").expect("empty profile store");
let empty = store.load().expect("empty store loads default");
assert!(empty.profiles.is_empty());
let oauth = AuthProfile::new_oauth(
"gmail",
"work",
TokenSet {
access_token: "access-round24".into(),
refresh_token: Some("refresh-round24".into()),
id_token: Some("id-round24".into()),
expires_at: Some(Utc::now() + ChronoDuration::minutes(30)),
token_type: Some("Bearer".into()),
scope: Some("email profile".into()),
},
);
let oauth_id = oauth.id.clone();
store.upsert_profile(oauth, true).expect("upsert oauth");
let token = AuthProfile::new_token("anthropic", "prod", "sk-round24".into());
let token_id = token.id.clone();
store.upsert_profile(token, false).expect("upsert token");
store
.set_active_profile("anthropic", &token_id)
.expect("set active token profile");
let updated = store
.update_profile(&token_id, |profile| {
profile.workspace_id = Some("workspace-round24".into());
profile.metadata.insert("tier".into(), "prod".into());
Ok(())
})
.expect("update profile");
assert_eq!(updated.workspace_id.as_deref(), Some("workspace-round24"));
let loaded = store.load().expect("load profiles");
assert_eq!(loaded.active_profiles.get("gmail"), Some(&oauth_id));
assert_eq!(loaded.active_profiles.get("anthropic"), Some(&token_id));
assert_eq!(
loaded
.profiles
.get(&token_id)
.and_then(|profile| profile.metadata.get("tier"))
.map(String::as_str),
Some("prod")
);
assert!(loaded
.profiles
.get(&oauth_id)
.and_then(|profile| profile.token_set.as_ref())
.is_some_and(|tokens| tokens.id_token.as_deref() == Some("id-round24")));
store
.clear_active_profile("gmail")
.expect("clear active oauth");
assert!(store
.remove_profile(&oauth_id)
.expect("remove oauth profile"));
assert!(!store
.remove_profile(&oauth_id)
.expect("second remove is false"));
let mut raw: Value =
serde_json::from_str(&std::fs::read_to_string(store.path()).expect("raw store"))
.expect("profile store json");
raw["schema_version"] = json!(999);
std::fs::write(store.path(), serde_json::to_vec_pretty(&raw).expect("json"))
.expect("write future schema");
let err = store.load().expect_err("future schema must fail");
assert!(
err.to_string()
.contains("Unsupported auth profile schema version"),
"unexpected error: {err:?}"
);
}
#[tokio::test]
async fn round24_threads_public_ops_cover_crud_and_error_branches() {
let _lock = env_lock();
let _harness = setup().await;
let created = thread_create_new(CreateConversationThreadRequest {
labels: Some(vec!["personal".into(), "round24".into()]),
personality_id: Some("default-personality".into()),
})
.await
.expect("create thread")
.value
.data
.expect("created data");
assert!(created.id.starts_with("thread-"));
assert_eq!(created.labels, vec!["personal", "round24"]);
assert_eq!(
created.personality_id.as_deref(),
Some("default-personality")
);
let msg = ConversationMessageRecord {
id: "msg-round24".into(),
content: "hello round24".into(),
message_type: "text".into(),
extra_metadata: Value::Null,
sender: "user".into(),
created_at: "2026-05-30T00:00:00Z".into(),
};
let appended = message_append(AppendConversationMessageRequest {
thread_id: created.id.clone(),
message: msg,
})
.await
.expect("append message")
.value
.data
.expect("appended data");
assert_eq!(appended.id, "msg-round24");
let patched = message_update(UpdateConversationMessageRequest {
thread_id: created.id.clone(),
message_id: "msg-round24".into(),
extra_metadata: Some(json!({ "source": "round24" })),
})
.await
.expect("update message")
.value
.data
.expect("patched data");
assert_eq!(patched.extra_metadata["source"], "round24");
let messages = messages_list(ConversationMessagesRequest {
thread_id: created.id.clone(),
})
.await
.expect("list messages")
.value
.data
.expect("messages data");
assert_eq!(messages.count, 1);
let relabeled = thread_update_labels(UpdateConversationThreadLabelsRequest {
thread_id: created.id.clone(),
labels: vec!["work".into()],
})
.await
.expect("update labels")
.value
.data
.expect("labels data");
assert_eq!(relabeled.labels, vec!["work"]);
let empty_title = thread_update_title(UpdateConversationThreadTitleRequest {
thread_id: created.id.clone(),
title: " ".into(),
})
.await
.expect_err("empty titles are rejected");
assert!(empty_title.contains("title must not be empty"));
let renamed = thread_update_title(UpdateConversationThreadTitleRequest {
thread_id: created.id.clone(),
title: " Round24 covered thread ".into(),
})
.await
.expect("update title")
.value
.data
.expect("title data");
assert_eq!(renamed.title, "Round24 covered thread");
let listed = threads_list(EmptyRequest {})
.await
.expect("list threads")
.value
.data
.expect("threads data");
assert_eq!(listed.count, 1);
let deleted = thread_delete(DeleteConversationThreadRequest {
thread_id: created.id,
deleted_at: "2026-05-30T00:01:00Z".into(),
})
.await
.expect("delete thread")
.value
.data
.expect("delete data");
assert!(deleted.deleted);
let purged = threads_purge(EmptyRequest {})
.await
.expect("purge threads")
.value
.data
.expect("purge data");
assert_eq!(purged.agent_threads_deleted, 0);
}