//! Round24 raw coverage for broad tools/composio cold branches. //! //! All HTTP traffic stays on loopback mocks. These tests drive public tool //! APIs so coverage lands on the same paths an agent/tool call uses. 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::{json, Value}; use openhuman_core::openhuman::config::{PolymarketClobCredentials, PolymarketConfig}; use openhuman_core::openhuman::security::{AutonomyLevel, SecurityPolicy}; use openhuman_core::openhuman::tools::{ComposioTool, PolymarketTool, Tool}; #[derive(Clone, Debug)] struct RecordedRequest { method: Method, path: String, query: String, body: Value, } #[derive(Clone, Default)] struct MockState { requests: Arc>>, market_failures_left: Arc>, } #[tokio::test] async fn round24_composio_direct_covers_v3_v2_fallbacks_and_account_shapes() { let state = MockState::default(); let base = start_loopback( Router::new() .fallback(any(composio_handler)) .with_state(state.clone()), ) .await; let tool = ComposioTool::new_with_base_urls_for_loopback( " ck_round24 ", Some(" entity-round24 "), Arc::new(SecurityPolicy::default()), format!("{base}/api/v2"), format!("{base}/api/v3"), ) .expect("loopback composio tool"); let connected = tool .list_connected_accounts() .await .expect("connected accounts"); assert_eq!(connected.len(), 3, "blank id row should be dropped"); assert_eq!( connected .iter() .map(|account| account.toolkit_slug().unwrap()) .collect::>(), vec!["gmail", "github", "slack"] ); let execute = tool .execute(json!({ "action": "execute", "tool_slug": "GMAIL_SEND_EMAIL", "params": { "to": "a@example.test" }, "connected_account_id": "conn-secret" })) .await .expect("execute falls back to v2"); assert!(!execute.is_error, "{}", execute.output()); assert!(execute.output().contains("v2-fallback-ok")); let connect = tool .execute(json!({ "action": "connect", "app": "gmail" })) .await .expect("connect resolves auth config"); assert!(!connect.is_error, "{}", connect.output()); assert!(connect .output() .contains("https://connect.example.test/round24")); let requests = state.requests.lock().expect("requests").clone(); let v3_execute = requests .iter() .find(|request| { request.method == Method::POST && request.path == "/api/v3/tools/execute/GMAIL_SEND_EMAIL" }) .expect("v3 execute request"); assert_eq!(v3_execute.body["connected_account_id"], "conn-secret"); assert_eq!(v3_execute.body["user_id"], "entity-round24"); let v2_execute = requests .iter() .find(|request| { request.method == Method::POST && request.path == "/api/v2/actions/GMAIL_SEND_EMAIL/execute" }) .expect("v2 execute fallback request"); assert_eq!(v2_execute.body["entityId"], "entity-round24"); assert!(requests.iter().any(|request| { request.method == Method::GET && request.path == "/api/v3/auth_configs" && request.query.contains("toolkit_slug=gmail") && request.query.contains("show_disabled=true") })); assert!(requests.iter().any(|request| { request.method == Method::POST && request.path == "/api/v3/connected_accounts/link" && request.body["auth_config_id"] == "auth-enabled" && request.body["user_id"] == "entity-round24" })); } #[tokio::test] async fn round24_polymarket_covers_retries_errors_and_signed_read_paths() { let state = MockState::default(); *state.market_failures_left.lock().expect("failure counter") = 1; let base = start_loopback( Router::new() .fallback(any(polymarket_handler)) .with_state(state.clone()), ) .await; let tool = polymarket_tool(&base); let markets = tool .execute(json!({ "action": "list_markets", "slug": "will-it-rain", "event_id": "evt-1", "limit": 2, "offset": 1, "cursor": "next", "active": true, "closed": false, "tag": "weather" })) .await .expect("market list after retry"); assert!(!markets.is_error, "{}", markets.output()); assert!(markets.output().contains("will-it-rain")); let missing_slug = tool .execute(json!({ "action": "get_market", "slug": "missing-market" })) .await .expect("missing slug result"); assert!(missing_slug.is_error); assert!(missing_slug .output() .contains("No Polymarket market found for slug")); let bad_side = tool .execute(json!({ "action": "get_price", "token_id": "token-1", "side": "hold" })) .await .expect("bad side result"); assert!(bad_side.is_error); assert!(bad_side.output().contains("Invalid 'side'")); let balance = tool .execute(json!({ "action": "get_balance", "user": "0x0000000000000000000000000000000000000001" })) .await .expect("signed balance read"); assert!(!balance.is_error, "{}", balance.output()); assert!(balance.output().contains("42.00")); let allowance = tool .execute(json!({ "action": "get_usdc_allowance", "user": "0x0000000000000000000000000000000000000001" })) .await .expect("allowance read"); assert!(!allowance.is_error, "{}", allowance.output()); let allowance_json: Value = serde_json::from_str(&allowance.output()).expect("allowance json"); assert_eq!(allowance_json["allowance"], "16"); let empty_orderbook_token = tool .execute(json!({ "action": "get_orderbook", "token_id": " " })) .await .expect("empty orderbook token result"); assert!(empty_orderbook_token.is_error); assert!(empty_orderbook_token .output() .contains("'token_id' cannot be empty")); let requests = state.requests.lock().expect("requests").clone(); let market_gets = requests .iter() .filter(|request| request.method == Method::GET && request.path == "/markets") .count(); assert!( market_gets >= 3, "429 retry plus slug lookup should hit /markets" ); assert!(requests.iter().any(|request| { request.method == Method::GET && request.path == "/data/balance" && request.query.contains("token=usdce") && request .query .contains("user=0x0000000000000000000000000000000000000001") })); assert!(requests.iter().any(|request| { request.method == Method::POST && request.path == "/" && request.body["method"] == "eth_call" })); } async fn start_loopback(app: Router) -> String { let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("bind loopback"); let addr = listener.local_addr().expect("local addr"); tokio::spawn(async move { axum::serve(listener, app).await.expect("loopback server"); }); format!("http://127.0.0.1:{}", addr.port()) } async fn composio_handler(State(state): State, request: Request) -> Response { let (parts, body) = request.into_parts(); let method = parts.method; let path = parts.uri.path().to_string(); let query = parts.uri.query().unwrap_or_default().to_string(); let body_bytes = to_bytes(body, 1024 * 1024).await.expect("body bytes"); let body_json = if body_bytes.is_empty() { Value::Null } else { serde_json::from_slice(&body_bytes).unwrap_or_else(|_| Value::Null) }; state .requests .lock() .expect("requests") .push(RecordedRequest { method: method.clone(), path: path.clone(), query, body: body_json, }); match (method, path.as_str()) { (Method::GET, "/api/v3/connected_accounts") => Json(json!({ "items": [ { "id": "conn-gmail", "status": "ACTIVE", "toolkit": { "slug": "gmail" }, "created_at": "2026-05-30T00:00:00Z" }, { "id": "", "status": "ACTIVE", "toolkit": "dropme" }, { "id": "conn-github", "status": "INITIATED", "toolkit": "github", "createdAt": "2026-05-30T00:00:00Z" }, { "id": "conn-slack", "status": "ACTIVE", "appName": "slack" } ] })) .into_response(), (Method::POST, "/api/v3/tools/execute/GMAIL_SEND_EMAIL") => ( StatusCode::BAD_REQUEST, Json(json!({ "error": { "message": "bad connected_account_id conn-secret for entity_id entity-round24" } })), ) .into_response(), (Method::POST, "/api/v2/actions/GMAIL_SEND_EMAIL/execute") => Json(json!({ "successful": true, "data": { "message": "v2-fallback-ok" } })) .into_response(), (Method::GET, "/api/v3/auth_configs") => Json(json!({ "items": [ { "id": "auth-disabled", "status": "disabled", "enabled": false }, { "id": "auth-enabled", "status": "enabled", "enabled": true } ] })) .into_response(), (Method::POST, "/api/v3/connected_accounts/link") => Json(json!({ "data": { "redirect_url": "https://connect.example.test/round24" } })) .into_response(), _ => (StatusCode::NOT_FOUND, Json(json!({ "message": "not found" }))).into_response(), } } fn polymarket_tool(base: &str) -> PolymarketTool { let config = PolymarketConfig { enabled: true, gamma_base_url: base.to_string(), clob_base_url: base.to_string(), polygon_rpc_url: base.to_string(), timeout_secs: 15, eoa_address: Some("0x0000000000000000000000000000000000000001".to_string()), usdc_contract: "0x0000000000000000000000000000000000000002".to_string(), clob_exchange_contract: "0x0000000000000000000000000000000000000003".to_string(), derived_clob_credentials: Some(PolymarketClobCredentials { api_key: "round24-key".to_string(), secret: "cm91bmQyNC1zZWNyZXQ=".to_string(), passphrase: "round24-passphrase".to_string(), }), ..PolymarketConfig::default() }; PolymarketTool::new( &config, Arc::new(SecurityPolicy { autonomy: AutonomyLevel::Supervised, ..SecurityPolicy::default() }), ) } async fn polymarket_handler(State(state): State, request: Request) -> Response { let (parts, body) = request.into_parts(); let method = parts.method; let path = parts.uri.path().to_string(); let query = parts.uri.query().unwrap_or_default().to_string(); let body_bytes = to_bytes(body, 1024 * 1024).await.expect("body bytes"); let body_json = if body_bytes.is_empty() { Value::Null } else { serde_json::from_slice(&body_bytes).unwrap_or_else(|_| Value::Null) }; state .requests .lock() .expect("requests") .push(RecordedRequest { method: method.clone(), path: path.clone(), query: query.clone(), body: body_json, }); match (method, path.as_str()) { (Method::GET, "/markets") if query.contains("slug=missing-market") => { Json(json!([])).into_response() } (Method::GET, "/markets") => { let mut failures = state .market_failures_left .lock() .expect("market failure counter"); if *failures > 0 { *failures -= 1; return ( StatusCode::TOO_MANY_REQUESTS, Json(json!({ "error": "try again" })), ) .into_response(); } Json(json!([ { "id": "m-1", "slug": "will-it-rain", "active": true } ])) .into_response() } (Method::GET, "/data/balance") => Json(json!({ "balance": "42.00", "token": "usdce" })) .into_response(), (Method::POST, "/") => Json(json!({ "jsonrpc": "2.0", "id": 1, "result": "0x10" })) .into_response(), _ => (StatusCode::NOT_FOUND, Json(json!({ "error": "not found" }))).into_response(), } }