use super::*;
use crate::lint::{run_lint, GateDetail, GateExtra, LintConfig};
const DIR_CASES: &[&str] = &[
"aprender", "zzz-aprender", "e-aprender", "A-aprender", "000", "zzz", ];
fn upstream_with_guarantees() -> &'static str {
"metadata:
version: 1.0.0
description: upstream
references: [ref]
equations:
produce:
formula: y = f(x)
guarantees:
shapes:
output: { dims: [batch, hidden] }
proof_obligations:
- type: invariant
property: p
falsification_tests:
- id: F-001
rule: r
prediction: p
if_fails: f
kani_harnesses:
- id: K-001
obligation: p
"
}
fn upstream_without_guarantees() -> &'static str {
"metadata:
version: 1.0.0
description: upstream
references: [ref]
equations:
produce:
formula: y = f(x)
proof_obligations:
- type: invariant
property: p
falsification_tests:
- id: F-001
rule: r
prediction: p
if_fails: f
kani_harnesses:
- id: K-001
obligation: p
"
}
fn downstream_assuming(from_contract: &str) -> String {
format!(
"metadata:
version: 1.0.0
description: downstream
references: [ref]
depends_on: [{from_contract}]
equations:
consume:
formula: z = g(y)
assumes:
from_contract: {from_contract}
from_equation: produce
shapes:
output: {{ dims: [batch, hidden] }}
proof_obligations:
- type: invariant
property: p
falsification_tests:
- id: F-002
rule: r
prediction: p
if_fails: f
kani_harnesses:
- id: K-002
obligation: p
"
)
}
fn build_tree(root: &std::path::Path, subdir: &str) -> std::path::PathBuf {
let contracts = root.join("contracts");
std::fs::create_dir_all(contracts.join(subdir)).unwrap();
std::fs::write(
contracts.join("x-architecture-v1.yaml"),
upstream_without_guarantees(),
)
.unwrap();
std::fs::write(
contracts.join(subdir).join("x-architecture-v1.yaml"),
upstream_with_guarantees(),
)
.unwrap();
std::fs::write(
contracts.join("downstream-v1.yaml"),
downstream_assuming("x-architecture-v1"),
)
.unwrap();
contracts
}
fn composition_of(report: &crate::lint::LintReport) -> (usize, usize, usize) {
for g in &report.gates {
if let GateDetail::Composition {
edges_checked,
edges_satisfied,
edges_broken,
} = &g.detail
{
return (*edges_checked, *edges_satisfied, *edges_broken);
}
}
panic!("composition gate missing from report");
}
#[test]
fn verdict_is_invariant_under_directory_rename() {
let mut observed: Vec<(&str, bool, (usize, usize, usize))> = Vec::new();
for subdir in DIR_CASES {
let tmp = tempfile::tempdir().unwrap();
let contracts = build_tree(tmp.path(), subdir);
let mut config = LintConfig::new(&contracts, None, 0.0);
config.no_cache = true;
let report = run_lint(&config);
let comp = report
.gates
.iter()
.find(|g| g.name == "composition")
.expect("composition gate");
observed.push((subdir, comp.passed, composition_of(&report)));
}
let (first_name, first_passed, first_counts) = observed[0];
for (name, passed, counts) in &observed {
assert_eq!(
(*passed, *counts),
(first_passed, first_counts),
"composition verdict moved when the directory was renamed \
`{first_name}` -> `{name}`: {first_passed}/{first_counts:?} vs {passed}/{counts:?}. \
No file content differs between these two trees."
);
}
assert!(first_passed, "composition must pass with a refused stem");
assert_eq!(
first_counts,
(1, 0, 0),
"1 edge checked, refused, not broken"
);
}
#[test]
fn ambiguous_stem_is_reported_not_silently_resolved() {
let tmp = tempfile::tempdir().unwrap();
let contracts = build_tree(tmp.path(), "aprender");
let mut config = LintConfig::new(&contracts, None, 0.0);
config.no_cache = true;
let report = run_lint(&config);
let gate = report
.gates
.iter()
.find(|g| g.name == "duplicate-stems")
.expect("duplicate-stems gate");
assert!(!gate.passed, "an unbaselined divergent stem must fail");
assert!(report
.findings
.iter()
.any(|f| f.rule_id == "PV-DUP-001" && f.message.contains("x-architecture-v1")));
assert!(report
.findings
.iter()
.any(|f| f.rule_id == "COMPOSITION-001" && f.message.contains("AMBIGUOUS")));
}
#[test]
fn identical_copies_are_not_ambiguous() {
let tmp = tempfile::tempdir().unwrap();
let contracts = tmp.path().join("contracts");
std::fs::create_dir_all(contracts.join("sub")).unwrap();
std::fs::write(
contracts.join("x-architecture-v1.yaml"),
upstream_with_guarantees(),
)
.unwrap();
std::fs::write(
contracts.join("sub").join("x-architecture-v1.yaml"),
upstream_with_guarantees(),
)
.unwrap();
std::fs::write(
contracts.join("downstream-v1.yaml"),
downstream_assuming("x-architecture-v1"),
)
.unwrap();
assert!(scan_duplicate_stems(&contracts).is_empty());
let mut config = LintConfig::new(&contracts, None, 0.0);
config.no_cache = true;
let report = run_lint(&config);
let gate = report
.gates
.iter()
.find(|g| g.name == "duplicate-stems")
.expect("gate");
assert!(gate.passed);
assert_eq!(composition_of(&report), (1, 1, 0));
}
#[test]
fn scan_reports_paths_and_variant_count() {
let tmp = tempfile::tempdir().unwrap();
let contracts = build_tree(tmp.path(), "aprender");
let dups = scan_duplicate_stems(&contracts);
assert_eq!(dups.len(), 1);
assert_eq!(dups[0].stem, "x-architecture-v1");
assert_eq!(dups[0].variants, 2);
assert_eq!(dups[0].paths.len(), 2);
}
#[test]
fn baseline_absorbs_known_divergence() {
let tmp = tempfile::tempdir().unwrap();
let contracts = build_tree(tmp.path(), "aprender");
std::fs::create_dir_all(tmp.path().join("scripts")).unwrap();
std::fs::write(
tmp.path().join(BASELINE_REL_PATH),
"# comment\n\nx-architecture-v1\n",
)
.unwrap();
let mut config = LintConfig::new(&contracts, None, 0.0);
config.no_cache = true;
let report = run_lint(&config);
let gate = report
.gates
.iter()
.find(|g| g.name == "duplicate-stems")
.expect("gate");
assert!(gate.passed, "baselined stem should not fail the gate");
}
#[test]
fn stale_baseline_entry_fails() {
let tmp = tempfile::tempdir().unwrap();
let contracts = tmp.path().join("contracts");
std::fs::create_dir_all(&contracts).unwrap();
std::fs::write(
contracts.join("x-architecture-v1.yaml"),
upstream_with_guarantees(),
)
.unwrap();
std::fs::create_dir_all(tmp.path().join("scripts")).unwrap();
std::fs::write(tmp.path().join(BASELINE_REL_PATH), "x-architecture-v1\n").unwrap();
let mut config = LintConfig::new(&contracts, None, 0.0);
config.no_cache = true;
let report = run_lint(&config);
let gate = report
.gates
.iter()
.find(|g| g.name == "duplicate-stems")
.expect("gate");
assert!(!gate.passed);
assert!(report.findings.iter().any(|f| f.rule_id == "PV-DUP-002"));
}
#[test]
fn missing_baseline_is_an_empty_baseline() {
let tmp = tempfile::tempdir().unwrap();
assert!(read_baseline(tmp.path()).is_empty());
}
#[test]
fn real_corpus_matches_its_baseline() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let dups = scan_duplicate_stems(&root.join("contracts"));
let found = ambiguous_stems(&dups);
let baseline = read_baseline(&root);
assert!(
!baseline.is_empty(),
"the ratchet baseline is REQUIRED; a missing {BASELINE_REL_PATH} silently \
disarms PV-DUP-001"
);
let unbaselined: Vec<_> = found.difference(&baseline).collect();
let stale: Vec<_> = baseline.difference(&found).collect();
assert!(
unbaselined.is_empty() && stale.is_empty(),
"duplicate-stem baseline drift — new: {unbaselined:?}, stale: {stale:?}. \
Deduplicate the stem; do NOT raise the baseline."
);
}
#[allow(clippy::type_complexity)]
fn census_of(
report: &crate::lint::LintReport,
) -> (usize, usize, Vec<String>, Vec<String>, Vec<String>) {
let gate = report
.gates
.iter()
.find(|g| g.name == "duplicate-stems")
.expect("duplicate-stems gate");
let Some(GateExtra::DuplicateStems {
divergent,
baselined,
unbaselined,
stale,
divergent_stems,
}) = gate.extra.as_ref()
else {
panic!("duplicate-stems gate carries no GateExtra::DuplicateStems payload");
};
(
*divergent,
*baselined,
unbaselined.clone(),
stale.clone(),
divergent_stems.clone(),
)
}
#[test]
fn the_gate_names_every_ambiguous_stem_with_its_paths() {
let tmp = tempfile::tempdir().unwrap();
let contracts = build_tree(tmp.path(), "aprender");
let mut config = LintConfig::new(&contracts, None, 0.0);
config.no_cache = true;
let report = run_lint(&config);
let (divergent, baselined, unbaselined, stale, stems) = census_of(&report);
assert_eq!(divergent, 1);
assert_eq!(baselined, 0);
assert_eq!(unbaselined, vec!["x-architecture-v1".to_string()]);
assert!(stale.is_empty());
assert_eq!(
stems.len(),
1,
"every divergent stem must appear: {stems:?}"
);
let line = &stems[0];
assert!(line.contains("x-architecture-v1"), "{line}");
assert!(line.contains("[2 variants]"), "{line}");
assert_eq!(line.matches("x-architecture-v1.yaml").count(), 2, "{line}");
assert!(line.contains("aprender"), "{line}");
}
#[test]
fn frozen_detail_numbers_agree_with_the_census() {
let tmp = tempfile::tempdir().unwrap();
let contracts = build_tree(tmp.path(), "aprender");
let mut config = LintConfig::new(&contracts, None, 0.0);
config.no_cache = true;
let report = run_lint(&config);
let (_, baselined, unbaselined, stale, _) = census_of(&report);
let gate = report
.gates
.iter()
.find(|g| g.name == "duplicate-stems")
.expect("gate");
let GateDetail::Validate {
contracts: implicated,
errors,
warnings,
error_messages,
} = &gate.detail
else {
panic!("duplicate-stems must report through the frozen `Validate` variant");
};
assert_eq!(
*implicated, 2,
"two contract FILES claim the ambiguous stem"
);
assert_eq!(*errors, unbaselined.len() + stale.len());
assert_eq!(*warnings, baselined);
assert_eq!(error_messages.len(), *errors);
assert!(error_messages[0].contains("x-architecture-v1"));
}
#[allow(dead_code)]
fn gate_detail_vocabulary_is_frozen_at_the_0_3_1_variants(detail: &GateDetail) {
match detail {
GateDetail::Validate { .. }
| GateDetail::Audit { .. }
| GateDetail::Score { .. }
| GateDetail::Verify { .. }
| GateDetail::Enforce { .. }
| GateDetail::ReverseCoverage { .. }
| GateDetail::Composition { .. }
| GateDetail::Skipped { .. } => {}
}
}
#[test]
fn real_corpus_census_names_every_baselined_stem() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let dups = scan_duplicate_stems(&root.join("contracts"));
let baseline = read_baseline(&root);
let (result, _) = run_duplicate_stem_gate(&dups, &baseline);
let Some(GateExtra::DuplicateStems {
divergent,
divergent_stems,
..
}) = result.extra.as_ref()
else {
panic!("duplicate-stems gate carries no census payload");
};
assert_eq!(*divergent, baseline.len());
assert_eq!(divergent_stems.len(), baseline.len());
for stem in &baseline {
let prefix = format!("{stem} [");
assert!(
divergent_stems.iter().any(|l| l.starts_with(&prefix)),
"the report does not name baselined stem `{stem}`"
);
}
}