Files
openhuman/tests/mcp_setup_e2e.rs
T

94 lines
3.7 KiB
Rust

//! End-to-end test for the MCP setup-agent flow.
//!
//! Exercises the ref machinery + install_and_connect path without going
//! through a real upstream registry — the test inserts an
//! `InstalledServer` row directly to stand in for what
//! `install_and_connect` would have synthesised from
//! `registry::registry_get`. The transport itself is the same
//! `test-mcp-stub` binary used by `mcp_registry_e2e.rs`.
use std::collections::HashMap;
use std::time::Duration;
use openhuman_core::openhuman::config::Config;
use openhuman_core::openhuman::mcp_registry::setup::{self, SecretRef};
#[tokio::test]
async fn request_secret_blocks_until_submit_then_resolves() {
// Caller mints + awaits in one task, fulfiller submits in another.
// The exact API the setup_ops::request_secret handler uses.
let (r, rx) = setup::mint_request("API_KEY").await;
let r_for_submit = r.clone();
let submit_task = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(20)).await;
let submitted = setup::fulfill(&r_for_submit, "shh-secret".to_string()).await;
assert!(submitted, "fulfill returns true on first submit");
});
// The await side: must not return before fulfill is called.
setup::await_fulfillment(&r, rx)
.await
.expect("await_fulfillment completes once submit lands");
submit_task.await.unwrap();
// Resolve maps {KEY: ref} -> {KEY: value} without exposing value to
// anywhere it shouldn't be.
let mut refs = HashMap::new();
refs.insert("API_KEY".to_string(), r.clone());
let resolved = setup::resolve_refs(&refs).await.expect("resolves");
assert_eq!(
resolved,
vec![("API_KEY".to_string(), "shh-secret".to_string())]
);
// The setup-agent contract: once install_and_connect persists the
// values, the refs are gone.
let _ = setup::consume_refs(&refs).await.expect("consumes");
assert!(
setup::resolve_refs(&refs).await.is_err(),
"post-consume resolve fails"
);
}
#[tokio::test]
async fn test_connection_against_stub_returns_tools() {
use openhuman_core::openhuman::mcp_client::McpStdioClient;
// Mirror what setup_ops::test_connection does end-to-end, minus the
// registry::registry_get step (we don't want to hit a real upstream
// from CI). The point of this test is the spawn + initialize +
// list_tools + teardown lifecycle the setup agent relies on.
let (r, _rx) = setup::mint_request("ECHO_TOKEN").await;
assert!(setup::fulfill(&r, "ignored-by-stub".to_string()).await);
let mut refs = HashMap::new();
refs.insert("ECHO_TOKEN".to_string(), r);
let env = setup::resolve_refs(&refs).await.expect("resolves");
assert_eq!(env.len(), 1);
let stub_path = env!("CARGO_BIN_EXE_test-mcp-stub");
let cfg = Config::default();
let identity = cfg.mcp_client.client_identity.clone();
let client = McpStdioClient::new(stub_path.to_string(), Vec::new(), env, None, identity);
client.initialize().await.expect("stub initialises");
let tools = client.list_tools().await.expect("stub lists tools");
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "echo");
client.close_session().await.expect("stub closes");
}
#[tokio::test]
async fn invalid_ref_id_rejected_by_submit_secret() {
// The submit_secret handler parses the ref id via SecretRef::parse.
// Validate the parser independently here so an upstream regression
// doesn't silently re-admit unsafe inputs.
assert!(SecretRef::parse("secret://abc123").is_some());
assert!(SecretRef::parse("abc123").is_some());
assert!(SecretRef::parse("secret://not-hex!!").is_none());
assert!(SecretRef::parse("").is_none());
assert!(SecretRef::parse("../../etc/passwd").is_none());
}