use std::time::Duration;
use super::{LANDED_CONTENT_CHECK, LandedContent, LandingAdmission, landed_content_verdict};
use crate::core::worktree_carried_by_pr::{CarriedByPr, MERGED_PR_ANCESTRY_CHECK};
use crate::core::worktree_landed_history::{ContentOnBase, content_on_base};
use crate::session_manager::worktree_git_fixture::GitWorktreeFixture;
const BOUND: Duration = Duration::from_secs(30);
#[test]
fn a_divergence_landed_by_another_route_reports_landed() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("donor");
fx.land_on_the_remote_only(&wt, "donor.txt");
match landed_content_verdict(&wt, BOUND) {
LandedContent::Landed { base, base_sha, .. } => {
assert!(base.contains("main"), "the base must be named: {base}");
assert_eq!(base_sha.len(), 40, "a full object id: {base_sha}");
}
other => panic!("a tree whose content is on origin/main must be landed: {other:?}"),
}
}
#[test]
fn an_unlanded_commit_reports_its_residual_path() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("unlanded");
GitWorktreeFixture::commit_unpushed(&wt);
match landed_content_verdict(&wt, BOUND) {
LandedContent::Residual { first_path, .. } => {
assert_eq!(first_path, "unpushed.txt", "the residue must be named");
}
other => panic!("work on no remote must not be reported landed: {other:?}"),
}
}
#[test]
fn a_worktree_with_no_resolvable_landing_base_is_unavailable() {
let tmp = tempfile::tempdir().expect("tempdir");
let verdict = landed_content_verdict(tmp.path(), BOUND);
assert!(
!verdict.is_landed(),
"a non-repository must never report landed: {verdict:?}"
);
}
#[test]
fn a_refresh_that_fails_never_reports_landed() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("stale");
fx.land_on_the_remote_only(&wt, "stale.txt");
let broken = fx.repos_root.join("no-such-remote.git");
let out = std::process::Command::new("git")
.arg("-C")
.arg(&wt)
.args(["remote", "set-url", "origin"])
.arg(&broken)
.output()
.expect("fixture: git remote set-url");
assert!(out.status.success(), "fixture: could not break the remote");
let verdict = landed_content_verdict(&wt, BOUND);
assert!(
matches!(verdict, LandedContent::Unavailable { .. }),
"a failed refresh must decide, not a comparison against stale refs: {verdict:?}"
);
assert!(
verdict.note().contains("refreshed"),
"the refusal must name the refresh: {}",
verdict.note()
);
}
fn git(dir: &std::path::Path, args: &[&str]) -> String {
let out = std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.expect("fixture: git could not be run");
assert!(
out.status.success(),
"fixture: `git {}` failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
#[test]
fn a_gitlink_bump_is_residue_even_when_submodule_diffs_are_ignored() {
let fx = GitWorktreeFixture::new();
let base = git(&fx.repo, &["rev-parse", "HEAD"]);
git(
&fx.repo,
&[
"update-index",
"--add",
"--cacheinfo",
&format!("160000,{base},sub"),
],
);
git(&fx.repo, &["commit", "-m", "add gitlink sub"]);
git(&fx.repo, &["push", "origin", "main"]);
git(&fx.repo, &["fetch", "origin"]);
let wt = fx.add_worktree("gitlink-bump");
let bumped = "1111111111111111111111111111111111111111";
git(
&wt,
&[
"update-index",
"--cacheinfo",
&format!("160000,{bumped},sub"),
],
);
git(&wt, &["commit", "-m", "bump sub"]);
git(&wt, &["config", "diff.ignoreSubmodules", "all"]);
git(&wt, &["config", "submodule.sub.ignore", "all"]);
match content_on_base(&wt, "origin/main") {
Ok(ContentOnBase::Residual { paths, .. }) => {
assert_eq!(
paths,
vec!["sub".to_string()],
"the gitlink bump is residue"
);
}
other => panic!("the merge must be answerable and leave residue: {other:?}"),
}
match landed_content_verdict(&wt, BOUND) {
LandedContent::Residual { first_path, .. } => assert_eq!(first_path, "sub"),
other => panic!("a gitlink bump must not read as landed: {other:?}"),
}
}
#[test]
fn merge_residue_against_an_unresolvable_base_is_an_error() {
let fx = GitWorktreeFixture::new();
let wt = fx.add_worktree("no-base");
assert!(
content_on_base(&wt, "origin/does-not-exist").is_err(),
"an unresolvable base must be an error"
);
assert!(
content_on_base(&wt, " ").is_err(),
"an empty base must be an error"
);
}
#[test]
fn the_note_names_the_admission_in_every_arm() {
let arms = [
LandedContent::Landed {
base: "origin/main".into(),
base_sha: "abc123".into(),
landed_at: None,
},
LandedContent::Landed {
base: "origin/main".into(),
base_sha: "abc123".into(),
landed_at: Some("def456".into()),
},
LandedContent::Conflicted {
base: "origin/main".into(),
detail: "merging HEAD into origin/main conflicts in 1 file(s)".into(),
},
LandedContent::Residual {
base: "origin/main".into(),
first_path: "src/lib.rs".into(),
},
LandedContent::unavailable("git said no"),
];
for arm in arms {
assert!(
arm.note().contains(LANDED_CONTENT_CHECK),
"every arm names the admission: {}",
arm.note()
);
}
let residual = LandedContent::Residual {
base: "origin/main".into(),
first_path: "src/lib.rs".into(),
};
assert!(
residual.note().contains("src/lib.rs"),
"the residual arm names the path: {}",
residual.note()
);
}
#[test]
fn admission_admits_on_either_route_and_names_both_refusals() {
let residual = LandedContent::Residual {
base: "origin/main".into(),
first_path: "src/lib.rs".into(),
};
let not_carried = CarriedByPr::NotCarried {
head: "1111111".into(),
};
let only_b = LandingAdmission::from(residual.clone());
assert!(!only_b.admits());
let carried = LandingAdmission {
content: residual.clone(),
carried: Some(CarriedByPr::Carried {
pr: 8328,
pr_head: "2222222".into(),
}),
};
assert!(carried.admits());
assert_eq!(carried.carried_pr(), Some(8328));
assert!(
carried.note().contains(MERGED_PR_ANCESTRY_CHECK),
"{}",
carried.note()
);
let neither = LandingAdmission {
content: residual,
carried: Some(not_carried),
};
assert!(!neither.admits());
assert_eq!(neither.carried_pr(), None);
let note = neither.note();
assert!(note.contains(LANDED_CONTENT_CHECK), "{note}");
assert!(note.contains(MERGED_PR_ANCESTRY_CHECK), "{note}");
assert!(note.contains("src/lib.rs"), "{note}");
let headless = LandingAdmission {
content: LandedContent::unavailable("x"),
carried: Some(CarriedByPr::Carried {
pr: 1,
pr_head: " ".into(),
}),
};
assert!(!headless.admits());
}