#![expect(clippy::expect_used, reason = "test scaffolding")]
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use llmenv::config::HashingMode;
use llmenv::materialize::{cache, materialize_with_mode};
use llmenv::merge::{BundleRef, merge};
use tempfile::tempdir;
fn empty_shape() -> String {
cache::shape(&BTreeSet::new(), &BTreeSet::new())
}
fn materialize_strict(
m: &llmenv::merge::MergedManifest,
root: &std::path::Path,
) -> std::path::PathBuf {
materialize_with_mode(m, root, HashingMode::Strict, &empty_shape())
.expect("materialize strict")
.path
}
fn fixture_bundle(name: &str) -> BundleRef {
BundleRef {
name: name.into(),
path: PathBuf::from(format!("tests/fixtures/bundles/{name}")),
precedence: 1,
}
}
fn empty_native() -> BTreeMap<String, serde_yaml::Value> {
BTreeMap::new()
}
#[test]
fn materializes_deterministically() {
let tmp = tempdir().expect("tempdir");
let m = merge(
&llmenv::config::Capabilities::default(),
&empty_native(),
&[fixture_bundle("base")],
)
.expect("merge");
let p1 = materialize_strict(&m, tmp.path());
let p2 = materialize_strict(&m, tmp.path());
assert_eq!(p1, p2, "same manifest hashes to same path");
assert!(!p1.join("AGENTS.md").exists());
assert!(p1.join("skills/hello/SKILL.md").exists());
}
#[test]
fn different_manifests_produce_different_dirs() {
let tmp = tempdir().expect("tempdir");
let m_base = merge(
&llmenv::config::Capabilities::default(),
&empty_native(),
&[fixture_bundle("base")],
)
.expect("merge base");
let m_both = merge(
&llmenv::config::Capabilities::default(),
&empty_native(),
&[fixture_bundle("base"), fixture_bundle("rust-defaults")],
)
.expect("merge both");
let p1 = materialize_strict(&m_base, tmp.path());
let p2 = materialize_strict(&m_both, tmp.path());
assert_ne!(p1, p2);
}
#[test]
fn normal_mode_reuses_one_folder_across_manifests() {
let tmp = tempdir().expect("tempdir");
let m_base = merge(
&llmenv::config::Capabilities::default(),
&empty_native(),
&[fixture_bundle("base")],
)
.expect("merge base");
let m_both = merge(
&llmenv::config::Capabilities::default(),
&empty_native(),
&[fixture_bundle("base"), fixture_bundle("rust-defaults")],
)
.expect("merge both");
let shape = empty_shape();
let r1 = materialize_with_mode(&m_base, tmp.path(), HashingMode::Normal, &shape)
.expect("materialize base");
let r2 = materialize_with_mode(&m_both, tmp.path(), HashingMode::Normal, &shape)
.expect("materialize both");
assert_eq!(r1.path, r2.path, "normal mode reuses the same folder");
assert_ne!(r1.hash, r2.hash, "but the content hash still differs");
}
#[test]
fn no_tmp_stage_dir_after_success() {
let tmp = tempdir().expect("tempdir");
let m = merge(
&llmenv::config::Capabilities::default(),
&empty_native(),
&[fixture_bundle("base")],
)
.expect("merge");
let _ = materialize_strict(&m, tmp.path());
for entry in std::fs::read_dir(tmp.path()).expect("read_dir") {
let p = entry.expect("entry").path();
assert!(
p.extension().is_none_or(|e| e != "tmp"),
"staging dir leaked: {}",
p.display()
);
}
}
#[test]
fn gc_removes_old_entries_and_keeps_fresh_ones() {
let tmp = tempdir().expect("tempdir");
let old = tmp.path().join("old");
std::fs::create_dir_all(&old).expect("mkdir old");
let marker = old.join("marker");
std::fs::write(&marker, "x").expect("write marker");
let old_time = SystemTime::now() - Duration::from_secs(60 * 60 * 24 * 30);
set_mtime(&marker, old_time);
set_mtime(&old, old_time);
let fresh = tmp.path().join("fresh");
std::fs::create_dir_all(&fresh).expect("mkdir fresh");
std::fs::write(fresh.join("marker"), "y").expect("write marker");
let report = cache::gc(tmp.path(), Duration::from_secs(60 * 60 * 24 * 7)).expect("gc");
assert!(!old.exists(), "old entry should have been removed");
assert!(fresh.exists(), "fresh entry should remain");
assert_eq!(report.kept, 1);
assert_eq!(report.removed.len(), 1);
}
#[test]
fn gc_removes_tmp_stage_dirs_regardless_of_age() {
let tmp = tempdir().expect("tempdir");
let stage = tmp.path().join("abcd.tmp");
std::fs::create_dir_all(&stage).expect("mkdir stage");
std::fs::write(stage.join("marker"), "x").expect("write marker");
let report = cache::gc(tmp.path(), Duration::from_secs(60 * 60 * 24 * 365)).expect("gc");
assert!(!stage.exists(), "stage dir should always be GC'd");
assert_eq!(report.removed.len(), 1);
}
#[test]
fn hash_is_unambiguous_across_field_boundaries() {
use llmenv::materialize::cache::hash_manifest;
use llmenv::merge::MergedManifest;
use std::collections::BTreeMap;
let tmp = tempdir().expect("tempdir");
let f_de = tmp.path().join("de");
let f_e = tmp.path().join("e");
std::fs::write(&f_de, b"FG").expect("write de");
std::fs::write(&f_e, b"FG").expect("write e");
let mut a_files = BTreeMap::new();
a_files.insert(PathBuf::from("DE"), f_de.clone());
let a = MergedManifest {
agents_md: "ABC".into(),
files: a_files,
..Default::default()
};
let mut b_files = BTreeMap::new();
b_files.insert(PathBuf::from("E"), f_e.clone());
let b = MergedManifest {
agents_md: "ABCD".into(),
files: b_files,
..Default::default()
};
let ha = hash_manifest(&a).expect("hash a");
let hb = hash_manifest(&b).expect("hash b");
assert_ne!(ha, hb, "hash must distinguish field boundaries");
}
#[test]
fn gc_on_missing_root_is_noop() {
let tmp = tempdir().expect("tempdir");
let missing = tmp.path().join("nope");
let report = cache::gc(&missing, Duration::from_secs(1)).expect("gc");
assert!(report.removed.is_empty());
assert_eq!(report.kept, 0);
}
fn set_mtime(p: &std::path::Path, t: SystemTime) {
let f = std::fs::OpenOptions::new()
.write(true)
.open(p)
.or_else(|_| std::fs::File::open(p))
.expect("open");
f.set_modified(t).expect("set_modified");
}