use super::test_support::{test_entry, test_finding};
use super::*;
use crate::artifact_contract_support::{assert_inventory_contract, parse_json_artifact};
use allow_core::FindingKind;
use serde_json::Value;
use std::path::Path;
#[test]
fn explain_entry_json_records_context_and_live_status() {
let mut cfg = AllowConfig::empty();
let mut entry = test_entry("allow-json", FindingKind::NonRustFile);
entry.family = Some("documentation".to_string());
entry.evidence = vec!["test:explain_fixture".to_string()];
entry.lifecycle.created = Some("2026-05-27".to_string());
entry.lifecycle.review_after = Some("2026-11-01".to_string());
cfg.allow.push(entry.clone());
let mut finding = test_finding(
FindingKind::NonRustFile,
Some("documentation"),
"tracked.file",
"tracked_file",
);
finding.identity.crate_name = Some("allow-core".to_string());
let json = explain_entry_json(
Path::new("."),
&cfg,
&entry,
&[finding],
ExplainContext {
inventory: allow_report::InventoryContext::source_syntax(
"git_tracked",
Some("H:/Code/Rust/cargo-allow"),
Some(47),
),
},
);
let value = parse_json_artifact("explain", &json, allow_report::EXPLAIN_SCHEMA_ID, "explain");
assert_inventory_contract(
"explain",
&value,
"git_tracked",
Some("H:/Code/Rust/cargo-allow"),
Some(47),
);
assert_eq!(
value.pointer("/allow_entry/id").and_then(Value::as_str),
Some("allow-json"),
"explain allow id"
);
assert_eq!(
value
.pointer("/summary/current_status")
.and_then(Value::as_str),
Some("matched"),
"explain current status"
);
assert_eq!(
value
.pointer("/summary/current_matches")
.and_then(Value::as_u64),
Some(1),
"explain current match count"
);
assert_eq!(
value
.pointer("/summary/broad_scope")
.and_then(Value::as_bool),
Some(false),
"explain broad scope"
);
assert_eq!(
value
.pointer("/current_findings/0/path")
.and_then(Value::as_str),
Some("tracked.file"),
"explain current finding path"
);
assert_eq!(
value
.pointer("/current_findings/0/source_package")
.and_then(Value::as_str),
Some("allow-core"),
"explain current finding source package"
);
assert_eq!(
value
.pointer("/evidence_references/0/status")
.and_then(Value::as_str),
Some("traceability_only"),
"explain evidence status"
);
assert_eq!(
value
.pointer("/evidence_references/0/category")
.and_then(Value::as_str),
Some("not_local"),
"explain evidence category"
);
assert_eq!(
value
.pointer("/next/suggested_actions")
.and_then(Value::as_array)
.map(Vec::len),
Some(0),
"explain suggested actions"
);
assert_eq!(
value
.pointer("/next/proof_commands")
.and_then(Value::as_array)
.map(Vec::len),
Some(0),
"explain proof commands"
);
}
#[test]
fn explain_entry_json_records_link_reference_diagnostics() {
let mut cfg = AllowConfig::empty();
let mut entry = test_entry("allow-link-diagnostics", FindingKind::NonRustFile);
entry.family = Some("documentation".to_string());
entry.links = vec![
"doc:docs/missing-link.md".to_string(),
"issue:123".to_string(),
];
cfg.allow.push(entry.clone());
let finding = test_finding(
FindingKind::NonRustFile,
Some("documentation"),
"tracked.file",
"tracked_file",
);
let json = explain_entry_json(
Path::new("target/cargo-allow-test-missing-link-root"),
&cfg,
&entry,
std::slice::from_ref(&finding),
ExplainContext {
inventory: allow_report::InventoryContext::source_syntax(
"filesystem_fallback",
Some("fixtures/source-snapshot"),
Some(1),
),
},
);
let value = parse_json_artifact("explain", &json, allow_report::EXPLAIN_SCHEMA_ID, "explain");
assert_eq!(
value
.pointer("/link_references/0/status")
.and_then(Value::as_str),
Some("local_file_missing"),
"explain should surface broken local link status"
);
assert_eq!(
value
.pointer("/link_references/0/category")
.and_then(Value::as_str),
Some("missing"),
"explain should surface broken local link category"
);
assert_eq!(
value
.pointer("/link_references/0/message")
.and_then(Value::as_str),
Some("local link file is missing"),
"explain should label link diagnostics as links"
);
assert_eq!(
value
.pointer("/link_references/1/status")
.and_then(Value::as_str),
Some("traceability_only"),
"explain should preserve non-local link diagnostics"
);
assert_eq!(
value
.pointer("/next/suggested_actions/0")
.and_then(Value::as_str),
Some("restore or commit the referenced local traceability file"),
"broken local links should route explain next steps to traceability repair"
);
assert!(
value
.pointer("/next/proof_commands")
.and_then(Value::as_array)
.unwrap_or_else(|| std::panic::panic_any("next proof commands should be an array"))
.iter()
.any(|command| command.as_str()
== Some("cargo-allow worklist --allow-id allow-link-diagnostics --format json")),
"explain should point broken local links at the allow-ID worklist queue"
);
let text = explain_entry_text(
Path::new("target/cargo-allow-test-missing-link-root"),
&cfg,
&entry,
&[finding],
);
assert!(text.contains("link diagnostics:"));
assert!(text.contains("doc:docs/missing-link.md"));
assert!(text.contains("message: local link file is missing"));
assert!(text.contains("action: restore or commit the referenced local traceability file"));
}
#[test]
fn explain_entry_json_records_allow_id_proof_command_for_attention_items() {
let mut cfg = AllowConfig::empty();
let mut entry = test_entry("allow-baseline", FindingKind::Panic);
entry.family = Some("unwrap".to_string());
entry.classification = "baseline_debt".to_string();
cfg.allow.push(entry.clone());
let finding = test_finding(
FindingKind::Panic,
Some("unwrap"),
"src/lib.rs",
"method_call",
);
let json = explain_entry_json(
Path::new("."),
&cfg,
&entry,
&[finding],
ExplainContext {
inventory: allow_report::InventoryContext::source_syntax(
"filesystem_fallback",
Some("fixtures/source-snapshot"),
Some(1),
),
},
);
let value = parse_json_artifact("explain", &json, allow_report::EXPLAIN_SCHEMA_ID, "explain");
let proof_commands = value
.pointer("/next/proof_commands")
.and_then(Value::as_array)
.unwrap_or_else(|| std::panic::panic_any("explain proof commands should be an array"));
assert!(
proof_commands.iter().any(|command| {
command.as_str() == Some("cargo-allow worklist --allow-id allow-baseline --format json")
}),
"explain proof commands should reopen the durable allow-id queue"
);
assert!(
proof_commands
.iter()
.any(|command| command.as_str() == Some("cargo-allow explain allow-baseline")),
"explain proof commands should keep the direct explain command"
);
}
#[test]
fn explain_entry_json_routes_broken_evidence_to_repair_queue() {
let mut cfg = AllowConfig::empty();
let mut entry = test_entry("allow-broken-evidence", FindingKind::NonRustFile);
entry.evidence = vec!["doc:docs/missing-evidence.md".to_string()];
cfg.allow.push(entry.clone());
let finding = test_finding(
FindingKind::NonRustFile,
None,
"tracked.file",
"tracked_file",
);
let json = explain_entry_json(
Path::new("target/cargo-allow-test-missing-root"),
&cfg,
&entry,
std::slice::from_ref(&finding),
ExplainContext {
inventory: allow_report::InventoryContext::source_syntax(
"filesystem_fallback",
Some("fixtures/source-snapshot"),
Some(1),
),
},
);
let value = parse_json_artifact("explain", &json, allow_report::EXPLAIN_SCHEMA_ID, "explain");
assert_eq!(
value
.pointer("/evidence_references/0/status")
.and_then(Value::as_str),
Some("local_file_missing"),
"explain should surface the broken local evidence diagnostic"
);
assert_eq!(
value
.pointer("/evidence_references/0/category")
.and_then(Value::as_str),
Some("missing"),
"explain should surface the broken local evidence category"
);
let suggested_actions = value
.pointer("/next/suggested_actions")
.and_then(Value::as_array)
.unwrap_or_else(|| std::panic::panic_any("explain suggested actions should be an array"));
assert!(
suggested_actions.iter().any(|action| action.as_str()
== Some("restore or commit the referenced local evidence artifact")),
"explain should suggest local evidence repair"
);
let proof_commands = value
.pointer("/next/proof_commands")
.and_then(Value::as_array)
.unwrap_or_else(|| std::panic::panic_any("explain proof commands should be an array"));
assert!(
proof_commands.iter().any(|command| command.as_str()
== Some("cargo-allow worklist --broken-evidence --format json")),
"explain should route broken evidence to the worklist repair queue"
);
assert!(
proof_commands
.iter()
.any(|command| command.as_str()
== Some("cargo-allow list --broken-evidence --format json")),
"explain should keep the ledger broken-evidence shortcut visible"
);
let text = explain_entry_text(
Path::new("target/cargo-allow-test-missing-root"),
&cfg,
&entry,
std::slice::from_ref(&finding),
);
assert!(
text.contains("proof: cargo-allow list --broken-evidence --format json"),
"human explain should keep the ledger broken-evidence shortcut visible"
);
}