use super::*;
use brazen::Content;
use serde_json::Value;
use std::cell::RefCell;
use std::io;
use std::path::PathBuf;
use tempfile::TempDir;
#[derive(Default)]
struct RecordGit {
runs: RefCell<Vec<(PathBuf, Vec<String>)>>,
}
impl GitRunner for RecordGit {
fn run(&self, dest: &Path, args: &[&str]) -> io::Result<()> {
self.runs.borrow_mut().push((
dest.to_path_buf(),
args.iter().map(|s| (*s).to_owned()).collect(),
));
Ok(())
}
fn run_capture(&self, dest: &Path, args: &[&str]) -> io::Result<String> {
self.run(dest, args)?;
Ok(String::new())
}
}
fn write_entry(worktree: &Path, name: &str) {
let dir = worktree.join(MESSAGES_DIR);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(name), b"[]").unwrap();
}
#[test]
fn next_seq_is_one_when_messages_dir_is_absent() {
let dir = TempDir::new().unwrap();
assert_eq!(next_seq(dir.path()).unwrap(), 1);
}
#[test]
fn next_seq_is_one_when_messages_dir_is_empty() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(MESSAGES_DIR)).unwrap();
assert_eq!(next_seq(dir.path()).unwrap(), 1);
}
#[test]
fn next_seq_is_max_prefix_plus_one_ignoring_unparsable_names() {
let dir = TempDir::new().unwrap();
write_entry(dir.path(), "001-claude-fable-5.json");
write_entry(dir.path(), "004-tool.json");
write_entry(dir.path(), "002-claude-fable-5.json");
write_entry(dir.path(), "notes.txt");
assert_eq!(next_seq(dir.path()).unwrap(), 5);
}
#[test]
fn deliver_message_renames_the_inbox_file_in_and_commits_it() {
let dir = TempDir::new().unwrap();
let worktree = dir.path();
write_entry(worktree, "001-claude-fable-5.json");
let src = dir.path().join("bob-005.md");
std::fs::write(&src, "---\nfrom: bob\n---\nhello").unwrap();
let git = RecordGit::default();
deliver_message(worktree, "conv-3", "bob", &src, &git).unwrap();
assert!(!src.exists(), "inbox file renamed away");
let entry = worktree.join("messages/002-bob.md");
assert_eq!(
std::fs::read_to_string(&entry).unwrap(),
"---\nfrom: bob\n---\nhello"
);
let runs = git.runs.borrow();
assert_eq!(runs[0].1, vec!["add", "messages/002-bob.md"]);
assert!(runs[1].1[2].contains("transcript 002: bob"));
assert!(runs[1].1[2].contains("[conv-3]"));
}
#[test]
fn commit_assistant_renames_staging_and_commits_at_the_model_id_origin() {
let dir = TempDir::new().unwrap();
let worktree = dir.path();
let staging = worktree.join("staging.json");
std::fs::write(&staging, br#"[{"type":"text","text":"hi"}]"#).unwrap();
let git = RecordGit::default();
commit_assistant(worktree, "conv-1", "claude-fable-5", &staging, &git).unwrap();
assert!(!staging.exists(), "staging renamed away");
let entry = worktree.join("messages/001-claude-fable-5.json");
let blocks: Vec<Content> = serde_json::from_slice(&std::fs::read(&entry).unwrap()).unwrap();
assert_eq!(blocks, vec![Content::Text("hi".into())]);
let runs = git.runs.borrow();
assert_eq!(runs.len(), 2);
assert_eq!(runs[0].0, worktree);
assert_eq!(runs[0].1, vec!["add", "messages/001-claude-fable-5.json"]);
assert_eq!(runs[1].1[0], "commit");
assert!(runs[1].1[2].contains("transcript 001: claude-fable-5"));
assert!(runs[1].1[2].contains("[conv-1]"));
}
#[test]
fn commit_assistant_declines_a_model_id_colliding_with_the_reserved_tool_token() {
let dir = TempDir::new().unwrap();
let staging = dir.path().join("staging.json");
std::fs::write(&staging, b"[]").unwrap();
let git = RecordGit::default();
let err = commit_assistant(dir.path(), "c", "tool", &staging, &git).unwrap_err();
assert!(
matches!(&err, Error::ReservedModelId(m) if m == "tool"),
"got {err:?}"
);
assert!(git.runs.borrow().is_empty());
assert!(staging.exists(), "staging untouched on decline");
}
#[test]
fn commit_tool_writes_a_single_block_array_and_commits_at_the_next_seq() {
let dir = TempDir::new().unwrap();
let worktree = dir.path();
write_entry(worktree, "001-claude-fable-5.json");
let tool_result = Content::ToolResult {
tool_use_id: "toolu_9".into(),
content: vec![Content::Text("out".into())],
is_error: false,
};
let git = RecordGit::default();
commit_tool(worktree, "conv-2", &tool_result, &git).unwrap();
let entry = worktree.join("messages/002-tool.json");
let raw: Value = serde_json::from_slice(&std::fs::read(&entry).unwrap()).unwrap();
assert_eq!(raw.as_array().unwrap().len(), 1);
assert_eq!(raw[0]["type"], "tool_result");
assert_eq!(raw[0]["tool_use_id"], "toolu_9");
let blocks: Vec<Content> = serde_json::from_value(raw).unwrap();
assert_eq!(blocks, vec![tool_result]);
let runs = git.runs.borrow();
assert_eq!(runs[0].1, vec!["add", "-A"]);
assert!(runs[1].1[2].contains("transcript 002: tool [conv-2]"));
}
#[test]
fn entry_rel_zero_pads_to_three_digits() {
assert_eq!(
entry_rel(7, "claude-fable-5"),
"messages/007-claude-fable-5.json"
);
assert_eq!(entry_rel(42, "tool"), "messages/042-tool.json");
}
#[test]
fn commit_add_failure_surfaces_as_a_git_error() {
#[derive(Default)]
struct FailGit;
impl GitRunner for FailGit {
fn run(&self, _: &Path, _: &[&str]) -> io::Result<()> {
Err(io::Error::other("boom"))
}
fn run_capture(&self, _: &Path, _: &[&str]) -> io::Result<String> {
Err(io::Error::other("boom"))
}
}
let dir = TempDir::new().unwrap();
let staging = dir.path().join("staging.json");
std::fs::write(&staging, b"[]").unwrap();
let err = commit_assistant(dir.path(), "c", "claude-fable-5", &staging, &FailGit).unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "transcript add",
..
}
),
"got {err:?}"
);
}
#[test]
fn commit_verb_failure_surfaces_as_a_git_error() {
struct FailCommit {
n: RefCell<usize>,
}
impl GitRunner for FailCommit {
fn run(&self, _: &Path, _: &[&str]) -> io::Result<()> {
let mut n = self.n.borrow_mut();
*n += 1;
if *n >= 2 {
Err(io::Error::other("boom"))
} else {
Ok(())
}
}
fn run_capture(&self, _: &Path, _: &[&str]) -> io::Result<String> {
Ok(String::new())
}
}
let dir = TempDir::new().unwrap();
let staging = dir.path().join("staging.json");
std::fs::write(&staging, b"[]").unwrap();
let git = FailCommit { n: RefCell::new(0) };
let err = commit_assistant(dir.path(), "c", "claude-fable-5", &staging, &git).unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "transcript commit",
..
}
),
"got {err:?}"
);
}
#[test]
fn next_seq_surfaces_a_non_not_found_io_error() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(MESSAGES_DIR), b"not a dir").unwrap();
let err = next_seq(dir.path()).unwrap_err();
assert!(matches!(err, Error::Io(_)), "got {err:?}");
}