use super::*;
use crate::template::RealGit;
use std::io;
fn repo(keep: &[&str]) -> tempfile::TempDir {
let dir = tempfile::TempDir::new().unwrap();
let (wt, g) = (dir.path(), RealGit::new());
g.run(wt, &["init", "-b", "agents/a1"]).unwrap();
g.run(wt, &["config", "user.email", "t@t"]).unwrap();
g.run(wt, &["config", "user.name", "t"]).unwrap();
g.run(wt, &["config", "core.hooksPath", "/dev/null"])
.unwrap();
std::fs::create_dir_all(wt.join("descriptions/tools")).unwrap();
std::fs::create_dir_all(wt.join("descriptions/skills")).unwrap();
for tool in ["bash", "message"] {
std::fs::write(
wt.join(format!("descriptions/tools/{tool}.json")),
format!("{{\"schema\":\"{tool}\"}}\n"),
)
.unwrap();
}
std::fs::write(wt.join("descriptions/skills/message.md"), "how to send\n").unwrap();
g.run(wt, &["add", "-A"]).unwrap();
g.run(wt, &["commit", "-m", "config"]).unwrap();
let drop: Vec<String> = ["bash", "message"]
.iter()
.filter(|t| !keep.contains(t))
.flat_map(|t| {
[
format!("descriptions/tools/{t}.json"),
format!("descriptions/skills/{t}.md"),
]
})
.collect();
if !drop.is_empty() {
let mut args: Vec<&str> = vec!["rm", "-q", "--ignore-unmatch", "--"];
args.extend(drop.iter().map(String::as_str));
g.run(wt, &args).unwrap();
g.run(wt, &["commit", "-m", "fork: cut to the grant"])
.unwrap();
}
dir
}
fn config_commit(wt: &std::path::Path) -> String {
RealGit::new()
.run_capture(wt, &["rev-list", "--max-parents=0", "HEAD"])
.unwrap()
.trim()
.to_string()
}
fn head(wt: &std::path::Path) -> String {
RealGit::new()
.run_capture(wt, &["rev-parse", "HEAD"])
.unwrap()
.trim()
.to_string()
}
fn run(wt: &std::path::Path, tools: &[&str]) -> Result<bool, Error> {
let owned: Vec<String> = tools.iter().map(|t| (*t).to_string()).collect();
refresh(
wt,
"a1",
&Grant {
role: "worker",
tools: &owned,
config_commit: &config_commit(wt),
},
&RealGit::new(),
)
}
#[test]
fn a_tip_that_widens_the_grant_brings_the_new_descriptors_into_the_tree() {
let dir = repo(&["bash"]);
let wt = dir.path();
let before = head(wt);
assert!(!wt.join("descriptions/tools/message.json").exists());
assert!(run(wt, &["bash", "message"]).unwrap(), "it committed");
assert!(wt.join("descriptions/tools/message.json").exists());
assert!(
wt.join("descriptions/skills/message.md").exists(),
"the claimed frontmatter comes with its schema (§3.3)"
);
assert_ne!(head(wt), before, "and it landed as a commit");
let subject = RealGit::new()
.run_capture(wt, &["log", "-1", "--format=%s"])
.unwrap();
assert!(subject.contains("descriptors: follow the config tip [a1]"));
}
#[test]
fn a_tip_that_revokes_a_grant_takes_the_stale_descriptors_out_of_the_tree() {
let dir = repo(&["bash", "message"]);
let wt = dir.path();
assert!(run(wt, &["bash"]).unwrap(), "it committed");
assert!(!wt.join("descriptions/tools/message.json").exists());
assert!(!wt.join("descriptions/skills/message.md").exists());
assert!(wt.join("descriptions/tools/bash.json").exists());
}
#[test]
fn an_unchanged_boundary_commits_nothing() {
let dir = repo(&["bash"]);
let wt = dir.path();
let before = head(wt);
assert!(!run(wt, &["bash"]).unwrap(), "nothing moved");
assert_eq!(head(wt), before, "so no commit was made");
}
#[test]
fn a_granted_tool_the_commit_does_not_describe_is_noticed_never_declined() {
let dir = repo(&["bash"]);
let wt = dir.path();
let before = head(wt);
assert!(!run(wt, &["bash", "ghost"]).unwrap());
assert_eq!(head(wt), before);
assert!(
wt.join("descriptions/tools/bash.json").exists(),
"and the described half of the grant is untouched"
);
}
#[test]
fn a_still_granted_tool_the_commit_stopped_describing_keeps_its_bytes() {
let dir = repo(&["bash", "message"]);
let wt = dir.path();
let g = RealGit::new();
g.run(wt, &["rm", "-q", "--", "descriptions/tools/message.json"])
.unwrap();
g.run(wt, &["commit", "-m", "config: stop describing message"])
.unwrap();
let narrowed = head(wt);
std::fs::write(
wt.join("descriptions/tools/message.json"),
"{\"schema\":\"message\"}\n",
)
.unwrap();
g.run(wt, &["add", "-A"]).unwrap();
g.run(wt, &["commit", "-m", "the tree still carries it"])
.unwrap();
let owned = ["bash".to_string(), "message".to_string()];
let moved = refresh(
wt,
"a1",
&Grant {
role: "worker",
tools: &owned,
config_commit: &narrowed,
},
&g,
)
.unwrap();
assert!(!moved);
assert!(wt.join("descriptions/tools/message.json").exists());
}
struct OneFails(&'static str);
impl GitRunner for OneFails {
fn run(&self, _dir: &std::path::Path, args: &[&str]) -> io::Result<()> {
match args.first() {
Some(op) if *op == self.0 => Err(io::Error::other("boom")),
_ => Ok(()),
}
}
fn run_capture(&self, _dir: &std::path::Path, args: &[&str]) -> io::Result<String> {
match args.first() {
Some(op) if *op == self.0 => Err(io::Error::other("boom")),
_ => Ok(" M descriptions/tools/bash.json\n".to_string()),
}
}
}
#[test]
fn a_failing_status_and_a_failing_commit_each_surface_as_a_named_git_error() {
let owned = ["bash".to_string()];
let grant = Grant {
role: "worker",
tools: &owned,
config_commit: "cafe1234",
};
let wt = std::path::Path::new("/nowhere");
let err = refresh(wt, "a1", &grant, &OneFails("status")).unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "descriptor refresh status",
..
}
),
"{err:?}"
);
let err = refresh(wt, "a1", &grant, &OneFails("commit")).unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "descriptor refresh commit",
..
}
),
"{err:?}"
);
}