#![cfg(unix)]
use std::collections::BTreeMap;
use std::rc::Rc;
use std::sync::Arc;
use harn_hostlib::process::{
install_spawner, ExitStatus, MockHandleController, MockProcessConfig, MockSpawner, SpawnerGuard,
};
use harn_hostlib::tools::long_running::register_completion_notifier;
use harn_hostlib::tools::ToolsCapability;
use harn_hostlib::{BuiltinRegistry, HostlibCapability, HostlibError};
use harn_vm::VmValue;
use sha2::{Digest, Sha256};
use tempfile::tempdir;
fn registry() -> BuiltinRegistry {
let mut registry = BuiltinRegistry::new();
ToolsCapability.register_builtins(&mut registry);
registry
}
fn call(builtin: &str, request: BTreeMap<String, VmValue>) -> 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(Rc::new(request));
(entry.handler)(&[arg])
}
fn dict() -> BTreeMap<String, VmValue> {
BTreeMap::new()
}
fn vstr(value: &str) -> VmValue {
VmValue::String(Rc::from(value))
}
fn vlist_str(values: &[&str]) -> VmValue {
VmValue::List(Rc::new(values.iter().map(|s| vstr(s)).collect()))
}
fn require_dict(value: VmValue) -> BTreeMap<String, VmValue> {
match value {
VmValue::Dict(map) => (*map).clone(),
other => panic!("expected dict response, got {other:?}"),
}
}
fn require_int(map: &BTreeMap<String, VmValue>, key: &str) -> i64 {
match map.get(key) {
Some(VmValue::Int(i)) => *i,
other => panic!("expected int at {key}, got {other:?}"),
}
}
fn require_str(map: &BTreeMap<String, VmValue>, key: &str) -> String {
match map.get(key) {
Some(VmValue::String(s)) => s.to_string(),
other => panic!("expected string at {key}, got {other:?}"),
}
}
fn require_bool(map: &BTreeMap<String, VmValue>, key: &str) -> bool {
match map.get(key) {
Some(VmValue::Bool(b)) => *b,
other => panic!("expected bool at {key}, got {other:?}"),
}
}
fn install_mock() -> (Arc<MockSpawner>, SpawnerGuard) {
let spawner = Arc::new(MockSpawner::new());
let guard = install_spawner(spawner.clone());
(spawner, guard)
}
fn install_mock_with(
config: MockProcessConfig,
) -> (Arc<MockSpawner>, MockHandleController, SpawnerGuard) {
let (spawner, guard) = install_mock();
let controller = spawner.enqueue(config);
(spawner, controller, guard)
}
#[test]
fn run_command_echoes_stdout_and_reports_exit_zero() {
let (_spawner, _controller, _guard) =
install_mock_with(MockProcessConfig::with_stdout(0, "hello\n"));
let mut req = dict();
req.insert("argv".into(), vlist_str(&["bash", "-c", "echo hello"]));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 0);
assert_eq!(require_str(&resp, "stdout").trim(), "hello");
assert_eq!(require_str(&resp, "stderr"), "");
assert!(!require_bool(&resp, "timed_out"));
assert_eq!(require_str(&resp, "status"), "completed");
assert!(require_str(&resp, "command_id").starts_with("cmd_"));
assert!(require_int(&resp, "pid") > 0);
assert!(require_int(&resp, "process_group_id") > 0);
assert!(require_str(&resp, "started_at").contains('T'));
assert!(require_str(&resp, "ended_at").contains('T'));
assert!(require_str(&resp, "audit_id").starts_with("audit_cmd_"));
assert!(matches!(resp.get("signal"), Some(VmValue::Nil)));
assert!(require_int(&resp, "duration_ms") >= 0);
let output_path = require_str(&resp, "output_path");
assert_eq!(
std::fs::read_to_string(&output_path).unwrap().trim(),
"hello"
);
let digest = format!(
"sha256:{}",
hex::encode(Sha256::digest(std::fs::read(&output_path).unwrap()))
);
assert_eq!(require_str(&resp, "output_sha256"), digest);
assert_eq!(require_int(&resp, "line_count"), 1);
assert!(require_int(&resp, "byte_count") >= 6);
}
#[test]
fn run_command_propagates_nonzero_exit_code() {
let (_spawner, _controller, _guard) = install_mock_with(MockProcessConfig::completed(7));
let mut req = dict();
req.insert("argv".into(), vlist_str(&["bash", "-c", "exit 7"]));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 7);
assert!(!require_bool(&resp, "timed_out"));
}
#[test]
fn run_command_pipes_stdin_into_child() {
let (spawner, controller, _guard) = install_mock_with(MockProcessConfig::completed(0));
let mut req = dict();
req.insert("argv".into(), vlist_str(&["cat"]));
req.insert("stdin".into(), vstr("from-stdin"));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 0);
let captured = spawner.captured();
assert_eq!(captured.len(), 1);
assert!(captured[0].use_stdin, "stdin should be wired up in spec");
assert_eq!(controller.stdin_written(), b"from-stdin");
}
#[test]
fn run_command_runs_in_supplied_cwd() {
let (spawner, _controller, _guard) = install_mock_with(MockProcessConfig::completed(0));
let dir = tempdir().unwrap();
let mut req = dict();
req.insert("argv".into(), vlist_str(&["bash", "-c", "pwd"]));
req.insert("cwd".into(), vstr(dir.path().to_str().unwrap()));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 0);
let captured = spawner.captured();
assert_eq!(captured.len(), 1);
let canon_cwd = std::fs::canonicalize(dir.path()).unwrap();
assert_eq!(captured[0].cwd.as_ref().unwrap(), &canon_cwd);
}
#[test]
fn run_command_kills_child_when_timeout_elapses() {
let config = MockProcessConfig {
force_timeout: true,
..MockProcessConfig::running()
};
let (_spawner, _controller, _guard) = install_mock_with(config);
let mut req = dict();
req.insert("argv".into(), vlist_str(&["sleep", "30"]));
req.insert("timeout_ms".into(), VmValue::Int(150));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert!(require_bool(&resp, "timed_out"));
assert_eq!(require_str(&resp, "status"), "timed_out");
assert!(matches!(resp.get("signal"), Some(VmValue::String(_))));
}
#[test]
fn run_command_capture_stderr_false_merges_into_stdout() {
let config = MockProcessConfig {
stdout: b"out\n".to_vec(),
stderr: b"err\n".to_vec(),
..MockProcessConfig::default()
};
let (_spawner, _controller, _guard) = install_mock_with(config);
let mut req = dict();
req.insert(
"argv".into(),
vlist_str(&["bash", "-c", "echo out; echo err 1>&2"]),
);
req.insert("capture_stderr".into(), VmValue::Bool(false));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
let stdout = require_str(&resp, "stdout");
assert!(stdout.contains("out"), "stdout was {stdout:?}");
assert!(stdout.contains("err"), "stdout was {stdout:?}");
assert_eq!(require_str(&resp, "stderr"), "");
}
#[test]
fn run_command_supports_explicit_shell_mode() {
let (spawner, _controller, _guard) =
install_mock_with(MockProcessConfig::with_stdout(0, "shell-ok\n"));
let mut shell: BTreeMap<String, VmValue> = BTreeMap::new();
shell.insert("id".into(), vstr("sh"));
let mut req = dict();
req.insert("mode".into(), vstr("shell"));
req.insert("command".into(), vstr("echo shell-ok"));
req.insert("shell".into(), VmValue::Dict(Rc::new(shell)));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert_eq!(require_str(&resp, "stdout").trim(), "shell-ok");
let captured = spawner.captured();
let argv_blob = format!("{} {}", captured[0].program, captured[0].args.join(" "));
assert!(
argv_blob.contains("echo shell-ok"),
"unexpected resolved argv: {argv_blob:?}"
);
}
#[test]
fn run_command_caps_inline_output_and_read_command_output_reads_artifact() {
let payload = vec![b'x'; 2000];
let (_spawner, _controller, _guard) =
install_mock_with(MockProcessConfig::with_stdout(0, payload));
let mut capture: BTreeMap<String, VmValue> = BTreeMap::new();
capture.insert("max_inline_bytes".into(), VmValue::Int(8));
let mut req = dict();
req.insert(
"argv".into(),
vlist_str(&["bash", "-c", "for i in $(seq 1 2000); do printf x; done"]),
);
req.insert("capture".into(), VmValue::Dict(Rc::new(capture)));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert_eq!(require_str(&resp, "stdout").len(), 8);
assert_eq!(require_int(&resp, "byte_count"), 2000);
let mut read_req = dict();
read_req.insert("command_id".into(), vstr(&require_str(&resp, "command_id")));
read_req.insert("offset".into(), VmValue::Int(1990));
read_req.insert("length".into(), VmValue::Int(20));
let read_resp = require_dict(call("hostlib_tools_read_command_output", read_req).unwrap());
assert_eq!(require_str(&read_resp, "content").len(), 10);
assert!(require_bool(&read_resp, "eof"));
}
#[test]
fn read_command_output_rejects_arbitrary_path_reads() {
let mut req = dict();
req.insert("path".into(), vstr("/etc/passwd"));
let err = call("hostlib_tools_read_command_output", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "path"));
}
#[test]
fn run_command_passes_env_when_supplied() {
let (spawner, _controller, _guard) =
install_mock_with(MockProcessConfig::with_stdout(0, "value-42\n"));
let mut env_dict: BTreeMap<String, VmValue> = BTreeMap::new();
env_dict.insert("PATH".into(), vstr("/bin:/usr/bin"));
env_dict.insert("HOSTLIB_TEST_VAR".into(), vstr("value-42"));
let mut req = dict();
req.insert(
"argv".into(),
vlist_str(&["bash", "-c", "echo $HOSTLIB_TEST_VAR"]),
);
req.insert("env".into(), VmValue::Dict(Rc::new(env_dict)));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
assert_eq!(require_str(&resp, "stdout").trim(), "value-42");
let captured = spawner.captured();
assert_eq!(
captured[0].env.get("HOSTLIB_TEST_VAR"),
Some(&"value-42".to_string())
);
}
#[test]
fn run_command_missing_argv_returns_missing_parameter() {
let err = call("hostlib_tools_run_command", dict()).unwrap_err();
match err {
HostlibError::MissingParameter { param, .. } => assert_eq!(param, "argv"),
other => panic!("expected MissingParameter, got {other:?}"),
}
}
#[test]
fn run_command_empty_argv_returns_invalid_parameter() {
let mut req = dict();
req.insert("argv".into(), VmValue::List(Rc::new(Vec::new())));
let err = call("hostlib_tools_run_command", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "argv"));
}
#[test]
fn run_command_rejects_nonexistent_cwd() {
let mut req = dict();
req.insert("argv".into(), vlist_str(&["true"]));
req.insert("cwd".into(), vstr("/this/does/not/exist/anywhere"));
let err = call("hostlib_tools_run_command", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "cwd"));
}
#[test]
fn run_command_argv_must_be_strings() {
let mut req = dict();
req.insert("argv".into(), VmValue::List(Rc::new(vec![VmValue::Int(1)])));
let err = call("hostlib_tools_run_command", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "argv"));
}
#[test]
fn run_test_explicit_argv_runs_and_returns_handle() {
let (_spawner, _controller, _guard) = install_mock_with(MockProcessConfig::completed(0));
let mut req = dict();
req.insert("argv".into(), vlist_str(&["true"]));
let resp = require_dict(call("hostlib_tools_run_test", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 0);
assert!(!require_str(&resp, "result_handle").is_empty());
}
#[test]
fn run_test_without_argv_or_manifest_errors() {
let dir = tempdir().unwrap();
let mut req = dict();
req.insert("cwd".into(), vstr(dir.path().to_str().unwrap()));
let err = call("hostlib_tools_run_test", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "argv"));
}
#[test]
fn run_test_inspect_returns_parsed_records_for_explicit_junit() {
let stdout = "running 2 tests\n\
test a::passes ... ok\n\
test a::fails ... FAILED\n\
\n\
test result: FAILED. 1 passed; 1 failed; 0 ignored\n";
let (_spawner, _controller, _guard) =
install_mock_with(MockProcessConfig::with_stdout(1, stdout));
let mut req = dict();
req.insert(
"argv".into(),
vlist_str(&["bash", "-c", "echo cargo libtest output"]),
);
let resp = require_dict(call("hostlib_tools_run_test", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 1);
let handle = require_str(&resp, "result_handle");
let mut inspect_req = dict();
inspect_req.insert("result_handle".into(), vstr(&handle));
inspect_req.insert("include_passing".into(), VmValue::Bool(true));
let inspect = require_dict(call("hostlib_tools_inspect_test_results", inspect_req).unwrap());
assert_eq!(require_str(&inspect, "result_handle"), handle);
let tests = match inspect.get("tests") {
Some(VmValue::List(l)) => (**l).clone(),
other => panic!("expected list, got {other:?}"),
};
assert_eq!(tests.len(), 2);
}
#[test]
fn run_test_summary_omitted_when_no_records_parsed() {
let (_spawner, _controller, _guard) =
install_mock_with(MockProcessConfig::with_stdout(0, "nothing\n"));
let mut req = dict();
req.insert("argv".into(), vlist_str(&["bash", "-c", "echo nothing"]));
let resp = require_dict(call("hostlib_tools_run_test", req).unwrap());
assert!(!resp.contains_key("summary"));
}
#[test]
fn inspect_test_results_unknown_handle_errors() {
let mut req = dict();
req.insert(
"result_handle".into(),
vstr("htr-deadbeef-this-is-not-real"),
);
let err = call("hostlib_tools_inspect_test_results", req).unwrap_err();
assert!(
matches!(err, HostlibError::InvalidParameter { param, .. } if param == "result_handle")
);
}
#[test]
fn inspect_test_results_missing_handle_errors() {
let err = call("hostlib_tools_inspect_test_results", dict()).unwrap_err();
assert!(
matches!(err, HostlibError::MissingParameter { param, .. } if param == "result_handle")
);
}
#[test]
fn run_build_command_explicit_argv_runs_and_parses_diagnostics() {
let config = MockProcessConfig {
stderr: b"src/foo.rs:3:7: error: parse error here\n".to_vec(),
..MockProcessConfig::completed(2)
};
let (_spawner, _controller, _guard) = install_mock_with(config);
let mut req = dict();
req.insert(
"argv".into(),
vlist_str(&[
"bash",
"-c",
"echo 'src/foo.rs:3:7: error: parse error here' 1>&2; exit 2",
]),
);
let resp = require_dict(call("hostlib_tools_run_build_command", req).unwrap());
assert_eq!(require_int(&resp, "exit_code"), 2);
let diagnostics = match resp.get("diagnostics") {
Some(VmValue::List(l)) => (**l).clone(),
other => panic!("expected list, got {other:?}"),
};
assert!(!diagnostics.is_empty());
}
#[test]
fn run_build_command_without_argv_or_manifest_errors() {
let dir = tempdir().unwrap();
let mut req = dict();
req.insert("cwd".into(), vstr(dir.path().to_str().unwrap()));
let err = call("hostlib_tools_run_build_command", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "argv"));
}
#[test]
fn manage_packages_missing_operation_errors() {
let err = call("hostlib_tools_manage_packages", dict()).unwrap_err();
assert!(matches!(err, HostlibError::MissingParameter { param, .. } if param == "operation"));
}
#[test]
fn manage_packages_unknown_operation_errors() {
let mut req = dict();
req.insert("operation".into(), vstr("frobnicate"));
req.insert("ecosystem".into(), vstr("npm"));
let err = call("hostlib_tools_manage_packages", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "operation"));
}
#[test]
fn manage_packages_no_ecosystem_no_manifest_errors() {
let dir = tempdir().unwrap();
let mut req = dict();
req.insert("operation".into(), vstr("install"));
req.insert("cwd".into(), vstr(dir.path().to_str().unwrap()));
let err = call("hostlib_tools_manage_packages", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "ecosystem"));
}
#[test]
fn manage_packages_unsupported_pair_for_ecosystem_errors() {
let mut req = dict();
req.insert("operation".into(), vstr("add"));
req.insert("ecosystem".into(), vstr("gradle"));
req.insert("packages".into(), vlist_str(&["junit"]));
let err = call("hostlib_tools_manage_packages", req).unwrap_err();
assert!(matches!(err, HostlibError::InvalidParameter { param, .. } if param == "operation"));
}
#[test]
fn manage_packages_runs_for_detected_ecosystem_with_explicit_cwd() {
let (spawner, _controller, _guard) = install_mock_with(MockProcessConfig::completed(0));
let dir = tempdir().unwrap();
let mut req = dict();
req.insert("operation".into(), vstr("update"));
req.insert("ecosystem".into(), vstr("bundler"));
req.insert("cwd".into(), vstr(dir.path().to_str().unwrap()));
let resp = require_dict(call("hostlib_tools_manage_packages", req).unwrap());
assert_eq!(require_str(&resp, "ecosystem"), "bundler");
assert_eq!(require_str(&resp, "operation"), "update");
assert!(matches!(
resp.get("lockfile_changed"),
Some(VmValue::Bool(_))
));
let captured = spawner.captured();
assert_eq!(captured[0].program, "bundle");
assert_eq!(captured[0].args, vec!["update".to_string()]);
}
#[test]
fn run_command_long_running_returns_handle_immediately() {
let (_spawner, _controller, _guard) = install_mock_with(MockProcessConfig::running());
let mut req = dict();
req.insert("argv".into(), vlist_str(&["sleep", "10"]));
req.insert("long_running".into(), VmValue::Bool(true));
let resp = require_dict(call("hostlib_tools_run_command", req).unwrap());
let handle_id = require_str(&resp, "handle_id");
assert!(!handle_id.is_empty(), "handle_id must be non-empty");
assert!(
handle_id.starts_with("hto-"),
"handle_id should start with hto-, got {handle_id}"
);
assert_eq!(require_str(&resp, "status"), "running");
assert!(require_str(&resp, "command_id").starts_with("cmd_"));
assert!(require_int(&resp, "pid") > 0);
assert!(require_int(&resp, "process_group_id") > 0);
assert!(require_str(&resp, "started_at").contains('T'));
let cmd = require_str(&resp, "command");
assert!(
cmd.contains("sleep"),
"command should contain 'sleep', got {cmd}"
);
let completion_rx = register_completion_notifier(&handle_id);
let mut cancel_req = dict();
cancel_req.insert("handle_id".into(), vstr(&handle_id));
let cancel_resp = require_dict(call("hostlib_tools_cancel_handle", cancel_req).unwrap());
assert!(require_bool(&cancel_resp, "cancelled"));
if let Some(rx) = completion_rx {
let _ = rx.recv();
}
}
#[test]
fn run_command_long_running_feedback_fires_after_exit() {
let session_id = format!(
"test-lr-feedback-{}-{:?}",
std::process::id(),
std::thread::current().id()
);
let (_spawner, controller, _guard) = install_mock_with(MockProcessConfig::running());
let info = harn_hostlib::tools::long_running::spawn_long_running(
"test_builtin",
"bash".into(),
vec![
"-c".into(),
"echo 'hello stdout'; echo 'hello stderr' 1>&2".into(),
],
None,
std::collections::BTreeMap::new(),
session_id.clone(),
)
.expect("spawn_long_running failed");
assert!(!info.handle_id.is_empty());
let completion_rx =
register_completion_notifier(&info.handle_id).expect("handle should still be live");
controller.append_stdout(b"hello stdout\n");
controller.append_stderr(b"hello stderr\n");
controller.complete_with(ExitStatus::from_code(0));
completion_rx.recv().expect("waiter completion never fired");
let items = harn_vm::drain_global_pending_feedback(&session_id);
assert_eq!(items.len(), 1, "expected exactly one feedback item");
let (kind, content) = &items[0];
assert_eq!(kind, "tool_result", "unexpected feedback kind: {kind}");
let payload: serde_json::Value =
serde_json::from_str(content).expect("feedback content not valid JSON");
assert_eq!(
payload["handle_id"].as_str().unwrap(),
info.handle_id,
"handle_id mismatch in feedback"
);
assert_eq!(payload["exit_code"], 0);
assert_eq!(payload["status"], "completed");
assert!(payload["output_path"]
.as_str()
.unwrap()
.contains("combined.txt"));
assert!(
payload["stdout"].as_str().unwrap().contains("hello stdout"),
"stdout missing: {}",
payload["stdout"]
);
assert!(
payload["stderr"].as_str().unwrap().contains("hello stderr"),
"stderr missing: {}",
payload["stderr"]
);
assert!(
payload["duration_ms"].as_i64().unwrap() >= 0,
"duration_ms must be non-negative"
);
}
#[test]
fn cancel_handle_kills_long_running_process() {
let session_id = format!(
"test-lr-cancel-{}-{:?}",
std::process::id(),
std::thread::current().id()
);
let (_spawner, _controller, _guard) = install_mock_with(MockProcessConfig::running());
let info = harn_hostlib::tools::long_running::spawn_long_running(
"test_builtin",
"sleep".into(),
vec!["30".into()],
None,
std::collections::BTreeMap::new(),
session_id.clone(),
)
.expect("spawn_long_running failed");
let completion_rx = register_completion_notifier(&info.handle_id);
let mut req = dict();
req.insert("handle_id".into(), vstr(&info.handle_id));
let resp = require_dict(call("hostlib_tools_cancel_handle", req).unwrap());
assert!(require_bool(&resp, "cancelled"));
assert_eq!(require_str(&resp, "handle_id"), info.handle_id);
let mut req2 = dict();
req2.insert("handle_id".into(), vstr(&info.handle_id));
let resp2 = require_dict(call("hostlib_tools_cancel_handle", req2).unwrap());
assert!(
!require_bool(&resp2, "cancelled"),
"second cancel should return false"
);
if let Some(rx) = completion_rx {
let _ = rx.recv();
}
let items = harn_vm::drain_global_pending_feedback(&session_id);
assert!(
items.is_empty(),
"cancelled handle should not push feedback, got {items:?}"
);
}
#[test]
fn cancel_handle_unknown_handle_returns_false() {
let mut req = dict();
req.insert("handle_id".into(), vstr("hto-deadbeef-no-such-handle"));
let resp = require_dict(call("hostlib_tools_cancel_handle", req).unwrap());
assert!(!require_bool(&resp, "cancelled"));
}
#[test]
fn run_test_long_running_returns_handle() {
let (_spawner, _controller, _guard) = install_mock_with(MockProcessConfig::running());
let mut req = dict();
req.insert("argv".into(), vlist_str(&["sleep", "10"]));
req.insert("long_running".into(), VmValue::Bool(true));
let resp = require_dict(call("hostlib_tools_run_test", req).unwrap());
let handle_id = require_str(&resp, "handle_id");
assert!(
handle_id.starts_with("hto-"),
"unexpected handle_id: {handle_id}"
);
let completion_rx = register_completion_notifier(&handle_id);
let mut cancel_req = dict();
cancel_req.insert("handle_id".into(), vstr(&handle_id));
call("hostlib_tools_cancel_handle", cancel_req).unwrap();
if let Some(rx) = completion_rx {
let _ = rx.recv();
}
}
#[test]
fn run_build_command_long_running_returns_handle() {
let (_spawner, _controller, _guard) = install_mock_with(MockProcessConfig::running());
let mut req = dict();
req.insert("argv".into(), vlist_str(&["sleep", "10"]));
req.insert("long_running".into(), VmValue::Bool(true));
let resp = require_dict(call("hostlib_tools_run_build_command", req).unwrap());
let handle_id = require_str(&resp, "handle_id");
assert!(
handle_id.starts_with("hto-"),
"unexpected handle_id: {handle_id}"
);
let completion_rx = register_completion_notifier(&handle_id);
let mut cancel_req = dict();
cancel_req.insert("handle_id".into(), vstr(&handle_id));
call("hostlib_tools_cancel_handle", cancel_req).unwrap();
if let Some(rx) = completion_rx {
let _ = rx.recv();
}
}