use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
fn sandbox_dir() -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before epoch")
.as_nanos();
let dir = std::env::temp_dir().join(format!("mermaid-ndjson-{}-{}", std::process::id(), nonce));
std::fs::create_dir_all(&dir).expect("create sandbox dir");
dir
}
fn run_sandboxed(home: &Path, extra_args: &[&str]) -> std::process::Output {
let work = home.join("work");
std::fs::create_dir_all(&work).expect("create workdir");
let mut args = vec!["--model", "anthropic/pty-exit-test"];
args.extend_from_slice(extra_args);
Command::new(env!("CARGO_BIN_EXE_mermaid"))
.args(&args)
.current_dir(&work)
.env("HOME", home)
.env("XDG_CONFIG_HOME", home.join("config"))
.env("XDG_DATA_HOME", home.join("data"))
.env_remove("ANTHROPIC_API_KEY")
.output()
.expect("spawn mermaid")
}
fn ndjson_lines(stdout: &str) -> Vec<serde_json::Value> {
stdout
.lines()
.filter(|l| !l.trim().is_empty())
.map(|line| {
serde_json::from_str(line)
.unwrap_or_else(|e| panic!("stream line is not JSON ({e}): {line:?}"))
})
.collect()
}
#[test]
fn run_ndjson_stream_opens_with_session_started_and_ends_with_result() {
let home = sandbox_dir();
let output = run_sandboxed(&home, &["run", "--format", "ndjson", "hi"]);
let stdout = String::from_utf8_lossy(&output.stdout);
let lines = ndjson_lines(&stdout);
assert!(
lines.len() >= 2,
"expected at least session_started + result, got: {stdout:?}"
);
for value in &lines {
assert!(
value.get("type").and_then(|t| t.as_str()).is_some(),
"stream line missing a `type` tag: {value:?}"
);
}
let first = &lines[0];
assert_eq!(
first["type"], "session_started",
"first line must open the stream"
);
assert_eq!(
first["protocol_version"], 1,
"protocol version must be pinned"
);
let last = &lines[lines.len() - 1];
assert_eq!(
last["type"], "result",
"last line must be the terminal result"
);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn run_emits_session_id_that_resumes_the_same_session() {
let home = sandbox_dir();
let output = run_sandboxed(&home, &["run", "--format", "ndjson", "hi"]);
let stdout = String::from_utf8_lossy(&output.stdout);
let lines = ndjson_lines(&stdout);
let session_id = lines[0]["session_id"]
.as_str()
.expect("session_started carries session_id")
.to_string();
assert_eq!(session_id.len(), 19, "unexpected id shape: {session_id}");
assert_eq!(
lines[lines.len() - 1]["session_id"].as_str(),
Some(session_id.as_str())
);
let conversation = home
.join("work")
.join(".mermaid")
.join("conversations")
.join(format!("{session_id}.json"));
assert!(
conversation.exists(),
"session file missing: {}",
conversation.display()
);
let first_len = std::fs::metadata(&conversation).expect("stat").len();
let output = run_sandboxed(
&home,
&[
"--resume",
&session_id,
"run",
"--format",
"ndjson",
"again",
],
);
let stdout = String::from_utf8_lossy(&output.stdout);
let lines = ndjson_lines(&stdout);
assert_eq!(
lines[0]["session_id"].as_str(),
Some(session_id.as_str()),
"resumed run must keep the session id"
);
let second_len = std::fs::metadata(&conversation).expect("stat").len();
assert!(
second_len > first_len,
"resumed run must append to the session file ({first_len} -> {second_len})"
);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn headless_resume_error_paths_are_clear() {
let home = sandbox_dir();
let output = run_sandboxed(&home, &["--resume", "19990101_000000_000", "run", "x"]);
assert!(
!output.status.success(),
"missing session id must fail the run"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("19990101_000000_000"),
"error must name the id: {stderr}"
);
let output = run_sandboxed(&home, &["--resume", "run", "x"]);
assert!(!output.status.success(), "bare --resume must fail headless");
let output = run_sandboxed(&home, &["--continue", "run", "x"]);
assert!(
!output.status.success(),
"--continue with no session must fail"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("no saved session"),
"error must explain: {stderr}"
);
let _ = std::fs::remove_dir_all(&home);
}