use std::process::Command;
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
#[test]
fn tool_call_stub_via_interp() {
let prog = r#"tool mytool"a helper" x:t>R _ t
main x:t>R _ t;mytool x"#;
let out = ilo()
.args([prog, "--run-tree", "main", "hello"])
.output()
.expect("ilo failed to start");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(stdout.trim(), "nil");
}
#[test]
fn tool_call_stub_via_vm() {
let prog = r#"tool mytool"a helper" x:t>R _ t
main x:t>R _ t;mytool x"#;
let out = ilo()
.args([prog, "--run-vm", "main", "hello"])
.output()
.expect("ilo failed to start");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(stdout.trim(), "nil");
}
#[test]
fn tools_flag_nonexistent_path() {
let prog = r#"tool mytool"a helper" x:t>R _ t
main x:t>R _ t;mytool x"#;
let out = ilo()
.args([
prog,
"--tools",
"/nonexistent/path/to/config.json",
"--run-tree",
"main",
"hello",
])
.output()
.expect("ilo failed to start");
assert!(
!out.status.success(),
"expected failure when tools config path does not exist"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("failed to read tools config") || stderr.contains("No such file"),
"expected file-not-found error, got: {}",
stderr
);
}
#[test]
fn tools_flag_with_valid_config() {
use std::io::Write;
let (path, mut file) = tempfile_in_tmp("tools_config_valid.json");
writeln!(
file,
r#"{{"tools": {{"mytool": {{"url": "http://127.0.0.1:19999/mytool"}}}}}}"#
)
.unwrap();
drop(file);
let prog = r#"tool mytool"a helper" x:t>R _ t
main x:t>t;"hello""#;
let out = ilo()
.args([prog, "--tools", &path, "--run-tree", "main", "world"])
.output()
.expect("ilo failed to start");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("thread 'main' panicked"),
"unexpected panic: {}",
stderr
);
std::fs::remove_file(&path).ok();
}
#[test]
fn tool_decl_in_ast_json() {
let prog = r#"tool mytool"a helper" x:t>R _ t"#;
let out = ilo().args([prog]).output().expect("ilo failed to start");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Tool") || stdout.contains("mytool"),
"expected tool in AST, got: {}",
stdout
);
}
#[test]
fn tools_flag_invalid_json_config() {
use std::io::Write;
let (path, mut file) = tempfile_in_tmp("bad_config.json");
write!(file, "not valid json").unwrap();
drop(file);
let prog = r#"tool mytool"a helper" x:t>R _ t
main x:t>R _ t;mytool x"#;
let out = ilo()
.args([prog, "--tools", &path, "--run-tree", "main", "hello"])
.output()
.expect("ilo failed to start");
assert!(
!out.status.success(),
"expected failure for invalid JSON config"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("failed to parse tools config")
|| stderr.contains("parse")
|| stderr.contains("JSON"),
"expected parse error in stderr, got: {}",
stderr
);
std::fs::remove_file(&path).ok();
}
#[test]
fn vm_tool_stub_returns_ok_nil() {
let prog = r#"tool mytool"a helper" x:t>R _ t
f>R _ t;mytool "test""#;
let out = ilo()
.args([prog, "--run-vm", "f"])
.output()
.expect("ilo failed to start");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "nil");
}
#[test]
fn vm_multiple_tool_stubs() {
let prog = r#"tool a"first" x:t>R _ t
tool b"second" x:t>R _ t
f>R _ t;b "test""#;
let out = ilo()
.args([prog, "--run-vm", "f"])
.output()
.expect("ilo failed to start");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "nil");
}
#[test]
#[ignore]
fn get_builtin_real_http() {
let prog = "main x:t>R t t;$x";
let out = ilo()
.args([prog, "--run-tree", "main", "https://httpbin.org/get"])
.output()
.expect("ilo failed to start");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn tools_cmd_mcp_empty_servers() {
use std::io::Write;
let (path, mut file) = tempfile_in_tmp("tools_mcp_empty.json");
writeln!(file, r#"{{"mcpServers": {{}}}}"#).unwrap();
drop(file);
let out = ilo()
.args(["tools", "--mcp", &path])
.output()
.expect("ilo failed to start");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("thread 'main' panicked"),
"unexpected panic: {stderr}"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn tools_cmd_http_human_format() {
use std::io::Write;
let (path, mut file) = tempfile_in_tmp("tools_cmd_human.json");
writeln!(
file,
r#"{{"tools": {{"mytool": {{"url": "http://127.0.0.1:19999/mytool"}}}}}}"#
)
.unwrap();
drop(file);
let out = ilo()
.args(["tools", "--tools", &path])
.output()
.expect("ilo failed to start");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("mytool"),
"expected tool name in output, got: {stdout}"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn tools_cmd_http_human_full() {
use std::io::Write;
let (path, mut file) = tempfile_in_tmp("tools_cmd_human_full.json");
writeln!(
file,
r#"{{"tools": {{"bigtool": {{"url": "http://127.0.0.1:19999/bigtool"}}}}}}"#
)
.unwrap();
drop(file);
let out = ilo()
.args(["tools", "--tools", &path, "--full"])
.output()
.expect("ilo failed to start");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("bigtool"),
"expected tool name in full output, got: {stdout}"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn tools_cmd_http_ilo_format() {
use std::io::Write;
let (path, mut file) = tempfile_in_tmp("tools_cmd_ilo.json");
writeln!(
file,
r#"{{"tools": {{"hello": {{"url": "http://127.0.0.1:19999/hello"}}}}}}"#
)
.unwrap();
drop(file);
let out = ilo()
.args(["tools", "--tools", &path, "--ilo"])
.output()
.expect("ilo failed to start");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("hello"),
"expected tool in ilo output, got: {stdout}"
);
assert!(
stdout.contains("tool"),
"expected 'tool' keyword in ilo output, got: {stdout}"
);
std::fs::remove_file(&path).ok();
}
#[test]
fn tools_cmd_http_json_format() {
use std::io::Write;
let (path, mut file) = tempfile_in_tmp("tools_cmd_json.json");
writeln!(
file,
r#"{{"tools": {{"greet": {{"url": "http://127.0.0.1:19999/greet"}}}}}}"#
)
.unwrap();
drop(file);
let out = ilo()
.args(["tools", "--tools", &path, "--json"])
.output()
.expect("ilo failed to start");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("expected valid JSON output");
assert!(parsed.is_array(), "expected JSON array, got: {parsed}");
let arr = parsed.as_array().unwrap();
assert!(
arr.iter().any(|t| t["name"] == "greet"),
"expected greet tool in JSON output"
);
std::fs::remove_file(&path).ok();
}
fn tempfile_in_tmp(name: &str) -> (String, std::fs::File) {
let mut path = std::env::temp_dir();
path.push(format!("ilo_test_{name}"));
let f = std::fs::File::create(&path).expect("create temp file");
(path.to_string_lossy().into_owned(), f)
}
#[cfg(feature = "tools")]
mod http_tests {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn http_provider_success() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/double"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(4.0)))
.mount(&server)
.await;
let config_json = serde_json::json!({
"tools": {
"double": {
"url": format!("{}/double", server.uri())
}
}
});
let mut p = std::env::temp_dir();
p.push("ilo_wiremock_test.json");
std::fs::write(&p, config_json.to_string()).unwrap();
let prog = r#"tool double"doubles a number" x:n>R n n
main x:n>R n n;double x"#;
let out = std::process::Command::new(env!("CARGO_BIN_EXE_ilo"))
.args([
prog,
"--tools",
p.to_str().unwrap(),
"--run-tree",
"main",
"2",
])
.output()
.expect("ilo failed to start");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(out.status.success(), "stderr: {stderr}\nstdout: {stdout}");
std::fs::remove_file(&p).ok();
}
#[tokio::test]
async fn http_provider_not_configured() {
let config_json = serde_json::json!({ "tools": {} });
let mut p = std::env::temp_dir();
p.push("ilo_wiremock_not_configured.json");
std::fs::write(&p, config_json.to_string()).unwrap();
let prog = r#"tool double"doubles a number" x:n>R n n
main x:n>R n n;double x"#;
let out = std::process::Command::new(env!("CARGO_BIN_EXE_ilo"))
.args([
prog,
"--tools",
p.to_str().unwrap(),
"--run-tree",
"main",
"2",
])
.output()
.expect("ilo failed to start");
assert!(
!out.status.success(),
"expected failure for unconfigured tool"
);
std::fs::remove_file(&p).ok();
}
#[tokio::test]
async fn http_provider_server_error() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/double"))
.respond_with(
ResponseTemplate::new(500).set_body_json(serde_json::json!({"error": "internal"})),
)
.mount(&server)
.await;
let config_json = serde_json::json!({
"tools": {
"double": {
"url": format!("{}/double", server.uri())
}
}
});
let mut p = std::env::temp_dir();
p.push("ilo_wiremock_server_error.json");
std::fs::write(&p, config_json.to_string()).unwrap();
let prog = r#"tool double"doubles a number" x:n>R n n
main x:n>R n n;double x"#;
let out = std::process::Command::new(env!("CARGO_BIN_EXE_ilo"))
.args([
prog,
"--tools",
p.to_str().unwrap(),
"--run-tree",
"main",
"2",
])
.output()
.expect("ilo failed to start");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("thread 'main' panicked"),
"unexpected panic: {stderr}"
);
std::fs::remove_file(&p).ok();
}
}