harn-hostlib 0.10.21

Opt-in code-intelligence and deterministic-tool host builtins for the Harn VM
Documentation
//! Focused mock coverage for `wait_command` direct result synchronization.

#![cfg(unix)]

use std::sync::Arc;

use harn_hostlib::process::{
    install_spawner, ExitStatus, MockHandleController, MockProcessConfig, MockSpawner, SpawnerGuard,
};
use harn_hostlib::tools::long_running::register_result_notifier;
use harn_hostlib::tools::ToolsCapability;
use harn_hostlib::{BuiltinRegistry, HostlibCapability, HostlibError};
use harn_vm::VmValue;

fn registry() -> BuiltinRegistry {
    let mut registry = BuiltinRegistry::new();
    ToolsCapability.register_builtins(&mut registry);
    registry
}

fn call(builtin: &str, request: harn_vm::value::DictMap) -> Result<VmValue, HostlibError> {
    harn_hostlib::tools::permissions::enable_for_test();
    let registry = registry();
    let entry = registry
        .find(builtin)
        .unwrap_or_else(|| panic!("builtin {builtin} not registered"));
    let arg = VmValue::dict(request);
    (entry.handler)(&[arg])
}

fn dict() -> harn_vm::value::DictMap {
    harn_vm::value::DictMap::new()
}

fn vstr(value: &str) -> VmValue {
    VmValue::String(arcstr::ArcStr::from(value))
}

fn vlist_str(values: &[&str]) -> VmValue {
    VmValue::List(Arc::new(values.iter().map(|s| vstr(s)).collect()))
}

fn require_dict(value: VmValue) -> harn_vm::value::DictMap {
    match value {
        VmValue::Dict(map) => (*map).clone(),
        other => panic!("expected dict response, got {other:?}"),
    }
}

fn require_str(map: &harn_vm::value::DictMap, key: &str) -> String {
    match map.get(key) {
        Some(VmValue::String(s)) => s.to_string(),
        other => panic!("expected string at {key}, got {other:?}"),
    }
}

fn install_mock_with(
    config: MockProcessConfig,
) -> (Arc<MockSpawner>, MockHandleController, SpawnerGuard) {
    let spawner = Arc::new(MockSpawner::new());
    let controller = spawner.enqueue(config);
    let guard = install_spawner(spawner.clone());
    (spawner, controller, guard)
}

fn unique_session_id(prefix: &str) -> String {
    format!(
        "{}-{}-{:?}",
        prefix,
        std::process::id(),
        std::thread::current().id()
    )
}

#[test]
fn wait_command_waits_for_live_handle_result_and_consumes_feedback() {
    let session_id = unique_session_id("test-wait-command-direct-result");
    let _session_guard = harn_vm::agent_sessions::enter_current_session(session_id.clone());
    let _ = harn_vm::orchestration::agent_inbox::drain(&session_id);
    let (_spawner, controller, _guard) = install_mock_with(MockProcessConfig::running());

    let mut req = dict();
    req.insert("argv".into(), vlist_str(&["sh", "-c", "echo direct"]));
    req.insert("background".into(), VmValue::Bool(true));
    let start = require_dict(call("hostlib_tools_run_command", req).unwrap());
    let handle_id = require_str(&start, "handle_id");

    let wait_handle_id = handle_id.clone();
    let wait_session_id = session_id.clone();
    let waiter = std::thread::spawn(move || {
        let _session_guard = harn_vm::agent_sessions::enter_current_session(wait_session_id);
        let mut wait_req = dict();
        wait_req.insert("handle_id".into(), vstr(&wait_handle_id));
        wait_req.insert("timeout_ms".into(), VmValue::Int(5_000));
        require_dict(call("hostlib_tools_wait_command", wait_req).unwrap())
    });

    controller.append_stdout(b"direct\n");
    controller.complete_with(ExitStatus::from_code(0));

    let waited = waiter.join().expect("waiter thread panicked");
    assert_eq!(require_str(&waited, "status"), "completed");
    assert_eq!(require_str(&waited, "feedback_kind"), "tool_result");
    assert_eq!(require_str(&waited, "handle_id"), handle_id);
    assert_eq!(require_str(&waited, "stdout"), "direct\n");

    let leftover = harn_vm::orchestration::agent_inbox::drain(&session_id);
    assert!(
        leftover.is_empty(),
        "explicit wait must consume matching tool_result feedback, got {leftover:?}"
    );
}

#[test]
fn long_running_result_notifier_fires_after_inbox_feedback_is_visible() {
    let session_id = unique_session_id("test-long-running-result-notifier-order");
    let _session_guard = harn_vm::agent_sessions::enter_current_session(session_id.clone());
    let _ = harn_vm::orchestration::agent_inbox::drain(&session_id);
    let (_spawner, controller, _guard) = install_mock_with(MockProcessConfig::running());

    let mut req = dict();
    req.insert("argv".into(), vlist_str(&["sh", "-c", "echo ordered"]));
    req.insert("background".into(), VmValue::Bool(true));
    let start = require_dict(call("hostlib_tools_run_command", req).unwrap());
    let handle_id = require_str(&start, "handle_id");
    let result_rx = register_result_notifier(&handle_id).expect("handle should still be live");

    controller.append_stdout(b"ordered\n");
    controller.complete_with(ExitStatus::from_code(0));

    let result = require_dict(result_rx.recv().expect("result notifier never fired"));
    assert_eq!(require_str(&result, "status"), "completed");
    assert_eq!(require_str(&result, "handle_id"), handle_id);
    assert_eq!(require_str(&result, "stdout"), "ordered\n");

    let inbox = harn_vm::orchestration::agent_inbox::drain(&session_id);
    assert_eq!(
        inbox.len(),
        1,
        "result notifier must fire after inbox publish"
    );
    assert_eq!(inbox[0].kind, "tool_result");
    let payload: serde_json::Value = serde_json::from_str(&inbox[0].content).expect("json");
    assert_eq!(
        payload.get("handle_id").and_then(|value| value.as_str()),
        Some(handle_id.as_str())
    );
}