allow-policy-legacy 0.1.9

Legacy policy adapters for cargo-allow migrations.
Documentation
use super::*;
use crate::test_support::*;
use allow_core::FindingKind;
use std::{fs, path::Path};

#[test]
fn migrates_no_panic_baseline_to_count_limited_baseline_debt() {
    let policy = no_panic_baseline_fixture_path();
    let cfg = load_legacy_or_canonical(&policy)
        .unwrap_or_else(|err| std::panic::panic_any(format!("no-panic baseline migrates: {err}")));

    assert_eq!(cfg.policy, "cargo-allow");
    assert_eq!(cfg.allow.len(), 2);
    let unwrap = cfg
        .allow
        .iter()
        .find(|entry| entry.family.as_deref() == Some("unwrap"))
        .unwrap_or_else(|| std::panic::panic_any("expected unwrap baseline entry"));
    assert_eq!(unwrap.kind, FindingKind::Panic);
    assert_eq!(unwrap.classification, "baseline_debt");
    assert_eq!(unwrap.owner, "unowned");
    assert_eq!(unwrap.occurrence_limit, Some(2));
    assert_current_baseline_window(&unwrap.lifecycle);
    assert_eq!(unwrap.selector.ast_kind.as_deref(), Some("method_call"));
    assert_eq!(unwrap.selector.callee.as_deref(), Some("unwrap"));
    assert!(unwrap.selector.normalized_snippet_hash.is_some());
    assert!(
        unwrap
            .evidence
            .iter()
            .any(|item| item == "baseline_count:2")
    );

    let panic = cfg
        .allow
        .iter()
        .find(|entry| entry.family.as_deref() == Some("panic_macro"))
        .unwrap_or_else(|| std::panic::panic_any("expected panic macro baseline entry"));
    assert_eq!(panic.selector.ast_kind.as_deref(), Some("macro_call"));
    assert_eq!(panic.selector.macro_name.as_deref(), Some("panic"));
    assert_eq!(panic.occurrence_limit, Some(1));
}

#[test]
fn no_panic_compat_loader_requires_no_panic_policy() {
    let policy = generated_policy_fixture_path();

    let err = load_no_panic_baseline_compat_config(&policy)
        .expect_err("generated policy should not load as no-panic compat");

    assert!(err.to_string().contains("not a no-panic-baseline policy"));
}

#[test]
fn no_panic_baseline_occurrence_limit_prevents_unbounded_matches() {
    let policy = no_panic_baseline_fixture_path();
    let cfg = load_legacy_or_canonical(&policy)
        .unwrap_or_else(|err| std::panic::panic_any(format!("no-panic baseline migrates: {err}")));
    let snippet = ["let value = maybe.", "unwrap();"].concat();
    let finding = panic_finding(
        "src/lib.rs",
        "unwrap",
        "method_call",
        Some("unwrap"),
        None,
        &snippet,
    );

    let outcomes = allow_match::evaluate(
        &cfg,
        &[finding.clone(), finding.clone(), finding],
        allow_match::CheckMode::NoNew,
    );

    assert_eq!(
        outcomes
            .iter()
            .filter(|outcome| outcome.status == allow_core::MatchStatus::Matched)
            .count(),
        2
    );
    assert!(
        outcomes
            .iter()
            .any(|outcome| outcome.status == allow_core::MatchStatus::New
                && outcome.message.contains("occurrence_limit exceeded"))
    );
}

#[test]
fn migrates_no_panic_allowlist_to_structural_panic_entries() {
    let policy = no_panic_allowlist_fixture_path();
    let cfg = load_legacy_or_canonical(&policy)
        .unwrap_or_else(|err| std::panic::panic_any(format!("no-panic allowlist migrates: {err}")));

    assert_eq!(cfg.policy, "cargo-allow");
    assert_eq!(cfg.allow.len(), 2);
    let unwrap = cfg
        .allow
        .iter()
        .find(|entry| entry.id == "no-panic-unwrap")
        .unwrap_or_else(|| std::panic::panic_any("expected unwrap allow entry"));
    assert_eq!(unwrap.kind, FindingKind::Panic);
    assert_eq!(unwrap.family.as_deref(), Some("unwrap"));
    assert_eq!(unwrap.reason, "Parser validates the optional value.");
    assert_eq!(unwrap.selector.ast_kind.as_deref(), Some("method_call"));
    assert_eq!(unwrap.selector.callee.as_deref(), Some("unwrap"));
    assert_eq!(unwrap.selector.container.as_deref(), Some("load"));
    assert_eq!(unwrap.selector.line_hint, Some(7));
    assert_eq!(
        unwrap
            .last_seen
            .as_ref()
            .map(|seen| (seen.line, seen.column)),
        Some((7, 12))
    );
    assert_eq!(unwrap.lifecycle.review_after.as_deref(), Some("2026-09-09"));

    let generated = cfg
        .allow
        .iter()
        .find(|entry| entry.id.starts_with("legacy-no-panic-"))
        .unwrap_or_else(|| std::panic::panic_any("expected generated no-panic entry"));
    assert_eq!(generated.classification, "baseline_debt");
    assert_eq!(generated.owner, "unowned");
    assert_eq!(generated.selector.macro_name.as_deref(), Some("panic"));
    assert_current_baseline_window(&generated.lifecycle);
}

#[test]
fn no_panic_allowlist_preserves_legacy_evidence_when_present() {
    let path = fixture_dir().join("no-panic-allowlist-with-evidence.toml");
    fs::write(
        &path,
        r#"schema_version = 1
policy = "no-panic-allowlist"

[[allow]]
id = "no-panic-reviewed"
path = "src/lib.rs"
family = "unwrap"
reason = "Parser validates optional value."
evidence = ["test:parser_validates_optional_value", "issue:#123"]

[allow.selector]
kind = "method-call"
callee = "unwrap"
"#,
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("fixture write: {err}")));

    let cfg = load_no_panic_allowlist_compat_config(&path).unwrap_or_else(|err| {
        std::panic::panic_any(format!("no-panic allowlist with evidence loads: {err}"))
    });

    let entry = cfg
        .allow
        .first()
        .unwrap_or_else(|| std::panic::panic_any("expected no-panic allow entry"));
    assert_eq!(
        entry.evidence,
        vec![
            "test:parser_validates_optional_value".to_string(),
            "issue:#123".to_string()
        ]
    );
    assert_eq!(
        allow_policy::weak_evidence_reference_count(Path::new("."), &cfg),
        0,
        "recognized legacy no-panic evidence should not be reported as weak"
    );
}

#[test]
fn no_panic_allowlist_accepts_covered_by_as_legacy_evidence() {
    let path = fixture_dir().join("no-panic-allowlist-covered-by.toml");
    fs::write(
        &path,
        r#"schema_version = 1
policy = "no-panic-allowlist"

[[allow]]
id = "no-panic-covered"
path = "src/lib.rs"
family = "panic"
explanation = "Crash path is unreachable after argument validation."
covered_by = "test:panic_path_unreachable"

[allow.selector]
kind = "macro-call"
callee = "panic"
"#,
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("fixture write: {err}")));

    let cfg = load_no_panic_allowlist_compat_config(&path).unwrap_or_else(|err| {
        std::panic::panic_any(format!("no-panic allowlist with covered_by loads: {err}"))
    });

    let entry = cfg
        .allow
        .first()
        .unwrap_or_else(|| std::panic::panic_any("expected no-panic allow entry"));
    assert_eq!(
        entry.evidence,
        vec!["test:panic_path_unreachable".to_string()]
    );
}

#[test]
fn no_panic_allowlist_compat_preserves_matched_new_and_stale_drift() {
    let policy = no_panic_allowlist_fixture_path();
    let cfg = load_no_panic_allowlist_compat_config(&policy).unwrap_or_else(|err| {
        std::panic::panic_any(format!("no-panic allowlist compat config loads: {err}"))
    });

    let mut finding = panic_finding(
        "src/lib.rs",
        "unwrap",
        "method_call",
        Some("unwrap"),
        None,
        "let value = maybe.unwrap();",
    );
    finding.identity.container = Some("load".to_string());
    let matched = allow_match::evaluate(&cfg, &[finding], allow_match::CheckMode::NoNew);
    assert!(
        matched
            .iter()
            .any(|outcome| outcome.status == allow_core::MatchStatus::Matched)
    );

    let missing_allow = allow_match::evaluate(
        &cfg,
        &[panic_finding(
            "src/lib.rs",
            "expect",
            "method_call",
            Some("expect"),
            None,
            "let value = maybe.expect(\"value\");",
        )],
        allow_match::CheckMode::NoNew,
    );
    assert!(
        missing_allow
            .iter()
            .any(|outcome| outcome.status == allow_core::MatchStatus::New)
    );

    let stale_allow = allow_match::evaluate(&cfg, &[], allow_match::CheckMode::Audit);
    assert!(
        stale_allow
            .iter()
            .any(|outcome| outcome.status == allow_core::MatchStatus::Stale)
    );
}

#[test]
fn no_panic_allowlist_loader_requires_allowlist_policy() {
    let policy = no_panic_baseline_fixture_path();

    let err = load_no_panic_allowlist_compat_config(&policy)
        .expect_err("baseline policy should not load as no-panic allowlist compat");

    assert!(err.to_string().contains("not a no-panic-allowlist policy"));
}