harn-vm 0.10.180

Async bytecode virtual machine for the Harn programming language
use super::*;

#[test]
fn cassette_recording_redacts_and_replays_with_new_ids() {
    let request = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
            "name": "lookup",
            "arguments": { "api_key": "secret-token" }
        }
    });
    let response = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "result": {
            "structuredContent": { "ok": true, "token": "secret-token" }
        }
    });
    let interaction = record_interaction(0, &request, &response, 7);
    assert_eq!(
        interaction.request["params"]["arguments"]["api_key"],
        crate::redact::REDACTED_PLACEHOLDER
    );
    assert_eq!(
        interaction.response["result"]["structuredContent"]["token"],
        crate::redact::REDACTED_PLACEHOLDER
    );

    let mut replayer = McpCassetteReplayer::new(McpCassette {
        interactions: vec![interaction],
        ..McpCassette::default()
    });
    let replay_request = json!({
        "jsonrpc": "2.0",
        "id": 99,
        "method": "tools/call",
        "params": {
            "name": "lookup",
            "arguments": { "api_key": "[redacted]" }
        }
    });
    let replayed = replayer
        .replay_request(&replay_request)
        .expect("replay succeeds")
        .expect("request has response");
    assert_eq!(replayed["id"], 99);
    assert!(replayer.is_finished());
}

#[test]
fn cassette_verify_flags_schema_and_structured_content_drift() {
    let recorded = McpCassette {
        interactions: vec![record_interaction(
            0,
            &json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}),
            &json!({"jsonrpc": "2.0", "id": 1, "result": {"tools": [{
                "name": "write",
                "inputSchema": {"type": "object"},
                "outputSchema": {"type": "object"},
                "annotations": {"readOnlyHint": false}
            }]}}),
            0,
        )],
        ..McpCassette::default()
    };
    let candidate = McpCassette {
        interactions: vec![record_interaction(
            0,
            &json!({"jsonrpc": "2.0", "id": 9, "method": "tools/list", "params": {}}),
            &json!({"jsonrpc": "2.0", "id": 9, "result": {"tools": [{
                "name": "write",
                "inputSchema": {"type": "object"},
                "outputSchema": {"type": "string"},
                "annotations": {"readOnlyHint": true}
            }]}}),
            0,
        )],
        ..McpCassette::default()
    };
    let report = verify_cassettes(&recorded, &candidate);
    assert!(!report.passed);
    assert!(report
        .divergences
        .iter()
        .any(|divergence| divergence.category == "tool_metadata"));
}

#[test]
fn world_runtime_mutates_state_and_scores_collateral_damage() {
    let spec = McpWorldSpec {
        initial_state: json!({
            "tickets": {
                "t1": { "status": "open", "title": "A" },
                "t2": { "status": "open", "title": "B" }
            }
        }),
        goal_state: Some(json!({
            "tickets": {
                "t1": { "status": "closed" }
            }
        })),
        tools: vec![McpWorldToolSpec {
            name: "close_ticket".to_string(),
            description: "Close a ticket".to_string(),
            input_schema: default_input_schema(),
            output_schema: None,
            annotations: Some(json!({"readOnlyHint": false, "destructiveHint": false})),
            operation: McpWorldOperation::Set {
                path: "/tickets/{id}/status".to_string(),
                value_arg: "status".to_string(),
            },
        }],
        ..McpWorldSpec::default()
    };
    let mut runtime = McpWorldRuntime::new(spec.clone());
    let response = runtime
        .handle_json_rpc(json!({
            "jsonrpc": "2.0",
            "id": 1,
            "method": "tools/call",
            "params": {
                "name": "close_ticket",
                "arguments": { "id": "t1", "status": "closed" }
            }
        }))
        .expect("response");
    assert_eq!(response["result"]["isError"], false);
    let score = score_world_state(&spec, runtime.state());
    assert!(score.passed);

    let damaged = json!({
        "tickets": {
            "t1": { "status": "closed", "title": "A" },
            "t2": { "status": "deleted", "title": "B" }
        }
    });
    let score = score_world_state(&spec, &damaged);
    assert!(!score.passed);
    assert_eq!(score.collateral_damage[0].path, "/tickets/t2/status");
}

#[test]
fn world_fault_partial_write_mutates_then_errors() {
    let spec = McpWorldSpec {
        initial_state: json!({ "rows": { "a": { "count": 0 } } }),
        tools: vec![McpWorldToolSpec {
            name: "increment".to_string(),
            description: String::new(),
            input_schema: default_input_schema(),
            output_schema: None,
            annotations: None,
            operation: McpWorldOperation::Noop { result: json!({}) },
        }],
        faults: vec![McpWorldFaultSpec {
            tool: "increment".to_string(),
            at_call: Some(1),
            every: None,
            fault: McpWorldFault::PartialWrite {
                path: "/rows/a/count".to_string(),
                value: json!(1),
                message: "write failed after commit".to_string(),
            },
        }],
        ..McpWorldSpec::default()
    };
    let mut runtime = McpWorldRuntime::new(spec);
    let response = runtime
        .handle_json_rpc(json!({
            "jsonrpc": "2.0",
            "id": 1,
            "method": "tools/call",
            "params": { "name": "increment", "arguments": {} }
        }))
        .expect("response");
    assert_eq!(response["result"]["isError"], true);
    assert_eq!(runtime.state().pointer("/rows/a/count"), Some(&json!(1)));
}

#[test]
fn world_idempotency_key_replays_after_partial_write_fault() {
    let spec = McpWorldSpec {
        initial_state: json!({ "rows": [] }),
        tools: vec![McpWorldToolSpec {
            name: "append".to_string(),
            description: String::new(),
            input_schema: default_input_schema(),
            output_schema: None,
            annotations: None,
            operation: McpWorldOperation::Append {
                path: "/rows".to_string(),
                value_arg: "row".to_string(),
                id_field: None,
                id_prefix: None,
            },
        }],
        faults: vec![McpWorldFaultSpec {
            tool: "append".to_string(),
            at_call: Some(1),
            every: None,
            fault: McpWorldFault::PartialWrite {
                path: "/rows".to_string(),
                value: json!([{ "id": "r1" }]),
                message: "write failed after commit".to_string(),
            },
        }],
        ..McpWorldSpec::default()
    };
    let mut runtime = McpWorldRuntime::new(spec);
    let request = |id| {
        json!({
            "jsonrpc": "2.0",
            "id": id,
            "method": "tools/call",
            "params": {
                "name": "append",
                "arguments": {
                    "row": {"id": "r1"},
                    "idempotency_key": "append-r1"
                }
            }
        })
    };

    let first = runtime.handle_json_rpc(request(1)).expect("first response");
    assert_eq!(first["result"]["isError"], true);
    let second = runtime
        .handle_json_rpc(request(2))
        .expect("second response");
    assert_eq!(second["result"]["isError"], false);
    assert_eq!(
        runtime.state().pointer("/rows"),
        Some(&json!([{ "id": "r1" }]))
    );
}