use super::*;
use crate::template::RealGit;
use std::cell::RefCell;
use std::path::PathBuf;
use tempfile::TempDir;
fn g() -> RealGit {
RealGit::new()
}
fn repo_at_checkpoint(files: &[(&str, &str)]) -> TempDir {
let dir = TempDir::new().unwrap();
let wt = dir.path();
let git = g();
git.run(wt, &["init", "-b", "agents/p1"]).unwrap();
git.run(wt, &["config", "user.email", "t@t"]).unwrap();
git.run(wt, &["config", "core.hooksPath", "/dev/null"])
.unwrap();
git.run(wt, &["config", "user.name", "t"]).unwrap();
for (rel, content) in files {
write(wt, rel, content);
}
git.run(wt, &["add", "-A"]).unwrap();
git.run(wt, &["commit", "-m", "checkpoint C"]).unwrap();
dir
}
fn write(wt: &Path, rel: &str, content: &str) {
let path = wt.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, content).unwrap();
}
fn compactor_branch(wt: &Path, summary: (&str, &str), deletions: &[&str]) {
compactor_branch_with_dialog(wt, summary, deletions, &[]);
}
fn compactor_branch_with_dialog(
wt: &Path,
summary: (&str, &str),
deletions: &[&str],
dialog: &[(&str, &str)],
) {
let git = g();
git.run(wt, &["checkout", "-b", "agents/p1-cmp"]).unwrap();
for (rel, content) in dialog {
write(wt, rel, content);
}
write(wt, summary.0, summary.1);
for path in deletions {
git.run(wt, &["rm", "--", path]).unwrap();
}
git.run(wt, &["add", "-A"]).unwrap();
git.run(wt, &["commit", "-m", "compaction"]).unwrap();
git.run(wt, &["checkout", "agents/p1"]).unwrap();
}
fn advance_live(wt: &Path, files: &[(&str, &str)]) {
let git = g();
for (rel, content) in files {
write(wt, rel, content);
}
git.run(wt, &["add", "-A"]).unwrap();
git.run(wt, &["commit", "-m", "live step"]).unwrap();
}
fn head_parents(wt: &Path) -> usize {
g().run_capture(wt, &["rev-list", "--parents", "-n", "1", "HEAD"])
.unwrap()
.split_whitespace()
.count()
- 1
}
#[test]
fn merge_lands_summary_and_clean_transcript_deletion() {
let dir = repo_at_checkpoint(&[("messages/001-user.md", "hi\n")]);
let wt = dir.path();
compactor_branch(
wt,
("summary/001.md", "digest\n"),
&["messages/001-user.md"],
);
advance_live(wt, &[("messages/002-a.md", "reply\n")]);
assert_eq!(merge(wt, "p1-cmp", &g()).unwrap(), MergeOutcome::Merged);
assert!(!wt.join("messages/001-user.md").exists(), "deletion landed");
assert!(wt.join("messages/002-a.md").exists(), "live append kept");
assert!(wt.join("summary/001.md").exists(), "summary landed");
assert_eq!(head_parents(wt), 2, "--no-ff two-parent merge commit");
}
#[test]
fn overlap_drops_work_product_deletion_live_branch_wins() {
let dir = repo_at_checkpoint(&[("code.txt", "v1\n")]);
let wt = dir.path();
compactor_branch(wt, ("summary/001.md", "digest\n"), &["code.txt"]);
advance_live(wt, &[("code.txt", "v2\n")]);
assert_eq!(merge(wt, "p1-cmp", &g()).unwrap(), MergeOutcome::Merged);
assert_eq!(
std::fs::read_to_string(wt.join("code.txt")).unwrap(),
"v2\n",
"live-branch-wins: the rewritten work product survives"
);
assert!(wt.join("summary/001.md").exists(), "summary still landed");
assert_eq!(head_parents(wt), 2);
}
#[test]
fn the_compactors_own_dialog_never_crosses_the_merge() {
let dir = repo_at_checkpoint(&[
("goal.md", "parent goal\n"),
("soul.md", "parent soul\n"),
("messages/001-user.md", "hi\n"),
("messages/002-a.md", "reply\n"),
]);
let wt = dir.path();
compactor_branch_with_dialog(
wt,
("summary/001.md", "digest\n"),
&["messages/001-user.md"],
&[
("goal.md", "compact the branch\n"),
("soul.md", "compactor soul\n"),
("messages/003-goal.md", "compact the branch\n"),
("messages/004-model.json", "{}\n"),
("messages/005-tool.json", "{\"error\":\"no such path\"}\n"),
],
);
advance_live(wt, &[("messages/006-b.md", "later\n")]);
assert_eq!(merge(wt, "p1-cmp", &g()).unwrap(), MergeOutcome::Merged);
assert_eq!(
std::fs::read_to_string(wt.join("summary/001.md")).unwrap(),
"digest\n",
"the compaction product lands"
);
assert!(!wt.join("messages/001-user.md").exists(), "deletion landed");
assert!(wt.join("messages/006-b.md").exists(), "live append kept");
for private in ["003-goal.md", "004-model.json", "005-tool.json"] {
assert!(
!wt.join("messages").join(private).exists(),
"compactor transcript entry {private} crossed the merge"
);
}
let read = |rel: &str| std::fs::read_to_string(wt.join(rel)).unwrap();
assert_eq!(read("goal.md"), "parent goal\n");
assert_eq!(read("soul.md"), "parent soul\n");
assert_eq!(g().run_capture(wt, &["status", "--porcelain"]).unwrap(), "");
assert!(
g().run_capture(
wt,
&["cat-file", "-e", "agents/p1-cmp:messages/004-model.json"]
)
.is_ok(),
"the compactor branch keeps its own dialog"
);
}
#[test]
fn already_up_to_date_is_a_noop() {
let dir = repo_at_checkpoint(&[("goal.md", "g\n")]);
let wt = dir.path();
let git = g();
git.run(wt, &["branch", "agents/p1-cmp"]).unwrap();
assert_eq!(merge(wt, "p1-cmp", &git).unwrap(), MergeOutcome::NoOp);
assert_eq!(head_parents(wt), 0, "no merge commit was created");
}
#[test]
fn a_bad_compactor_ref_is_declined_loudly() {
let dir = repo_at_checkpoint(&[("goal.md", "g\n")]);
let err = merge(dir.path(), "does-not-exist", &g()).unwrap_err();
assert_git_op(err, "compaction merge");
}
#[test]
fn a_content_conflict_is_refused_and_marked_never_committed() {
let dir = repo_at_checkpoint(&[("messages/001-user.md", "hi\n")]);
let wt = dir.path();
compactor_branch(wt, ("summary/001.md", "compactor B digest\n"), &[]);
advance_live(wt, &[("summary/001.md", "compactor A digest\n")]);
let before = g().run_capture(wt, &["rev-parse", "HEAD"]).unwrap();
assert_eq!(
merge(wt, "p1-cmp", &g()).unwrap(),
MergeOutcome::Conflicted(vec!["summary/001.md".to_string()]),
);
assert_eq!(g().run_capture(wt, &["rev-parse", "HEAD"]).unwrap(), before);
assert_eq!(head_parents(wt), 1, "no merge commit");
let live = std::fs::read_to_string(wt.join("summary/001.md")).unwrap();
assert_eq!(live, "compactor A digest\n");
assert!(!live.contains("<<<<<<<"), "no conflict markers: {live}");
assert!(
g().run_capture(wt, &["rev-parse", "--verify", "-q", "MERGE_HEAD"])
.is_err(),
"the merge was aborted"
);
let marked = g()
.run_capture(wt, &["rev-parse", "refs/lernie/conflicted/p1-cmp"])
.unwrap();
let compactor_tip = g()
.run_capture(wt, &["rev-parse", "agents/p1-cmp"])
.unwrap();
assert_eq!(marked, compactor_tip);
}
#[test]
fn a_modify_delete_overlap_is_not_a_content_conflict() {
let dir = repo_at_checkpoint(&[("code.txt", "v1\n")]);
let wt = dir.path();
compactor_branch(wt, ("summary/001.md", "digest\n"), &["code.txt"]);
advance_live(wt, &[("code.txt", "v2\n")]);
g().run(
wt,
&[
"merge",
"--no-ff",
"--no-commit",
"--no-edit",
"agents/p1-cmp",
],
)
.ok();
assert!(
content_conflicts(wt, &g()).unwrap().is_empty(),
"modify/delete carries no markers"
);
g().run(wt, &["merge", "--abort"]).unwrap();
}
fn assert_git_op(err: Error, want: &str) {
match err {
Error::Git { op, .. } => assert_eq!(op, want),
other => panic!("{other:?}"),
}
}
mod stub;