use anyhow::{Context, Result};
fn run_required_post_builds(
languages: &[crate::core::config::Language],
config: &crate::core::config::ResolvedCrateConfig,
base_dir: &std::path::Path,
) -> Result<()> {
for &language in languages {
let Some(backend) = crate::cli::registry::try_get_backend(language) else {
continue;
};
let Some(build_config) = backend.build_config_with_config(config) else {
continue;
};
if build_config.post_build.is_empty() {
continue;
}
tracing::info!(" [{language}] running post-build...");
crate::cli::pipeline::run_post_build(language, &build_config, config, base_dir)
.with_context(|| format!("failed to run required post-build steps for {language}"))?;
tracing::info!(" [{language}] post-build processing complete");
}
Ok(())
}
pub(crate) fn complete_generated_artifacts(
languages: &[crate::core::config::Language],
config: &crate::core::config::ResolvedCrateConfig,
base_dir: &std::path::Path,
) -> Result<()> {
run_required_post_builds(languages, config, base_dir)?;
if !languages.contains(&crate::core::config::Language::Ffi) {
return Ok(());
}
crate::cli::pipeline::ensure_ffi_header_freshness(config, base_dir, || {
crate::cli::pipeline::build(config, &[crate::core::config::Language::Ffi], false)
})
}
pub(crate) fn generated_files_match_disk(
lang_files: &[crate::core::backend::GeneratedFile],
base_dir: &std::path::Path,
) -> bool {
lang_files.iter().all(|file| {
let normalized = crate::cli::pipeline::normalize_content(&file.path, &file.content);
match std::fs::read_to_string(base_dir.join(&file.path)) {
Ok(disk) => crate::core::hash::strip_hash_line(&disk) == crate::core::hash::strip_hash_line(&normalized),
Err(_) => false,
}
})
}
pub(crate) fn default_log_level(verbose: u8, quiet: bool) -> &'static str {
if quiet {
return "error";
}
match verbose {
0 => "info",
1 => "debug",
_ => "trace",
}
}
pub(crate) fn init_tracing(verbose: u8, quiet: bool, no_color: bool) {
use tracing_subscriber::EnvFilter;
let default_level = default_log_level(verbose, quiet);
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_ansi(!no_color)
.with_writer(std::io::stderr)
.without_time()
.with_target(false)
.init();
}
pub(crate) fn load_config(
path: &std::path::Path,
) -> Result<(
crate::core::config::WorkspaceConfig,
Vec<crate::core::config::ResolvedCrateConfig>,
)> {
let content =
std::fs::read_to_string(path).with_context(|| format!("Failed to read config: {}", path.display()))?;
crate::core::config::detect_legacy_keys(&content).with_context(|| {
format!(
"legacy schema detected in {} — run `alef migrate` to update automatically",
path.display()
)
})?;
let mut toml_value: toml::Value =
toml::from_str(&content).with_context(|| format!("Failed to parse alef.toml ({})", path.display()))?;
let deprecation_warnings = crate::core::config::legacy::strip_deprecated_keys(&mut toml_value);
for warning in &deprecation_warnings {
tracing::warn!("{}", warning);
}
let cfg: crate::core::config::NewAlefConfig = toml_value
.try_into()
.with_context(|| format!("Failed to deserialize alef.toml ({})", path.display()))?;
let resolved = cfg
.resolve()
.with_context(|| format!("failed to resolve crates in {}", path.display()))?;
for resolved_cfg in &resolved {
crate::core::config::validation::validate_resolved(resolved_cfg)
.with_context(|| format!("invalid resolved config for crate `{}`", resolved_cfg.name))?;
}
Ok((cfg.workspace, resolved))
}
pub(crate) fn resolve_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
) -> Result<Vec<crate::core::config::Language>> {
resolve_languages_inner(config, filter, false)
}
pub(crate) fn resolve_doc_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
) -> Result<Vec<crate::core::config::Language>> {
resolve_languages_inner(config, filter, true)
}
pub(crate) fn resolve_readme_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
) -> Result<Vec<crate::core::config::Language>> {
resolve_languages_inner(config, filter, true)
}
pub(crate) fn resolve_test_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
include_e2e: bool,
) -> Result<Vec<crate::core::config::Language>> {
match filter {
Some(langs) => {
let mut result = vec![];
for lang_str in langs {
let lang = parse_language(lang_str)?;
if config.languages.contains(&lang) || config.test.contains_key(&lang.to_string()) {
result.push(lang);
} else {
anyhow::bail!("Language '{lang_str}' not in config languages list or test configuration");
}
}
Ok(result)
}
None => {
let mut langs = config.languages.clone();
if include_e2e {
let mut extra_test_langs = vec![];
for (lang_str, test_config) in &config.test {
if test_config.e2e.is_none() {
continue;
}
let lang = parse_language(lang_str)
.with_context(|| format!("Invalid test language in alef.toml: {lang_str}"))?;
if !langs.contains(&lang) {
extra_test_langs.push(lang);
}
}
extra_test_langs.sort_by_key(|lang| lang.to_string());
for lang in extra_test_langs {
if !langs.contains(&lang) {
langs.push(lang);
}
}
}
Ok(langs)
}
}
}
pub(crate) fn resolve_languages_inner(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
allow_rust: bool,
) -> Result<Vec<crate::core::config::Language>> {
match filter {
Some(langs) => {
let mut result = vec![];
for lang_str in langs {
let lang = parse_language(lang_str)?;
if config.languages.contains(&lang) || (allow_rust && lang == crate::core::config::Language::Rust) {
result.push(lang);
} else {
anyhow::bail!("Language '{lang_str}' not in config languages list");
}
}
Ok(result)
}
None => {
let mut langs = config.languages.clone();
if allow_rust && !langs.contains(&crate::core::config::Language::Rust) {
langs.push(crate::core::config::Language::Rust);
}
Ok(langs)
}
}
}
pub(crate) fn parse_language(lang_str: &str) -> Result<crate::core::config::Language> {
toml::Value::String(lang_str.to_string())
.try_into()
.with_context(|| format!("Unknown language: {lang_str}"))
}
pub(crate) fn format_languages(languages: &[crate::core::config::Language]) -> String {
languages.iter().map(|l| l.to_string()).collect::<Vec<_>>().join(", ")
}
pub(crate) struct StaleMismatch {
pub(crate) path: String,
pub(crate) embedded: String,
pub(crate) computed: Vec<String>,
}
const VERIFY_SKIP_DIRS: &[&str] = &[
".git",
".alef",
"target",
"node_modules",
"_build",
"deps",
"parsers",
"dist",
"dist-node",
"vendor",
".venv",
".cache",
".remote-cache",
"__pycache__",
"build",
"tmp",
"out",
".idea",
".vscode",
"worktrees",
];
const VERIFY_SCAN_DOT_DIRS: &[&str] = &[
".cargo", ".github",
".agents", ".claude", ".codex", ".cursor", ".gemini",
];
const VERIFY_SCAN_EXTENSIONS: &[&str] = &[
"rs",
"py",
"pyi",
"ts",
"tsx",
"js",
"mjs",
"cjs",
"rb",
"rbs",
"php",
"phpstub",
"go",
"java",
"cs",
"ex",
"exs",
"R",
"r",
"toml",
"json",
"md",
"h",
"c",
"yaml",
"yml",
"zig",
"dart",
"kt",
"kts",
"swift",
"gleam",
"properties",
"pro",
"sh",
"props",
"xml",
"csproj",
"zon",
"cmake",
"gemspec",
];
const VERIFY_SCAN_FILENAMES: &[&str] = &[
".gitignore",
".gitattributes",
".editorconfig",
"Makefile",
"GNUmakefile",
"makefile",
"go.mod",
"Rakefile",
"Makevars",
"Makevars.in",
"Makevars.win.in",
];
fn collect_alef_hashes(base_dir: &std::path::Path) -> Vec<(std::path::PathBuf, Option<String>, String)> {
let mut found = Vec::new();
let mut stack: Vec<std::path::PathBuf> = vec![base_dir.to_path_buf()];
while let Some(dir) = stack.pop() {
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if file_type.is_dir() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let pruned_as_dotfile = name.starts_with('.') && !VERIFY_SCAN_DOT_DIRS.contains(&name);
if VERIFY_SKIP_DIRS.contains(&name) || pruned_as_dotfile {
continue;
}
stack.push(path);
continue;
}
if !file_type.is_file() {
continue;
}
let name_ok = path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| VERIFY_SCAN_FILENAMES.contains(&n));
let ext_ok = name_ok
|| path
.extension()
.and_then(|e| e.to_str())
.map(|e| {
VERIFY_SCAN_EXTENSIONS
.iter()
.any(|allowed| allowed.eq_ignore_ascii_case(e))
})
.unwrap_or(false);
if !ext_ok {
continue;
}
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
if crate::core::hash::content_has_alef_marker(&content) {
found.push((path, crate::core::hash::extract_hash(&content), content));
}
}
}
found
}
pub(crate) struct StampDisagreement {
pub(crate) key: String,
pub(crate) examples: Vec<(String, String)>,
}
pub(crate) fn find_stamp_disagreement(base_dir: &std::path::Path, key: &str) -> Option<StampDisagreement> {
let mut by_value: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
for (path, _hash, content) in collect_alef_hashes(base_dir) {
let Some(value) = crate::core::hash::extract_stamp(&content, key) else {
continue;
};
by_value.entry(value).or_insert_with(|| path.display().to_string());
}
if by_value.len() < 2 {
return None;
}
Some(StampDisagreement {
key: key.to_string(),
examples: by_value.into_iter().map(|(value, path)| (path, value)).collect(),
})
}
fn missing_managed_paths(files: &[crate::core::backend::GeneratedFile], base_dir: &std::path::Path) -> Vec<String> {
crate::cli::pipeline::managed_generated_files(files)
.into_iter()
.filter(|file| !base_dir.join(&file.path).exists())
.map(|file| base_dir.join(&file.path).display().to_string())
.collect()
}
pub(crate) struct FrozenFile {
pub(crate) path: String,
pub(crate) remedy: Option<String>,
pub(crate) near_miss: Option<String>,
}
fn marker_line(content: &str) -> Option<&str> {
content
.lines()
.find(|line| crate::core::hash::content_has_alef_marker(line))
}
fn frozen_managed_paths(files: &[crate::core::backend::GeneratedFile], base_dir: &std::path::Path) -> Vec<FrozenFile> {
crate::cli::pipeline::managed_generated_files(files)
.into_iter()
.filter_map(|file| {
let full_path = base_dir.join(&file.path);
let existing = std::fs::read_to_string(&full_path).ok()?;
if crate::core::hash::content_has_alef_marker(&existing) {
return None;
}
let remedy = marker_line(&file.content).map(str::to_owned).or_else(|| {
let header = crate::cli::pipeline::provenance_header_for_path(&file.path)?;
marker_line(&header).map(str::to_owned)
});
let near_miss = crate::core::hash::near_miss_marker(&existing).map(str::to_owned);
Some(FrozenFile {
path: full_path.display().to_string(),
remedy,
near_miss,
})
})
.collect()
}
#[derive(Default)]
pub(crate) struct MissingAndFrozenFiles {
pub(crate) missing: Vec<String>,
pub(crate) frozen: Vec<FrozenFile>,
pub(crate) stage_failures: Vec<String>,
}
pub(crate) fn find_missing_and_frozen_generated_files(
languages: &[crate::core::config::Language],
api: &crate::core::ir::ApiSurface,
config: &crate::core::config::ResolvedCrateConfig,
config_path: &std::path::Path,
base_dir: &std::path::Path,
) -> anyhow::Result<MissingAndFrozenFiles> {
let (surface, stage_failures) = collect_managed_surface(languages, api, config, config_path, base_dir)?;
let mut result = MissingAndFrozenFiles {
missing: missing_managed_paths(&surface, base_dir),
frozen: frozen_managed_paths(&surface, base_dir),
stage_failures: stage_failures
.into_iter()
.map(|failure| format!("[{}] {}", failure.stage, failure.message))
.collect(),
};
result.missing.sort();
result.missing.dedup();
result.frozen.sort_by(|a, b| a.path.cmp(&b.path));
result.frozen.dedup_by(|a, b| a.path == b.path);
result.stage_failures.sort();
result.stage_failures.dedup();
Ok(result)
}
fn absorb_stage(
surface: &mut std::collections::BTreeMap<std::path::PathBuf, crate::core::backend::GeneratedFile>,
files: Vec<crate::core::backend::GeneratedFile>,
) {
for file in files {
surface.insert(file.path.clone(), file);
}
}
pub(crate) struct StageFailure {
pub(crate) stage: &'static str,
pub(crate) message: String,
pub(crate) paths: Vec<std::path::PathBuf>,
}
impl StageFailure {
pub(crate) fn affects_any(&self, targets: &[String]) -> bool {
self.paths.iter().any(|path| {
targets
.iter()
.any(|target| crate::cli::commands::adopt::matches_target(target, path))
})
}
}
pub(crate) fn collect_managed_surface(
languages: &[crate::core::config::Language],
api: &crate::core::ir::ApiSurface,
config: &crate::core::config::ResolvedCrateConfig,
config_path: &std::path::Path,
base_dir: &std::path::Path,
) -> anyhow::Result<(Vec<crate::core::backend::GeneratedFile>, Vec<StageFailure>)> {
type StageOutcome = anyhow::Result<(Vec<crate::core::backend::GeneratedFile>, Vec<StageFailure>)>;
type Stage<'a> = Box<dyn Fn() -> StageOutcome + Send + Sync + 'a>;
let bindings_stage: Stage<'_> = Box::new(|| {
let mut files = Vec::new();
for (_, produced) in crate::cli::pipeline::generate(api, config, languages, false, config_path, false)? {
files.extend(produced);
}
for (_, produced) in crate::cli::pipeline::generate_service_api(api, config, languages)? {
files.extend(produced);
}
Ok((files, Vec::new()))
});
let scaffold_stage: Stage<'_> = Box::new(|| {
let mut files = crate::cli::pipeline::scaffold(api, config, languages, config_path)?;
for (_, produced) in crate::cli::pipeline::generate_stubs(api, config, languages)? {
files.extend(produced);
}
if config.generate.public_api {
for (_, produced) in crate::cli::pipeline::generate_public_api(api, config, languages, config_path)? {
files.extend(produced);
}
}
Ok((files, Vec::new()))
});
let e2e_local_stage: Stage<'_> = Box::new(|| {
let Some(e2e_config) = &config.e2e else {
return Ok((Vec::new(), Vec::new()));
};
let (files, generator_error) = crate::e2e::generate_e2e(
config,
e2e_config,
None,
&api.types,
&api.enums,
&api.functions,
&api.errors,
)
.context("failed to render the e2e stage of alef's managed output")?;
Ok(stage_failure_for("e2e", generator_error, files))
});
let e2e_registry_stage: Stage<'_> = Box::new(|| {
let Some(e2e_config) = &config.e2e else {
return Ok((Vec::new(), Vec::new()));
};
let mut registry_config = e2e_config.clone();
registry_config.dep_mode = crate::core::config::e2e::DependencyMode::Registry;
let (files, generator_error) = crate::e2e::generate_e2e(
config,
®istry_config,
None,
&api.types,
&api.enums,
&api.functions,
&api.errors,
)
.context("failed to render the registry-mode test-app stage of alef's managed output")?;
Ok(stage_failure_for("test-apps (registry mode)", generator_error, files))
});
let readme_stage: Stage<'_> = Box::new(|| {
let readme_languages = crate::readme::expand_configured_readme_languages(config, languages);
Ok((
crate::cli::pipeline::readme(api, config, &readme_languages)?,
Vec::new(),
))
});
let docs_stage: Stage<'_> = Box::new(|| {
let doc_languages = resolve_doc_languages(config, None)?;
let (doc_files, doc_result) = crate::docs::generate_docs_stage_without_snippet_compile_validation(
api,
config,
&doc_languages,
None,
base_dir,
);
if let Err(error) = doc_result {
tracing::debug!("docs stage reported {error:#}; using the pages it rendered before the failure");
}
Ok((doc_files, Vec::new()))
});
let stages = [
bindings_stage,
scaffold_stage,
e2e_local_stage,
e2e_registry_stage,
readme_stage,
docs_stage,
];
let outcomes: Vec<StageOutcome> = {
use rayon::prelude::*;
stages.par_iter().map(|stage| stage()).collect()
};
let mut surface = std::collections::BTreeMap::new();
let mut stage_failures = Vec::new();
for outcome in outcomes {
let (files, failures) = outcome?;
stage_failures.extend(failures);
absorb_stage(&mut surface, files);
}
Ok((surface.into_values().collect(), stage_failures))
}
fn stage_failure_for(
stage: &'static str,
generator_error: Option<anyhow::Error>,
files: Vec<crate::core::backend::GeneratedFile>,
) -> (Vec<crate::core::backend::GeneratedFile>, Vec<StageFailure>) {
let failures = generator_error
.map(|error| StageFailure {
stage,
message: format!("{error:#}"),
paths: files.iter().map(|file| file.path.clone()).collect(),
})
.into_iter()
.collect();
(files, failures)
}
pub(crate) fn verify_walk_multi(
base_dir: &std::path::Path,
inputs_hashes: &[String],
) -> anyhow::Result<Vec<StaleMismatch>> {
if inputs_hashes.is_empty() {
return Ok(Vec::new());
}
if inputs_hashes.len() == 1 {
return verify_walk(base_dir, &inputs_hashes[0]);
}
let mut stale: Vec<StaleMismatch> = collect_alef_hashes(base_dir)
.into_iter()
.filter(|(_, disk_hash, content)| {
disk_hash.as_ref().is_none_or(|disk_hash| {
!inputs_hashes
.iter()
.any(|inputs_hash| crate::core::hash::compute_file_hash(inputs_hash, content) == *disk_hash)
})
})
.map(|(path, disk_hash, content)| StaleMismatch {
path: path.display().to_string(),
embedded: disk_hash.unwrap_or_else(|| "<missing>".to_owned()),
computed: inputs_hashes
.iter()
.map(|inputs_hash| crate::core::hash::compute_file_hash(inputs_hash, &content))
.collect(),
})
.collect();
stale.sort_by(|a, b| a.path.cmp(&b.path));
Ok(stale)
}
pub(crate) fn verify_walk(base_dir: &std::path::Path, inputs_hash: &str) -> anyhow::Result<Vec<StaleMismatch>> {
let mut stale: Vec<StaleMismatch> = collect_alef_hashes(base_dir)
.into_iter()
.filter(|(_, disk_hash, content)| {
disk_hash
.as_ref()
.is_none_or(|disk_hash| crate::core::hash::compute_file_hash(inputs_hash, content) != *disk_hash)
})
.map(|(path, disk_hash, content)| StaleMismatch {
path: path.display().to_string(),
embedded: disk_hash.unwrap_or_else(|| "<missing>".to_owned()),
computed: vec![crate::core::hash::compute_file_hash(inputs_hash, &content)],
})
.collect();
stale.sort_by(|a, b| a.path.cmp(&b.path));
Ok(stale)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::Language;
fn resolved_test_config() -> crate::core::config::ResolvedCrateConfig {
let cfg: crate::core::config::NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["python"]
[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]
[crates.test.python]
command = "pytest"
[crates.test.rust]
e2e = "cargo test"
"#,
)
.unwrap();
cfg.resolve().unwrap().remove(0)
}
fn scanned_names(names: &[&str]) -> Vec<String> {
let directory = tempfile::tempdir().expect("temporary project");
for name in names {
let path = directory.path().join(name);
let marker = crate::core::hash::header(crate::core::hash::CommentStyle::Hash);
std::fs::write(&path, format!("{marker}\nseeded = true\n")).expect("seed stamped file");
}
let mut found: Vec<String> = collect_alef_hashes(directory.path())
.into_iter()
.filter_map(|(path, _, _)| path.file_name()?.to_str().map(str::to_owned))
.collect();
found.sort();
found
}
fn scanned_relative_paths(relative_paths: &[&str]) -> Vec<String> {
let directory = tempfile::tempdir().expect("temporary project");
for relative in relative_paths {
let path = directory.path().join(relative);
std::fs::create_dir_all(path.parent().expect("seeded path has a parent")).expect("seed parent directory");
let marker = crate::core::hash::header(crate::core::hash::CommentStyle::Hash);
std::fs::write(&path, format!("{marker}\nseeded = true\n")).expect("seed stamped file");
}
let mut found: Vec<String> = collect_alef_hashes(directory.path())
.into_iter()
.filter_map(|(path, _, _)| {
path.strip_prefix(directory.path())
.ok()?
.to_str()
.map(|value| value.replace('\\', "/"))
})
.collect();
found.sort();
found
}
#[test]
fn the_ownership_walk_reaches_the_dot_directories_alef_stamps() {
let found = scanned_relative_paths(&[
"packages/reachable.toml",
".cargo/config.toml",
".github/skills/api/SKILL.md",
".venv/lib/cached.toml",
]);
assert!(
found.contains(&"packages/reachable.toml".to_string()),
"control: a stamped file outside every dot-directory must be found, else this test \
proves nothing about the dot-directory cases; walk returned {found:?}"
);
assert!(
found.contains(&".cargo/config.toml".to_string()),
"alef writes and stamps `.cargo/config.toml` itself; a stamp nothing ever reads back \
is not a freshness check. Walk returned {found:?}"
);
assert!(
found.contains(&".github/skills/api/SKILL.md".to_string()),
"generated agent skills are stamped alef output and must be verifiable; walk returned \
{found:?}"
);
assert!(
!found.contains(&".venv/lib/cached.toml".to_string()),
"the dot-directory prune must still keep the walk out of tool caches -- the fix is an \
allowlist of the dot-directories alef writes into, not a removal of the prune. Walk \
returned {found:?}"
);
}
#[test]
fn the_ownership_walk_does_not_descend_into_a_nested_worktree() {
let found = scanned_relative_paths(&[".claude/skills/api/SKILL.md", ".claude/worktrees/other/config.toml"]);
assert!(
found.contains(&".claude/skills/api/SKILL.md".to_string()),
"control: `.claude` must be walked, else the exclusion below is vacuous; walk \
returned {found:?}"
);
assert!(
!found.contains(&".claude/worktrees/other/config.toml".to_string()),
"a nested worktree is a different checkout of this repository; its stamps are not \
this tree's. Walk returned {found:?}"
);
}
#[test]
fn both_consumers_build_their_managed_set_only_from_the_shared_surface() {
let stage_calls = [
"pipeline::generate(",
"pipeline::generate_stubs(",
"pipeline::generate_service_api(",
"pipeline::generate_public_api(",
"pipeline::scaffold(",
"pipeline::readme(",
"e2e::generate_e2e(",
"docs::generate_docs_stage(",
];
let regions = [
(
"alef verify's frozen report",
include_str!("helpers.rs")
.split("pub(crate) fn collect_managed_surface")
.next()
.expect("helpers splits on the shared collector"),
),
(
"alef adopt's candidate set",
include_str!("aux_commands.rs")
.split("Commands::Adopt {")
.nth(1)
.and_then(|rest| rest.split("Commands::Migrate {").next())
.expect("aux_commands splits on the adopt arm"),
),
];
for (name, region) in regions {
for call in stage_calls {
assert!(
!region.contains(call),
"{name} calls {call} directly -- the frozen report and the candidate set \
must not enumerate generation stages separately, or they disagree again"
);
}
assert!(
region.contains("collect_managed_surface("),
"{name} must derive its managed set from the shared surface"
);
}
}
#[test]
fn ownership_walk_opens_every_extension_the_emit_side_stamps() {
assert_eq!(
scanned_names(&[
"foo-config.cmake",
"app.csproj",
"gem.gemspec",
"build.zig.zon",
"pom.xml"
]),
vec![
"app.csproj",
"build.zig.zon",
"foo-config.cmake",
"gem.gemspec",
"pom.xml"
],
);
}
#[test]
fn ownership_walk_opens_the_filename_keyed_files_the_emit_side_stamps() {
assert_eq!(scanned_names(&["makefile"]), vec!["makefile"]);
assert_eq!(
scanned_names(&[
"Makefile",
"GNUmakefile",
"Rakefile",
"Makevars",
"Makevars.in",
"Makevars.win.in",
"go.mod"
]),
vec![
"GNUmakefile",
"Makefile",
"Makevars",
"Makevars.in",
"Makevars.win.in",
"Rakefile",
"go.mod"
],
);
}
#[test]
fn ownership_walk_still_skips_an_extension_alef_never_stamps() {
assert!(scanned_names(&["notes.rtf", "archive.tar"]).is_empty());
}
#[test]
fn default_log_level_maps_verbosity_to_levels() {
assert_eq!(default_log_level(0, false), "info");
assert_eq!(default_log_level(1, false), "debug");
assert_eq!(default_log_level(2, false), "trace");
assert_eq!(default_log_level(9, false), "trace");
assert_eq!(default_log_level(0, true), "error");
assert_eq!(default_log_level(3, true), "error");
}
#[test]
fn required_post_build_failure_is_propagated_with_language_context() {
let directory = tempfile::tempdir().expect("temporary project");
let error = run_required_post_builds(
&[Language::Swift],
&crate::core::config::ResolvedCrateConfig::default(),
directory.path(),
)
.expect_err("missing Swift build project must fail");
assert!(
error
.to_string()
.contains("failed to run required post-build steps for swift")
);
}
#[test]
fn resolve_test_languages_allows_explicit_test_only_language() {
let config = resolved_test_config();
let langs = resolve_test_languages(&config, Some(&["rust".to_string()]), true).unwrap();
assert_eq!(langs, vec![Language::Rust]);
}
#[test]
fn resolve_test_languages_appends_e2e_only_languages() {
let config = resolved_test_config();
let langs = resolve_test_languages(&config, None, true).unwrap();
assert_eq!(langs, vec![Language::Python, Language::Rust]);
}
#[test]
fn resolve_test_languages_omits_e2e_only_languages_without_e2e() {
let config = resolved_test_config();
let langs = resolve_test_languages(&config, None, false).unwrap();
assert_eq!(langs, vec![Language::Python]);
}
fn gen_file(rel: &str, content: &str) -> crate::core::backend::GeneratedFile {
crate::core::backend::GeneratedFile {
path: std::path::PathBuf::from(rel),
content: content.to_string(),
generated_header: true,
}
}
#[test]
fn generated_files_match_disk_true_when_bodies_match() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("binding.go"), "package x\n\nvar a = 1\n").unwrap();
let files = vec![gen_file("binding.go", "package x\n\nvar a = 1\n")];
assert!(generated_files_match_disk(&files, dir.path()));
}
#[test]
fn generated_files_match_disk_ignores_embedded_hash_line() {
let dir = tempfile::tempdir().unwrap();
let generated = "// This file is auto-generated by alef — DO NOT EDIT.\npackage x\n\nvar a = 1\n";
std::fs::write(
dir.path().join("binding.go"),
"// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:deadbeef\npackage x\n\nvar a = 1\n",
)
.unwrap();
let files = vec![gen_file("binding.go", generated)];
assert!(generated_files_match_disk(&files, dir.path()));
}
#[test]
fn generated_files_match_disk_false_when_body_differs() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("binding.go"), "package x\n\nvar a = 1\n").unwrap();
let files = vec![gen_file("binding.go", "package x\n\nimport \"fmt\"\n\nvar a = 1\n")];
assert!(!generated_files_match_disk(&files, dir.path()));
}
#[test]
fn generated_files_match_disk_false_when_file_missing() {
let dir = tempfile::tempdir().unwrap();
let files = vec![gen_file("binding.go", "package x\n")];
assert!(!generated_files_match_disk(&files, dir.path()));
}
fn gen_file_unheadered(rel: &str, content: &str) -> crate::core::backend::GeneratedFile {
crate::core::backend::GeneratedFile {
path: std::path::PathBuf::from(rel),
content: content.to_string(),
generated_header: false,
}
}
#[test]
fn missing_managed_paths_reports_an_absent_headered_file() {
let dir = tempfile::tempdir().expect("tempdir");
let files = vec![gen_file("SomeType.java", "final class SomeType {}\n")];
let missing = missing_managed_paths(&files, dir.path());
assert_eq!(missing, vec![dir.path().join("SomeType.java").display().to_string()]);
}
#[test]
fn missing_managed_paths_reports_nothing_when_every_headered_file_exists() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("SomeType.java"),
"final class SomeType { /* stale */ }\n",
)
.unwrap();
let files = vec![gen_file("SomeType.java", "final class SomeType {}\n")];
assert!(missing_managed_paths(&files, dir.path()).is_empty());
}
#[test]
fn missing_managed_paths_ignores_an_absent_unheadered_scaffold_file() {
let dir = tempfile::tempdir().expect("tempdir");
let files = vec![gen_file_unheadered("Cargo.toml", "[package]\nname = \"demo\"\n")];
assert!(missing_managed_paths(&files, dir.path()).is_empty());
}
#[test]
fn marker_line_finds_the_line_carrying_the_provenance_marker() {
let header = crate::core::hash::header(crate::core::hash::CommentStyle::DoubleSlash);
assert_eq!(
marker_line(&header),
Some("// This file is auto-generated by alef — DO NOT EDIT.")
);
}
#[test]
fn marker_line_finds_nothing_in_content_without_a_marker() {
assert_eq!(marker_line("final class SomeType {}\n"), None);
}
#[test]
fn frozen_managed_paths_reports_an_unmarked_pre_existing_file() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("SomeType.java"), "final class SomeType {}\n").unwrap();
let files = vec![gen_file("SomeType.java", "final class SomeType {}\n")];
let frozen = frozen_managed_paths(&files, dir.path());
assert_eq!(frozen.len(), 1);
assert_eq!(frozen[0].path, dir.path().join("SomeType.java").display().to_string());
assert_eq!(
frozen[0].remedy.as_deref(),
Some("// This file is auto-generated by alef — DO NOT EDIT.")
);
assert_eq!(
frozen[0].near_miss, None,
"plain hand-written content has no near miss to report"
);
}
#[test]
fn frozen_managed_paths_reports_a_near_miss_when_one_is_present() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("SomeType.java"),
"// This alef-generated file should not be edited.\nfinal class SomeType {}\n",
)
.unwrap();
let files = vec![gen_file("SomeType.java", "final class SomeType {}\n")];
let frozen = frozen_managed_paths(&files, dir.path());
assert_eq!(frozen.len(), 1);
assert_eq!(
frozen[0].near_miss.as_deref(),
Some("// This alef-generated file should not be edited.")
);
}
#[test]
fn frozen_managed_paths_reports_nothing_when_the_existing_file_already_carries_the_marker() {
let dir = tempfile::tempdir().expect("tempdir");
let marked = format!(
"{}final class SomeType {{}}\n",
crate::core::hash::header(crate::core::hash::CommentStyle::DoubleSlash)
);
std::fs::write(dir.path().join("SomeType.java"), &marked).unwrap();
let files = vec![gen_file("SomeType.java", "final class SomeType {}\n")];
assert!(frozen_managed_paths(&files, dir.path()).is_empty());
}
#[test]
fn frozen_managed_paths_reports_nothing_when_the_file_does_not_exist() {
let dir = tempfile::tempdir().expect("tempdir");
let files = vec![gen_file("SomeType.java", "final class SomeType {}\n")];
assert!(frozen_managed_paths(&files, dir.path()).is_empty());
}
#[test]
fn frozen_managed_paths_ignores_a_hand_written_scaffold_file() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"demo\"\n").unwrap();
let files = vec![gen_file_unheadered("Cargo.toml", "[package]\nname = \"demo\"\n")];
assert!(frozen_managed_paths(&files, dir.path()).is_empty());
}
#[test]
fn frozen_managed_paths_reads_the_remedy_from_self_marked_content() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("Foo.swift"), "struct Foo {}\n").unwrap();
let files = vec![gen_file_unheadered(
"Foo.swift",
"// Generated by alef. Do not edit by hand.\nstruct Foo {}\n",
)];
let frozen = frozen_managed_paths(&files, dir.path());
assert_eq!(frozen.len(), 1);
assert_eq!(
frozen[0].remedy.as_deref(),
Some("// Generated by alef. Do not edit by hand.")
);
}
#[test]
fn frozen_managed_paths_reports_no_remedy_for_an_unmarkable_extension() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("manifest.json"), "{}\n").unwrap();
let files = vec![gen_file("manifest.json", "{}\n")];
let frozen = frozen_managed_paths(&files, dir.path());
assert_eq!(frozen.len(), 1);
assert_eq!(frozen[0].remedy, None);
}
#[test]
fn verify_walk_detects_an_edited_generated_file() {
let directory = tempfile::tempdir().expect("tempdir");
let path = directory.path().join("binding.rs");
let inputs_hash = "generation-inputs";
let original = "// This file is auto-generated by alef — DO NOT EDIT.\nfn value() -> u8 { 1 }\n";
let hash = crate::core::hash::compute_file_hash(inputs_hash, original);
let finalized = crate::core::hash::inject_hash_line(original, &hash);
std::fs::write(&path, finalized.replace("{ 1 }", "{ 2 }")).expect("edit generated file");
let stale = verify_walk(directory.path(), inputs_hash).expect("verify generated files");
assert_eq!(stale.len(), 1);
assert_eq!(stale[0].path, path.display().to_string());
}
#[test]
fn verify_walk_detects_a_hand_edited_dependency_version_in_a_generated_manifest() {
let directory = tempfile::tempdir().expect("tempdir");
let path = directory.path().join("Cargo.toml");
let inputs_hash = "generation-inputs";
let original = "# This file is auto-generated by alef — DO NOT EDIT.\n\
[dependencies]\n\
base64 = \"0.22\"\n";
let hash = crate::core::hash::compute_file_hash(inputs_hash, original);
let finalized = crate::core::hash::inject_hash_line(original, &hash);
std::fs::write(&path, finalized.replace("0.22", "0.23")).expect("hand-edit generated manifest");
let stale = verify_walk(directory.path(), inputs_hash).expect("verify generated files");
assert_eq!(
stale.len(),
1,
"a hand-edited dependency version must be reported stale even though inputs_hash is unchanged"
);
assert_eq!(stale[0].path, path.display().to_string());
}
#[test]
fn verify_walk_detects_a_mixed_stamped_and_unstamped_generated_tree() {
let directory = tempfile::tempdir().expect("tempdir");
let inputs_hash = "generation-inputs";
let path = directory.path().join("unstamped.rs");
std::fs::write(
&path,
"// This file is auto-generated by alef — DO NOT EDIT.\nfn generated() {}\n",
)
.expect("write generated file");
let stamped_path = directory.path().join("stamped.rs");
let stamped_body = "// This file is auto-generated by alef — DO NOT EDIT.\nfn stamped() {}\n";
let stamped_hash = crate::core::hash::compute_file_hash(inputs_hash, stamped_body);
std::fs::write(
&stamped_path,
crate::core::hash::inject_hash_line(stamped_body, &stamped_hash),
)
.expect("write stamped generated file");
let stale = verify_walk(directory.path(), inputs_hash).expect("verify generated files");
assert_eq!(stale.len(), 1);
assert_eq!(stale[0].path, path.display().to_string());
assert_eq!(stale[0].embedded, "<missing>");
}
fn write_stamped(dir: &std::path::Path, name: &str, key: &str, value: &str) -> std::path::PathBuf {
let path = dir.join(name);
let body = "// This file is auto-generated by alef — DO NOT EDIT.\nfn generated() {}\n";
let stamped = crate::core::hash::inject_stamp_line(body, key, value);
let hash = crate::core::hash::compute_file_hash("test-inputs-hash", &stamped);
std::fs::write(&path, crate::core::hash::inject_hash_line(&stamped, &hash)).expect("write stamped file");
path
}
#[test]
fn write_stamped_produces_a_file_the_hash_walk_actually_collects() {
let dir = tempfile::tempdir().expect("tempdir");
write_stamped(dir.path(), "header.h", "handle-abi", "1");
let collected = collect_alef_hashes(dir.path());
assert_eq!(
collected.len(),
1,
"the stamped fixture must be visible to the hash walk"
);
assert_eq!(
crate::core::hash::extract_stamp(&collected[0].2, "handle-abi").as_deref(),
Some("1"),
"the stamp must survive alongside the hash line"
);
}
#[test]
fn find_stamp_disagreement_reports_two_distinct_values() {
let dir = tempfile::tempdir().expect("tempdir");
write_stamped(dir.path(), "header.h", "handle-abi", "1");
write_stamped(dir.path(), "binding.zig", "handle-abi", "2");
let disagreement =
find_stamp_disagreement(dir.path(), "handle-abi").expect("two distinct stamp values must be reported");
assert_eq!(disagreement.key, "handle-abi");
assert_eq!(disagreement.examples.len(), 2);
let values: Vec<&str> = disagreement.examples.iter().map(|(_, v)| v.as_str()).collect();
assert!(values.contains(&"1"));
assert!(values.contains(&"2"));
}
#[test]
fn find_stamp_disagreement_is_none_when_every_stamped_file_agrees() {
let dir = tempfile::tempdir().expect("tempdir");
write_stamped(dir.path(), "header.h", "handle-abi", "2");
write_stamped(dir.path(), "binding.zig", "handle-abi", "2");
assert!(find_stamp_disagreement(dir.path(), "handle-abi").is_none());
}
#[test]
fn find_stamp_disagreement_is_none_when_nothing_is_stamped() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("header.h"),
"// This file is auto-generated by alef — DO NOT EDIT.\nfn generated() {}\n",
)
.expect("write unstamped file");
assert!(find_stamp_disagreement(dir.path(), "handle-abi").is_none());
}
#[test]
fn find_stamp_disagreement_ignores_a_different_key() {
let dir = tempfile::tempdir().expect("tempdir");
write_stamped(dir.path(), "header.h", "some-other-marker", "1");
write_stamped(dir.path(), "binding.zig", "some-other-marker", "2");
assert!(find_stamp_disagreement(dir.path(), "handle-abi").is_none());
}
fn stage_failure(paths: &[&str]) -> StageFailure {
StageFailure {
stage: "e2e",
message: "56 e2e assertion(s) reference a field the availability oracle cannot resolve".to_owned(),
paths: paths.iter().map(std::path::PathBuf::from).collect(),
}
}
#[test]
fn a_stage_failure_confined_to_e2e_paths_does_not_affect_an_unrelated_ownership_target() {
let failure = stage_failure(&["e2e/python/test_smoke.py", "e2e/go/smoke_test.go"]);
assert!(!failure.affects_any(&["packages/dart/rust/Cargo.toml".to_owned()]));
assert!(!failure.affects_any(&["packages/**/*.gemspec".to_owned()]));
}
#[test]
fn a_stage_failure_that_rendered_the_requested_target_does_affect_it() {
let failure = stage_failure(&["e2e/python/test_smoke.py", "e2e/go/smoke_test.go"]);
assert!(failure.affects_any(&["e2e/python/test_smoke.py".to_owned()]));
assert!(failure.affects_any(&["e2e/python/*.py".to_owned()]));
assert!(failure.affects_any(&[
"packages/dart/rust/Cargo.toml".to_owned(),
"e2e/go/smoke_test.go".to_owned(),
]));
}
#[test]
fn a_stage_failure_never_affects_an_empty_target_list() {
let failure = stage_failure(&["e2e/python/test_smoke.py"]);
assert!(!failure.affects_any(&[]));
}
}