#![cfg(unix)]
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
fn init_request() -> serde_json::Value {
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {},
})
}
#[tokio::test]
async fn worker_stdout_is_pure_json_rpc() {
let bin = locate_newt_bin();
spawn_and_assert_pure(&bin, &["worker"]).await;
}
#[tokio::test]
async fn mcp_stdout_is_pure_json_rpc() {
let bin = locate_newt_bin();
spawn_and_assert_pure(&bin, &["mcp"]).await;
}
async fn spawn_and_assert_pure(bin: &PathBuf, args: &[&str]) {
let mut cmd = Command::new(bin);
cmd.args(args)
.env("OLLAMA_HOST", "http://127.0.0.1:1")
.env("RUST_LOG", "debug")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
let mut child = cmd.spawn().expect("spawn newt");
{
let mut stdin = child.stdin.take().expect("take stdin");
let line = format!(
"{}\n",
serde_json::to_string(&init_request()).expect("serialize init")
);
stdin.write_all(line.as_bytes()).await.expect("write init");
}
let output = tokio::time::timeout(Duration::from_secs(10), child.wait_with_output())
.await
.expect("worker timed out")
.expect("collect worker output");
let stdout = String::from_utf8_lossy(&output.stdout);
let mut saw_response = false;
for (idx, line) in stdout.lines().enumerate() {
if line.trim().is_empty() {
continue;
}
let parsed: Result<serde_json::Value, _> = serde_json::from_str(line);
assert!(
parsed.is_ok(),
"stdout line {} is not valid JSON ({} args={:?})\n line: {:?}\n\nFull stdout:\n{}\n\nFull stderr:\n{}",
idx + 1,
bin.display(),
args,
line,
stdout,
String::from_utf8_lossy(&output.stderr),
);
let v = parsed.unwrap();
assert_eq!(
v.get("jsonrpc").and_then(|j| j.as_str()),
Some("2.0"),
"every stdout line must be a JSON-RPC 2.0 frame: {line}"
);
saw_response = true;
}
assert!(
saw_response,
"expected at least one JSON-RPC response on stdout (got none)\n\nFull stdout:\n{}\n\nFull stderr:\n{}",
stdout,
String::from_utf8_lossy(&output.stderr),
);
}
fn cargo_target_dir() -> Option<PathBuf> {
cargo_metadata::MetadataCommand::new()
.exec()
.ok()
.map(|m| m.target_directory.into_std_path_buf())
}
fn locate_newt_bin() -> PathBuf {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace_root = manifest.parent().expect("manifest dir has parent");
let mut target_dirs: Vec<PathBuf> = Vec::new();
if let Some(tdir) = std::env::var_os("CARGO_TARGET_DIR") {
target_dirs.push(PathBuf::from(tdir));
}
if let Some(tdir) = cargo_target_dir() {
target_dirs.push(tdir);
}
target_dirs.push(workspace_root.join("target"));
target_dirs.push(workspace_root.join("target").join("llvm-cov-target"));
for tdir in &target_dirs {
for profile in ["debug", "release"] {
let candidate = tdir.join(profile).join("newt");
if candidate.exists() {
return candidate;
}
}
}
let _ = std::process::Command::new(env!("CARGO"))
.args(["build", "--bin", "newt"])
.output();
cargo_target_dir()
.unwrap_or_else(|| workspace_root.join("target"))
.join("debug")
.join("newt")
}