use super::*;
use crate::cli::cache_identity::key_for_test as test_key;
fn api_with_ordered_entries(entries: &[(&str, &str)]) -> crate::core::ir::ApiSurface {
let mut api = crate::core::ir::ApiSurface {
crate_name: "sample_crate".to_string(),
..Default::default()
};
for (name, path) in entries {
api.excluded_type_paths.insert((*name).to_string(), (*path).to_string());
api.excluded_trait_names.insert((*name).to_string());
}
api
}
#[test]
fn validate_cache_crate_name_accepts_normal_names() {
validate_cache_crate_name("my-lib").unwrap();
validate_cache_crate_name("sample_crate").unwrap();
validate_cache_crate_name("sample_markdown").unwrap();
}
#[test]
fn validate_cache_crate_name_rejects_path_separators() {
assert!(validate_cache_crate_name("../escape").is_err());
assert!(validate_cache_crate_name("foo/bar").is_err());
assert!(validate_cache_crate_name("foo\\bar").is_err());
}
#[test]
fn validate_cache_crate_name_rejects_dot_aliases() {
assert!(validate_cache_crate_name("..").is_err());
assert!(validate_cache_crate_name(".").is_err());
}
#[test]
fn validate_cache_crate_name_rejects_nul_byte() {
assert!(validate_cache_crate_name("foo\0bar").is_err());
}
#[test]
fn ir_cache_dir_scopes_by_crate_name() {
assert_eq!(ir_cache_dir("crate-a"), Path::new(CACHE_DIR).join("crate-a"));
assert_eq!(ir_cache_dir("crate-b"), Path::new(CACHE_DIR).join("crate-b"));
assert_ne!(ir_cache_dir("crate-a"), ir_cache_dir("crate-b"));
}
#[test]
fn repeated_ir_serialization_preserves_cache_and_provenance_hashes() {
let first = api_with_ordered_entries(&[
("Gamma", "sample_crate::gamma::Gamma"),
("Alpha", "sample_crate::alpha::Alpha"),
("Beta", "sample_crate::beta::Beta"),
]);
let second = api_with_ordered_entries(&[
("Beta", "sample_crate::beta::Beta"),
("Gamma", "sample_crate::gamma::Gamma"),
("Alpha", "sample_crate::alpha::Alpha"),
]);
let first_json = serde_json::to_string_pretty(&first).expect("serialize first IR");
let second_json = serde_json::to_string_pretty(&second).expect("serialize second IR");
let generated = "// auto-generated by alef\npub fn sample() {}\n";
let first_cache_hash = compute_lang_hash(&first_json, "sample", "[sample]\n");
let second_cache_hash = compute_lang_hash(&second_json, "sample", "[sample]\n");
let first_file_hash = crate::core::hash::compute_file_hash(generated);
let second_file_hash = crate::core::hash::compute_file_hash(generated);
assert_eq!(first_json, second_json);
assert_eq!(first_cache_hash, second_cache_hash);
assert_eq!(first_file_hash, second_file_hash);
assert_eq!(
crate::core::hash::inject_hash_line(generated, &first_file_hash),
crate::core::hash::inject_hash_line(generated, &second_file_hash)
);
}
#[test]
fn manifest_is_sorted_deduplicated_and_newline_terminated() {
let directory = tempfile::tempdir().expect("tempdir");
let manifest = directory.path().join("rust.manifest");
let alpha = directory.path().join("alpha.rs");
let beta = directory.path().join("beta.rs");
write_manifest(&manifest, &[beta.clone(), alpha.clone(), beta.clone()]).expect("write manifest");
let content = std::fs::read_to_string(manifest).expect("read manifest");
assert_eq!(content, format!("{}\n{}\n", alpha.display(), beta.display()));
}
#[test]
fn empty_manifest_is_not_a_cache_hit() {
let directory = tempfile::tempdir().expect("tempdir");
let manifest = directory.path().join("rust.manifest");
std::fs::write(&manifest, "").expect("write empty manifest");
assert!(!outputs_exist(&manifest));
}
#[test]
fn unreadable_output_manifest_is_a_cache_miss() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let generated = tmp.path().join("bindings.py");
std::fs::write(&generated, "# generated\n").expect("write generated output");
write_lang_hash("sample-crate", "python", &test_key("hash-1"), &[generated]).expect("write hash and manifest");
assert!(
is_lang_cached("sample-crate", "python", &test_key("hash-1")),
"a matching hash whose manifested outputs are all present must be a hit"
);
let manifest = hashes_dir("sample-crate").join("python.manifest");
std::fs::remove_file(&manifest).expect("remove the manifest, leaving the hash behind");
assert!(
!is_lang_cached("sample-crate", "python", &test_key("hash-1")),
"a hash with no manifest at all must not validate a cache hit"
);
std::fs::create_dir_all(&manifest).expect("put something unreadable where the manifest belongs");
assert!(
!is_lang_cached("sample-crate", "python", &test_key("hash-1")),
"a manifest that exists but cannot be read must not validate a cache hit either"
);
}
#[test]
fn a_deleted_recorded_output_downgrades_a_cache_hit_to_a_miss() {
struct Scenario {
name: &'static str,
stage: &'static str,
recorded_outputs: &'static [&'static str],
delete_output: Option<&'static str>,
query_hash: &'static str,
expect_hit: bool,
}
let scenarios = [
Scenario {
name: "matching hash with every recorded output present is a hit",
stage: "hit",
recorded_outputs: &["a.rs", "b.rs"],
delete_output: None,
query_hash: "hash-1",
expect_hit: true,
},
Scenario {
name: "matching hash with one recorded output deleted is a miss",
stage: "deleted-output",
recorded_outputs: &["a.rs", "b.rs"],
delete_output: Some("b.rs"),
query_hash: "hash-1",
expect_hit: false,
},
Scenario {
name: "non-matching input hash is a miss regardless of outputs",
stage: "stale-hash",
recorded_outputs: &["a.rs"],
delete_output: None,
query_hash: "hash-2",
expect_hit: false,
},
Scenario {
name: "empty recorded paths is a miss, not an automatic hit",
stage: "empty",
recorded_outputs: &[],
delete_output: None,
query_hash: "hash-1",
expect_hit: false,
},
];
for scenario in scenarios {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let outputs: Vec<PathBuf> = scenario
.recorded_outputs
.iter()
.map(|name| {
let path = tmp.path().join(name);
std::fs::write(&path, "// generated\n").expect("write generated output");
path
})
.collect();
write_stage_hash("sample-crate", scenario.stage, test_key("hash-1").as_str(), &outputs)
.expect("write stage hash and manifest");
if let Some(to_delete) = scenario.delete_output {
std::fs::remove_file(tmp.path().join(to_delete)).expect("delete recorded output");
}
assert_eq!(
is_stage_cached("sample-crate", scenario.stage, &test_key(scenario.query_hash)),
scenario.expect_hit,
"scenario `{}` expected hit={}",
scenario.name,
scenario.expect_hit
);
}
}
#[test]
fn scaffold_manifest_round_trips_through_write_and_read() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let composer = tmp.path().join("packages/php/composer.json");
let cargo_toml = tmp.path().join("Cargo.toml");
let write_result = write_scaffold_manifest("sample-crate", &[composer.clone(), cargo_toml.clone()]);
let read_back = read_scaffold_manifest("sample-crate");
write_result.expect("write scaffold manifest");
assert_eq!(
read_back,
vec![cargo_toml, composer],
"manifest must round-trip both paths in sorted order"
);
}
#[test]
fn scaffold_manifest_reads_empty_when_never_written() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let read_back = read_scaffold_manifest("never-scaffolded-crate");
assert_eq!(read_back, Vec::<PathBuf>::new());
}
#[test]
fn scaffold_manifest_wiring_lets_next_run_reclaim_dropped_manifest() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let package_dir = tmp.path().join("packages/php");
std::fs::create_dir_all(&package_dir).expect("create package dir");
let composer_json = package_dir.join("composer.json");
std::fs::write(&composer_json, "{\n \"name\": \"acme/demo\"\n}\n").expect("write composer.json");
write_scaffold_manifest("sample-php", std::slice::from_ref(&composer_json)).expect("write manifest for run 1");
let previous_scaffold = read_scaffold_manifest("sample-php");
let keep = std::collections::HashSet::new();
let removed =
crate::cli::pipeline::sweep_manifest_orphans(&previous_scaffold, &keep, &[package_dir], &[]).expect("sweep");
assert_eq!(
removed, 1,
"composer.json recorded by run 1's manifest must be reclaimed in run 2"
);
assert!(!composer_json.exists(), "orphaned composer.json must be deleted");
}
#[test]
fn all_bindings_ownership_baseline_survives_the_lang_manifest_collision_that_used_to_erase_it() {
let tmp = tempfile::tempdir().expect("tempdir");
let dropped_type_file = tmp.path().join("packages/python/dropped_type.py");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let result = (|| -> anyhow::Result<(Vec<PathBuf>, Vec<PathBuf>)> {
write_stage_hash(
"sample",
"all-bindings-python-ownership",
"sources-hash-n-minus-1",
std::slice::from_ref(&dropped_type_file),
)?;
write_lang_hash("sample", "python", &test_key("lang-hash-n"), &[])?;
let dedicated_baseline = read_stage_paths("sample", "all-bindings-python-ownership");
let lang_manifest = read_lang_manifest("sample", "python");
Ok((dedicated_baseline, lang_manifest))
})();
let (dedicated_baseline, lang_manifest) = result.expect("baseline read");
assert_eq!(
dedicated_baseline,
vec![dropped_type_file],
"the dedicated ownership stage must still report last run's file list, unaffected by \
`write_lang_hash` overwriting the unrelated `<lang>.manifest` file"
);
assert!(
lang_manifest.is_empty(),
"`<lang>.manifest` itself is expected to have been overwritten by `write_lang_hash` -- \
that overwrite is legitimate cache-invalidation behaviour; the fix is to stop reading \
this file as the sweep baseline, not to change what it stores"
);
}
#[test]
fn all_bindings_ownership_correct_baseline_sweeps_a_binding_this_run_no_longer_emits() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/python");
std::fs::create_dir_all(&package_dir).expect("create package dir");
let kept_file = package_dir.join("kept_type.py");
let dropped_file = package_dir.join("dropped_type.py");
std::fs::write(&kept_file, "kept\n").expect("write kept file");
let header = crate::core::hash::header(crate::core::hash::CommentStyle::Hash);
let hashed = crate::core::hash::inject_hash_line(&header, &"0".repeat(64));
std::fs::write(&dropped_file, &hashed).expect("write dropped file");
let previous_paths = vec![kept_file.clone(), dropped_file.clone()];
let mut keep = std::collections::HashSet::new();
keep.insert(kept_file.clone());
let removed =
crate::cli::pipeline::sweep_manifest_orphans(&previous_paths, &keep, &[package_dir], &[]).expect("sweep");
assert_eq!(removed, 1, "exactly the dropped binding must be swept");
assert!(
!dropped_file.exists(),
"the binding this run no longer emits must be deleted"
);
assert!(
kept_file.exists(),
"a binding still in this run's keep set must survive"
);
}
#[test]
fn all_bindings_ownership_missing_baseline_sweeps_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let package_dir = dir.path().join("packages/python");
std::fs::create_dir_all(&package_dir).expect("create package dir");
let untouched_file = package_dir.join("untouched_type.py");
let header = crate::core::hash::header(crate::core::hash::CommentStyle::Hash);
let hashed = crate::core::hash::inject_hash_line(&header, &"0".repeat(64));
std::fs::write(&untouched_file, &hashed).expect("write file");
let previous_paths = read_stage_paths(
"crate-with-no-prior-all-bindings-ownership-record",
"all-bindings-python-ownership",
);
assert!(previous_paths.is_empty(), "a never-written stage must read back empty");
let keep = std::collections::HashSet::new();
let removed =
crate::cli::pipeline::sweep_manifest_orphans(&previous_paths, &keep, &[package_dir], &[]).expect("sweep");
assert_eq!(removed, 0, "a missing baseline must sweep nothing, never everything");
assert!(
untouched_file.exists(),
"a file must never be deleted on the strength of an absent baseline"
);
}
#[test]
fn all_bindings_ownership_never_owned_path_is_left_untouched_even_when_present_in_sweep_root() {
let tmp = tempfile::tempdir().expect("tempdir");
let _cwd = crate::test_support::CwdGuard::enter(tmp.path());
let result = (|| -> anyhow::Result<(usize, bool, bool, bool)> {
let package_dir = tmp.path().join("packages/python");
std::fs::create_dir_all(&package_dir)?;
let owned_file = package_dir.join("owned_type.py");
let header = crate::core::hash::header(crate::core::hash::CommentStyle::Hash);
let hashed = crate::core::hash::inject_hash_line(&header, &"0".repeat(64));
std::fs::write(&owned_file, &hashed)?;
let foreign_file = package_dir.join("hand_written.py");
std::fs::write(&foreign_file, "# never generated by alef\n")?;
write_stage_hash(
"sample",
"all-bindings-python-ownership",
"sources-hash",
std::slice::from_ref(&owned_file),
)?;
let previous_paths = read_stage_paths("sample", "all-bindings-python-ownership");
let leaked = previous_paths.iter().any(|path| path.ends_with("hand_written.py"));
let keep = std::collections::HashSet::new();
let removed = crate::cli::pipeline::sweep_manifest_orphans(&previous_paths, &keep, &[package_dir], &[])?;
Ok((removed, owned_file.exists(), foreign_file.exists(), leaked))
})();
let (removed, owned_exists, foreign_exists, leaked) = result.expect("sweep");
assert!(!leaked, "the never-owned file must not have leaked into the baseline");
assert_eq!(removed, 1, "only the recorded, owned path may be removed");
assert!(!owned_exists, "the recorded, no-longer-kept binding must be swept");
assert!(
foreign_exists,
"a path alef never recorded owning must survive the sweep"
);
}
#[test]
fn is_alef_derived_output_recognises_the_snippet_coverage_ledger() {
assert!(is_alef_derived_output(Path::new(
"docs-site/src/snippets-generated/.alef-snippet-coverage.json"
)));
assert!(is_alef_derived_output(Path::new(
crate::e2e::snippets::COVERAGE_MANIFEST
)));
}
#[test]
fn is_alef_derived_output_refuses_every_hand_growable_generated_path() {
for hand_growable in [
"packages/php/composer.json",
"packages/node/package.json",
"packages/java/pom.xml",
"packages/zig/build.zig",
"packages/zig/test/sample_core_test.zig",
"packages/dart/test/sample_core_test.dart",
"e2e/go/helpers_test.go",
] {
assert!(
!is_alef_derived_output(Path::new(hand_growable)),
"{hand_growable} is content a human grows: it must never be classified as derived output"
);
}
}
#[test]
fn is_alef_derived_output_requires_the_reserved_namespace_not_only_list_membership() {
for name in ALEF_DERIVED_OUTPUT_NAMES {
assert!(
name.starts_with(ALEF_RESERVED_NAME_PREFIX),
"{name} is registered as derived output but sits outside alef's reserved namespace, \
so the backstop silently disables it"
);
}
assert!(
!is_alef_derived_output(Path::new("docs/snippets/.alef-snippet-coverage.json.bak")),
"a name that merely contains the ledger's name must not match"
);
assert!(
!is_alef_derived_output(Path::new("docs/snippets/.alef-unregistered-state.json")),
"the reserved prefix alone is not enough: membership in the registry is still required"
);
}