use super::*;
use crate::github_path::GithubPath;
fn git(dir: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.unwrap_or_else(|e| {
panic!(
"git {args:?} in {dir:?} could not spawn: {e}. These tests guard \
the #4207 partitioning key and must never pass without running."
)
});
assert!(
out.status.success(),
"git {args:?} in {dir:?} exited {}: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn init_empty_repo(dir: &Path) {
std::fs::create_dir_all(dir).expect("create repo dir");
git(dir, &["-c", "init.defaultBranch=main", "init"]);
git(dir, &["config", "user.email", "t@t.test"]);
git(dir, &["config", "user.name", "t"]);
git(dir, &["config", "commit.gpgsign", "false"]);
}
fn commit_file(dir: &Path, name: &str, body: &str) {
std::fs::write(dir.join(name), body).expect("write file");
git(dir, &["add", "."]);
git(dir, &["commit", "-m", "commit"]);
}
fn init_repo(dir: &Path, url: &str) {
init_empty_repo(dir);
commit_file(dir, "README.md", "hi");
git(dir, &["remote", "add", "origin", url]);
}
fn identity(owner: &str, repo: &str, root: &str, operator: Option<&str>) -> ProjectIdentity {
ProjectIdentity {
origin: Some(RepoIdentity::GitHub(GithubPath {
owner: owner.into(),
repo: repo.into(),
})),
root: PathBuf::from(root),
operator: operator.map(str::to_string),
}
}
#[test]
fn sibling_clones_of_same_repo_derive_distinct_ids() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let upstream = tmp.path().join("origin.git");
std::fs::create_dir_all(&upstream).expect("create upstream dir");
git(&upstream, &["init", "--bare"]);
let upstream_url = upstream.to_string_lossy().into_owned();
let seed = tmp.path().join("seed");
init_empty_repo(&seed);
commit_file(&seed, "README.md", "hi");
git(&seed, &["remote", "add", "origin", &upstream_url]);
git(&seed, &["push", "origin", "HEAD:refs/heads/main"]);
let a = tmp.path().join("widget");
let b = tmp.path().join("widget-review");
for dest in [&a, &b] {
let dest_arg = dest.to_string_lossy().into_owned();
git(tmp.path(), &["clone", &upstream_url, &dest_arg]);
git(
dest,
&[
"remote",
"set-url",
"origin",
"git@github.com:acme/widget.git",
],
);
}
let group_a = RepoIdentity::derive(&a).map(|r| r.canonical());
let group_b = RepoIdentity::derive(&b).map(|r| r.canonical());
assert_eq!(
group_a, group_b,
"precondition: sibling clones must share the repo-level grouping key"
);
assert_eq!(group_a, Some("acme/widget".to_string()));
let id_a = derive_project_index_id(&a);
let id_b = derive_project_index_id(&b);
assert_ne!(
id_a, id_b,
"sibling clones are distinct content trees and must not share an index id"
);
assert!(id_a.starts_with("acme-widget-"), "unexpected id: {id_a}");
assert!(id_b.starts_with("acme-widget-"), "unexpected id: {id_b}");
}
#[test]
fn linked_worktrees_of_same_repo_derive_distinct_ids() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let main = tmp.path().join("repo");
init_repo(&main, "git@github.com:acme/widget.git");
let wt_a = tmp.path().join("wt-a");
let wt_b = tmp.path().join("wt-b");
for (dir, branch) in [(&wt_a, "feat-a"), (&wt_b, "feat-b")] {
let dir_arg = dir.to_string_lossy().into_owned();
git(&main, &["worktree", "add", "-b", branch, &dir_arg]);
}
let group: Vec<_> = [&main, &wt_a, &wt_b]
.into_iter()
.map(|d| RepoIdentity::derive(d).map(|r| r.canonical()))
.collect();
assert_eq!(group[0], group[1]);
assert_eq!(group[1], group[2]);
let ids: Vec<String> = [&main, &wt_a, &wt_b]
.into_iter()
.map(|d| derive_project_index_id(d))
.collect();
assert_ne!(ids[0], ids[1], "main checkout vs worktree A: {ids:?}");
assert_ne!(ids[0], ids[2], "main checkout vs worktree B: {ids:?}");
assert_ne!(ids[1], ids[2], "worktree A vs worktree B: {ids:?}");
}
#[test]
fn different_operators_derive_distinct_ids() {
let one = identity("acme", "widget", "/srv/widget", Some("a@example.test"));
let two = identity("acme", "widget", "/srv/widget", Some("b@example.test"));
assert_ne!(one.index_id(), two.index_id());
let none = identity("acme", "widget", "/srv/widget", None);
assert_ne!(none.index_id(), one.index_id());
assert_ne!(none.index_id(), two.index_id());
}
#[test]
fn unrelated_projects_sharing_a_basename_derive_distinct_ids() {
let a = ProjectIdentity {
origin: None,
root: PathBuf::from("/srv/alpha/docs"),
operator: None,
};
let b = ProjectIdentity {
origin: None,
root: PathBuf::from("/srv/beta/docs"),
operator: None,
};
assert_ne!(a.index_id(), b.index_id());
assert!(a.index_id().starts_with("docs-"));
assert!(b.index_id().starts_with("docs-"));
}
#[test]
fn label_ambiguity_does_not_collide() {
let a = identity("foo-bar", "baz", "/srv/x", None);
let b = identity("foo", "bar-baz", "/srv/x", None);
assert_eq!(a.label(), b.label(), "precondition: labels collide");
assert_ne!(a.index_id(), b.index_id(), "digests must not collide");
}
#[test]
fn id_changes_when_first_commit_lands() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let repo = tmp.path().join("nocommit");
init_empty_repo(&repo);
let before = ProjectIdentity::derive(&repo);
assert_eq!(before.origin, None, "no commits, no remote ⇒ no origin");
commit_file(&repo, "README.md", "hi");
let after = ProjectIdentity::derive(&repo);
assert!(
matches!(after.origin, Some(RepoIdentity::ContentHash(_))),
"first commit must yield a content-hash origin, got {:?}",
after.origin
);
assert_ne!(
before.index_id(),
after.index_id(),
"KNOWN DRIFT: the first commit re-derives the id for the same tree"
);
}
#[test]
fn id_changes_when_origin_remote_is_added() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let repo = tmp.path().join("nocommit");
init_empty_repo(&repo);
commit_file(&repo, "README.md", "hi");
let before = ProjectIdentity::derive(&repo);
assert!(matches!(before.origin, Some(RepoIdentity::ContentHash(_))));
git(
&repo,
&["remote", "add", "origin", "git@github.com:acme/widget.git"],
);
let after = ProjectIdentity::derive(&repo);
assert!(matches!(after.origin, Some(RepoIdentity::GitHub(_))));
assert_ne!(
before.index_id(),
after.index_id(),
"KNOWN DRIFT: adding the origin remote re-derives the id"
);
assert!(before.index_id().starts_with("nocommit-"));
assert!(after.index_id().starts_with("acme-widget-"));
}
#[test]
fn id_changes_on_orphan_root_commit() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let repo = tmp.path().join("orphan");
init_empty_repo(&repo);
commit_file(&repo, "README.md", "hi");
let before = ProjectIdentity::derive(&repo);
git(&repo, &["checkout", "--orphan", "second-root"]);
commit_file(&repo, "OTHER.md", "other");
let after = ProjectIdentity::derive(&repo);
assert_ne!(
before.origin, after.origin,
"precondition: the root commit must actually have moved"
);
assert_ne!(
before.index_id(),
after.index_id(),
"KNOWN DRIFT: a new root commit re-derives the id for a remoteless repo"
);
}
#[test]
fn derivation_is_deterministic_across_calls() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let repo = tmp.path().join("repo");
init_repo(&repo, "git@github.com:acme/widget.git");
let first = derive_project_index_id(&repo);
let second = derive_project_index_id(&repo);
assert_eq!(first, second);
let nested = repo.join("src/deep");
std::fs::create_dir_all(&nested).expect("nested dir");
assert_eq!(derive_project_index_id(&nested), first);
}
#[test]
fn index_id_is_pinned_for_a_known_input() {
let pinned = identity(
"bobmatnyc",
"trusty-tools",
"/Users/me/code/trusty-tools",
Some("bob@example.test"),
);
assert_eq!(pinned.index_id(), "bobmatnyc-trusty-tools-f3ef22158eced5cb");
}
#[test]
fn directory_without_git_origin_derives_stable_id() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let plain = tmp.path().join("loose-files");
std::fs::create_dir_all(&plain).expect("dir");
let ident = ProjectIdentity::derive(&plain);
assert_eq!(ident.origin, None, "no git repo means no grouping key");
let id = ident.index_id();
assert!(id.starts_with("loose-files-"), "unexpected id: {id}");
assert_eq!(derive_project_index_id(&plain), id, "must be reproducible");
}
#[cfg(unix)]
#[test]
fn symlinked_root_derives_same_id_as_real_root() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let real = tmp.path().join("repo");
init_repo(&real, "git@github.com:acme/widget.git");
let link = tmp.path().join("repo-link");
std::os::unix::fs::symlink(&real, &link).expect("create symlink");
assert_eq!(
derive_project_index_id(&link),
derive_project_index_id(&real)
);
}
#[test]
fn index_id_is_a_single_url_safe_segment() {
let id = identity("Acme Corp", "My_Widget!", "/srv/x", Some("a@b.test")).index_id();
assert!(!id.is_empty());
assert!(
id.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
"id must be lowercase alnum + hyphen: {id}"
);
assert!(
!id.starts_with('-') && !id.ends_with('-'),
"bad edges: {id}"
);
assert!(
id.starts_with("acme-corp-my-widget-"),
"unexpected id: {id}"
);
}
#[test]
fn empty_label_falls_back_to_placeholder() {
let ident = ProjectIdentity {
origin: None,
root: PathBuf::from("/"),
operator: None,
};
assert_eq!(ident.label(), FALLBACK_LABEL);
assert!(ident.index_id().starts_with("project-"));
let unicode = ProjectIdentity {
origin: None,
root: PathBuf::from("/srv/プロジェクト"),
operator: None,
};
assert!(unicode.index_id().starts_with("project-"));
assert_ne!(ident.index_id(), unicode.index_id());
}
#[test]
fn long_label_is_truncated_without_losing_uniqueness() {
let long = "x".repeat(200);
let a = identity(&long, "repo", "/srv/a", None);
let b = identity(&long, "repo", "/srv/b", None);
let (id_a, id_b) = (a.index_id(), b.index_id());
assert_eq!(a.label().chars().count(), MAX_LABEL_LEN);
assert_ne!(id_a, id_b);
assert_eq!(id_a.len(), MAX_LABEL_LEN + 1 + 16);
}
#[test]
fn digest_is_stable_for_identical_inputs() {
let a = identity("acme", "widget", "/srv/x", Some("a@b.test"));
let b = identity("acme", "widget", "/srv/x", Some("a@b.test"));
assert_eq!(a.digest(), b.digest());
assert_eq!(a.index_id(), b.index_id());
assert_eq!(fnv1a_64(b"abc"), fnv1a_64(b"abc"));
assert_ne!(fnv1a_64(b"abc"), fnv1a_64(b"abd"));
}
#[test]
fn resolve_operator_identity_prefers_repo_local_git_identity() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let repo = tmp.path().join("repo");
init_repo(&repo, "git@github.com:acme/widget.git");
git(&repo, &["config", "user.email", "local@example.test"]);
assert_eq!(
resolve_operator_identity(&repo),
Some("local@example.test".to_string())
);
}
#[test]
fn resolve_operator_identity_ignores_ambient_env_override() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let repo = tmp.path().join("repo");
init_repo(&repo, "git@github.com:acme/widget.git");
git(&repo, &["config", "user.email", "local@example.test"]);
assert_eq!(
resolve_operator_identity(&repo),
Some("local@example.test".to_string()),
"the git identity is the only operator source"
);
let src = include_str!("project_index_id.rs");
let reads_env = src
.lines()
.filter(|l| {
let t = l.trim_start();
!t.starts_with("//") && !t.starts_with("*")
})
.any(|l| l.contains("env::var") || l.contains("env!("));
assert!(
!reads_env,
"derivation must stay hermetic: no environment reads in project_index_id.rs"
);
}