use super::fixture::Fixture;
use crate::git_tree::{GitTree, GitTreeError, ToolCallState};
use tempfile::tempdir;
#[test]
fn from_repo_errors_when_repo_missing() {
let dir = tempdir().unwrap();
let Err(err) = GitTree::from_repo(&dir.path().join("nope")) else {
panic!("expected error");
};
assert!(
matches!(err, GitTreeError::Git { .. } | GitTreeError::Spawn(_)),
"got {err:?}"
);
}
#[test]
fn from_repo_errors_when_repo_git_absent() {
let dir = tempdir().unwrap();
let err = GitTree::from_repo(dir.path()).unwrap_err();
assert!(
matches!(err, GitTreeError::Git { .. } | GitTreeError::Spawn(_)),
"got {err:?}"
);
}
#[test]
fn from_repo_surfaces_the_config_lineage_as_the_trunk() {
let fx = Fixture::new();
fx.commit_other("workflow.yaml", "events: {}\n");
let tree = GitTree::from_repo(&fx.path).unwrap();
assert_eq!(tree.commits.len(), 2);
assert_eq!(tree.commits[0].subject, "config: init [config/default]");
assert_eq!(tree.commits[1].subject, "add workflow.yaml");
assert_eq!(tree.commits[0].short_oid.len(), 8);
assert!(tree.agents.is_empty());
}
#[test]
fn from_repo_agent_surfaces_with_steps_and_preview() {
let fx = Fixture::new();
fx.build_agent("20260422T120500Z-b002", "ping v03");
let tree = GitTree::from_repo(&fx.path).unwrap();
assert_eq!(tree.commits.len(), 1, "config lineage untouched");
assert_eq!(tree.agents.len(), 1);
let agent = &tree.agents[0];
assert_eq!(agent.branch_name, "agents/20260422T120500Z-b002");
assert_eq!(agent.agent_id, "20260422T120500Z-b002");
assert_eq!(agent.preview.as_deref(), Some("ping v03"));
assert_eq!(agent.steps.len(), 2);
assert!(agent.steps[0].subject.contains("dispatch"));
assert!(
agent.steps[1].subject.contains("Merge"),
"{:?}",
agent.steps[1].subject
);
assert_eq!(agent.tip_short_oid.len(), 8);
assert_eq!(agent.pending_messages, 0);
assert!(agent.conflicted_oid.is_none());
assert!(agent.budget_oid.is_none());
assert!(agent.abandoned_oid.is_none());
assert!(agent.notify_oid.is_none());
}
#[test]
fn from_repo_multiple_agents_appear() {
let fx = Fixture::new();
fx.build_agent("20260422T120000Z-old0", "first prompt");
fx.build_agent("20260422T120500Z-new0", "second prompt");
let tree = GitTree::from_repo(&fx.path).unwrap();
assert_eq!(tree.commits.len(), 1);
assert_eq!(tree.agents.len(), 2);
let ids: Vec<&str> = tree.agents.iter().map(|a| a.agent_id.as_str()).collect();
assert!(ids.contains(&"20260422T120000Z-old0"), "{ids:?}");
assert!(ids.contains(&"20260422T120500Z-new0"), "{ids:?}");
}
#[test]
fn from_repo_surfaces_pending_message_count() {
let fx = Fixture::new();
fx.build_agent("20260422T121000Z-msg0", "with mail");
fx.deposit_message("20260422T121000Z-msg0", "user-001.md", "hi");
fx.deposit_message("20260422T121000Z-msg0", "p1-002.md", "steer");
fx.deposit_message("20260422T121000Z-msg0", ".user-003.md.tmp", "partial");
let tree = GitTree::from_repo(&fx.path).unwrap();
assert_eq!(tree.agents[0].pending_messages, 2);
}
#[test]
fn from_repo_surfaces_all_four_mark_oids() {
let fx = Fixture::new();
fx.build_agent("20260422T121500Z-mrk0", "marked");
fx.mark_ref("refs/lernie/conflicted/20260422T121500Z-mrk0");
fx.mark_ref("refs/lernie/budget-exhausted/20260422T121500Z-mrk0");
fx.mark_ref("refs/lernie/abandoned/20260422T121500Z-mrk0");
fx.mark_ref("refs/lernie/notify/20260422T121500Z-mrk0");
let tree = GitTree::from_repo(&fx.path).unwrap();
let agent = &tree.agents[0];
for oid in [
&agent.conflicted_oid,
&agent.budget_oid,
&agent.abandoned_oid,
&agent.notify_oid,
] {
assert_eq!(oid.as_deref().map(str::len), Some(40));
}
}
#[test]
fn from_repo_marks_only_the_named_agent() {
let fx = Fixture::new();
fx.build_agent("20260422T122000Z-aaa0", "marked");
fx.build_agent("20260422T122000Z-bbb0", "clean");
fx.mark_ref("refs/lernie/budget-exhausted/20260422T122000Z-aaa0");
let tree = GitTree::from_repo(&fx.path).unwrap();
let marked = tree
.agents
.iter()
.find(|a| a.agent_id == "20260422T122000Z-aaa0")
.unwrap();
let clean = tree
.agents
.iter()
.find(|a| a.agent_id == "20260422T122000Z-bbb0")
.unwrap();
assert!(marked.budget_oid.is_some());
assert!(clean.budget_oid.is_none());
}
#[test]
fn from_repo_enumerates_a_child_agent_for_the_descent_tree() {
let fx = Fixture::new();
fx.build_agent("20260422T123000Z-par0", "parent");
fx.build_child("20260422T123000Z-par0", "20260422T123000Z-par0-c1");
let tree = GitTree::from_repo(&fx.path).unwrap();
let ids: Vec<&str> = tree.agents.iter().map(|a| a.agent_id.as_str()).collect();
assert!(ids.contains(&"20260422T123000Z-par0"), "{ids:?}");
assert!(ids.contains(&"20260422T123000Z-par0-c1"), "{ids:?}");
}
#[test]
fn from_repo_agent_without_request_json_drops_preview() {
let fx = Fixture::new();
fx.build_agent("20260422T130000Z-xxxx", "seed");
std::fs::remove_dir_all(fx.path.join("steps/20260422T130000Z-xxxx")).unwrap();
let tree = GitTree::from_repo(&fx.path).unwrap();
assert_eq!(tree.agents.len(), 1);
assert!(tree.agents[0].preview.is_none());
assert!(tree.agents[0].streaming_text.is_none());
}
#[test]
fn from_repo_surfaces_partial_response_text() {
let fx = Fixture::new();
fx.build_agent("20260427T120100Z-strm", "summarize Rust ownership");
fx.write_response_events(
"20260427T120100Z-strm",
1,
&[
r#"{"type":"message_start","message":{"id":"m1"}}"#,
r#"{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#,
r#"{"type":"content_delta","index":0,"delta":{"text_delta":"Rust"}}"#,
r#"{"type":"content_delta","index":0,"delta":{"text_delta":" tracks"}}"#,
r#"{"type":"content_delta","index":0,"delta":{"text_delta":" ownership"}}"#,
],
);
let tree = GitTree::from_repo(&fx.path).unwrap();
assert_eq!(tree.agents.len(), 1);
assert_eq!(
tree.agents[0].streaming_text.as_deref(),
Some("Rust tracks ownership")
);
}
#[test]
fn from_repo_picks_latest_step_response_text() {
let fx = Fixture::new();
fx.build_agent("20260427T120200Z-loop", "step into the loop");
fx.write_response_events(
"20260427T120200Z-loop",
1,
&[r#"{"type":"content_delta","index":0,"delta":{"text_delta":"first step body"}}"#],
);
fx.write_response_events(
"20260427T120200Z-loop",
2,
&[r#"{"type":"content_delta","index":0,"delta":{"text_delta":"second step partial"}}"#],
);
let tree = GitTree::from_repo(&fx.path).unwrap();
assert_eq!(
tree.agents[0].streaming_text.as_deref(),
Some("second step partial")
);
}
#[test]
fn from_repo_with_response_but_no_text_deltas_yet() {
let fx = Fixture::new();
fx.build_agent("20260427T120300Z-prep", "still preparing");
fx.write_response_events(
"20260427T120300Z-prep",
1,
&[r#"{"type":"message_start","message":{"id":"m1"}}"#],
);
let tree = GitTree::from_repo(&fx.path).unwrap();
assert!(tree.agents[0].streaming_text.is_none());
}
#[test]
fn from_repo_surfaces_in_flight_and_complete_tool_calls() {
let fx = Fixture::new();
fx.build_agent("20260427T140000Z-tool", "run two tools");
fx.write_tool_call("20260427T140000Z-tool", 1, "toolu_done", Some(b"{}"));
fx.write_tool_call("20260427T140000Z-tool", 1, "toolu_live", None);
let tree = GitTree::from_repo(&fx.path).unwrap();
let calls = &tree.agents[0].tool_calls;
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].tool_id, "toolu_done");
assert_eq!(calls[0].state, ToolCallState::Complete);
assert_eq!(calls[1].tool_id, "toolu_live");
assert_eq!(calls[1].state, ToolCallState::InFlight);
}
#[test]
fn from_repo_without_tool_calls_yields_empty_vec() {
let fx = Fixture::new();
fx.build_agent("20260427T140100Z-bare", "no tools yet");
let tree = GitTree::from_repo(&fx.path).unwrap();
assert!(tree.agents[0].tool_calls.is_empty());
}