#![allow(dead_code)]
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
use serde_json::{json, Value};
pub const EXIT_OK: i32 = 0;
pub const EXIT_DIAGNOSTICS: i32 = 1;
pub const EXIT_FAILURE: i32 = 2;
pub fn binary() -> &'static str {
env!("CARGO_BIN_EXE_ingot")
}
pub fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("the crate must live two levels below the repository root")
.to_path_buf()
}
pub struct TempDir(PathBuf);
impl TempDir {
pub fn new(tag: &str) -> TempDir {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock is before the epoch")
.as_nanos();
let counter = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"ingot-run-{tag}-{unique}-{}-{counter}",
std::process::id()
));
std::fs::create_dir_all(&path).expect("creating the scratch directory");
TempDir(path)
}
pub fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
pub struct StubProvider {
pub url: String,
pub served: Arc<AtomicUsize>,
}
pub fn stub_provider(replies: Vec<Value>) -> StubProvider {
let listener = TcpListener::bind("127.0.0.1:0").expect("binding a local port");
let port = listener.local_addr().unwrap().port();
let served = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&served);
thread::spawn(move || {
for stream in listener.incoming().take(replies.len()) {
let Ok(stream) = stream else { break };
let index = counter.fetch_add(1, Ordering::SeqCst);
let reply = replies.get(index).cloned().unwrap_or(Value::Null);
let _ = answer(stream, &reply);
}
});
StubProvider {
url: format!("http://127.0.0.1:{port}/v1/messages"),
served,
}
}
fn answer(mut stream: TcpStream, reply: &Value) -> std::io::Result<()> {
let mut reader = BufReader::new(stream.try_clone()?);
let mut line = String::new();
reader.read_line(&mut line)?;
let mut content_length = 0usize;
loop {
let mut header = String::new();
if reader.read_line(&mut header)? == 0 {
break;
}
let header = header.trim_end();
if header.is_empty() {
break;
}
if let Some((name, value)) = header.split_once(':') {
if name.trim().eq_ignore_ascii_case("content-length") {
content_length = value.trim().parse().unwrap_or(0);
}
}
}
let mut body = vec![0u8; content_length];
reader.read_exact(&mut body)?;
let request: Value = serde_json::from_slice(&body).unwrap_or(Value::Null);
if request.get("stream") == Some(&Value::Bool(true)) {
let (content_type, payload) = (String::from("text/event-stream"), as_event_stream(reply));
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
payload.len()
)?;
stream.write_all(payload.as_bytes())?;
return stream.flush();
}
let payload = serde_json::to_vec(reply)?;
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
payload.len()
)?;
stream.write_all(&payload)?;
stream.flush()
}
fn as_event_stream(reply: &Value) -> String {
let mut out = String::new();
let mut push = |name: &str, data: Value| {
if !name.is_empty() {
out.push_str(&format!("event: {name}\n"));
}
out.push_str(&format!("data: {data}\n\n"));
};
if let Some(content) = reply.get("content") {
let text: String = content
.as_array()
.map(|blocks| {
blocks
.iter()
.filter(|block| block.get("type").and_then(Value::as_str) == Some("text"))
.filter_map(|block| block.get("text").and_then(Value::as_str))
.collect::<Vec<_>>()
.join("")
})
.unwrap_or_default();
push(
"message_start",
json!({ "type": "message_start", "message": {
"id": reply.get("id").cloned().unwrap_or(Value::Null),
"model": reply.get("model").cloned().unwrap_or(Value::Null),
"usage": reply.get("usage").cloned().unwrap_or(json!({})),
}}),
);
for fragment in fragments(&text) {
push(
"content_block_delta",
json!({ "type": "content_block_delta",
"delta": { "type": "text_delta", "text": fragment } }),
);
}
push(
"message_delta",
json!({ "type": "message_delta", "delta": {
"stop_reason": reply.get("stop_reason").cloned().unwrap_or(Value::Null),
"stop_details": reply.get("stop_details").cloned().unwrap_or(Value::Null),
}, "usage": reply.get("usage").cloned().unwrap_or(json!({})) }),
);
push("message_stop", json!({ "type": "message_stop" }));
return out;
}
if let Some(choice) = reply
.get("choices")
.and_then(Value::as_array)
.and_then(|choices| choices.first())
{
let text = choice
.get("message")
.and_then(|message| message.get("content"))
.and_then(Value::as_str)
.unwrap_or_default();
let model = reply.get("model").cloned().unwrap_or(Value::Null);
for fragment in fragments(text) {
push(
"",
json!({ "model": model, "choices": [
{ "index": 0, "delta": { "content": fragment }, "finish_reason": Value::Null }
]}),
);
}
push(
"",
json!({ "model": model, "choices": [{
"index": 0,
"delta": {},
"finish_reason": choice.get("finish_reason").cloned().unwrap_or(Value::Null),
}], "usage": reply.get("usage").cloned().unwrap_or(Value::Null) }),
);
push("", json!("[DONE]"));
return out.replace("data: \"[DONE]\"", "data: [DONE]");
}
format!("data: {reply}\n\n")
}
fn fragments(text: &str) -> Vec<&str> {
if text.is_empty() {
return Vec::new();
}
let mut pieces = Vec::new();
let mut rest = text;
while !rest.is_empty() {
let mut cut = rest.len().min(16);
while cut > 0 && !rest.is_char_boundary(cut) {
cut -= 1;
}
let (piece, tail) = rest.split_at(cut.max(1).min(rest.len()));
pieces.push(piece);
rest = tail;
}
pieces
}
pub fn text_reply(text: &str) -> Value {
json!({
"id": "msg_stub",
"model": "claude-opus-5",
"stop_reason": "end_turn",
"content": [{ "type": "text", "text": text }],
"usage": { "input_tokens": 120, "output_tokens": 40 },
})
}
pub fn openai_reply(text: &str) -> Value {
json!({
"id": "chatcmpl-stub",
"model": "gpt-test",
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": text },
"finish_reason": "stop",
}],
"usage": { "prompt_tokens": 120, "completion_tokens": 40 },
})
}
pub fn run(args: &[&str], base_url: Option<&str>) -> Output {
match base_url {
Some(url) => run_env(
args,
&[
("ANTHROPIC_API_KEY", "stub-key"),
("INGOT_ANTHROPIC_BASE_URL", url),
],
),
None => run_env(args, &[]),
}
}
pub fn run_env(args: &[&str], env: &[(&str, &str)]) -> Output {
let mut command = Command::new(binary());
command.args(args).arg("--color").arg("never");
for name in [
"ANTHROPIC_API_KEY",
"INGOT_ANTHROPIC_BASE_URL",
"OPENAI_API_KEY",
"INGOT_OPENAI_BASE_URL",
] {
command.env_remove(name);
}
for (name, value) in env {
command.env(name, value);
}
command.output().expect("the ingot binary must be runnable")
}
pub fn run_in(cwd: &Path, args: &[&str]) -> Output {
let mut command = Command::new(binary());
command
.args(args)
.arg("--color")
.arg("never")
.current_dir(cwd);
for name in [
"ANTHROPIC_API_KEY",
"INGOT_ANTHROPIC_BASE_URL",
"OPENAI_API_KEY",
"INGOT_OPENAI_BASE_URL",
] {
command.env_remove(name);
}
command.output().expect("the ingot binary must be runnable")
}
pub fn stdout(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
pub fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
pub fn code(output: &Output) -> i32 {
output
.status
.code()
.expect("the process must exit normally")
}
pub fn fs_server() -> PathBuf {
static BUILD: std::sync::Once = std::sync::Once::new();
let mut dir = std::env::current_exe().expect("the test binary has a path");
dir.pop();
if dir.ends_with("deps") {
dir.pop();
}
let path = dir.join(format!("ingot-mcp-fs{}", std::env::consts::EXE_SUFFIX));
BUILD.call_once(|| {
if path.is_file() {
return;
}
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let mut command = Command::new(cargo);
command.current_dir(repo_root()).args([
"build",
"-p",
"ingot-mcp",
"--bin",
"ingot-mcp-fs",
]);
if dir.ends_with("release") {
command.arg("--release");
}
let status = command.status().expect("cargo must be runnable");
assert!(status.success(), "building ingot-mcp-fs failed");
});
assert!(
path.is_file(),
"expected the reference MCP server at {}",
path.display()
);
path
}
pub fn toml_string(value: &str) -> String {
format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
}
pub fn authoring_cassette(dir: &Path, name: &str, replies: &[&str]) -> PathBuf {
let interactions: Vec<Value> = replies
.iter()
.enumerate()
.map(|(index, reply)| {
json!({
"index": index,
"node": format!("authoring.{index}"),
"requestDigest": "0".repeat(64),
"responseType": "text",
"value": format!("```ingot\n{reply}```"),
"usage": { "inputTokens": 800, "outputTokens": 200 },
"model": "test/authoring",
})
})
.collect();
let cassette = json!({
"cassetteVersion": "0.1",
"agent": "ingot.authoring",
"interactions": interactions,
});
let path = dir.join(name);
std::fs::write(
&path,
serde_json::to_string_pretty(&cassette).expect("a cassette is serializable"),
)
.expect("writing the authoring cassette");
path
}