use std::fs;
use std::path::{Path, PathBuf};
use mkit_core::hash;
use mkit_core::index::{self, EntryStatus, Index, IndexEntry};
use mkit_core::refs::{
self, Head, REFS_DIR, Ref, RefError, RefWriteCondition, decode_ref_wire, validate_ref_name,
};
use mkit_core::repo_lock;
use mkit_core::store::ObjectStore;
fn goldens_root() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir
.parent()
.and_then(Path::parent)
.map(|workspace| workspace.join("tests").join("golden").join("phase4"))
.expect("workspace layout: crates/mkit-core/.. → workspace root")
}
fn read_golden(name: &str) -> Vec<u8> {
let path = goldens_root().join(name);
fs::read(&path).unwrap_or_else(|e| {
panic!(
"failed to read golden vector {}: {e}. Run `cargo run -p mkit-core --example generate_phase4_goldens`.",
path.display()
)
})
}
#[test]
fn manifest_blake3_digests_match_bin_files() {
let manifest_path = goldens_root().join("MANIFEST.txt");
let manifest = fs::read_to_string(&manifest_path).unwrap_or_else(|e| {
panic!(
"failed to read MANIFEST.txt at {}: {e}",
manifest_path.display()
)
});
let mut checked = 0usize;
for line in manifest.lines() {
if line.starts_with('#') || line.is_empty() {
continue;
}
let mut parts = line.split_whitespace();
let name = parts.next().expect("manifest line missing name");
let expected = parts
.next()
.expect("manifest line missing blake3")
.to_string();
let bytes = read_golden(&format!("{name}.bin"));
let actual = hash::to_hex(&hash::hash(&bytes));
assert_eq!(
actual, expected,
"BLAKE3 mismatch for {name}: regenerate goldens"
);
checked += 1;
}
assert!(checked >= 4, "expected ≥ 4 vectors, got {checked}");
}
#[test]
fn golden_index_empty_round_trips() {
let bytes = read_golden("index_empty.bin");
assert_eq!(bytes.len(), 9);
assert_eq!(&bytes[..4], b"MKIX");
let idx = index::deserialize(&bytes).unwrap();
assert!(idx.entries.is_empty());
let upgraded = idx.serialize();
assert_eq!(upgraded[4], index::FORMAT_VERSION, "writer emits v2");
assert_eq!(index::deserialize(&upgraded).unwrap(), idx);
}
#[test]
fn golden_index_3entries_round_trips() {
let bytes = read_golden("index_3entries.bin");
let idx = index::deserialize(&bytes).unwrap();
assert_eq!(idx.entries.len(), 3);
let by_path: std::collections::HashMap<_, _> = idx
.entries
.iter()
.map(|e| (e.path.as_str(), e.status))
.collect();
assert_eq!(by_path["README.md"], EntryStatus::Blob);
assert_eq!(by_path["src"], EntryStatus::Tree);
assert_eq!(by_path["scripts/build"], EntryStatus::Executable);
assert!(idx.entries.iter().all(|e| e.mtime_ns == 0 && e.size == 0));
let upgraded = idx.serialize();
assert_eq!(upgraded[4], index::FORMAT_VERSION, "writer emits v2");
assert_eq!(index::deserialize(&upgraded).unwrap(), idx);
}
#[test]
fn golden_ref_detached_decodes() {
let bytes = read_golden("ref_detached.bin");
assert_eq!(bytes.len(), 65);
assert_eq!(*bytes.last().unwrap(), b'\n');
assert!(bytes[..64].iter().all(|b| !b.is_ascii_uppercase()));
let h = decode_ref_wire(&bytes).unwrap();
assert_eq!(refs::encode_ref_wire(&h).as_slice(), bytes.as_slice());
}
#[test]
fn golden_head_symbolic_parses() {
let bytes = read_golden("head_symbolic.bin");
let s = std::str::from_utf8(&bytes).unwrap();
assert!(s.starts_with("ref: refs/heads/"));
assert!(s.ends_with('\n'));
let dir = tempfile::TempDir::new().unwrap();
let mkit = dir.path().join(".mkit");
fs::create_dir_all(&mkit).unwrap();
fs::write(mkit.join("HEAD"), &bytes).unwrap();
let head = refs::read_head(&mkit).unwrap();
assert_eq!(head, Head::Branch("main".to_string()));
}
#[test]
fn cas_any_works_against_object_store_repo() {
let dir = tempfile::TempDir::new().unwrap();
let _store = ObjectStore::init(dir.path()).expect("init store");
let mkit = dir.path().join(".mkit");
refs::init(&mkit).unwrap();
let h1 = hash::hash(b"any-1");
refs::update_ref(&mkit, "main", RefWriteCondition::Any, &h1).unwrap();
assert_eq!(refs::read_ref(&mkit, "main").unwrap(), Some(h1));
let h2 = hash::hash(b"any-2");
refs::update_ref(&mkit, "main", RefWriteCondition::Any, &h2).unwrap();
assert_eq!(refs::read_ref(&mkit, "main").unwrap(), Some(h2));
}
#[test]
fn cas_missing_then_match_then_conflict() {
let dir = tempfile::TempDir::new().unwrap();
let _store = ObjectStore::init(dir.path()).expect("init store");
let mkit = dir.path().join(".mkit");
refs::init(&mkit).unwrap();
let v1 = hash::hash(b"v1");
let v2 = hash::hash(b"v2");
refs::update_ref(&mkit, "main", RefWriteCondition::Missing, &v1).unwrap();
let err = refs::update_ref(&mkit, "main", RefWriteCondition::Missing, &v2).unwrap_err();
assert!(matches!(err, RefError::Conflict(_)));
refs::update_ref(&mkit, "main", RefWriteCondition::Match(v1), &v2).unwrap();
assert_eq!(refs::read_ref(&mkit, "main").unwrap(), Some(v2));
let bogus = hash::hash(b"bogus");
let err = refs::update_ref(&mkit, "main", RefWriteCondition::Match(bogus), &v1).unwrap_err();
assert!(matches!(err, RefError::Conflict(_)));
assert_eq!(refs::read_ref(&mkit, "main").unwrap(), Some(v2));
}
#[test]
fn list_refs_returns_sorted_unique_entries() {
let dir = tempfile::TempDir::new().unwrap();
let _store = ObjectStore::init(dir.path()).expect("init store");
let mkit = dir.path().join(".mkit");
refs::init(&mkit).unwrap();
refs::write_ref(&mkit, "main", &hash::hash(b"m")).unwrap();
refs::write_ref(&mkit, "feature/topic", &hash::hash(b"ft")).unwrap();
refs::write_ref(&mkit, "feature/another", &hash::hash(b"fa")).unwrap();
let listed: Vec<Ref> = refs::list_refs(&mkit).unwrap();
let names: Vec<&str> = listed.iter().map(|r| r.name.as_str()).collect();
assert_eq!(names, vec!["feature/another", "feature/topic", "main"]);
let mut sorted = names.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), names.len());
}
#[test]
fn refs_init_creates_layout() {
let dir = tempfile::TempDir::new().unwrap();
let mkit = dir.path().join(".mkit");
fs::create_dir_all(&mkit).unwrap();
refs::init(&mkit).unwrap();
assert!(mkit.join(REFS_DIR).is_dir());
assert!(mkit.join("refs/heads").is_dir());
assert!(mkit.join("refs/tags").is_dir());
let head = refs::read_head(&mkit).unwrap();
assert_eq!(head, Head::Branch("main".to_string()));
}
#[test]
fn validate_ref_name_examples_from_spec() {
assert!(validate_ref_name("main"));
assert!(validate_ref_name("feat/v1.0-beta"));
assert!(validate_ref_name("release/2024_09"));
assert!(!validate_ref_name("feat/.."));
assert!(!validate_ref_name("/main"));
assert!(!validate_ref_name("main@v1"));
assert!(!validate_ref_name("feat\\branch"));
assert!(!validate_ref_name(""));
}
#[test]
fn repo_lock_blocks_concurrent_acquire() {
let dir = tempfile::TempDir::new().unwrap();
let mkit = dir.path().join(".mkit");
fs::create_dir_all(&mkit).unwrap();
let _held = repo_lock::acquire_default(&mkit, "index.lock").unwrap();
let err =
repo_lock::acquire(&mkit, "index.lock", std::time::Duration::from_millis(100)).unwrap_err();
assert!(matches!(err, repo_lock::LockError::Busy(_)));
}
#[test]
fn index_round_trip_via_disk() {
let dir = tempfile::TempDir::new().unwrap();
fs::create_dir_all(dir.path().join(".mkit")).unwrap();
let mut idx = Index::new();
idx.entries.push(IndexEntry {
path: "README.md".into(),
status: EntryStatus::Blob,
object_hash: hash::hash(b"r"),
mtime_ns: 0,
size: 0,
ino: 0,
ctime_ns: 0,
});
idx.entries.push(IndexEntry {
path: "old.txt".into(),
status: EntryStatus::Removed,
object_hash: [0u8; 32],
mtime_ns: 0,
size: 0,
ino: 0,
ctime_ns: 0,
});
index::write_index(dir.path(), &idx).unwrap();
let read = index::read_index(dir.path()).unwrap();
assert_eq!(read, idx);
}
#[test]
fn worktree_build_tree_against_object_store() {
let dir = tempfile::TempDir::new().unwrap();
let store = ObjectStore::init(dir.path()).unwrap();
let work = tempfile::TempDir::new().unwrap();
fs::write(work.path().join("a.txt"), b"a").unwrap();
fs::write(work.path().join("b.txt"), b"b").unwrap();
let h1 = mkit_core::worktree::build_tree(&store, work.path()).unwrap();
let h2 = mkit_core::worktree::build_tree(&store, work.path()).unwrap();
assert_eq!(h1, h2, "build_tree must be deterministic");
let obj = store.read_object(&h1).unwrap();
if let mkit_core::object::Object::Tree(t) = obj {
assert_eq!(t.entries.len(), 2);
} else {
panic!("expected tree object");
}
}