use crate::prompt::Error;
use crate::template::GitRunner;
use std::path::Path;
pub mod validate;
const DISPATCH_PREFIX: &str = "dispatch: ";
pub fn derive(
dir: &Path,
start: &str,
agent_id: &str,
git: &dyn GitRunner,
) -> Result<Option<String>, Error> {
let pattern = format!("^dispatch: .+ \\[{agent_id}\\]$");
let subject = git
.run_capture(
dir,
&[
"log",
"-n",
"1",
"--format=%s",
"-E",
"--grep",
pattern.as_str(),
start,
],
)
.map_err(|source| Error::Git {
op: "role derive log",
source,
})?;
Ok(parse_role(subject.trim()))
}
pub fn founding_sha(
dir: &Path,
start: &str,
agent_id: &str,
git: &dyn GitRunner,
) -> Result<Option<String>, Error> {
let pattern = format!("^dispatch: .+ \\[{agent_id}\\]$");
let sha = git
.run_capture(
dir,
&[
"log",
"-n",
"1",
"--format=%H",
"-E",
"--grep",
pattern.as_str(),
start,
],
)
.map_err(|source| Error::Git {
op: "founding sha log",
source,
})?;
let sha = sha.trim();
Ok((!sha.is_empty()).then(|| sha.to_string()))
}
fn parse_role(subject: &str) -> Option<String> {
subject
.strip_prefix(DISPATCH_PREFIX)?
.split_whitespace()
.next()
.map(str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::io;
struct SubjGit {
subject: String,
args: RefCell<Vec<String>>,
fail: bool,
}
impl SubjGit {
fn returning(subject: &str) -> Self {
Self {
subject: subject.to_string(),
args: RefCell::new(Vec::new()),
fail: false,
}
}
}
impl GitRunner for SubjGit {
fn run(&self, _dest: &Path, _args: &[&str]) -> io::Result<()> {
unreachable!("role derive only captures")
}
fn run_capture(&self, _dest: &Path, args: &[&str]) -> io::Result<String> {
*self.args.borrow_mut() = args.iter().map(|s| s.to_string()).collect();
if self.fail {
Err(io::Error::other("log boom"))
} else {
Ok(self.subject.clone())
}
}
}
#[test]
fn parse_role_reads_the_role_token() {
assert_eq!(
parse_role("dispatch: compactor [a-b]").as_deref(),
Some("compactor")
);
assert_eq!(
parse_role("dispatch: worker [a-b-c]").as_deref(),
Some("worker")
);
}
#[test]
fn parse_role_is_none_for_a_root_or_empty_subject() {
assert_eq!(parse_role("step 001: dispatch [a-b]"), None);
assert_eq!(parse_role(""), None);
}
#[test]
fn derive_reads_the_dispatch_subject_and_anchors_the_id() {
let git = SubjGit::returning("dispatch: verifier [p-1-c-2]\n");
let role = derive(Path::new("/wt"), "termref", "p-1-c-2", &git).unwrap();
assert_eq!(role.as_deref(), Some("verifier"));
let args = git.args.borrow();
assert!(
args.iter().any(|a| a == "^dispatch: .+ \\[p-1-c-2\\]$"),
"{args:?}"
);
assert!(args.iter().any(|a| a == "termref"));
}
#[test]
fn derive_is_none_when_no_dispatch_commit_matches() {
let git = SubjGit::returning("");
assert_eq!(derive(Path::new("/wt"), "ref", "a-b", &git).unwrap(), None);
}
#[test]
fn derive_surfaces_a_git_failure() {
let mut git = SubjGit::returning("");
git.fail = true;
let err = derive(Path::new("/wt"), "ref", "a-b", &git).unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "role derive log",
..
}
),
"{err:?}"
);
}
}