//! Round17 raw/E2E coverage for Composio tools, ops, trigger history, and //! nearby local state/profile paths. //! //! This binary uses loopback Composio routes plus temp workspace/keyring state. //! It intentionally drives public Rust surfaces so coverage lands on the same //! paths used by JSON-RPC controllers and agent-callable tools. use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, OnceLock}; use axum::body::to_bytes; use axum::extract::{Request, State}; use axum::http::{Method, StatusCode}; use axum::response::{IntoResponse, Response}; use axum::routing::any; use axum::{Json, Router}; 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::composio::ops::{ composio_execute, composio_list_tools, composio_list_trigger_history, }; use openhuman_core::openhuman::composio::trigger_history::ComposioTriggerHistoryStore; use openhuman_core::openhuman::composio::{ init_composio_trigger_history, invalidate_connected_integrations_cache, }; use openhuman_core::openhuman::config::Config; use openhuman_core::openhuman::credentials::profiles::{AuthProfile, AuthProfilesStore, TokenSet}; use openhuman_core::openhuman::credentials::{ AuthService, APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME, }; use openhuman_core::openhuman::tools::{ ComposioAuthorizeTool, ComposioExecuteTool, ComposioListConnectionsTool, ComposioListToolsTool, Tool, ToolCallOptions, }; static ROUND17_ENV_LOCK: OnceLock> = OnceLock::new(); #[derive(Clone, Default)] struct MockState { requests: Arc>>, scenario: Arc>, } #[derive(Clone, Debug, Default)] enum Scenario { #[default] Normal, ConnectionsFail, ToolsFail, DropboxOnly, } #[derive(Clone, Debug)] struct RecordedRequest { method: String, path: String, query: String, body: Value, } struct EnvGuard { key: &'static str, old: Option, } 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, workspace: PathBuf, config: Config, _guards: Vec, } fn env_lock() -> std::sync::MutexGuard<'static, ()> { ROUND17_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("composio-tools-ops-state-round17-") .tempdir_in("target") .expect("round17 tempdir") } async fn setup(api_url: &str) -> 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"), ]; let mut config = Config { workspace_dir: workspace.clone(), config_path: root.join("config.toml"), api_url: Some(api_url.to_string()), ..Config::default() }; config.observability.analytics_enabled = false; config.secrets.encrypt = false; config.save().await.expect("save config"); Harness { _tmp: tmp, workspace, config, _guards: guards, } } #[tokio::test] async fn round17_agent_tools_cover_authorize_filters_scopes_and_error_branches() { let _lock = env_lock(); let state = MockState::default(); let base = start_loopback_backend( Router::new() .fallback(any(composio_backend_handler)) .with_state(state.clone()), ) .await; let harness = setup(&base).await; store_app_session_token(&harness.config, "round17-session-token"); invalidate_connected_integrations_cache(); let config = Arc::new(harness.config.clone()); let authorize = ComposioAuthorizeTool::new(config.clone()); let auth = authorize .execute(json!({ "toolkit": " Gmail " })) .await .expect("authorize tool"); assert!(!auth.is_error); assert!(auth.text().contains("https://connect.example/round17")); assert!(auth.text().contains("conn-authorized-round17")); let connections = ComposioListConnectionsTool::new(config.clone()) .execute(json!({})) .await .expect("connections tool"); assert!(!connections.is_error); assert!(connections.text().contains("conn-gmail")); assert!(!connections.text().contains("conn-github-expired")); let list_tools = ComposioListToolsTool::new(config.clone()); let connected_markdown = list_tools .execute_with_options( json!({ "toolkits": ["gmail", "github"], "tags": ["repos"], "include_unconnected": false }), ToolCallOptions { prefer_markdown: true, ..ToolCallOptions::default() }, ) .await .expect("connected list tools"); assert!(!connected_markdown.is_error); assert!(connected_markdown.text().contains("GMAIL_FETCH_EMAILS")); assert!(!connected_markdown .text() .contains("GITHUB_GET_A_REPOSITORY")); assert!(connected_markdown .markdown_formatted .as_deref() .unwrap_or_default() .contains("query")); let include_unconnected = list_tools .execute(json!({ "toolkits": ["github"], "tags": ["repos", "stars"], "include_unconnected": true })) .await .expect("include unconnected list tools"); assert!(!include_unconnected.is_error); assert!(include_unconnected .text() .contains("GITHUB_GET_A_REPOSITORY")); *state.scenario.lock().expect("scenario") = Scenario::ConnectionsFail; let filter_error = list_tools .execute(json!({ "toolkits": ["gmail"], "include_unconnected": false })) .await .expect("connection filter error is tool result"); assert!(filter_error.is_error); assert!(filter_error .text() .contains("include_unconnected=true to skip this check")); *state.scenario.lock().expect("scenario") = Scenario::DropboxOnly; let unsupported = list_tools .execute(json!({ "toolkits": ["zendesk"], "include_unconnected": false })) .await .expect("uncurated toolkit error"); assert!(unsupported.is_error); assert!(unsupported .text() .contains("no agent-ready actions are available")); let execute = ComposioExecuteTool::new(config.clone()); let admin_blocked = execute .execute(json!({ "tool": "GMAIL_DELETE_MESSAGE", "arguments": { "message_id": "m1" } })) .await .expect("admin scope block"); assert!(admin_blocked.is_error); assert!(admin_blocked.text().contains("classified `admin`")); assert!(admin_blocked.text().contains("Connections")); let not_curated = execute .execute(json!({ "tool": "GMAIL_UNKNOWN_EXPERIMENT", "arguments": {} })) .await .expect("not curated block"); assert!(not_curated.is_error); assert!(not_curated.text().contains("not in the curated whitelist")); *state.scenario.lock().expect("scenario") = Scenario::Normal; let success = execute .execute(json!({ "tool": "GMAIL_FETCH_EMAILS", "connection_id": "conn-gmail", "arguments": { "query": "label:INBOX" } })) .await .expect("execute success"); assert!(!success.is_error); assert_eq!(success.text(), "Fetched round17 inbox"); *state.scenario.lock().expect("scenario") = Scenario::ToolsFail; let ops_error = composio_list_tools(&harness.config, Some(vec!["gmail".into()]), None) .await .expect_err("ops list_tools backend failure"); assert!(ops_error.contains("[composio] list_tools failed")); let requests = state.requests.lock().expect("requests").clone(); assert!(requests.iter().any(|req| { req.method == "GET" && req.path == "/agent-integrations/composio/tools" && req.query.contains("toolkits=github") && req.query.contains("tags=") })); assert!(requests.iter().any(|req| { req.method == "POST" && req.path == "/agent-integrations/composio/authorize" && req.body["toolkit"] == "Gmail" })); } #[tokio::test] async fn round17_ops_trigger_history_app_state_and_profiles_cover_local_edges() { let _lock = env_lock(); let state = MockState::default(); let base = start_loopback_backend( Router::new() .fallback(any(composio_backend_handler)) .with_state(state), ) .await; let harness = setup(&base).await; store_app_session_token(&harness.config, "round17-session-token"); let missing_history = composio_list_trigger_history(&harness.config, Some(5)) .await .expect_err("history not initialized"); assert!(missing_history.contains("archive store is not initialized")); let store = ComposioTriggerHistoryStore::new(&harness.workspace).expect("history store"); for idx in 0..3 { store .record_trigger( "gmail", "GMAIL_NEW_GMAIL_MESSAGE", &format!("metadata-{idx}"), &format!("uuid-{idx}"), &json!({ "idx": idx }), ) .expect("record trigger"); } init_composio_trigger_history(harness.workspace.clone()).expect("init history"); let clamped = composio_list_trigger_history(&harness.config, Some(9999)) .await .expect("list initialized history") .value; assert_eq!(clamped.entries.len(), 3); assert!(clamped.archive_dir.ends_with("/state/triggers")); assert!(clamped.current_day_file.ends_with(".jsonl")); let state_dir = harness.workspace.join("state"); std::fs::create_dir_all(&state_dir).expect("state dir"); std::fs::write(state_dir.join("app-state.json"), "{not-json").expect("corrupt state"); let updated = update_local_state(StoredAppStatePatch { keyring_consent: None, encryption_key: Some(Some(" round17-key ".into())), onboarding_tasks: Some(Some(StoredOnboardingTasks { accessibility_permission_granted: false, local_model_consent_given: true, local_model_download_started: false, enabled_tools: vec!["gmail".into(), "github".into()], connected_sources: vec!["gmail".into()], updated_at_ms: Some(17), })), }) .await .expect("update state after corrupt file") .value; assert_eq!(updated.encryption_key.as_deref(), Some("round17-key")); let snap = snapshot().await.expect("snapshot").value; assert!(snap.auth.is_authenticated); assert_eq!(snap.session_token.as_deref(), Some("round17-session-token")); let profile_store = AuthProfilesStore::new(&harness.workspace.join("profiles"), false); let oauth = AuthProfile::new_oauth( "gmail", "round17", TokenSet { access_token: "access-round17".into(), refresh_token: Some("refresh-round17".into()), id_token: Some("id-round17".into()), expires_at: Some(Utc::now() + ChronoDuration::minutes(2)), token_type: Some("Bearer".into()), scope: Some("email profile".into()), }, ); profile_store .upsert_profile(oauth.clone(), true) .expect("insert oauth"); let updated_profile = profile_store .update_profile(&oauth.id, |profile| { profile.metadata = BTreeMap::from([("round".into(), "17".into())]); profile.account_id = Some("acct-round17".into()); Ok(()) }) .expect("update profile"); assert_eq!( updated_profile.metadata.get("round"), Some(&"17".to_string()) ); assert!(updated_profile .token_set .as_ref() .expect("token set") .is_expiring_within(std::time::Duration::from_secs(180))); let direct_ops_success = composio_execute( &harness.config, "GMAIL_FETCH_EMAILS", Some(json!({ "query": "from:round17" })), None, ) .await .expect("ops execute success") .value; assert!(direct_ops_success.successful); assert_eq!( direct_ops_success.markdown_formatted.as_deref(), Some("Fetched round17 inbox") ); } async fn composio_backend_handler(State(state): State, request: Request) -> Response { let method = request.method().clone(); let uri = request.uri().clone(); let path = uri.path().to_string(); let query = uri.query().unwrap_or_default().to_string(); let body_bytes = to_bytes(request.into_body(), usize::MAX) .await .expect("mock request body"); let body: Value = if body_bytes.is_empty() { json!({}) } else { serde_json::from_slice(&body_bytes).expect("json body") }; state .requests .lock() .expect("requests") .push(RecordedRequest { method: method.as_str().to_string(), path: path.clone(), query: query.clone(), body: body.clone(), }); let scenario = state.scenario.lock().expect("scenario").clone(); match (method, path.as_str(), scenario) { (Method::GET, "/agent-integrations/composio/toolkits", _) => ok(json!({ "toolkits": ["gmail", "github", "dropbox"] })), (Method::GET, "/agent-integrations/composio/connections", Scenario::ConnectionsFail) => { fail(StatusCode::BAD_GATEWAY, "connections unavailable") } (Method::GET, "/agent-integrations/composio/connections", Scenario::DropboxOnly) => { ok(json!({ "connections": [{ "id": "conn-zendesk", "toolkit": "zendesk", "status": "ACTIVE", "createdAt": "2026-05-29T12:00:00Z" }] })) } (Method::GET, "/agent-integrations/composio/connections", _) => ok(json!({ "connections": [ { "id": "conn-gmail", "toolkit": "gmail", "status": "ACTIVE", "createdAt": "2026-05-29T12:00:00Z" }, { "id": "conn-github-expired", "toolkit": "github", "status": "EXPIRED", "createdAt": "2026-05-28T12:00:00Z" } ] })), (Method::POST, "/agent-integrations/composio/authorize", _) => ok(json!({ "connectUrl": "https://connect.example/round17", "connectionId": "conn-authorized-round17" })), (Method::GET, "/agent-integrations/composio/tools", Scenario::ToolsFail) => { fail(StatusCode::SERVICE_UNAVAILABLE, "tools unavailable") } (Method::GET, "/agent-integrations/composio/tools", Scenario::DropboxOnly) => { ok(json!({ "tools": [] })) } (Method::GET, "/agent-integrations/composio/tools", _) => ok(json!({ "tools": [ { "type": "function", "function": { "name": "GMAIL_FETCH_EMAILS", "description": "Fetch Gmail messages over multiple lines\nfor markdown collapsing.", "parameters": { "type": "object", "required": ["query"], "properties": { "query": { "type": "string" }, "max_results": { "type": "number" } } } } }, { "type": "function", "function": { "name": "GMAIL_DELETE_EMAIL", "description": "Delete Gmail message", "parameters": { "type": "object" } } }, { "type": "function", "function": { "name": "GITHUB_GET_A_REPOSITORY", "description": "Star repository", "parameters": { "type": "object" } } } ] })), (Method::POST, "/agent-integrations/composio/execute", _) => { match body.get("tool").and_then(Value::as_str) { Some("GMAIL_FETCH_EMAILS") => ok(json!({ "data": { "messages": [{ "id": "msg-round17" }] }, "successful": true, "error": null, "costUsd": 0.04, "markdownFormatted": "Fetched round17 inbox" })), other => fail( StatusCode::BAD_REQUEST, &format!("unexpected execute tool: {other:?}"), ), } } _ => fail(StatusCode::NOT_FOUND, &format!("unhandled {path}")), } } async fn start_loopback_backend(app: Router) -> String { let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("bind mock backend"); let addr = listener.local_addr().expect("mock backend addr"); tokio::spawn(async move { let _ = axum::serve(listener, app).await; }); format!("http://127.0.0.1:{}", addr.port()) } fn store_app_session_token(config: &Config, token: &str) { AuthService::from_config(config) .store_provider_token( APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME, token, HashMap::new(), true, ) .expect("store app session token"); } fn ok(data: Value) -> Response { Json(json!({ "success": true, "data": data })).into_response() } fn fail(status: StatusCode, error: &str) -> Response { ( status, Json(json!({ "success": false, "error": error.to_string() })), ) .into_response() }