use crate::boundary::{BoundaryRow, Provenance as BoundaryProvenance};
use crate::git::{self, WorktreeRecord};
use crate::ledger::Boundaries;
use crate::worktree::{Apply, classify_import, run_gc};
use anyhow::{Context, bail};
use serde::Serialize;
use std::path::{Path, PathBuf};
const FUNNEL_MARKER: &str = "dispatch-funnel";
pub(crate) fn funnel_message(slice: u32, phase: &str) -> String {
format!("{FUNNEL_MARKER}: SL-{slice:03} {phase}")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CoordRefusal {
UnknownSlice,
Ambiguous,
Stale,
}
impl CoordRefusal {
pub(crate) fn token(self) -> &'static str {
match self {
CoordRefusal::UnknownSlice => "unknown-slice",
CoordRefusal::Ambiguous => "ambiguous",
CoordRefusal::Stale => "stale",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CoordFacts {
pub(crate) branch_hits: usize,
pub(crate) path: Option<PathBuf>,
pub(crate) prunable: bool,
pub(crate) dir_exists: bool,
pub(crate) tip: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CoordTarget {
pub(crate) root: PathBuf,
pub(crate) tip: String,
}
pub(crate) fn classify_coord(facts: CoordFacts) -> Result<CoordTarget, CoordRefusal> {
if facts.branch_hits == 0 {
return Err(CoordRefusal::UnknownSlice);
}
if facts.branch_hits > 1 {
return Err(CoordRefusal::Ambiguous);
}
let Some(root) = facts.path else {
return Err(CoordRefusal::Stale);
};
if facts.prunable || !facts.dir_exists {
return Err(CoordRefusal::Stale);
}
let Some(tip) = facts.tip else {
return Err(CoordRefusal::Stale);
};
Ok(CoordTarget { root, tip })
}
pub(crate) fn resolve_coord(root: &Path, slice: u32) -> Result<CoordTarget, CoordRefusal> {
let branch_ref = format!("refs/heads/dispatch/{slice:03}");
let records = git::list_worktrees(root).unwrap_or_default();
let hits: Vec<&WorktreeRecord> = records
.iter()
.filter(|r| r.branch.as_deref() == Some(branch_ref.as_str()))
.collect();
let branch_hits = hits.len();
let single = hits.first().copied();
let path = single.map(|r| r.path.clone());
let prunable = single.is_some_and(|r| r.prunable);
let dir_exists = single.is_some_and(|r| r.path.exists());
let tip = git::git_opt(
root,
&[
"rev-parse",
"--verify",
"--quiet",
&format!("{branch_ref}^{{commit}}"),
],
)
.ok()
.flatten();
classify_coord(CoordFacts {
branch_hits,
path,
prunable,
dir_exists,
tip,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Identity {
pub(crate) name: String,
pub(crate) email: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Provenance {
Import {
author: Identity,
committer: Identity,
},
Conclude { who: Identity },
}
impl Provenance {
fn author(&self) -> &Identity {
match self {
Provenance::Import { author, .. } => author,
Provenance::Conclude { who } => who,
}
}
fn committer(&self) -> &Identity {
match self {
Provenance::Import { committer, .. } => committer,
Provenance::Conclude { who } => who,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CommitRefusal {
EmptyDelta,
LostRefRace,
}
impl CommitRefusal {
pub(crate) fn token(self) -> &'static str {
match self {
CommitRefusal::EmptyDelta => "empty-delta",
CommitRefusal::LostRefRace => "lost-ref-race",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CommitOutcome {
Landed { oid: String },
Refused(CommitRefusal),
}
fn commit_tree_as(
root: &Path,
tree: &str,
parent: &str,
message: &str,
prov: &Provenance,
) -> anyhow::Result<String> {
let author = prov.author();
let committer = prov.committer();
let output = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args(["commit-tree", tree, "-p", parent, "-m", message])
.env("GIT_AUTHOR_NAME", &author.name)
.env("GIT_AUTHOR_EMAIL", &author.email)
.env("GIT_COMMITTER_NAME", &committer.name)
.env("GIT_COMMITTER_EMAIL", &committer.email)
.output()
.with_context(|| format!("spawn git commit-tree in {}", root.display()))?;
if !output.status.success() {
bail!(
"commit-tree {tree} -p {parent}: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}
pub(crate) fn commit_on_behalf(
coord_root: &Path,
expected_old: &str,
tree: &str,
message: &str,
prov: &Provenance,
) -> anyhow::Result<CommitOutcome> {
let parent_tree = git::git_text(
coord_root,
&["rev-parse", &format!("{expected_old}^{{tree}}")],
)
.context("resolve parent tree")?;
let new_tree = git::git_text(coord_root, &["rev-parse", &format!("{tree}^{{tree}}")])
.context("resolve composed tree")?;
if new_tree == parent_tree {
return Ok(CommitOutcome::Refused(CommitRefusal::EmptyDelta));
}
let oid = commit_tree_as(coord_root, tree, expected_old, message, prov)?;
let branch_ref = git::git_text(coord_root, &["rev-parse", "--symbolic-full-name", "HEAD"])
.context("resolve coord branch ref")?;
match git::update_ref_cas(coord_root, &branch_ref, &oid, expected_old)? {
git::RefCas::Updated => Ok(CommitOutcome::Landed { oid }),
git::RefCas::Moved { .. } => Ok(CommitOutcome::Refused(CommitRefusal::LostRefRace)),
}
}
pub(crate) const TOOL_DISPATCH_IMPORT: &str = "dispatch_import";
pub(crate) const TOOL_DISPATCH_CONCLUDE_PHASE: &str = "dispatch_conclude_phase";
pub(crate) const TOOL_DISPATCH_REAP: &str = "dispatch_reap";
const DISPATCH_NAME: &str = "dispatch";
const DISPATCH_EMAIL: &str = "dispatch@doctrine";
const MERGE_CONFLICT: &str = "merge-conflict";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) enum FunnelOutcome {
Imported { coord_tip: String },
Concluded { coord_tip: String },
Reaped { fork: String },
Refused { reason: String, detail: String },
}
fn funnel_refused(reason: &str, detail: String) -> FunnelOutcome {
FunnelOutcome::Refused {
reason: reason.to_owned(),
detail,
}
}
fn dispatch_identity() -> Identity {
Identity {
name: DISPATCH_NAME.to_owned(),
email: DISPATCH_EMAIL.to_owned(),
}
}
fn commit_author(root: &Path, rev: &str) -> anyhow::Result<Identity> {
let raw = git::git_text(root, &["log", "-1", "--format=%an%n%ae", rev])
.with_context(|| format!("read author of {rev}"))?;
let mut lines = raw.lines();
Ok(Identity {
name: lines.next().unwrap_or_default().to_owned(),
email: lines.next().unwrap_or_default().to_owned(),
})
}
pub(crate) fn dispatch_import(
root: &Path,
slice: u32,
name: &str,
) -> anyhow::Result<FunnelOutcome> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(funnel_refused(refusal.token(), String::new())),
};
let selectors = crate::slice::selectors(
&coord.root,
slice,
Some(crate::slice::SelectorIntent::DesignTarget),
)?;
import_compose(&coord, slice, name, &selectors)
}
fn import_compose(
coord: &CoordTarget,
slice: u32,
name: &str,
selectors: &[String],
) -> anyhow::Result<FunnelOutcome> {
let fork_tip = git::git_text(&coord.root, &["rev-parse", &format!("{name}^{{commit}}")])
.with_context(|| format!("resolve fork tip {name}"))?;
let base = git::git_text(&coord.root, &["merge-base", &coord.tip, &fork_tip])
.with_context(|| format!("merge-base {} {fork_tip}", coord.tip))?;
let fork_parent = git::git_opt(
&coord.root,
&["rev-parse", "--verify", &format!("{name}^^{{commit}}")],
)?;
let single_commit = fork_parent.as_deref() == Some(base.as_str());
let diff = git::git_text(
&coord.root,
&[
"-c",
"core.quotePath=false",
"diff",
"--name-only",
"--no-renames",
&format!("{base}..{fork_tip}"),
],
)?;
let delta_paths: Vec<String> = diff.lines().map(str::to_owned).collect();
match classify_import(true, true, single_commit, &delta_paths, selectors) {
Ok(Apply::Ok) => {}
Err(refusal) => return Ok(funnel_refused(refusal.token(), String::new())),
}
let tree = if coord.tip == base {
git::git_text(&coord.root, &["rev-parse", &format!("{fork_tip}^{{tree}}")])
.with_context(|| format!("resolve fork tree {fork_tip}"))?
} else {
match git::merge_tree(&coord.root, &base, &coord.tip, &fork_tip)? {
git::MergeTree::Clean { tree } => tree,
git::MergeTree::Conflict => return Ok(funnel_refused(MERGE_CONFLICT, String::new())),
}
};
let prov = Provenance::Import {
author: commit_author(&coord.root, &fork_tip)?,
committer: dispatch_identity(),
};
let message = funnel_message(slice, &format!("import {name}"));
match commit_on_behalf(&coord.root, &coord.tip, &tree, &message, &prov)? {
CommitOutcome::Landed { oid } => Ok(FunnelOutcome::Imported { coord_tip: oid }),
CommitOutcome::Refused(refusal) => Ok(funnel_refused(refusal.token(), String::new())),
}
}
pub(crate) fn dispatch_conclude_phase(
root: &Path,
slice: u32,
phase: &str,
code_start: &str,
code_end: &str,
note: Option<&str>,
) -> anyhow::Result<FunnelOutcome> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(funnel_refused(refusal.token(), String::new())),
};
let now = crate::clock::now_timestamp()?;
crate::state::set_phase_status(
&coord.root,
slice,
phase,
crate::state::PhaseStatus::Completed,
note,
&now,
)?;
conclude_boundary_commit(&coord.root, &coord.tip, slice, phase, code_start, code_end)
}
fn conclude_boundary_commit(
coord_root: &Path,
tip: &str,
slice: u32,
phase: &str,
code_start: &str,
code_end: &str,
) -> anyhow::Result<FunnelOutcome> {
let path = format!(".doctrine/dispatch/{slice:03}/boundaries.toml");
let mut boundaries = match git::read_path_at(coord_root, tip, &path)? {
Some(text) => Boundaries::parse(&text)
.with_context(|| format!("parse committed boundaries.toml at {tip}"))?,
None => Boundaries::default(),
};
let row = BoundaryRow {
phase: phase.to_owned(),
code_start_oid: code_start.to_owned(),
code_end_oid: code_end.to_owned(),
provenance: BoundaryProvenance::Funnel,
};
match boundaries.rows.iter_mut().find(|r| r.phase == row.phase) {
Some(existing) => *existing = row,
None => boundaries.rows.push(row),
}
let canonical = boundaries.to_toml()?;
let tip_tree = git::git_text(coord_root, &["rev-parse", &format!("{tip}^{{tree}}")])
.with_context(|| format!("resolve tip tree {tip}"))?;
let tree = git::tree_with_file(coord_root, &tip_tree, &path, &canonical)?;
let prov = Provenance::Conclude {
who: dispatch_identity(),
};
match commit_on_behalf(coord_root, tip, &tree, &funnel_message(slice, phase), &prov)? {
CommitOutcome::Landed { oid } => Ok(FunnelOutcome::Concluded { coord_tip: oid }),
CommitOutcome::Refused(refusal) => Ok(funnel_refused(refusal.token(), String::new())),
}
}
pub(crate) fn dispatch_reap(root: &Path, slice: u32, name: &str) -> anyhow::Result<FunnelOutcome> {
let coord = match resolve_coord(root, slice) {
Ok(coord) => coord,
Err(refusal) => return Ok(funnel_refused(refusal.token(), String::new())),
};
run_gc(Some(coord.root), name, None, false, false)?;
Ok(FunnelOutcome::Reaped {
fork: name.to_owned(),
})
}
#[cfg(test)]
#[expect(
clippy::unwrap_used,
reason = "tests: fail-fast unwrap on fixture setup is idiomatic"
)]
mod tests {
use super::*;
use crate::worktree::Refusal;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
fn git_run(dir: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.unwrap();
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn disp() -> Provenance {
Provenance::Conclude {
who: Identity {
name: "dispatch".to_string(),
email: "dispatch@doctrine".to_string(),
},
}
}
fn primary_with_coord(slice: u32) -> (tempfile::TempDir, PathBuf, PathBuf, String, String) {
let tmp = tempfile::tempdir().unwrap();
let primary = fs::canonicalize(tmp.path()).unwrap().join("primary");
fs::create_dir_all(&primary).unwrap();
git_run(&primary, &["init", "-q", "-b", "main"]);
git_run(&primary, &["config", "user.email", "t@t"]);
git_run(&primary, &["config", "user.name", "t"]);
fs::write(primary.join("seed"), "base\n").unwrap();
git_run(&primary, &["add", "-A"]);
git_run(&primary, &["commit", "-q", "-m", "base"]);
let base = git_run(&primary, &["rev-parse", "HEAD^{commit}"]);
let base_tree = git_run(&primary, &["rev-parse", "HEAD^{tree}"]);
let coord = fs::canonicalize(tmp.path()).unwrap().join("coord");
git_run(
&primary,
&[
"worktree",
"add",
"-q",
"-b",
&format!("dispatch/{slice:03}"),
coord.to_str().unwrap(),
&base,
],
);
let coord = fs::canonicalize(&coord).unwrap();
(tmp, primary, coord, base, base_tree)
}
#[test]
fn classify_coord_happy_returns_the_live_target() {
let facts = CoordFacts {
branch_hits: 1,
path: Some(PathBuf::from("/x/coord")),
prunable: false,
dir_exists: true,
tip: Some("abc".to_string()),
};
assert_eq!(
classify_coord(facts),
Ok(CoordTarget {
root: PathBuf::from("/x/coord"),
tip: "abc".to_string(),
})
);
}
#[test]
fn classify_coord_zero_hits_is_unknown_slice() {
let facts = CoordFacts {
branch_hits: 0,
path: None,
prunable: false,
dir_exists: false,
tip: None,
};
assert_eq!(classify_coord(facts), Err(CoordRefusal::UnknownSlice));
assert_eq!(CoordRefusal::UnknownSlice.token(), "unknown-slice");
}
#[test]
fn classify_coord_multi_hit_is_ambiguous() {
let facts = CoordFacts {
branch_hits: 2,
path: Some(PathBuf::from("/x/coord")),
prunable: false,
dir_exists: true,
tip: Some("abc".to_string()),
};
assert_eq!(classify_coord(facts), Err(CoordRefusal::Ambiguous));
assert_eq!(CoordRefusal::Ambiguous.token(), "ambiguous");
}
#[test]
fn classify_coord_single_hit_inconsistencies_are_stale() {
let stale_variants = [
CoordFacts {
branch_hits: 1,
path: Some(PathBuf::from("/x/coord")),
prunable: false,
dir_exists: false,
tip: Some("abc".to_string()),
},
CoordFacts {
branch_hits: 1,
path: Some(PathBuf::from("/x/coord")),
prunable: true,
dir_exists: true,
tip: Some("abc".to_string()),
},
CoordFacts {
branch_hits: 1,
path: Some(PathBuf::from("/x/coord")),
prunable: false,
dir_exists: true,
tip: None,
},
];
for facts in stale_variants {
assert_eq!(classify_coord(facts), Err(CoordRefusal::Stale));
}
assert_eq!(CoordRefusal::Stale.token(), "stale");
}
#[test]
fn resolve_coord_finds_the_live_coord_and_refuses_an_absent_slice() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(199);
assert_eq!(
resolve_coord(&primary, 199),
Ok(CoordTarget {
root: coord,
tip: base,
})
);
assert_eq!(
resolve_coord(&primary, 200),
Err(CoordRefusal::UnknownSlice)
);
}
#[test]
fn commit_on_behalf_happy_lands_one_non_merge_commit() {
let (_tmp, _primary, coord, base, base_tree) = primary_with_coord(199);
let tree = git::tree_with_file(&coord, &base_tree, "added.txt", "hi\n").unwrap();
let out = commit_on_behalf(&coord, &base, &tree, "m", &disp()).unwrap();
let oid = match out {
CommitOutcome::Landed { oid } => oid,
other => panic!("expected Landed, got {other:?}"),
};
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
oid
);
let parents = git_run(&coord, &["rev-list", "--parents", "-n", "1", &oid]);
let cols: Vec<&str> = parents.split_whitespace().collect();
assert_eq!(cols.len(), 2, "exactly one parent: {parents}");
assert_eq!(cols[1], base, "C^ == expected_old");
assert_eq!(
git_run(&coord, &["rev-parse", &format!("{oid}^{{tree}}")]),
tree
);
}
#[test]
fn commit_on_behalf_empty_delta_refuses_and_leaves_the_tip() {
let (_tmp, _primary, coord, base, base_tree) = primary_with_coord(199);
let out = commit_on_behalf(&coord, &base, &base_tree, "m", &disp()).unwrap();
assert_eq!(out, CommitOutcome::Refused(CommitRefusal::EmptyDelta));
assert_eq!(out.token_is("empty-delta"), true);
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
base
);
}
#[test]
fn commit_on_behalf_lost_ref_race_refuses_and_leaves_the_tip() {
let (_tmp, _primary, coord, base, base_tree) = primary_with_coord(199);
let other = git_run(
&coord,
&["commit-tree", &base_tree, "-p", &base, "-m", "other"],
);
git_run(&coord, &["update-ref", "refs/heads/dispatch/199", &other]);
let tree = git::tree_with_file(&coord, &base_tree, "added.txt", "hi\n").unwrap();
let out = commit_on_behalf(&coord, &base, &tree, "m", &disp()).unwrap();
assert_eq!(out, CommitOutcome::Refused(CommitRefusal::LostRefRace));
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
other
);
}
fn commit_idents(dir: &Path, oid: &str) -> (String, String, String, String, String) {
let raw = git_run(dir, &["log", "-1", "--format=%an%n%ae%n%cn%n%ce%n%s", oid]);
let mut it = raw.lines();
(
it.next().unwrap().to_string(),
it.next().unwrap().to_string(),
it.next().unwrap().to_string(),
it.next().unwrap().to_string(),
it.next().unwrap().to_string(),
)
}
#[test]
fn commit_on_behalf_import_mode_preserves_worker_author_and_dispatch_committer() {
let (_tmp, _primary, coord, base, base_tree) = primary_with_coord(199);
let tree = git::tree_with_file(&coord, &base_tree, "added.txt", "hi\n").unwrap();
let prov = Provenance::Import {
author: Identity {
name: "worker-x".to_string(),
email: "worker-x@doctrine".to_string(),
},
committer: Identity {
name: "dispatch".to_string(),
email: "dispatch@doctrine".to_string(),
},
};
let msg = funnel_message(199, "PHASE-02");
let oid = match commit_on_behalf(&coord, &base, &tree, &msg, &prov).unwrap() {
CommitOutcome::Landed { oid } => oid,
other => panic!("expected Landed, got {other:?}"),
};
let (an, ae, cn, ce, subject) = commit_idents(&coord, &oid);
assert_eq!(
(an.as_str(), ae.as_str()),
("worker-x", "worker-x@doctrine")
);
assert_eq!(
(cn.as_str(), ce.as_str()),
("dispatch", "dispatch@doctrine")
);
assert_eq!(subject, msg);
assert!(
subject.starts_with(FUNNEL_MARKER),
"grep-stable marker: {subject}"
);
assert!(subject.contains("SL-199") && subject.contains("PHASE-02"));
}
#[test]
fn commit_on_behalf_conclude_mode_sets_author_equal_committer() {
let (_tmp, _primary, coord, base, base_tree) = primary_with_coord(199);
let tree = git::tree_with_file(&coord, &base_tree, "added.txt", "hi\n").unwrap();
let msg = funnel_message(199, "PHASE-02");
let oid = match commit_on_behalf(&coord, &base, &tree, &msg, &disp()).unwrap() {
CommitOutcome::Landed { oid } => oid,
other => panic!("expected Landed, got {other:?}"),
};
let (an, ae, cn, ce, _s) = commit_idents(&coord, &oid);
assert_eq!(an, "dispatch");
assert_eq!(ae, "dispatch@doctrine");
assert_eq!((an, ae), (cn, ce), "author == committer == dispatch id");
}
fn dirty_coord(coord: &Path) {
fs::write(coord.join("seed"), "dirtied\n").unwrap(); fs::write(coord.join("staged.txt"), "s\n").unwrap();
git_run(coord, &["add", "staged.txt"]); }
#[test]
fn commit_on_behalf_composes_the_named_tree_not_the_live_index() {
let (_tmp, _primary, coord, base, base_tree) = primary_with_coord(199);
dirty_coord(&coord);
let index_before = git_run(&coord, &["ls-files", "--stage"]);
let tree = git::tree_with_file(&coord, &base_tree, "committed.txt", "c\n").unwrap();
let oid = match commit_on_behalf(&coord, &base, &tree, "m", &disp()).unwrap() {
CommitOutcome::Landed { oid } => oid,
other => panic!("expected Landed, got {other:?}"),
};
let names = git_run(&coord, &["ls-tree", "-r", "--name-only", &oid]);
let names: Vec<&str> = names.lines().collect();
assert!(
names.contains(&"committed.txt"),
"named tree landed: {names:?}"
);
assert!(
!names.contains(&"staged.txt"),
"index NOT swept in: {names:?}"
);
assert_eq!(
git_run(&coord, &["cat-file", "-p", &format!("{oid}:seed")]),
"base",
"the dirty worktree seed is NOT in the commit"
);
assert_eq!(
git_run(&coord, &["ls-files", "--stage"]),
index_before,
"index untouched"
);
assert_eq!(fs::read_to_string(coord.join("seed")).unwrap(), "dirtied\n");
assert_eq!(fs::read_to_string(coord.join("staged.txt")).unwrap(), "s\n");
}
#[test]
fn commit_on_behalf_fault_leaves_ref_index_and_worktree_byte_unchanged() {
let (_tmp, _primary, coord, base, base_tree) = primary_with_coord(199);
let other = git_run(
&coord,
&["commit-tree", &base_tree, "-p", &base, "-m", "other"],
);
git_run(&coord, &["update-ref", "refs/heads/dispatch/199", &other]);
dirty_coord(&coord);
let ref_before = git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]);
let status_before = git_run(&coord, &["status", "--porcelain"]);
let tree = git::tree_with_file(&coord, &base_tree, "committed.txt", "c\n").unwrap();
let out = commit_on_behalf(&coord, &base, &tree, "m", &disp()).unwrap();
assert_eq!(out, CommitOutcome::Refused(CommitRefusal::LostRefRace));
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
ref_before
);
assert_eq!(git_run(&coord, &["status", "--porcelain"]), status_before);
}
impl CommitOutcome {
fn token_is(&self, want: &str) -> bool {
matches!(self, CommitOutcome::Refused(r) if r.token() == want)
}
}
fn ct(coord: &Path, tip: &str) -> CoordTarget {
CoordTarget {
root: coord.to_path_buf(),
tip: tip.to_string(),
}
}
fn add_fork(dir: &Path, base: &str, branch: &str, file: &str, content: &str) -> String {
let base_tree = git_run(dir, &["rev-parse", &format!("{base}^{{tree}}")]);
let tree = git::tree_with_file(dir, &base_tree, file, content).unwrap();
let out = Command::new("git")
.arg("-C")
.arg(dir)
.args(["commit-tree", &tree, "-p", base, "-m", "worker work"])
.env("GIT_AUTHOR_NAME", "worker-x")
.env("GIT_AUTHOR_EMAIL", "worker-x@doctrine")
.env("GIT_COMMITTER_NAME", "worker-x")
.env("GIT_COMMITTER_EMAIL", "worker-x@doctrine")
.output()
.unwrap();
assert!(
out.status.success(),
"commit-tree: {}",
String::from_utf8_lossy(&out.stderr)
);
let commit = String::from_utf8_lossy(&out.stdout).trim().to_string();
git_run(
dir,
&["update-ref", &format!("refs/heads/{branch}"), &commit],
);
commit
}
#[test]
fn dispatch_import_happy_advances_the_coord_tip_by_one() {
let (_tmp, _p, coord, base, _bt) = primary_with_coord(199);
add_fork(&coord, &base, "dispatch/wk", "src/f.rs", "fn f() {}\n");
let out = import_compose(&ct(&coord, &base), 199, "dispatch/wk", &[]).unwrap();
let coord_tip = match out {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("expected Imported, got {other:?}"),
};
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
coord_tip
);
assert_ne!(coord_tip, base, "coord tip advanced");
let parents = git_run(&coord, &["rev-list", "--parents", "-n", "1", &coord_tip]);
let cols: Vec<&str> = parents.split_whitespace().collect();
assert_eq!(cols.len(), 2, "exactly one parent: {parents}");
assert_eq!(cols[1], base, "parent == coord tip (B)");
let names = git_run(&coord, &["ls-tree", "-r", "--name-only", &coord_tip]);
assert!(
names.lines().any(|l| l == "src/f.rs"),
"worker file imported: {names}"
);
let raw = git_run(&coord, &["log", "-1", "--format=%an%n%ae", &coord_tip]);
let mut it = raw.lines();
assert_eq!(it.next().unwrap(), "worker-x", "worker author preserved");
assert_eq!(it.next().unwrap(), "worker-x@doctrine");
}
#[test]
fn dispatch_import_undeclared_scope_refuses_before_compose_and_leaves_the_tip() {
let (_tmp, _p, coord, base, _bt) = primary_with_coord(199);
add_fork(&coord, &base, "dispatch/wk", "docs/readme.md", "hi\n");
let selectors = vec!["src/**".to_string()];
let out = import_compose(&ct(&coord, &base), 199, "dispatch/wk", &selectors).unwrap();
assert_eq!(
out,
funnel_refused(Refusal::UndeclaredScope.token(), String::new())
);
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
base
);
}
#[test]
fn dispatch_import_composes_a_second_disjoint_fork_via_merge_tree() {
let (_tmp, _p, coord, base, _bt) = primary_with_coord(199);
add_fork(&coord, &base, "dispatch/wk1", "src/a.rs", "a\n");
let tip1 = match import_compose(&ct(&coord, &base), 199, "dispatch/wk1", &[]).unwrap() {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("expected Imported, got {other:?}"),
};
add_fork(&coord, &base, "dispatch/wk2", "src/b.rs", "b\n");
let tip2 = match import_compose(&ct(&coord, &tip1), 199, "dispatch/wk2", &[]).unwrap() {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("expected Imported, got {other:?}"),
};
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
tip2
);
let names = git_run(&coord, &["ls-tree", "-r", "--name-only", &tip2]);
assert!(
names.lines().any(|l| l == "src/a.rs"),
"first delta: {names}"
);
assert!(
names.lines().any(|l| l == "src/b.rs"),
"second delta: {names}"
);
}
#[test]
fn dispatch_import_unknown_slice_refuses_via_resolve_coord() {
let (_tmp, primary, _coord, _base, _bt) = primary_with_coord(199);
let out = dispatch_import(&primary, 200, "dispatch/wk").unwrap();
assert_eq!(out, funnel_refused("unknown-slice", String::new()));
}
#[test]
fn dispatch_import_is_transport_agnostic_routed_only_by_declared_args() {
let (_tmp_a, primary_a, _coord_a, _base_a, _bt_a) = primary_with_coord(199);
let (_tmp_b, primary_b, _coord_b, base_b, _bt_b) = primary_with_coord(200);
let found = dispatch_import(&primary_a, 199, "dispatch/wk");
assert!(
!matches!(&found, Ok(o) if *o == funnel_refused("unknown-slice", String::new())),
"resolve_coord matched the live dispatch/199 coord at the declared root, \
not the process cwd — must not short-circuit unknown-slice: {found:?}",
);
assert_eq!(
dispatch_import(&primary_b, 199, "dispatch/wk").unwrap(),
funnel_refused("unknown-slice", String::new()),
"the root arg keys resolve_coord — not cwd/agent_id/payload",
);
assert_eq!(
dispatch_import(&primary_a, 200, "dispatch/wk").unwrap(),
funnel_refused("unknown-slice", String::new()),
"the slice arg keys resolve_coord — not cwd/agent_id/payload",
);
assert_eq!(
git_run(&primary_b, &["rev-parse", "refs/heads/dispatch/200"]),
base_b,
"sibling fixture untouched — resolution never keyed off ambient state",
);
}
#[test]
fn conclude_boundary_commit_happy_lands_the_true_range_row() {
let (_tmp, _p, coord, base, _bt) = primary_with_coord(199);
add_fork(&coord, &base, "dispatch/wk", "src/f.rs", "x\n");
let code_end = match import_compose(&ct(&coord, &base), 199, "dispatch/wk", &[]).unwrap() {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("{other:?}"),
};
let out =
conclude_boundary_commit(&coord, &code_end, 199, "PHASE-01", &base, &code_end).unwrap();
let tip = match out {
FunnelOutcome::Concluded { coord_tip } => coord_tip,
other => panic!("expected Concluded, got {other:?}"),
};
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
tip
);
let committed = git_run(
&coord,
&[
"show",
&format!("{tip}:.doctrine/dispatch/199/boundaries.toml"),
],
);
let parsed = Boundaries::parse(&committed).unwrap();
assert_eq!(parsed.rows.len(), 1);
assert_eq!(parsed.rows[0].phase, "PHASE-01");
assert_eq!(parsed.rows[0].code_start_oid, base, "code_start == B");
assert_eq!(
parsed.rows[0].code_end_oid, code_end,
"code_end == coord_tip"
);
}
#[test]
fn conclude_boundary_commit_upserts_by_phase_never_duplicates() {
let (_tmp, _p, coord, base, _bt) = primary_with_coord(199);
let tip1 =
match conclude_boundary_commit(&coord, &base, 199, "PHASE-01", &base, &base).unwrap() {
FunnelOutcome::Concluded { coord_tip } => coord_tip,
other => panic!("{other:?}"),
};
let tip2 =
match conclude_boundary_commit(&coord, &tip1, 199, "PHASE-01", &base, &tip1).unwrap() {
FunnelOutcome::Concluded { coord_tip } => coord_tip,
other => panic!("{other:?}"),
};
let committed = git_run(
&coord,
&[
"show",
&format!("{tip2}:.doctrine/dispatch/199/boundaries.toml"),
],
);
let parsed = Boundaries::parse(&committed).unwrap();
assert_eq!(parsed.rows.len(), 1, "upsert replaces, never appends");
assert_eq!(parsed.rows[0].code_end_oid, tip1, "the row was updated");
}
#[test]
fn conclude_boundary_commit_fault_lands_no_boundary_and_leaves_porcelain_clean() {
let (_tmp, _p, coord, base, base_tree) = primary_with_coord(199);
let other = git_run(
&coord,
&["commit-tree", &base_tree, "-p", &base, "-m", "other"],
);
git_run(&coord, &["update-ref", "refs/heads/dispatch/199", &other]);
let status_before = git_run(&coord, &["status", "--porcelain"]);
let out = conclude_boundary_commit(&coord, &base, 199, "PHASE-01", &base, &base).unwrap();
assert_eq!(out, funnel_refused("lost-ref-race", String::new()));
assert_eq!(
git_run(&coord, &["rev-parse", "refs/heads/dispatch/199"]),
other
);
assert!(
git::read_path_at(&coord, &other, ".doctrine/dispatch/199/boundaries.toml")
.unwrap()
.is_none(),
"no boundary committed on the fault"
);
assert_eq!(
git_run(&coord, &["status", "--porcelain"]),
status_before,
"porcelain byte-unchanged"
);
}
#[test]
fn dispatch_conclude_phase_flips_the_gitignored_sheet_and_lands_the_boundary() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(199);
let phases_dir = crate::state::phases_dir(&coord, 199);
fs::create_dir_all(&phases_dir).unwrap();
fs::write(
phases_dir.join("phase-01.toml"),
"status = \"in_progress\"\n",
)
.unwrap();
add_fork(&coord, &base, "dispatch/wk", "src/f.rs", "x\n");
let code_end = match import_compose(&ct(&coord, &base), 199, "dispatch/wk", &[]).unwrap() {
FunnelOutcome::Imported { coord_tip } => coord_tip,
other => panic!("{other:?}"),
};
let out =
dispatch_conclude_phase(&primary, 199, "PHASE-01", &base, &code_end, Some("done"))
.unwrap();
let tip = match out {
FunnelOutcome::Concluded { coord_tip } => coord_tip,
other => panic!("expected Concluded, got {other:?}"),
};
let committed = git_run(
&coord,
&[
"show",
&format!("{tip}:.doctrine/dispatch/199/boundaries.toml"),
],
);
assert!(
committed.contains("PHASE-01"),
"boundary committed: {committed}"
);
assert_eq!(
crate::state::read_phase_status(&phases_dir, "phase-01").unwrap(),
Some("completed".to_string()),
"sheet flipped to completed"
);
let committed_names = git_run(&coord, &["ls-tree", "-r", "--name-only", &tip]);
assert!(
!committed_names
.lines()
.any(|l| l.starts_with(".doctrine/state/")),
"the phase sheet never enters committed history"
);
}
#[test]
fn dispatch_reap_reaps_a_landed_fork_and_refuses_an_unlanded_one() {
let (_tmp, primary, coord, base, _bt) = primary_with_coord(199);
let landed_branch = "dispatch/landed";
add_fork(&coord, &base, landed_branch, "src/landed.rs", "l\n");
let landed_wt = coord.join(".worktrees").join("landed");
git_run(
&coord,
&[
"worktree",
"add",
"-q",
landed_wt.to_str().unwrap(),
landed_branch,
],
);
import_compose(&ct(&coord, &base), 199, landed_branch, &[]).unwrap();
let out = dispatch_reap(&primary, 199, landed_branch).unwrap();
assert_eq!(
out,
FunnelOutcome::Reaped {
fork: landed_branch.to_string()
}
);
assert!(
git::git_opt(&coord, &["rev-parse", "--verify", "--quiet", landed_branch])
.unwrap()
.is_none(),
"the landed fork branch was reaped"
);
let unlanded_branch = "dispatch/unlanded";
add_fork(&coord, &base, unlanded_branch, "src/unlanded.rs", "u\n");
let err = dispatch_reap(&primary, 199, unlanded_branch).unwrap_err();
assert!(
err.to_string().contains("not-landed"),
"gc refuses an unlanded fork: {err}"
);
assert!(
git::git_opt(
&coord,
&["rev-parse", "--verify", "--quiet", unlanded_branch]
)
.unwrap()
.is_some(),
"the unlanded fork branch survives the refusal"
);
}
#[test]
fn dispatch_reap_unknown_slice_refuses_via_resolve_coord() {
let (_tmp, primary, _coord, _base, _bt) = primary_with_coord(199);
let out = dispatch_reap(&primary, 200, "dispatch/wk").unwrap();
assert_eq!(out, funnel_refused("unknown-slice", String::new()));
}
}