#![allow(clippy::result_large_err)]
use gemel::store::{fsck, InitOptions, Repo};
use gemel::workflow::{self, BeginOptions, FinishOptions};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn temp_root(tag: &str) -> PathBuf {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("gemel-p6-{tag}-{}-{n}", std::process::id()));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write_file(root: &Path, rel: &str, content: &str) {
let path = root.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, content).unwrap();
}
fn seed(root: &Path) -> Repo {
write_file(
root,
"src/lib.rs",
"pub fn greet() -> &'static str { \"hi\" }\n",
);
let repo = Repo::init(root, &InitOptions::default()).unwrap();
workflow::begin_change(
&repo,
&BeginOptions {
intent_summary: Some("greeting".into()),
..Default::default()
},
)
.unwrap();
workflow::finish_change(
&repo,
&FinishOptions {
summary: "add greet".into(),
..Default::default()
},
)
.unwrap();
let state = repo
.read_ref(gemel::store::REF_STATE_HEAD)
.unwrap()
.unwrap();
let producer = gemel::content::object_identity(
&repo,
&gemel::defaults::automation_producer_object_at(gemel::semantic::INDEXER_PRODUCER_NAME, 0),
)
.unwrap();
gemel::semantic::index_state(&repo, &state, &producer).unwrap();
repo
}
fn open_remote(path: &Path) -> gemel::sync::FileTransport {
gemel::sync::FileTransport::open(path, false).unwrap()
}
#[test]
fn native_roundtrip_identical_identities() {
let a = temp_root("rt-a");
let b = temp_root("rt-b");
let repo_a = seed(&a);
let repo_b = Repo::init(&b, &InitOptions::default()).unwrap();
let ta = open_remote(&a);
let pulled = gemel::sync::pull(&repo_b, "origin", &ta).unwrap();
assert!(pulled.fast_forwarded);
assert!(pulled.fetch.transferred > 0);
assert_eq!(
repo_a.read_ref(gemel::store::REF_HEAD).unwrap(),
repo_b.read_ref(gemel::store::REF_HEAD).unwrap()
);
assert_eq!(
repo_a.read_ref(gemel::store::REF_STATE_HEAD).unwrap(),
repo_b.read_ref(gemel::store::REF_STATE_HEAD).unwrap()
);
let seeds = repo_b
.read_ref(gemel::store::REF_HEAD)
.unwrap()
.map(|h| vec![h])
.unwrap_or_default();
let a_ids = gemel::sync::reachable_ids(
&repo_a,
&repo_a
.read_ref(gemel::store::REF_HEAD)
.unwrap()
.map(|h| vec![h])
.unwrap_or_default(),
)
.unwrap();
let b_ids = gemel::sync::reachable_ids(&repo_b, &seeds).unwrap();
assert_eq!(a_ids, b_ids, "reachable closures must be identical");
let why = gemel::query::why(&repo_b, "greet").unwrap();
assert!(why.introduced_by.is_some());
let state = repo_b
.read_ref(gemel::store::REF_STATE_HEAD)
.unwrap()
.unwrap();
assert!(gemel::semantic::index_for_state(&repo_b, &state)
.unwrap()
.is_some());
}
#[test]
fn negotiation_dedup_and_idempotence() {
let a = temp_root("dedup-a");
let b = temp_root("dedup-b");
seed(&a);
let repo_b = Repo::init(&b, &InitOptions::default()).unwrap();
let ta = open_remote(&a);
let tb = open_remote(&b);
let _first = gemel::sync::push(&repo_b, "origin", &ta).unwrap();
let second = gemel::sync::push(&repo_b, "origin", &ta).unwrap();
assert_eq!(second.missing_on_remote, 0);
assert_eq!(second.transferred, 0);
let fetched = gemel::sync::fetch(&repo_b, "origin", &ta).unwrap();
assert!(fetched.transferred > 0);
let again = gemel::sync::fetch(&repo_b, "origin", &ta).unwrap();
assert_eq!(again.wanted, 0);
assert_eq!(again.transferred, 0);
let _ = tb;
let head = repo_b
.read_ref(&format!("{}/origin/head", gemel::sync::REF_REMOTES))
.unwrap()
.unwrap();
let closure = gemel::sync::reachable_ids(&repo_b, &[head]).unwrap();
let again = gemel::sync::fetch(&repo_b, "origin", &ta).unwrap();
assert_eq!(again.transferred, 0);
let closure2 = gemel::sync::reachable_ids(&repo_b, &[head]).unwrap();
assert_eq!(closure, closure2);
}
#[test]
fn tracking_refs_record_remote_knowledge() {
let a = temp_root("track-a");
let b = temp_root("track-b");
let repo_a = seed(&a);
let repo_b = Repo::init(&b, &InitOptions::default()).unwrap();
let ta = open_remote(&a);
gemel::sync::fetch(&repo_b, "origin", &ta).unwrap();
let tracked = gemel::sync::tracked_refs(&repo_b, "origin").unwrap();
assert!(tracked.iter().any(|(n, g)| {
n == "refs/remotes/origin/head"
&& *g == repo_a.read_ref(gemel::store::REF_HEAD).unwrap().unwrap()
}));
assert!(tracked
.iter()
.any(|(n, _)| n.starts_with("refs/remotes/origin/names/")));
assert!(tracked
.iter()
.any(|(n, _)| n.starts_with("refs/remotes/origin/trajectories/")));
assert!(repo_b.read_ref(gemel::store::REF_HEAD).unwrap().is_none());
}
#[test]
fn multi_producer_and_semantic_objects_travel() {
let a = temp_root("fam-a");
let b = temp_root("fam-b");
let repo_a = seed(&a);
let repo_b = Repo::init(&b, &InitOptions::default()).unwrap();
let ta = open_remote(&a);
gemel::sync::pull(&repo_b, "origin", &ta).unwrap();
let a_refs = gemel::sync::public_refs(&repo_a).unwrap();
let a_seeds: Vec<gemel::gid::Gid> = a_refs.iter().map(|(_, g)| *g).collect();
let a_closure = gemel::sync::reachable_ids(&repo_a, &a_seeds).unwrap();
let fams: std::collections::HashSet<gemel::family::Family> =
a_closure.iter().map(|g| g.family()).collect();
assert!(fams.contains(&gemel::family::Family::Producer));
assert!(fams.contains(&gemel::family::Family::SemanticEntity));
assert!(fams.contains(&gemel::family::Family::SemanticIndex));
assert!(fams.contains(&gemel::family::Family::Blob));
let b_refs = gemel::sync::public_refs(&repo_b).unwrap();
let b_seeds: Vec<gemel::gid::Gid> = b_refs.iter().map(|(_, g)| *g).collect();
let b_closure = gemel::sync::reachable_ids(&repo_b, &b_seeds).unwrap();
let b_fams: std::collections::HashSet<gemel::family::Family> =
b_closure.iter().map(|g| g.family()).collect();
for f in &fams {
assert!(b_fams.contains(f), "family {f} did not travel");
}
}
#[test]
fn corrupted_remote_fails_closed() {
let a = temp_root("corrupt-a");
let b = temp_root("corrupt-b");
let repo_a = seed(&a);
let repo_b = Repo::init(&b, &InitOptions::default()).unwrap();
let mut corrupted = false;
let objects = a.join(".gemel").join("objects");
for entry in std::fs::read_dir(&objects).unwrap() {
let dir = entry.unwrap().path();
for f in std::fs::read_dir(&dir).unwrap() {
let path = f.unwrap().path();
if path.extension().and_then(|e| e.to_str()) == Some("gce") {
let mut bytes = std::fs::read(&path).unwrap();
let last = bytes.len() - 1;
bytes[last] ^= 0x01;
std::fs::write(&path, bytes).unwrap();
corrupted = true;
break;
}
}
if corrupted {
break;
}
}
assert!(corrupted, "expected at least one object file");
let ta = open_remote(&a);
assert!(gemel::sync::fetch(&repo_b, "origin", &ta).is_err());
assert!(repo_b
.read_ref(&format!("{}/origin/head", gemel::sync::REF_REMOTES))
.unwrap()
.is_none());
let report = fsck::run(
&repo_a,
&fsck::FsckOptions {
repair: false,
rebuild_index: false,
verbose: false,
},
)
.unwrap();
assert!(!report.problems.is_empty());
}
#[test]
fn conflicting_identity_is_fatal() {
let a = temp_root("conflict-a");
let b = temp_root("conflict-b");
let repo_a = seed(&a);
let repo_b = Repo::init(&b, &InitOptions::default()).unwrap();
let ta = open_remote(&a);
gemel::sync::pull(&repo_b, "origin", &ta).unwrap();
let b_head = repo_b.read_ref(gemel::store::REF_HEAD).unwrap().unwrap();
let closure = gemel::sync::reachable_ids(&repo_b, &[b_head]).unwrap();
let blob = closure
.iter()
.find(|g| g.family() == gemel::family::Family::Blob)
.unwrap();
let blob_path = gemel::store::objects::object_path(repo_b.meta_dir(), blob);
let mut bytes = std::fs::read(&blob_path).unwrap();
let last = bytes.len() - 1;
bytes[last] ^= 0x01;
std::fs::write(&blob_path, bytes).unwrap();
let tb = open_remote(&b);
let err = gemel::sync::push(&repo_b, "origin", &tb).unwrap_err();
let text = format!("{err}");
assert!(!text.is_empty());
assert!(repo_a.read_ref(gemel::store::REF_HEAD).unwrap().is_some());
}
#[test]
fn diverged_pull_refuses_and_preserves_local_work() {
let a = temp_root("div-a");
let b = temp_root("div-b");
let repo_a = seed(&a);
let repo_b = Repo::init(&b, &InitOptions::default()).unwrap();
let ta = open_remote(&a);
gemel::sync::pull(&repo_b, "origin", &ta).unwrap();
write_file(&b, "src/lib.rs", "pub fn local_only() {}\n");
workflow::begin_change(
&repo_b,
&BeginOptions {
intent_summary: Some("local".into()),
..Default::default()
},
)
.unwrap();
workflow::finish_change(
&repo_b,
&FinishOptions {
summary: "local divergence".into(),
..Default::default()
},
)
.unwrap();
let local_head = repo_b.read_ref(gemel::store::REF_HEAD).unwrap().unwrap();
write_file(&a, "src/lib.rs", "pub fn remote_only() {}\n");
workflow::begin_change(
&repo_a,
&BeginOptions {
intent_summary: Some("remote".into()),
..Default::default()
},
)
.unwrap();
workflow::finish_change(
&repo_a,
&FinishOptions {
summary: "remote divergence".into(),
..Default::default()
},
)
.unwrap();
let err = gemel::sync::pull(&repo_b, "origin", &ta).unwrap_err();
assert!(format!("{err}").contains("diverged"));
assert_eq!(
repo_b.read_ref(gemel::store::REF_HEAD).unwrap().unwrap(),
local_head
);
assert!(!gemel::sync::tracked_refs(&repo_b, "origin")
.unwrap()
.is_empty());
}
#[test]
fn git_only_remote_uses_deterministic_projection() {
let a = temp_root("git-a");
let repo_a = seed(&a);
let git_remote = temp_root("git-remote");
git(&git_remote, &["init", "-q"]);
git(&git_remote, &["config", "user.email", "court@example.com"]);
git(&git_remote, &["config", "user.name", "Gemel Court"]);
let ta = open_remote(&a);
let out = gemel::git_interop::export_git(
&repo_a,
&gemel::git_interop::ExportGitOptions {
git_dir: git_remote.join(".git"),
branch: "main".into(),
include_claims: false,
},
)
.unwrap();
assert!(out.commits >= 1);
let log = git(&git_remote, &["--no-pager", "log", "--oneline", "-1"]);
assert!(!log.trim().is_empty());
let b = temp_root("git-b");
let repo_b = Repo::init(&b, &InitOptions::default()).unwrap();
let imported = gemel::git_interop::import_git(
&repo_b,
&gemel::git_interop::ImportGitOptions {
git_dir: git_remote.join(".git"),
head: "HEAD".into(),
},
)
.unwrap();
assert!(imported.changes >= 1);
let _ = ta;
}
#[test]
fn non_repository_remote_fails_closed() {
let a = temp_root("norepo-a");
seed(&a);
let not_a_repo = temp_root("norepo");
assert!(gemel::sync::FileTransport::open(¬_a_repo, false).is_err());
let bin = env!("CARGO_BIN_EXE_gemel");
let out = Command::new(bin)
.arg("--repo")
.arg(&a)
.args(["push", not_a_repo.to_str().unwrap()])
.output()
.unwrap();
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("neither a gemel repository nor a git repository"),
"unexpected stderr: {stderr}"
);
}
#[test]
fn fsck_clean_after_sync_on_both_sides() {
let a = temp_root("fsck-a");
let b = temp_root("fsck-b");
let repo_a = seed(&a);
let repo_b = Repo::init(&b, &InitOptions::default()).unwrap();
let ta = open_remote(&a);
gemel::sync::pull(&repo_b, "origin", &ta).unwrap();
for (label, repo) in [("local", &repo_b), ("remote", &repo_a)] {
let report = fsck::run(
repo,
&fsck::FsckOptions {
repair: false,
rebuild_index: false,
verbose: false,
},
)
.unwrap();
assert!(
report
.problems
.iter()
.all(|p| p.code == "exchange-omitted" || p.code == "missing-object"),
"{label} fsck problems: {:?}",
report.problems
);
}
}
fn git(root: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.arg("-C")
.arg(root)
.args(args)
.output()
.expect("run git");
assert!(
out.status.success(),
"git {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).into_owned()
}