use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
use serde_json::{json, Value};
const BIN: &str = env!("CARGO_BIN_EXE_buildwithnexus");
static SEQ: AtomicU64 = AtomicU64::new(0);
fn tmp(tag: &str) -> PathBuf {
let id = SEQ.fetch_add(1, Ordering::Relaxed);
let p = std::env::temp_dir().join(format!("bwn-it-{tag}-{}-{id}", std::process::id()));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
}
fn serve(script: Vec<String>) -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
thread::spawn(move || {
let mut served = 0usize;
for stream in listener.incoming() {
let Ok(mut stream) = stream else { continue };
let method = read_request(&mut stream);
let body = if method == "POST" {
let b = script
.get(served)
.cloned()
.unwrap_or_else(|| finish("auto"));
served += 1;
b
} else {
r#"{"object":"list","data":[]}"#.to_string()
};
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(), body
);
let _ = stream.write_all(resp.as_bytes());
let _ = stream.flush();
if method == "POST" && served >= script.len() {
break; }
}
});
port
}
fn read_request(stream: &mut std::net::TcpStream) -> String {
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut first = String::new();
if reader.read_line(&mut first).is_err() {
return String::new();
}
let method = first.split_whitespace().next().unwrap_or("").to_string();
let mut len = 0usize;
loop {
let mut line = String::new();
if reader.read_line(&mut line).unwrap_or(0) == 0 {
break;
}
if line == "\r\n" || line == "\n" {
break;
}
if let Some(v) = line.to_lowercase().strip_prefix("content-length:") {
len = v.trim().parse().unwrap_or(0);
}
}
if len > 0 {
let mut body = vec![0u8; len];
let _ = reader.read_exact(&mut body);
}
method
}
fn tool_call(id: &str, name: &str, args: Value) -> String {
json!({"choices": [{"message": {"content": "", "tool_calls": [
{"id": id, "type": "function",
"function": {"name": name, "arguments": args.to_string()}}
]}}]})
.to_string()
}
fn tool_call_raw_args(id: &str, name: &str, raw_args: &str) -> String {
json!({"choices": [{"message": {"content": "", "tool_calls": [
{"id": id, "type": "function",
"function": {"name": name, "arguments": raw_args}}
]}}]})
.to_string()
}
fn finish(summary: &str) -> String {
tool_call("done", "finish", json!({"summary": summary}))
}
fn write_config(home: &Path, provider: &str, permission: &str, port: u16) {
let cfg = json!({
"provider": provider,
"model": "test-model",
"permission": permission,
"base_url": format!("http://127.0.0.1:{port}/v1"),
});
std::fs::write(home.join("config.json"), cfg.to_string()).unwrap();
}
struct Run {
success: bool,
events: Vec<Value>,
stderr: String,
}
impl Run {
fn has_event(&self, ty: &str) -> bool {
self.events.iter().any(|e| e["type"] == ty)
}
fn find(&self, ty: &str) -> Option<&Value> {
self.events.iter().find(|e| e["type"] == ty)
}
fn text_of(&self, ty: &str) -> String {
self.events
.iter()
.filter(|e| e["type"] == ty)
.map(|e| e.to_string())
.collect()
}
}
fn run(home: &Path, cwd: &Path, task: &str) -> Run {
let out = Command::new(BIN)
.args(["--json", "run", task])
.current_dir(cwd)
.env("NEXUS_HOME", home)
.env("NO_COLOR", "1")
.stdin(Stdio::null()) .output()
.expect("spawn binary");
let stdout = String::from_utf8_lossy(&out.stdout);
let events = stdout
.lines()
.filter_map(|l| serde_json::from_str::<Value>(l).ok())
.collect();
Run {
success: out.status.success(),
events,
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
}
}
#[test]
fn executes_tool_then_finishes() {
let home = tmp("home");
let cwd = tmp("proj");
let port = serve(vec![
tool_call(
"c1",
"write_file",
json!({"path": "out.txt", "content": "hello world"}),
),
finish("wrote the file"),
]);
write_config(&home, "ollama", "auto", port);
let r = run(&home, &cwd, "create a file");
assert!(r.success, "stderr: {}", r.stderr);
assert_eq!(
std::fs::read_to_string(cwd.join("out.txt")).unwrap(),
"hello world"
);
assert_eq!(r.find("finish").unwrap()["summary"], "wrote the file");
assert!(r.has_event("tool_call"));
assert!(r.has_event("tool_result"));
}
#[test]
fn invalid_tool_args_are_fed_back_not_executed() {
let home = tmp("home");
let cwd = tmp("proj");
let port = serve(vec![
tool_call_raw_args("c1", "write_file", "{not valid json"),
finish("recovered"),
]);
write_config(&home, "ollama", "auto", port);
let r = run(&home, &cwd, "do a thing");
assert!(r.success, "stderr: {}", r.stderr);
assert!(r.has_event("tool_denied"));
assert!(r.text_of("tool_denied").contains("not valid JSON"));
assert!(!cwd.join("out.txt").exists());
}
#[test]
fn readonly_allows_out_of_cwd_read() {
let home = tmp("home");
let cwd = tmp("proj");
let port = serve(vec![
tool_call("c1", "read_file", json!({"path": "/etc/hostname"})),
finish("done"),
]);
write_config(&home, "ollama", "readonly", port);
let r = run(&home, &cwd, "read a system file");
assert!(
!r.has_event("tool_denied"),
"reads outside cwd should be allowed in readonly mode"
);
}
#[test]
fn readonly_blocks_mutation() {
let home = tmp("home");
let cwd = tmp("proj");
let port = serve(vec![
tool_call(
"c1",
"write_file",
json!({"path": "x.txt", "content": "nope"}),
),
finish("done"),
]);
write_config(&home, "ollama", "readonly", port);
let r = run(&home, &cwd, "write a file");
assert!(r.text_of("tool_denied").contains("read-only"));
assert!(!cwd.join("x.txt").exists());
}
#[test]
fn sensitive_path_auto_denies_without_hanging() {
let home = tmp("home");
let cwd = tmp("proj");
let port = serve(vec![
tool_call("c1", "read_file", json!({"path": "server.pem"})),
finish("done"),
]);
write_config(&home, "ollama", "auto", port);
let r = run(&home, &cwd, "read the cert");
assert!(r.has_event("tool_denied"));
assert!(r.text_of("tool_denied").contains("sensitive"));
}
#[test]
fn catastrophic_command_auto_denies() {
let home = tmp("home");
let cwd = tmp("proj");
let port = serve(vec![
tool_call("c1", "run_command", json!({"command": "rm -rf /"})),
finish("done"),
]);
write_config(&home, "ollama", "auto", port);
let r = run(&home, &cwd, "clean up");
assert!(r.has_event("tool_denied"));
assert!(r.text_of("tool_denied").contains("dangerous"));
}
#[test]
fn repeated_identical_tool_errors_stop_the_loop() {
let home = tmp("home");
let cwd = tmp("proj");
let miss = json!({"path": "does-not-exist.txt"});
let script = vec![
tool_call("c1", "read_file", miss.clone()),
tool_call("c2", "read_file", miss.clone()),
tool_call("c3", "read_file", miss.clone()),
tool_call("c4", "read_file", miss.clone()),
];
let port = serve(script);
write_config(&home, "ollama", "auto", port);
let r = run(&home, &cwd, "loop forever");
assert!(!r.success, "a stopped loop should not report success");
assert!(r.has_event("assistant"));
assert!(r.text_of("assistant").contains("repeated tool loop"));
}
#[test]
fn https_guard_rejects_keyed_http_endpoint() {
let home = tmp("home");
let cwd = tmp("proj");
let cfg = json!({
"provider": "openai", "model": "gpt-4o", "permission": "auto",
"base_url": "http://127.0.0.1:9/v1",
});
std::fs::write(home.join("config.json"), cfg.to_string()).unwrap();
let out = Command::new(BIN)
.args(["--json", "run", "hi"])
.current_dir(&cwd)
.env("NEXUS_HOME", &home)
.env("NO_COLOR", "1")
.stdin(Stdio::null())
.output()
.unwrap();
assert!(!out.status.success());
assert!(String::from_utf8_lossy(&out.stderr).contains("non-HTTPS"));
}
#[cfg(unix)]
#[test]
fn home_pre_tool_use_hook_can_deny() {
let home = tmp("home");
let cwd = tmp("proj");
let settings = json!({
"hooks": {
"PreToolUse": [
{ "matcher": "write_file",
"hooks": [{ "type": "command",
"command": "echo blocked-by-home-hook >&2; exit 2" }] }
]
}
});
std::fs::write(home.join("settings.json"), settings.to_string()).unwrap();
let port = serve(vec![
tool_call(
"c1",
"write_file",
json!({"path": "out.txt", "content": "x"}),
),
finish("done"),
]);
write_config(&home, "ollama", "auto", port);
let r = run(&home, &cwd, "write a file");
assert!(r.text_of("tool_denied").contains("blocked-by-home-hook"));
assert!(!cwd.join("out.txt").exists());
}
#[test]
fn spawn_subagent_recurses_and_returns() {
let home = tmp("home");
let cwd = tmp("proj");
let port = serve(vec![
tool_call("c1", "spawn_subagent", json!({"task": "do the subtask"})),
finish("subagent done"), finish("parent done"), ]);
write_config(&home, "ollama", "auto", port);
let r = run(&home, &cwd, "delegate something");
assert!(r.success, "stderr: {}", r.stderr);
let finishes: Vec<&Value> = r.events.iter().filter(|e| e["type"] == "finish").collect();
assert_eq!(finishes.len(), 2);
assert!(finishes.iter().any(|e| e["summary"] == "parent done"));
assert!(finishes.iter().any(|e| e["summary"] == "subagent done"));
}
#[test]
fn json_events_are_one_object_per_line() {
let home = tmp("home");
let cwd = tmp("proj");
let port = serve(vec![finish("ok")]);
write_config(&home, "ollama", "auto", port);
let r = run(&home, &cwd, "just finish");
assert!(r.success);
assert!(!r.events.is_empty());
assert!(r.events.iter().all(|e| e["type"].is_string()));
}