Files
openhuman/tests/composio_ops_raw_coverage_e2e.rs
T

745 lines
26 KiB
Rust

//! Focused raw integration coverage for Composio ops.
//!
//! This test binary stays on loopback mocks and temp stores. It drives the
//! public ops layer instead of unit-test-only helpers so coverage lands on the
//! RPC-facing paths used by controllers, tools, and prompt integration fetches.
use std::sync::{Arc, Mutex};
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 serde_json::Map;
use serde_json::{json, Value};
use tempfile::tempdir;
use openhuman_core::core::all::RegisteredController;
use openhuman_core::openhuman::composio::ops::{
cached_active_integrations, composio_authorize, composio_create_trigger,
composio_delete_connection, composio_disable_trigger, composio_enable_trigger,
composio_execute, composio_get_mode, composio_list_agent_ready_toolkits,
composio_list_available_triggers, composio_list_capabilities, composio_list_connections,
composio_list_github_repos, composio_list_toolkits, composio_list_tools,
composio_list_trigger_history, composio_list_triggers, composio_set_api_key, composio_sync,
fetch_connected_integrations, fetch_connected_integrations_status,
invalidate_connected_integrations_cache, FetchConnectedIntegrationsStatus,
};
use openhuman_core::openhuman::composio::{
all_composio_controller_schemas, all_composio_registered_controllers,
};
use openhuman_core::openhuman::composio::{init_composio_trigger_history, ComposioActionTool};
use openhuman_core::openhuman::config::Config;
use openhuman_core::openhuman::credentials::{
AuthService, APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME,
};
use openhuman_core::openhuman::tools::{ComposioExecuteTool, Tool};
#[derive(Clone, Default)]
struct MockState {
requests: Arc<Mutex<Vec<RecordedRequest>>>,
}
#[derive(Clone, Debug)]
struct RecordedRequest {
method: String,
path: String,
query: String,
body: Value,
}
#[tokio::test]
async fn composio_ops_use_loopback_backend_for_happy_and_error_paths() {
let state = MockState::default();
let app = Router::new()
.fallback(any(composio_backend_handler))
.with_state(state.clone());
let base = start_loopback_backend(app).await;
let dir = tempdir().expect("tempdir");
let config = Config {
workspace_dir: dir.path().join("workspace"),
config_path: dir.path().join("config.toml"),
api_url: Some(base.clone()),
..Config::default()
};
std::fs::create_dir_all(&config.workspace_dir).expect("workspace dir");
config.save().await.expect("save config snapshot");
store_app_session_token(&config, "round14-session-token");
invalidate_connected_integrations_cache();
let toolkits = composio_list_toolkits(&config)
.await
.expect("list toolkits")
.into_cli_compatible_json()
.expect("toolkits json");
assert_eq!(
toolkits.pointer("/result/toolkits/0"),
Some(&json!("gmail"))
);
let capabilities = composio_list_capabilities(&config)
.await
.expect("capabilities")
.into_cli_compatible_json()
.expect("capabilities json");
assert!(capabilities
.pointer("/result/capabilities")
.and_then(Value::as_array)
.is_some_and(|items| !items.is_empty()));
let ready = composio_list_agent_ready_toolkits()
.await
.expect("agent ready")
.into_cli_compatible_json()
.expect("ready json");
assert!(ready
.pointer("/result/toolkits")
.and_then(Value::as_array)
.is_some_and(|items| items.iter().any(|item| item == "gmail")));
let connections = composio_list_connections(&config)
.await
.expect("list connections")
.into_cli_compatible_json()
.expect("connections json");
assert_eq!(
connections.pointer("/result/connections/0/id"),
Some(&json!("conn-gmail"))
);
let integrations = fetch_connected_integrations(&config).await;
assert!(integrations.iter().any(|item| item.toolkit == "gmail"
&& item.connected
&& item
.tools
.iter()
.any(|tool| tool.name == "GMAIL_FETCH_EMAILS")));
assert!(integrations.iter().any(|item| item.toolkit == "github"
&& !item.connected
&& item.non_active_status.as_deref() == Some("EXPIRED")));
assert!(cached_active_integrations(&config).is_some());
assert!(matches!(
fetch_connected_integrations_status(&config).await,
FetchConnectedIntegrationsStatus::Authoritative(_)
));
let authorize = composio_authorize(
&config,
" Gmail ",
Some(json!({ "oauth_scopes": "profile", "custom": "value" })),
)
.await
.expect("authorize")
.into_cli_compatible_json()
.expect("authorize json");
assert_eq!(
authorize.pointer("/result/connectUrl"),
Some(&json!("https://connect.example/Gmail"))
);
let tools = composio_list_tools(
&config,
Some(vec![" github ".into(), "gmail".into()]),
Some(vec![" repos ".into(), " ".into()]),
)
.await
.expect("list tools")
.into_cli_compatible_json()
.expect("tools json");
let tool_names: Vec<String> = tools
.pointer("/result/tools")
.and_then(Value::as_array)
.expect("tools array")
.iter()
.filter_map(|tool| tool.pointer("/function/name").and_then(Value::as_str))
.map(ToString::to_string)
.collect();
assert!(tool_names.contains(&"GMAIL_FETCH_EMAILS".to_string()));
assert!(tool_names.contains(&"GMAIL_DELETE_EMAIL".to_string()));
let execute = composio_execute(
&config,
"GMAIL_FETCH_EMAILS",
Some(json!({ "query": "label:INBOX" })),
None,
)
.await
.expect("execute")
.into_cli_compatible_json()
.expect("execute json");
assert_eq!(
execute.pointer("/result/data/messages/0/id"),
Some(&json!("msg-1"))
);
let provider_error = composio_execute(
&config,
"GMAIL_SEND_EMAIL",
Some(json!({ "to": "person@example.test" })),
None,
)
.await
.expect("provider error stays in response")
.into_cli_compatible_json()
.expect("provider error json");
assert_eq!(
provider_error.pointer("/result/successful"),
Some(&json!(false))
);
assert!(provider_error
.pointer("/result/error")
.and_then(Value::as_str)
.unwrap_or_default()
.starts_with("[composio:error:validation]"));
let repos = composio_list_github_repos(&config, Some(" conn-github ".into()))
.await
.expect("github repos")
.into_cli_compatible_json()
.expect("repos json");
assert_eq!(
repos.pointer("/result/repositories/0/fullName"),
Some(&json!("tinyhumansai/openhuman"))
);
let created = composio_create_trigger(
&config,
"GITHUB_PULL_REQUEST_EVENT",
Some("conn-github".into()),
Some(json!({ "owner": "tinyhumansai", "repo": "openhuman" })),
)
.await
.expect("create trigger")
.into_cli_compatible_json()
.expect("create json");
assert_eq!(
created.pointer("/result/triggerId"),
Some(&json!("trigger-created"))
);
let available = composio_list_available_triggers(&config, "github", Some("conn-github".into()))
.await
.expect("available triggers")
.into_cli_compatible_json()
.expect("available json");
assert_eq!(
available.pointer("/result/triggers/0/repo/repo"),
Some(&json!("openhuman"))
);
let active = composio_list_triggers(&config, Some(" gmail ".into()))
.await
.expect("active triggers")
.into_cli_compatible_json()
.expect("active json");
assert_eq!(
active.pointer("/result/triggers/0/id"),
Some(&json!("trigger-active"))
);
let enabled = composio_enable_trigger(
&config,
" conn-gmail ",
" GMAIL_NEW_GMAIL_MESSAGE ",
Some(json!({ "label": "INBOX" })),
)
.await
.expect("enable trigger")
.into_cli_compatible_json()
.expect("enable json");
assert_eq!(
enabled.pointer("/result/connectionId"),
Some(&json!("conn-gmail"))
);
let disabled = composio_disable_trigger(&config, " trigger-active ")
.await
.expect("disable trigger")
.into_cli_compatible_json()
.expect("disable json");
assert_eq!(disabled.pointer("/result/deleted"), Some(&json!(true)));
let deleted = composio_delete_connection(&config, "conn-slack", false)
.await
.expect("delete connection")
.into_cli_compatible_json()
.expect("delete json");
assert_eq!(deleted.pointer("/result/deleted"), Some(&json!(true)));
assert!(cached_active_integrations(&config).is_some());
let missing_provider = composio_sync(&config, "conn-slack", Some("manual".into()))
.await
.expect_err("slack has no native provider in this test path");
assert!(missing_provider.contains("no native provider"));
let bad_reason = composio_sync(&config, "conn-gmail", Some("typo".into()))
.await
.expect_err("bad sync reason validates before network");
assert!(bad_reason.contains("unrecognized sync reason"));
init_composio_trigger_history(config.workspace_dir.clone())
.expect("init trigger history store");
let store = openhuman_core::openhuman::composio::global_composio_trigger_history()
.expect("global trigger history");
store
.record_trigger(
"gmail",
"GMAIL_NEW_GMAIL_MESSAGE",
"metadata-round14",
"uuid-round14",
&json!({ "subject": "coverage" }),
)
.expect("record trigger history");
let history = composio_list_trigger_history(&config, Some(5000))
.await
.expect("list trigger history")
.into_cli_compatible_json()
.expect("history json");
assert_eq!(
history.pointer("/result/entries/0/metadata_id"),
Some(&json!("metadata-round14"))
);
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,gmail")
&& req.query.contains("tags=repos")
}));
assert!(requests.iter().any(|req| {
req.method == "POST"
&& req.path == "/agent-integrations/composio/authorize"
&& req.body["oauth_scopes"].as_array().is_some_and(|scopes| {
scopes
.iter()
.any(|scope| scope == "https://www.googleapis.com/auth/gmail.readonly")
})
}));
}
#[tokio::test]
async fn composio_direct_key_ops_and_agent_tools_take_local_validation_paths() {
let dir = tempdir().expect("tempdir");
let mut config = Config {
workspace_dir: dir.path().join("workspace"),
config_path: dir.path().join("config.toml"),
..Config::default()
};
std::fs::create_dir_all(&config.workspace_dir).expect("workspace dir");
config.save().await.expect("save config");
let set = composio_set_api_key(&config, " cmp_round14_key ", false)
.await
.expect("store direct api key")
.into_cli_compatible_json()
.expect("set key json");
assert_eq!(set.pointer("/result/stored"), Some(&json!(true)));
assert_eq!(set.pointer("/result/mode"), Some(&json!("backend")));
let mode = composio_get_mode(&config)
.await
.expect("get mode")
.into_cli_compatible_json()
.expect("mode json");
assert_eq!(mode.pointer("/result/api_key_set"), Some(&json!(true)));
config.composio.mode = "direct".to_string();
config.composio.api_key = Some("cmp_inline_round14".to_string());
config.save().await.expect("save direct config");
let direct_toolkits = composio_list_toolkits(&config)
.await
.expect("direct list toolkits is local")
.into_cli_compatible_json()
.expect("direct toolkits json");
assert_eq!(
direct_toolkits.pointer("/result/toolkits"),
Some(&json!([]))
);
let direct_validation = composio_execute(
&config,
"GMAIL_SEND_EMAIL",
Some(json!({ "subject": "missing recipient" })),
None,
)
.await
.expect_err("direct execution validates before network");
assert!(direct_validation.starts_with("[composio:error:"));
assert!(direct_validation.contains("recipient"));
let arc_config = Arc::new(config.clone());
let execute_tool = ComposioExecuteTool::new(arc_config.clone());
let execute_result = execute_tool
.execute(json!({ "tool": "GMAIL_SEND_EMAIL", "arguments": { "subject": "no to" } }))
.await
.expect("agent execute validation result");
assert!(execute_result.is_error);
assert!(execute_result.text().contains("recipient"));
let action_tool = ComposioActionTool::new(
arc_config,
"GMAIL_SEND_EMAIL".to_string(),
"Send mail".to_string(),
Some(json!({ "type": "object" })),
);
let action_result = action_tool
.execute(json!({ "subject": "no to" }))
.await
.expect("per-action validation result");
assert!(action_result.is_error);
assert!(action_result.text().contains("recipient"));
}
#[tokio::test]
async fn composio_controller_registry_validates_params_without_backend_network() {
let schemas = all_composio_controller_schemas();
let controllers = all_composio_registered_controllers();
assert_eq!(schemas.len(), controllers.len());
assert!(schemas.iter().any(|schema| schema.function == "list_tools"));
assert!(controllers.iter().all(|controller| {
controller
.rpc_method_name()
.starts_with("openhuman.composio_")
}));
for (function, input_count) in [
("list_toolkits", 0),
("list_capabilities", 0),
("list_agent_ready_toolkits", 0),
("list_connections", 0),
("authorize", 2),
("delete_connection", 2),
("list_tools", 2),
("execute", 3),
("list_github_repos", 1),
("create_trigger", 3),
("get_user_profile", 1),
("refresh_all_identities", 0),
("sync", 2),
("list_trigger_history", 1),
("get_user_scopes", 1),
("set_user_scopes", 4),
("list_available_triggers", 2),
("list_triggers", 1),
("enable_trigger", 3),
("disable_trigger", 1),
("get_mode", 0),
("set_api_key", 2),
("clear_api_key", 0),
] {
let schema = openhuman_core::openhuman::composio::schemas::schemas(function);
assert_eq!(schema.namespace, "composio");
assert_eq!(schema.function, function);
assert_eq!(schema.inputs.len(), input_count, "{function}");
assert!(!schema.description.is_empty(), "{function}");
}
let unknown = openhuman_core::openhuman::composio::schemas::schemas("missing");
assert_eq!(unknown.function, "unknown");
let authorize_missing = composio_call(controller(&controllers, "authorize"), json!({}))
.await
.expect_err("authorize toolkit required");
assert!(authorize_missing.contains("missing required param 'toolkit'"));
let delete_blank = composio_call(
controller(&controllers, "delete_connection"),
json!({ "connection_id": " " }),
)
.await
.expect_err("delete rejects blank connection id");
assert!(delete_blank.contains("'connection_id' must not be empty"));
let list_tools_bad = composio_call(
controller(&controllers, "list_tools"),
json!({ "toolkits": "gmail" }),
)
.await
.expect_err("toolkits must be array");
assert!(list_tools_bad.contains("invalid 'toolkits'"));
let execute_missing = composio_call(controller(&controllers, "execute"), json!({}))
.await
.expect_err("execute tool required");
assert!(execute_missing.contains("missing required param 'tool'"));
let create_blank = composio_call(
controller(&controllers, "create_trigger"),
json!({ "slug": " " }),
)
.await
.expect_err("create trigger rejects blank slug");
assert!(create_blank.contains("'slug' must not be empty"));
let sync_bad_reason = composio_call(
controller(&controllers, "sync"),
json!({ "connection_id": "conn-1", "reason": "surprise" }),
)
.await
.expect_err("bad sync reason rejects before backend");
assert!(sync_bad_reason.contains("unrecognized sync reason"));
let available_blank = composio_call(
controller(&controllers, "list_available_triggers"),
json!({ "toolkit": " " }),
)
.await
.expect_err("available triggers rejects blank toolkit");
assert!(available_blank.contains("'toolkit' must not be empty"));
let enable_blank_connection = composio_call(
controller(&controllers, "enable_trigger"),
json!({ "connection_id": " ", "slug": "GMAIL_NEW_GMAIL_MESSAGE" }),
)
.await
.expect_err("enable trigger rejects blank connection");
assert!(enable_blank_connection.contains("'connection_id' must not be empty"));
let disable_missing = composio_call(controller(&controllers, "disable_trigger"), json!({}))
.await
.expect_err("disable trigger id required");
assert!(disable_missing.contains("missing required param 'trigger_id'"));
let set_key_blank = composio_call(
controller(&controllers, "set_api_key"),
json!({ "api_key": "" }),
)
.await
.expect_err("set api key rejects blank key");
assert!(set_key_blank.contains("'api_key' must not be empty"));
let bad_history = composio_call(
controller(&controllers, "list_trigger_history"),
json!({ "limit": "many" }),
)
.await
.expect_err("history limit must be numeric");
assert!(bad_history.contains("invalid params"));
}
async fn composio_backend_handler(State(state): State<MockState>, 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(),
});
match (method, path.as_str()) {
(Method::GET, "/agent-integrations/composio/toolkits") => ok(json!({
"toolkits": ["gmail", "github", "slack"]
})),
(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",
"toolkit": "github",
"status": "EXPIRED"
},
{
"id": "conn-slack",
"toolkit": "slack",
"status": "ACTIVE"
}
]
})),
(Method::POST, "/agent-integrations/composio/authorize") => ok(json!({
"connectUrl": format!(
"https://connect.example/{}",
body.get("toolkit").and_then(Value::as_str).unwrap_or("unknown")
),
"connectionId": "conn-authorized"
})),
(Method::GET, "/agent-integrations/composio/tools") => ok(json!({
"tools": [
{
"type": "function",
"function": {
"name": "GMAIL_FETCH_EMAILS",
"description": "Fetch Gmail messages",
"parameters": {
"type": "object",
"required": ["query"],
"properties": {
"query": { "type": "string" },
"max_results": { "type": "number" }
}
}
}
},
{
"type": "function",
"function": {
"name": "GMAIL_DELETE_EMAIL",
"description": "Delete Gmail messages",
"parameters": { "type": "object" }
}
},
{
"type": "function",
"function": {
"name": "GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER",
"description": "Star repository",
"parameters": { "type": "object" }
}
}
]
})),
(Method::POST, "/agent-integrations/composio/execute") => {
match body.get("tool").and_then(Value::as_str) {
Some("GMAIL_SEND_EMAIL") => ok(json!({
"data": null,
"successful": false,
"error": "missing required field to",
"costUsd": 0.0
})),
Some("GMAIL_FETCH_EMAILS") => ok(json!({
"data": {
"messages": [{ "id": "msg-1", "subject": "hello" }]
},
"successful": true,
"error": null,
"costUsd": 0.02,
"markdownFormatted": "Fetched 1 message"
})),
other => (
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"error": format!("unexpected execute tool: {other:?}")
})),
)
.into_response(),
}
}
(Method::GET, "/agent-integrations/composio/github/repos") => ok(json!({
"connectionId": "conn-github",
"repositories": [{
"owner": "tinyhumansai",
"repo": "openhuman",
"fullName": "tinyhumansai/openhuman",
"private": false,
"defaultBranch": "main",
"htmlUrl": "https://github.com/tinyhumansai/openhuman"
}]
})),
(Method::POST, "/agent-integrations/composio/triggers") => {
if body.get("slug").and_then(Value::as_str) == Some("GITHUB_PULL_REQUEST_EVENT") {
ok(json!({
"triggerId": "trigger-created",
"status": "enabled"
}))
} else if body.get("connectionId").is_some() {
ok(json!({
"triggerId": "trigger-enabled",
"slug": "GMAIL_NEW_GMAIL_MESSAGE",
"connectionId": "conn-gmail"
}))
} else {
ok(json!({
"triggerId": "trigger-created-generic",
"status": "enabled"
}))
}
}
(Method::GET, "/agent-integrations/composio/triggers/available") => ok(json!({
"triggers": [{
"slug": "GITHUB_PULL_REQUEST_EVENT",
"scope": "github_repo",
"defaultConfig": { "event": "pull_request" },
"requiredConfigKeys": ["owner", "repo"],
"repo": { "owner": "tinyhumansai", "repo": "openhuman" }
}]
})),
(Method::GET, "/agent-integrations/composio/triggers") => ok(json!({
"triggers": [{
"id": "trigger-active",
"slug": "GMAIL_NEW_GMAIL_MESSAGE",
"toolkit": "gmail",
"connectionId": "conn-gmail",
"triggerConfig": { "label": "INBOX" },
"state": "enabled"
}]
})),
(Method::DELETE, path) if path.starts_with("/agent-integrations/composio/triggers/") => {
ok(json!({ "deleted": true }))
}
(Method::DELETE, path) if path.starts_with("/agent-integrations/composio/connections/") => {
ok(json!({ "deleted": true, "memory_chunks_deleted": 0 }))
}
_ => (
StatusCode::NOT_FOUND,
Json(json!({ "success": false, "error": format!("unhandled {path}") })),
)
.into_response(),
}
}
fn controller<'a>(
controllers: &'a [RegisteredController],
function: &str,
) -> &'a RegisteredController {
controllers
.iter()
.find(|controller| controller.schema.function == function)
.unwrap_or_else(|| panic!("controller {function} registered"))
}
async fn composio_call(controller: &RegisteredController, params: Value) -> Result<Value, String> {
let params: Map<String, Value> = params.as_object().cloned().unwrap_or_default();
(controller.handler)(params).await
}
async fn start_loopback_backend(app: Router) -> String {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind mock composio 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,
std::collections::HashMap::new(),
true,
)
.expect("store app session token");
}
fn ok(data: Value) -> Response {
Json(json!({ "success": true, "data": data })).into_response()
}