//! Round26 raw/E2E coverage for tools + Composio cold network paths. //! //! All outbound HTTP is routed to loopback mocks. The tests drive the public //! tool surfaces so coverage lands on the same paths used by agent calls. 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, PermissionLevel, PolymarketTool, Tool}; #[derive(Clone, Debug)] struct RecordedRequest { method: Method, path: String, query: String, body: Value, } #[derive(Clone, Default)] struct MockState { requests: Arc>>, } #[tokio::test] async fn round26_polymarket_tool_covers_read_schema_and_error_edges() { let state = MockState::default(); let base = start_loopback( Router::new() .fallback(any(polymarket_handler)) .with_state(state.clone()), ) .await; let tool = polymarket_tool(&base); assert_eq!(tool.name(), "polymarket"); assert_eq!(tool.permission_level(), PermissionLevel::ReadOnly); assert!(tool.is_concurrency_safe(&json!({ "action": "list_markets" }))); assert!(!tool.is_concurrency_safe(&json!({ "action": "place_order" }))); assert!(!tool.is_concurrency_safe(&json!({ "action": "cancel_order" }))); assert!(tool .parameters_schema() .pointer("/properties/action/enum") .and_then(Value::as_array) .expect("action enum") .iter() .any(|item| item == "get_open_orders")); let market_by_id = tool .execute(json!({ "action": "get_market", "market_id": "m-round26" })) .await .expect("market by id"); assert!(!market_by_id.is_error, "{}", market_by_id.output()); assert!(market_by_id.output().contains("round26 market")); let events = tool .execute(json!({ "action": "list_events", "limit": 3, "offset": 2, "cursor": "next-cursor", "active": true, "closed": false, "tag": "politics" })) .await .expect("list events"); assert!(!events.is_error, "{}", events.output()); assert!(events.output().contains("event-round26")); let event_by_id = tool .execute(json!({ "action": "list_events", "event_id": "evt-round26" })) .await .expect("event by id"); assert!(!event_by_id.is_error, "{}", event_by_id.output()); assert!(event_by_id.output().contains("event exact")); let orderbook = tool .execute(json!({ "action": "get_orderbook", "token_id": "token-round26" })) .await .expect("orderbook"); assert!(!orderbook.is_error, "{}", orderbook.output()); assert!(orderbook.output().contains("0.44")); let price = tool .execute(json!({ "action": "get_price", "token_id": "token-round26", "side": "SELL" })) .await .expect("price"); assert!(!price.is_error, "{}", price.output()); assert!(price.output().contains("0.56")); let positions = tool .execute(json!({ "action": "get_positions", "user": "0x0000000000000000000000000000000000000001" })) .await .expect("positions"); assert!(!positions.is_error, "{}", positions.output()); assert!(positions.output().contains("position-round26")); let open_orders = tool .execute(json!({ "action": "get_open_orders", "user": "0x0000000000000000000000000000000000000001" })) .await .expect("open orders"); assert!(!open_orders.is_error, "{}", open_orders.output()); assert!(open_orders.output().contains("order-round26")); let client_error = tool .execute(json!({ "action": "get_orderbook", "token_id": "client-error" })) .await .expect("client error is a tool result"); assert!(client_error.is_error); assert!(client_error .output() .contains("Polymarket client error 400")); assert!(client_error.output().contains("bad token")); let bad_json = tool .execute(json!({ "action": "get_market", "market_id": "bad-json" })) .await .expect("bad json is a tool result"); assert!(bad_json.is_error); assert!(bad_json .output() .contains("Failed to deserialize Polymarket response")); let server_error = tool .execute(json!({ "action": "list_events", "tag": "server-error" })) .await .expect("server error is a tool result"); assert!(server_error.is_error); assert!(server_error .output() .contains("Polymarket transient server error 500")); let bad_address = tool .execute(json!({ "action": "get_positions", "user": "not-an-address" })) .await .expect("bad address is a tool result"); assert!(bad_address.is_error); assert!(bad_address.output().contains("Invalid EVM address")); let blocked_write = tool .execute(json!({ "action": "place_order", "side": "BUY", "token_id": "token-round26", "price": 0.5, "size": 1.0, "approved": false, "user": "0x0000000000000000000000000000000000000001" })) .await .expect("unapproved write is a tool result"); assert!(blocked_write.is_error); assert!(blocked_write.output().contains("explicit user approval")); let requests = state.requests.lock().expect("requests").clone(); assert!(requests.iter().any(|request| { request.method == Method::GET && request.path == "/events" && request.query.contains("limit=3") && request.query.contains("closed=false") && request.query.contains("tag=politics") })); assert!(requests.iter().any(|request| { request.method == Method::GET && request.path == "/book" && request.query.contains("token_id=token-round26") })); assert!(requests.iter().any(|request| { request.method == Method::GET && request.path == "/orders" && request .query .contains("user=0x0000000000000000000000000000000000000001") })); } #[tokio::test] async fn round26_composio_direct_tool_covers_connect_execute_and_error_fallbacks() { 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_round26 ", Some(" entity-round26 "), Arc::new(SecurityPolicy { autonomy: AutonomyLevel::Supervised, ..SecurityPolicy::default() }), format!("{base}/api/v2"), format!("{base}/api/v3"), ) .expect("loopback composio"); assert_eq!(tool.name(), "composio"); assert!(tool.external_effect()); assert!(!tool.external_effect_with_args(&json!({ "action": "list" }))); assert!(!tool.external_effect_with_args(&json!({ "action": "connect" }))); assert!(tool.external_effect_with_args(&json!({ "action": "execute" }))); let missing_action = tool .execute(json!({})) .await .expect_err("missing action is an anyhow validation error"); assert!(missing_action.to_string().contains("Missing 'action'")); let unknown = tool .execute(json!({ "action": "inspect" })) .await .expect("unknown action returns tool result"); assert!(unknown.is_error); assert!(unknown.output().contains("Unknown action")); let listed = tool .execute(json!({ "action": "list", "app": "fallback-list" })) .await .expect("list with v2 fallback"); assert!(!listed.is_error, "{}", listed.output()); assert!(listed.output().contains("LEGACY_ROUND26_ACTION")); let executed = tool .execute(json!({ "action": "execute", "tool_slug": "ROUND26_ACTION", "params": { "value": 42 }, "connected_account_id": " account-round26 " })) .await .expect("execute v3 success"); assert!(!executed.is_error, "{}", executed.output()); assert!(executed.output().contains("v3-execute-round26")); let v2_execute = tool .execute(json!({ "action": "execute", "action_name": "ROUND26_V2_ONLY", "params": { "value": "fallback" } })) .await .expect("execute v2 fallback"); assert!(!v2_execute.is_error, "{}", v2_execute.output()); assert!(v2_execute.output().contains("v2-execute-round26")); let direct_auth_config = tool .execute(json!({ "action": "connect", "auth_config_id": "auth-direct-round26" })) .await .expect("connect via auth_config_id"); assert!( !direct_auth_config.is_error, "{}", direct_auth_config.output() ); assert!(direct_auth_config .output() .contains("https://connect.example.test/direct-round26")); let v2_connect = tool .execute(json!({ "action": "connect", "app": "fallback-connect" })) .await .expect("connect v2 fallback"); assert!(!v2_connect.is_error, "{}", v2_connect.output()); assert!(v2_connect .output() .contains("https://connect.example.test/v2-round26")); let missing_auth = tool .execute(json!({ "action": "connect", "app": "missing-auth" })) .await .expect("missing auth config returns tool result"); assert!(missing_auth.is_error); assert!(missing_auth.output().contains("No auth config found")); let failed_no_app_fallback = tool .execute(json!({ "action": "connect", "auth_config_id": "auth-link-fails-round26" })) .await .expect("v3 failure without app returns tool result"); assert!(failed_no_app_fallback.is_error); assert!(failed_no_app_fallback .output() .contains("v2 fallback requires 'app'")); 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/ROUND26_ACTION" }) .expect("v3 execute request"); assert_eq!(v3_execute.body["user_id"], "entity-round26"); assert_eq!(v3_execute.body["connected_account_id"], "account-round26"); assert!(requests.iter().any(|request| { request.method == Method::GET && request.path == "/api/v3/tools" && request.query.contains("toolkits=fallback-list") })); assert!(requests.iter().any(|request| { request.method == Method::POST && request.path == "/api/v2/connectedAccounts" && request.body["integrationId"] == "fallback-connect" && request.body["entityId"] == "entity-round26" })); } 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()) } 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: "round26-key".to_string(), secret: "cm91bmQyNi1zZWNyZXQ=".to_string(), passphrase: "round26-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 (method, path, query, body_json) = record_request_parts(request).await; 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/m-round26") => Json(json!({ "id": "m-round26", "question": "round26 market" })) .into_response(), (Method::GET, "/markets/bad-json") => (StatusCode::OK, "this is not json").into_response(), (Method::GET, "/events/evt-round26") => Json(json!({ "id": "evt-round26", "title": "event exact" })) .into_response(), (Method::GET, "/events") if query.contains("tag=server-error") => ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": "server is tired" })), ) .into_response(), (Method::GET, "/events") => Json(json!([ { "id": "event-round26", "title": "round26 event list" } ])) .into_response(), (Method::GET, "/book") if query.contains("token_id=client-error") => ( StatusCode::BAD_REQUEST, Json(json!({ "error": "bad token supplied for round26" })), ) .into_response(), (Method::GET, "/book") => Json(json!({ "bids": [{ "price": "0.44", "size": "12" }], "asks": [{ "price": "0.56", "size": "9" }] })) .into_response(), (Method::GET, "/price") => Json(json!({ "price": "0.56", "side": "sell" })).into_response(), (Method::GET, "/data/positions") => Json(json!([ { "asset": "position-round26", "size": "1.5" } ])) .into_response(), (Method::GET, "/orders") => Json(json!([ { "id": "order-round26", "status": "open" } ])) .into_response(), _ => (StatusCode::NOT_FOUND, Json(json!({ "error": "not found" }))).into_response(), } } async fn composio_handler(State(state): State, request: Request) -> Response { let (method, path, query, body_json) = record_request_parts(request).await; 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, "/api/v3/tools") if query.contains("toolkits=fallback-list") => ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": { "message": "v3 list unavailable" } })), ) .into_response(), (Method::GET, "/api/v2/actions") => Json(json!({ "items": [ { "name": "LEGACY_ROUND26_ACTION", "appName": "legacy", "description": "legacy list fallback", "enabled": true } ] })) .into_response(), (Method::POST, "/api/v3/tools/execute/ROUND26_ACTION") => Json(json!({ "successful": true, "data": { "message": "v3-execute-round26" } })) .into_response(), (Method::POST, "/api/v3/tools/execute/ROUND26_V2_ONLY") => ( StatusCode::BAD_GATEWAY, Json(json!({ "message": "v3 execute unavailable" })), ) .into_response(), (Method::POST, "/api/v2/actions/ROUND26_V2_ONLY/execute") => Json(json!({ "successful": true, "data": { "message": "v2-execute-round26" } })) .into_response(), (Method::POST, "/api/v3/connected_accounts/link") if path.as_str() == "/api/v3/connected_accounts/link" => { match state.requests.lock().expect("requests").last() { Some(record) if record.body["auth_config_id"] == "auth-direct-round26" => { Json(json!({ "redirectUrl": "https://connect.example.test/direct-round26" })) .into_response() } Some(record) if record.body["auth_config_id"] == "auth-link-fails-round26" => ( StatusCode::BAD_REQUEST, Json(json!({ "message": "link failed for round26" })), ) .into_response(), Some(record) if record.body["auth_config_id"] == "auth-fallback-round26" => ( StatusCode::BAD_GATEWAY, Json(json!({ "message": "force v2 connect fallback" })), ) .into_response(), _ => ( StatusCode::BAD_REQUEST, Json(json!({ "message": "unexpected auth config" })), ) .into_response(), } } (Method::GET, "/api/v3/auth_configs") if query.contains("toolkit_slug=missing-auth") => { Json(json!({ "items": [] })).into_response() } (Method::GET, "/api/v3/auth_configs") if query.contains("toolkit_slug=fallback-connect") => { Json(json!({ "items": [ { "id": "auth-fallback-round26", "status": "enabled" } ] })) .into_response() } (Method::POST, "/api/v2/connectedAccounts") => { match state.requests.lock().expect("requests").last() { Some(record) if record.body["integrationId"] == "missing-auth" => ( StatusCode::BAD_REQUEST, Json(json!({ "message": "v2 missing auth config" })), ) .into_response(), _ => Json(json!({ "redirectUrl": "https://connect.example.test/v2-round26" })) .into_response(), } } _ => ( StatusCode::NOT_FOUND, Json(json!({ "message": "not found" })), ) .into_response(), } } async fn record_request_parts(request: Request) -> (Method, String, String, Value) { 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) }; (method, path, query, body_json) }