use super::*;
use std::fs;
use std::path::Path;
use harn_mcp_compat::fake_client::{stable_meta, stable_request};
use harn_mcp_compat::fixtures::{load_named, WireFixtureKind};
use serde_json::json;
use tempfile::TempDir;
use crate::tests::common::harn_state_lock::lock_harn_state_async;
fn write_file(dir: &Path, relative: &str, contents: &str) {
let path = dir.join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, contents).unwrap();
}
fn write_minimal_fixture(temp: &TempDir) {
write_file(
temp.path(),
"harn.toml",
r#"
[package]
name = "mcp-compat-fixture"
[exports]
handlers = "lib.harn"
"#,
);
write_file(
temp.path(),
"lib.harn",
r#"
pub fn ping() -> string {
return "pong"
}
"#,
);
}
fn fixture_args(temp: &TempDir) -> McpServeArgs {
let state_dir = temp.path().join("state");
fs::create_dir_all(&state_dir).unwrap();
McpServeArgs {
local: OrchestratorLocalArgs {
config: temp.path().join("harn.toml"),
state_dir,
},
transport: McpServeTransport::Stdio,
bind: "127.0.0.1:0".parse().unwrap(),
path: "/mcp".to_string(),
}
}
async fn fresh_service() -> (McpOrchestratorService, TempDir) {
let temp = TempDir::new().unwrap();
write_minimal_fixture(&temp);
let args = fixture_args(&temp);
let service = McpOrchestratorService::new_local(args.local).unwrap();
(service, temp)
}
#[tokio::test(flavor = "current_thread")]
async fn orchestrator_stable_tools_list_carries_envelope_and_cache_hint() {
let _guard = lock_harn_state_async().await;
let (service, _temp) = fresh_service().await;
let mut session = ConnectionState::default();
let request = stable_request(1, "tools/list", json!({}), "harn-mcp-compat-client");
let response = service.handle_request(&mut session, request).await;
let result = response.get("result").expect("tools/list result");
assert_eq!(result["resultType"], json!("complete"));
assert!(
result.get("ttlMs").is_some(),
"stable tools/list must include cache hint"
);
assert_eq!(result["cacheScope"], json!("private"));
}
#[tokio::test(flavor = "current_thread")]
async fn orchestrator_stable_ping_carries_result_type_envelope() {
let _guard = lock_harn_state_async().await;
let (service, _temp) = fresh_service().await;
let mut session = ConnectionState::default();
let request = stable_request(1, "ping", json!({}), "harn-mcp-compat-client");
let response = service.handle_request(&mut session, request).await;
let result = response.get("result").expect("ping result");
assert_eq!(result["resultType"], json!("complete"));
}
#[tokio::test(flavor = "current_thread")]
async fn orchestrator_unsupported_meta_version_returns_minus_32022() {
let _guard = lock_harn_state_async().await;
let (service, _temp) = fresh_service().await;
let mut session = ConnectionState::default();
let request = json!({
"jsonrpc": "2.0",
"id": 7,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2099-01-01",
"io.modelcontextprotocol/clientInfo": {"name": "fuzz", "version": "1"},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
});
let response = service.handle_request(&mut session, request).await;
let error = response.get("error").expect("error response");
assert_eq!(error["code"], json!(-32022));
let supported = error["data"]["supported"]
.as_array()
.expect("supported array");
assert!(supported.iter().any(|v| v == &json!("2026-07-28")));
}
#[tokio::test(flavor = "current_thread")]
async fn orchestrator_server_discover_returns_stable_capabilities() {
let _guard = lock_harn_state_async().await;
let (service, _temp) = fresh_service().await;
let mut session = ConnectionState::default();
let request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {"_meta": stable_meta("harn-mcp-compat-client")}
});
let response = service.handle_request(&mut session, request).await;
let result = response.get("result").expect("discover result");
assert_eq!(result["resultType"], json!("complete"));
let supported = result["supportedVersions"]
.as_array()
.expect("supportedVersions array");
assert_eq!(supported.as_slice(), [json!("2026-07-28")]);
assert!(session.authenticated);
}
#[tokio::test(flavor = "current_thread")]
async fn orchestrator_replays_published_stable_success_fixture() {
let _guard = lock_harn_state_async().await;
let (service, _temp) = fresh_service().await;
let fixture = load_named("stable_success.json");
assert_eq!(fixture.kind, WireFixtureKind::Exchange);
let mut session = ConnectionState::default();
for pair in fixture.documents.chunks(2) {
if pair.len() != 2 {
continue;
}
let request = pair[0].clone();
let expected = &pair[1];
let method = request["method"].as_str().unwrap_or_default().to_string();
if method == "tools/call" {
continue;
}
let response = service.handle_request(&mut session, request).await;
let result = response.get("result").expect("result");
let expected_result = expected.get("result").expect("expected result");
assert_eq!(
result["resultType"], expected_result["resultType"],
"{method} resultType drift against published fixture"
);
}
}