Files
openhuman/tests/tools_network_channels_raw_coverage_e2e.rs
T

618 lines
21 KiB
Rust

//! Round 15 raw integration coverage for network tools plus web-channel paths.
//!
//! Everything here stays local-only: loopback HTTP mocks, temp git/cron
//! workspaces, and validation/error branches that do not touch the desktop.
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use axum::body::Bytes;
use axum::extract::State;
use axum::http::{HeaderMap, Method, StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use axum::routing::any;
use axum::Router;
use serde_json::json;
use tempfile::{tempdir, TempDir};
use tokio::time::timeout;
use openhuman_core::core::socketio::WebChannelEvent;
use openhuman_core::openhuman::channels::providers::web::{
all_web_channel_controller_schemas, all_web_channel_registered_controllers, cancel_chat,
channel_web_cancel, publish_web_channel_event, schemas as web_channel_schema, start_chat,
subscribe_web_channel_events, ChatRequestMetadata,
};
use openhuman_core::openhuman::config::{
AutonomyConfig, Config, PolymarketClobCredentials, PolymarketConfig,
};
use openhuman_core::openhuman::security::{AutonomyLevel, SecurityPolicy};
use openhuman_core::openhuman::tools::{
ComposioTool, GitOperationsTool, MouseTool, PolymarketTool, ScheduleTool, Tool, ToolCallOptions,
};
#[derive(Clone, Debug)]
struct MockRequest {
method: Method,
path: String,
query: Option<String>,
body: String,
poly_api_key: Option<String>,
}
#[derive(Clone, Default)]
struct MockState {
requests: Arc<Mutex<Vec<MockRequest>>>,
}
async fn start_polymarket_mock() -> (String, MockState) {
let state = MockState::default();
let app = Router::new()
.route("/", any(polymarket_handler))
.fallback(any(polymarket_handler))
.with_state(state.clone());
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind mock");
let addr = listener.local_addr().expect("mock addr");
tokio::spawn(async move {
axum::serve(listener, app).await.expect("mock serve");
});
(format!("http://127.0.0.1:{}", addr.port()), state)
}
async fn polymarket_handler(
State(state): State<MockState>,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Bytes,
) -> Response {
let path = uri.path().to_string();
let query = uri.query().map(str::to_string);
let body = String::from_utf8_lossy(&body).to_string();
let poly_api_key = headers
.get("poly_api_key")
.and_then(|value| value.to_str().ok())
.map(str::to_string);
state
.requests
.lock()
.expect("requests lock")
.push(MockRequest {
method: method.clone(),
path: path.clone(),
query,
body,
poly_api_key,
});
let payload = match (method, path.as_str()) {
(Method::GET, "/markets") => json!([
{
"id": "m-1",
"slug": "will-it-rain",
"question": "Will it rain tomorrow?"
}
]),
(Method::GET, "/markets/m-1") => json!({
"id": "m-1",
"slug": "will-it-rain",
"outcomes": ["Yes", "No"]
}),
(Method::GET, "/events") => json!({
"data": [
{ "id": "e-1", "slug": "weather" }
],
"next_cursor": "cursor-2"
}),
(Method::GET, "/events/e-1") => json!({
"id": "e-1",
"title": "Weather"
}),
(Method::GET, "/book") => json!({
"bids": [["0.42", "10"]],
"asks": [["0.43", "12"]]
}),
(Method::GET, "/price") => json!({ "price": "0.42" }),
(Method::GET, "/data/positions") => json!({
"positions": [
{ "asset": "token-yes", "size": "3.5" }
]
}),
(Method::GET, "/data/balance") => json!({ "balance": "125.50" }),
(Method::GET, "/orders") => json!({ "orders": [] }),
(Method::POST, "/") => json!({
"jsonrpc": "2.0",
"id": 1,
"result": "0x00000000000000000000000000000000000000000000000000000000000f4240"
}),
_ => {
return (
StatusCode::NOT_FOUND,
axum::Json(json!({ "error": format!("unhandled {path}") })),
)
.into_response();
}
};
axum::Json(payload).into_response()
}
fn full_security(workspace: &std::path::Path) -> Arc<SecurityPolicy> {
Arc::new(SecurityPolicy::from_config(
&AutonomyConfig {
level: AutonomyLevel::Full,
max_actions_per_hour: 10_000,
..Default::default()
},
workspace,
workspace,
))
}
fn readonly_security(workspace: &std::path::Path) -> Arc<SecurityPolicy> {
Arc::new(SecurityPolicy::from_config(
&AutonomyConfig {
level: AutonomyLevel::ReadOnly,
max_actions_per_hour: 10_000,
..Default::default()
},
workspace,
workspace,
))
}
fn temp_config() -> (TempDir, Config) {
let tmp = tempdir().expect("tempdir");
let mut config = Config::default();
config.workspace_dir = tmp.path().join("workspace");
config.config_path = tmp.path().join("config.toml");
std::fs::create_dir_all(&config.workspace_dir).expect("workspace");
(tmp, config)
}
fn text(result: &openhuman_core::openhuman::tools::ToolResult) -> String {
result.output()
}
fn assert_contains(haystack: &str, needle: &str) {
assert!(
haystack.contains(needle),
"expected {haystack:?} to contain {needle:?}"
);
}
#[tokio::test]
async fn polymarket_loopback_exercises_gamma_clob_and_polygon_read_paths() {
let (_tmp, config) = temp_config();
let (base, state) = start_polymarket_mock().await;
let user = "0x1111111111111111111111111111111111111111";
let tool = PolymarketTool::new(
&PolymarketConfig {
enabled: true,
gamma_base_url: base.clone(),
clob_base_url: base.clone(),
polygon_rpc_url: base,
timeout_secs: 2,
eoa_address: Some(user.to_string()),
usdc_contract: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174".to_string(),
clob_exchange_contract: "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E".to_string(),
derived_clob_credentials: Some(PolymarketClobCredentials {
api_key: "local-key".to_string(),
secret: "bG9jYWwtc2VjcmV0".to_string(),
passphrase: "local-pass".to_string(),
}),
},
full_security(&config.workspace_dir),
);
let cases = [
json!({"action": "list_markets", "slug": "will-it-rain", "limit": 5, "active": true}),
json!({"action": "get_market", "market_id": "m-1"}),
json!({"action": "get_market", "slug": "will-it-rain"}),
json!({"action": "list_events", "limit": 2, "closed": false, "tag": "weather"}),
json!({"action": "list_events", "event_id": "e-1"}),
json!({"action": "get_orderbook", "token_id": "token-yes"}),
json!({"action": "get_price", "token_id": "token-yes", "side": "BUY"}),
json!({"action": "get_positions", "user": user}),
json!({"action": "get_balance", "user": user, "token": "usdc"}),
json!({"action": "get_open_orders", "user": user}),
json!({"action": "get_usdc_allowance", "user": user}),
];
for args in cases {
let result = tool.execute(args).await.expect("execute");
assert!(
!result.is_error,
"unexpected polymarket error: {}",
text(&result)
);
}
let missing = tool
.execute(json!({"action": "get_market"}))
.await
.expect("missing lookup");
assert!(missing.is_error);
assert_contains(&text(&missing), "get_market requires");
let invalid_side = tool
.execute(json!({"action": "get_price", "token_id": "token-yes", "side": "maybe"}))
.await
.expect("invalid side");
assert!(invalid_side.is_error);
assert_contains(&text(&invalid_side), "Invalid 'side'");
let requests = state.requests.lock().expect("requests").clone();
assert!(
requests.iter().any(|request| request.path == "/markets"
&& request
.query
.as_deref()
.unwrap_or("")
.contains("slug=will-it-rain")),
"list_markets query was not captured: {requests:?}"
);
assert!(
requests
.iter()
.any(|request| request.path == "/data/positions"
&& request.poly_api_key.as_deref() == Some("local-key")),
"signed CLOB read did not include credential headers: {requests:?}"
);
assert!(
requests.iter().any(|request| request.method == Method::POST
&& request.path == "/"
&& request.body.contains("eth_call")),
"Polygon allowance RPC was not captured: {requests:?}"
);
}
#[tokio::test]
async fn git_operations_cover_read_write_markdown_and_safety_rejections() {
let tmp = tempdir().expect("repo tempdir");
let repo = tmp.path();
run_git(repo, &["init"]);
run_git(repo, &["config", "user.email", "round15@example.test"]);
run_git(repo, &["config", "user.name", "Round Fifteen"]);
std::fs::write(repo.join("tracked.txt"), "first\n").expect("write tracked");
run_git(repo, &["add", "tracked.txt"]);
run_git(repo, &["commit", "-m", "initial"]);
std::fs::write(repo.join("tracked.txt"), "first\nsecond\n").expect("modify tracked");
std::fs::write(repo.join("untracked.txt"), "new\n").expect("write untracked");
let tool = GitOperationsTool::new(full_security(repo), repo.to_path_buf());
let status = tool
.execute(json!({"operation": "status"}))
.await
.expect("status");
assert!(!status.is_error);
assert_contains(&text(&status), "untracked.txt");
assert_contains(
status.markdown_formatted.as_deref().unwrap_or(""),
"untracked",
);
let diff = tool
.execute(json!({"operation": "diff", "files": "tracked.txt"}))
.await
.expect("diff");
assert!(!diff.is_error);
assert_contains(&text(&diff), "second");
let blocked_diff = tool
.execute(json!({"operation": "diff", "files": "tracked.txt;rm"}))
.await
.expect_err("blocked diff should hard fail in sanitizer");
assert_contains(&blocked_diff.to_string(), "Blocked potentially");
let add = tool
.execute(json!({"operation": "add", "paths": "tracked.txt"}))
.await
.expect("add");
assert!(!add.is_error, "add failed: {}", text(&add));
let commit = tool
.execute(json!({"operation": "commit", "message": "\n round15 commit \n"}))
.await
.expect("commit");
assert!(!commit.is_error, "commit failed: {}", text(&commit));
let log = tool
.execute(json!({"operation": "log", "limit": 2}))
.await
.expect("log");
assert!(!log.is_error);
assert_contains(&text(&log), "round15 commit");
let branch = tool
.execute_with_options(
json!({"operation": "branch"}),
ToolCallOptions {
prefer_markdown: true,
},
)
.await
.expect("branch");
assert!(!branch.is_error);
assert_contains(
branch.markdown_formatted.as_deref().unwrap_or(""),
"current",
);
let bad_checkout = tool
.execute(json!({"operation": "checkout", "branch": "main~1"}))
.await
.expect_err("invalid branch should be a hard validation error");
assert_contains(&bad_checkout.to_string(), "invalid characters");
let readonly = GitOperationsTool::new(readonly_security(repo), repo.to_path_buf());
let blocked = readonly
.execute(json!({"operation": "add", "paths": "untracked.txt"}))
.await
.expect("readonly add");
assert!(blocked.is_error);
assert_contains(&text(&blocked), "[policy-blocked]");
}
#[tokio::test]
async fn schedule_tool_covers_cron_once_agent_prompt_and_policy_edges() {
let (_tmp, config) = temp_config();
let tool = ScheduleTool::new(full_security(&config.workspace_dir), config.clone());
let empty = tool.execute(json!({"action": "list"})).await.expect("list");
assert!(!empty.is_error);
assert_contains(&text(&empty), "No scheduled jobs");
let natural_language = tool
.execute(json!({
"action": "create",
"delay": "30m",
"command": "remind me to stretch",
"name": "stretch"
}))
.await
.expect("agent prompt");
assert!(!natural_language.is_error, "{}", text(&natural_language));
assert_contains(&text(&natural_language), "Created agent job");
let recurring = tool
.execute(json!({
"action": "add",
"expression": "*/15 * * * *",
"command": "echo round15"
}))
.await
.expect("recurring");
assert!(!recurring.is_error, "{}", text(&recurring));
let recurring_id = text(&recurring)
.split_whitespace()
.nth(3)
.expect("job id")
.to_string();
let once = tool
.execute(json!({
"action": "once",
"run_at": "2035-01-01T00:00:00Z",
"command": "echo future"
}))
.await
.expect("once");
assert!(!once.is_error, "{}", text(&once));
let list = tool.execute(json!({"action": "list"})).await.expect("list");
assert!(!list.is_error);
assert_contains(&text(&list), "echo round15");
assert_contains(&text(&list), "[one-shot]");
let get = tool
.execute(json!({"action": "get", "id": recurring_id}))
.await
.expect("get");
assert!(!get.is_error);
assert_contains(&text(&get), "echo round15");
let id = text(&get)
.lines()
.find(|line| line.contains("\"id\""))
.and_then(|line| line.split('"').nth(3))
.expect("json id")
.to_string();
for action in ["pause", "resume", "cancel"] {
let result = tool
.execute(json!({"action": action, "id": id}))
.await
.unwrap_or_else(|err| panic!("{action}: {err}"));
assert!(!result.is_error, "{action} failed: {}", text(&result));
}
let missing_command = tool
.execute(json!({"action": "create", "expression": "* * * * *"}))
.await
.expect("missing command");
assert!(missing_command.is_error);
assert_contains(&text(&missing_command), "Provide 'command'");
let invalid_once = tool
.execute(json!({
"action": "once",
"delay": "5m",
"run_at": "2035-01-01T00:00:00Z",
"command": "echo invalid"
}))
.await
.expect("invalid once");
assert!(invalid_once.is_error);
assert_contains(&text(&invalid_once), "not both");
let readonly = ScheduleTool::new(readonly_security(&config.workspace_dir), config);
let blocked = readonly
.execute(json!({
"action": "create",
"expression": "* * * * *",
"command": "echo blocked"
}))
.await
.expect("readonly create");
assert!(blocked.is_error);
assert_contains(&text(&blocked), "read-only");
}
#[tokio::test]
async fn composio_direct_and_mouse_tools_cover_validation_policy_and_schema_paths() {
let (_tmp, config) = temp_config();
let full = full_security(&config.workspace_dir);
let readonly = readonly_security(&config.workspace_dir);
let composio = ComposioTool::new(" local-test-key ", Some(" entity-1 "), full.clone());
assert_eq!(composio.name(), "composio");
assert!(composio.external_effect());
assert!(!composio.external_effect_with_args(&json!({"action": "list"})));
assert!(!composio.external_effect_with_args(&json!({"action": "connect"})));
assert!(composio.external_effect_with_args(&json!({"action": "execute"})));
assert_contains(
&composio.parameters_schema().to_string(),
"connected_account_id",
);
let unknown = composio
.execute(json!({"action": "wat"}))
.await
.expect("unknown composio");
assert!(unknown.is_error);
assert_contains(&text(&unknown), "Unknown action");
let missing_connect = composio
.execute(json!({"action": "connect"}))
.await
.expect_err("connect without app/auth_config_id should hard fail before network");
assert_contains(&missing_connect.to_string(), "Missing 'app'");
let readonly_composio = ComposioTool::new("local-test-key", None, readonly.clone());
let blocked_execute = readonly_composio
.execute(json!({
"action": "execute",
"tool_slug": "GMAIL_SEND_EMAIL",
"params": { "to": "nobody@example.test" }
}))
.await
.expect("readonly execute");
assert!(blocked_execute.is_error);
assert_contains(&text(&blocked_execute), "policy");
let mouse = MouseTool::new(readonly);
assert_eq!(mouse.name(), "mouse");
assert_contains(&mouse.parameters_schema().to_string(), "double_click");
let blocked_mouse = mouse
.execute(json!({"action": "move", "x": 1, "y": 1, "human_like": false}))
.await
.expect("readonly mouse");
assert!(blocked_mouse.is_error);
assert_contains(&text(&blocked_mouse), "read-only");
let mouse = MouseTool::new(full);
let missing_xy = mouse
.execute(json!({"action": "click", "button": "left"}))
.await
.expect_err("missing xy should hard fail before enigo");
assert_contains(&missing_xy.to_string(), "Missing required 'x'");
let bad_coord = mouse
.execute(json!({"action": "move", "x": -1, "y": 0, "human_like": false}))
.await
.expect_err("bad coord should hard fail before enigo");
assert_contains(&bad_coord.to_string(), "out of range");
let bad_button = mouse
.execute(json!({"action": "click", "x": 1, "y": 1, "button": "side"}))
.await
.expect_err("bad button should hard fail before enigo");
assert_contains(&bad_button.to_string(), "Invalid mouse button");
let zero_scroll = mouse
.execute(json!({"action": "scroll", "scroll_x": 0, "scroll_y": 0}))
.await
.expect("zero scroll");
assert!(zero_scroll.is_error);
assert_contains(&text(&zero_scroll), "non-zero");
let unknown_mouse = mouse
.execute(json!({"action": "teleport"}))
.await
.expect("unknown mouse");
assert!(unknown_mouse.is_error);
assert_contains(&text(&unknown_mouse), "Unknown mouse action");
}
#[tokio::test]
async fn web_channel_public_paths_cover_validation_cancel_schema_and_event_bus() {
assert_eq!(all_web_channel_controller_schemas().len(), 4);
assert_eq!(all_web_channel_registered_controllers().len(), 4);
assert_eq!(web_channel_schema("chat").function, "web_chat");
assert_eq!(web_channel_schema("cancel").function, "web_cancel");
assert_eq!(web_channel_schema("missing").function, "unknown");
let missing_client = start_chat(
" ",
"thread",
"hello",
None,
None,
None,
None,
None,
ChatRequestMetadata::default(),
)
.await
.expect_err("blank client");
assert_contains(&missing_client, "client_id is required");
let missing_thread = cancel_chat("client", " ").await.expect_err("blank thread");
assert_contains(&missing_thread, "thread_id is required");
let none = cancel_chat("client", "round15-thread")
.await
.expect("no in-flight cancel");
assert_eq!(none, None);
let outcome = channel_web_cancel(" client ", " round15-thread ")
.await
.expect("cancel rpc outcome");
assert_eq!(outcome.value["cancelled"], false);
assert_eq!(outcome.value["client_id"], "client");
assert_eq!(outcome.value["thread_id"], "round15-thread");
let mut rx = subscribe_web_channel_events();
publish_web_channel_event(WebChannelEvent {
event: "round15_probe".to_string(),
client_id: "client".to_string(),
thread_id: "thread".to_string(),
request_id: "request".to_string(),
message: Some("payload".to_string()),
..Default::default()
});
let event = timeout(Duration::from_secs(1), rx.recv())
.await
.expect("event timeout")
.expect("event");
assert_eq!(event.event, "round15_probe");
assert_eq!(event.message.as_deref(), Some("payload"));
}
fn run_git(repo: &std::path::Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(repo)
.output()
.expect("spawn git");
assert!(
output.status.success(),
"git {args:?} failed: stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}