Files
openhuman/tests/raw_coverage/credentials_threads_round22_raw_coverage_e2e.rs
T

327 lines
9.5 KiB
Rust

use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use chrono::Utc;
use openhuman_core::openhuman::config::rpc as config_rpc;
use openhuman_core::openhuman::credentials::profiles::{
AuthProfile, AuthProfileKind, AuthProfilesStore, TokenSet,
};
use openhuman_core::openhuman::memory::{
AppendConversationMessageRequest, ConversationMessageRecord, ConversationMessagesRequest,
DeleteConversationThreadRequest, EmptyRequest, UpdateConversationMessageRequest,
UpdateConversationThreadLabelsRequest, UpdateConversationThreadTitleRequest,
UpsertConversationThreadRequest,
};
use openhuman_core::openhuman::threads::ops as thread_ops;
use serde_json::json;
use tempfile::{Builder, TempDir};
static ENV_LOCK: &OnceLock<Mutex<()>> = &crate::SHARED_ENV_LOCK;
struct EnvGuard {
key: &'static str,
old: Option<String>,
}
impl EnvGuard {
fn set_path(key: &'static str, path: &Path) -> Self {
let old = std::env::var(key).ok();
unsafe { std::env::set_var(key, path.as_os_str()) };
Self { key, old }
}
fn set(key: &'static str, value: impl Into<String>) -> Self {
let old = std::env::var(key).ok();
unsafe { std::env::set_var(key, value.into()) };
Self { key, old }
}
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<EnvGuard>,
}
impl Harness {
fn workspace_dir(&self) -> PathBuf {
self.root.join("workspace")
}
}
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
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("target dir");
Builder::new()
.prefix("credentials-threads-round22-")
.tempdir_in("target")
.expect("tempdir")
}
fn setup() -> Harness {
let tmp = tempdir();
let root = tmp.path().join("openhuman");
std::fs::create_dir_all(&root).expect("root dir");
std::fs::write(
root.join("config.toml"),
r#"api_url = "http://127.0.0.1:9"
default_model = "round22-coverage-model"
default_temperature = 0.2
onboarding_completed = true
chat_onboarding_completed = false
[observability]
analytics_enabled = 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
"#,
)
.expect("config");
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::set("OPENHUMAN_MEMORY_EMBED_ENDPOINT", ""),
EnvGuard::set("OPENHUMAN_MEMORY_EMBED_MODEL", ""),
EnvGuard::unset("OPENHUMAN_API_URL"),
EnvGuard::unset("BACKEND_URL"),
EnvGuard::unset("VITE_BACKEND_URL"),
];
Harness {
_tmp: tmp,
root,
_guards: guards,
}
}
#[test]
fn round22_credentials_profiles_cover_schema_quarantine_and_oauth_save_paths() {
let _lock = env_lock();
let harness = setup();
let state_dir = harness.root.join("profile-state");
let store = AuthProfilesStore::new(&state_dir, false);
let oauth = AuthProfile::new_oauth(
"github",
"work",
TokenSet {
access_token: "access-round22".to_string(),
refresh_token: Some("refresh-round22".to_string()),
id_token: Some("id-round22".to_string()),
expires_at: Some(Utc::now() + chrono::Duration::hours(1)),
token_type: Some("Bearer".to_string()),
scope: Some("repo read:user".to_string()),
},
);
assert_eq!(oauth.kind, AuthProfileKind::OAuth);
store
.upsert_profile(oauth.clone(), true)
.expect("save oauth profile");
let loaded = store.load().expect("load oauth");
assert_eq!(
loaded
.profiles
.get(&oauth.id)
.and_then(|profile| profile.token_set.as_ref())
.map(|tokens| tokens.access_token.as_str()),
Some("access-round22")
);
let persisted = std::fs::read_to_string(store.path()).expect("persisted oauth json");
assert!(persisted.contains("access_token"));
assert!(persisted.contains("refresh_token"));
std::fs::write(store.path(), b"{not-json").expect("corrupt store");
let recovered = store.load().expect("corrupt store is quarantined");
assert!(recovered.profiles.is_empty());
assert!(
std::fs::read_dir(&state_dir)
.expect("state dir")
.filter_map(Result::ok)
.any(|entry| entry
.file_name()
.to_string_lossy()
.contains("auth-profiles.corrupt")),
"corrupt store should be quarantined"
);
std::fs::write(
store.path(),
serde_json::to_vec_pretty(&json!({
"schema_version": 99,
"updated_at": Utc::now().to_rfc3339(),
"active_profiles": {},
"profiles": {}
}))
.expect("future schema json"),
)
.expect("future schema");
let schema_err = store
.load()
.expect_err("future schema rejected")
.to_string();
assert!(schema_err.contains("Unsupported auth profile schema version 99"));
}
#[tokio::test]
async fn round22_threads_cover_update_delete_and_error_edges() {
let _lock = env_lock();
let harness = setup();
let _ = config_rpc::load_config_with_timeout()
.await
.expect("config loads");
let created_at = Utc::now().to_rfc3339();
let thread = thread_ops::thread_upsert(UpsertConversationThreadRequest {
id: "round22-thread".to_string(),
title: "Round22 Original".to_string(),
created_at: created_at.clone(),
parent_thread_id: Some("parent-round22".to_string()),
labels: Some(vec!["alpha".to_string()]),
personality_id: Some("persona-round22".to_string()),
})
.await
.expect("upsert thread")
.value
.data
.expect("thread data");
assert_eq!(thread.parent_thread_id.as_deref(), Some("parent-round22"));
let message = ConversationMessageRecord {
id: "msg-round22".to_string(),
content: "Round22 message".to_string(),
message_type: "text".to_string(),
extra_metadata: json!({"old": true}),
sender: "user".to_string(),
created_at,
};
thread_ops::message_append(AppendConversationMessageRequest {
thread_id: "round22-thread".to_string(),
message,
})
.await
.expect("append message");
let updated_message = thread_ops::message_update(UpdateConversationMessageRequest {
thread_id: "round22-thread".to_string(),
message_id: "msg-round22".to_string(),
extra_metadata: Some(json!({"round": 22})),
})
.await
.expect("update message")
.value
.data
.expect("message data");
assert_eq!(updated_message.extra_metadata["round"], 22);
let listed = thread_ops::messages_list(ConversationMessagesRequest {
thread_id: "round22-thread".to_string(),
})
.await
.expect("list messages")
.value
.data
.expect("messages data");
assert_eq!(listed.count, 1);
let labels = thread_ops::thread_update_labels(UpdateConversationThreadLabelsRequest {
thread_id: "round22-thread".to_string(),
labels: Vec::new(),
})
.await
.expect("clear labels")
.value
.data
.expect("labels data");
assert!(labels.labels.is_empty());
let empty_title = thread_ops::thread_update_title(UpdateConversationThreadTitleRequest {
thread_id: "round22-thread".to_string(),
title: " ".to_string(),
})
.await
.expect_err("empty title rejected");
assert!(empty_title.contains("title must not be empty"));
let renamed = thread_ops::thread_update_title(UpdateConversationThreadTitleRequest {
thread_id: "round22-thread".to_string(),
title: " Round22 Renamed ".to_string(),
})
.await
.expect("rename thread")
.value
.data
.expect("rename data");
assert_eq!(renamed.title, "Round22 Renamed");
let deleted = thread_ops::thread_delete(DeleteConversationThreadRequest {
thread_id: "round22-thread".to_string(),
deleted_at: Utc::now().to_rfc3339(),
})
.await
.expect("delete thread")
.value
.data
.expect("delete data");
assert!(deleted.deleted);
let after_delete = thread_ops::threads_list(EmptyRequest {})
.await
.expect("list after delete")
.value
.data
.expect("list data");
assert_eq!(after_delete.count, 0);
let deleted_again = thread_ops::thread_delete(DeleteConversationThreadRequest {
thread_id: "round22-thread".to_string(),
deleted_at: Utc::now().to_rfc3339(),
})
.await
.expect("delete missing thread is idempotent")
.value
.data
.expect("delete missing data");
assert!(!deleted_again.deleted);
let workspace = harness.workspace_dir();
assert!(workspace.exists());
}