use super::{NoAdapter, NoLauncher, NoSleeper, Recorder, Resolution, branch_with_step};
use crate::config::Workflow;
use crate::template::{GitRunner, RealGit};
use brazen::Content;
use serde_json::json;
use std::io;
use std::path::Path;
use std::sync::atomic::AtomicBool;
use tempfile::TempDir;
const AGENTS_ONLY: &str = "events: {}\ncontext_files: [AGENTS.md]\n";
fn repo_with_context_files(git: &RealGit) -> TempDir {
let dir = TempDir::new().unwrap();
git.run(dir.path(), &["init"]).unwrap();
std::fs::write(dir.path().join("AGENTS.md"), "top rules\n").unwrap();
std::fs::create_dir(dir.path().join("sub")).unwrap();
std::fs::write(dir.path().join("sub/AGENTS.md"), "sub rules\n").unwrap();
std::fs::write(dir.path().join("sub/CLAUDE.md"), "claude rules\n").unwrap();
dir
}
fn seated_at(ws: &TempDir, agent_id: &str, dir: &Path, git: &RealGit) {
let repo = crate::workspace::repo_git(ws.path());
git.run(ws.path(), &["init", "--bare", repo.to_str().unwrap()])
.unwrap();
let resolved = crate::workspace::cwd::resolve(dir).unwrap();
crate::workspace::cwd::write(ws.path(), agent_id, &resolved, git).unwrap();
}
fn appended(ws: &TempDir, worktree: &Path, agent: &str, yaml: &str, git: &dyn GitRunner) -> String {
let workflow = Workflow::parse(yaml, Path::new("workflow.yaml")).unwrap();
let mut content = b"Exit code: 0\nok\n".to_vec();
super::super::context::append(&mut content, ws.path(), worktree, agent, &workflow, git)
.unwrap();
String::from_utf8(content).unwrap()
}
fn entry_framing(worktree: &Path, seq: &str, path: &Path) {
let block = Content::ToolResult {
tool_use_id: "t0".into(),
content: vec![Content::Text(format!(
"Exit code: 0\n<file path=\"{}\">\nrules\n</file>\n",
path.display()
))],
is_error: false,
};
let entry = serde_json::to_vec(&[block]).unwrap();
std::fs::write(worktree.join(format!("messages/{seq}-tool.json")), entry).unwrap();
}
#[test]
fn every_directory_from_the_repository_top_down_to_the_cwd_rides_top_first() {
let agent_id = "agent-b66b";
let ws = TempDir::new().unwrap();
let git = RealGit::new();
let (worktree, _) = branch_with_step(&ws, agent_id, &git);
let repo = repo_with_context_files(&git);
seated_at(&ws, agent_id, &repo.path().join("sub"), &git);
let out = appended(&ws, &worktree, agent_id, AGENTS_ONLY, &git);
let top = out.find("top rules").expect("the top-level file rides");
let sub = out.find("sub rules").expect("the cwd's own file rides");
assert!(top < sub, "the path is walked downward: top first\n{out}");
assert!(!out.contains("claude rules"), "{out}");
}
#[test]
fn an_omitted_list_discovers_nothing() {
let agent_id = "agent-none";
let ws = TempDir::new().unwrap();
let git = RealGit::new();
let (worktree, _) = branch_with_step(&ws, agent_id, &git);
let repo = repo_with_context_files(&git);
seated_at(&ws, agent_id, repo.path(), &git);
let out = appended(&ws, &worktree, agent_id, "events: {}\n", &git);
assert_eq!(out, "Exit code: 0\nok\n");
}
#[test]
fn a_file_a_committed_entry_already_frames_is_not_shown_again() {
let agent_id = "agent-shown";
let ws = TempDir::new().unwrap();
let git = RealGit::new();
let (worktree, _) = branch_with_step(&ws, agent_id, &git);
let repo = repo_with_context_files(&git);
seated_at(&ws, agent_id, repo.path(), &git);
let framed = std::fs::canonicalize(repo.path().join("AGENTS.md")).unwrap();
entry_framing(&worktree, "002", &framed);
assert_eq!(
appended(&ws, &worktree, agent_id, AGENTS_ONLY, &git),
"Exit code: 0\nok\n"
);
std::fs::remove_file(worktree.join("messages/002-tool.json")).unwrap();
assert!(appended(&ws, &worktree, agent_id, AGENTS_ONLY, &git).contains("top rules"));
}
#[test]
fn a_cwd_outside_any_repository_carries_that_directory_alone() {
let agent_id = "agent-loose";
let ws = TempDir::new().unwrap();
let git = RealGit::new();
let (worktree, _) = branch_with_step(&ws, agent_id, &git);
let loose = TempDir::new().unwrap();
std::fs::write(loose.path().join("AGENTS.md"), "loose rules").unwrap();
seated_at(&ws, agent_id, loose.path(), &git);
let out = appended(&ws, &worktree, agent_id, AGENTS_ONLY, &git);
assert!(out.ends_with("\nloose rules\n</file>\n"), "{out}");
}
struct Astray;
impl GitRunner for Astray {
fn run(&self, _dest: &Path, _args: &[&str]) -> io::Result<()> {
unreachable!("the append only captures")
}
fn run_capture(&self, _dest: &Path, _args: &[&str]) -> io::Result<String> {
Ok("/no/such/toplevel".to_string())
}
}
#[test]
fn a_toplevel_that_is_not_the_cwds_prefix_falls_back_to_the_cwd_alone() {
let agent_id = "agent-astray";
let ws = TempDir::new().unwrap();
let git = RealGit::new();
let (worktree, _) = branch_with_step(&ws, agent_id, &git);
std::fs::write(worktree.join("AGENTS.md"), "worktree rules").unwrap();
let out = appended(&ws, &worktree, agent_id, AGENTS_ONLY, &Astray);
assert!(out.contains("worktree rules"), "{out}");
}
#[test]
fn a_result_not_ending_in_a_newline_gains_a_separator_before_the_frame() {
let agent_id = "agent-sep";
let ws = TempDir::new().unwrap();
let git = RealGit::new();
let (worktree, _) = branch_with_step(&ws, agent_id, &git);
std::fs::write(worktree.join("AGENTS.md"), "worktree rules\n").unwrap();
let workflow = Workflow::parse(AGENTS_ONLY, Path::new("workflow.yaml")).unwrap();
let mut content = b"Exit code: 0\nno newline".to_vec();
super::super::context::append(
&mut content,
ws.path(),
&worktree,
agent_id,
&workflow,
&git,
)
.unwrap();
let out = String::from_utf8(content).unwrap();
assert!(
out.starts_with("Exit code: 0\nno newline\n<file path="),
"{out}"
);
}
#[test]
fn the_seeded_cwds_files_ride_the_first_result_and_no_later_one() {
let agent_id = "agent-seed";
let ws = TempDir::new().unwrap();
let git = RealGit::new();
let (worktree, step_dir_rel) = branch_with_step(&ws, agent_id, &git);
let repo = repo_with_context_files(&git);
seated_at(&ws, agent_id, repo.path(), &git);
let mut resolution = Resolution::new();
resolution.workflow = Workflow::parse(AGENTS_ONLY, Path::new("workflow.yaml")).unwrap();
let recorder = Recorder(std::cell::RefCell::new(Vec::new()));
let stop = AtomicBool::new(false);
let clock = crate::prompt::clock::SystemClock;
let id_gen = crate::prompt::NanoIdGen;
let cfg = TempDir::new().unwrap();
let deps = crate::prompt::Deps {
adapter: &NoAdapter,
sleeper: &NoSleeper,
git: &git,
clock: &clock,
id_gen: &id_gen,
tool_executor: &recorder,
config_root: cfg.path(),
data_root: cfg.path(),
adapter_target: None,
stop: &stop,
launcher: &NoLauncher,
rng: crate::workspace::agent_name::mint::test_rng(),
};
let grant = ["bash".to_string()];
for id in ["t1", "t2"] {
let content = vec![Content::ToolUse {
id: id.into(),
name: "bash".into(),
input: json!({"command": "true"}),
signature: None,
}];
super::super::run_tool_calls(
ws.path(),
&worktree,
agent_id,
&resolution.of(crate::prompt::WORKER_ROLE, &grant),
&step_dir_rel,
&content,
&deps,
)
.unwrap();
}
let first = std::fs::read_to_string(worktree.join("messages/002-tool.json")).unwrap();
let second = std::fs::read_to_string(worktree.join("messages/003-tool.json")).unwrap();
assert!(first.contains("top rules"), "{first}");
assert!(!second.contains("top rules"), "{second}");
}