harn-cli 0.10.53

CLI for the Harn programming language — run, test, REPL, format, and lint
Documentation
//! Orchestrator-server stable compatibility tests.
//!
//! Drives the shared `harn-mcp-compat` fixtures + fake-client helpers
//! against the in-process `McpOrchestratorService`. Failures here
//! localize to the **orchestrator-server** surface — see
//! `crates/harn-mcp-compat/tests/generic_server.rs` for the parallel
//! coverage on the generic `harn-serve` MCP wrapper.
//!
//! The in-process drive avoids the 30–40 s cold-start the subprocess
//! tests pay so this suite stays in the fast `make test` path.

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();
        // The orchestrator does not host a `tools/call` for the harness
        // fixture's `echo` tool; skip the call step (the response shape
        // is exercised against the generic server instead). Discover +
        // list still validate the orchestrator's stable envelope/cache hint
        // semantics against the published fixture.
        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"
        );
    }
}