pub mod checker;
pub mod effects;
pub mod generate;
pub mod proof_term;
pub use checker::{check_bundle, check_proof, BundleReport, CheckOutcome, ProofCheck};
pub use generate::{
artifact_digest, derive_capability_containment_witness, derive_capability_isolation_witness,
derive_compliance_coverage_witness, derive_effect_row_soundness_witness,
derive_endpoint_retry_witness, derive_shield_halt_witness, derive_socket_credit_witness,
derive_tool_call_soundness_witness, generate_all_proofs,
generate_capability_containment_proofs, generate_capability_isolation_proofs,
generate_compliance_coverage_proofs, generate_effect_row_soundness_proofs,
generate_resource_bounds_proofs, generate_shield_halt_guarantee_proofs,
generate_tool_call_soundness_proofs,
};
pub use proof_term::{
CapabilityContainmentWitness, CapabilityIsolationWitness, ComplianceCoverageWitness,
EffectRowSoundnessWitness, ProofBundle, ProofTerm, PropertyClass, ResourceBoundsWitness,
ShieldHaltGuaranteeWitness, ToolCallSoundnessWitness, Witness, MAX_RETRIES,
VALID_BREACH_POLICIES,
};
#[cfg(test)]
mod tests {
use super::*;
use crate::ir_nodes::{IRAxonEndpoint, IRProgram, IRShield, IRToolSpec};
const VERSION: &str = "2.4.0-test";
fn empty_ir() -> IRProgram {
IRProgram::new()
}
fn endpoint(name: &str, compliance: &[&str], shield_ref: &str) -> IRAxonEndpoint {
IRAxonEndpoint {
node_type: "endpoint",
source_line: 1,
source_column: 1,
name: name.to_string(),
method: "POST".to_string(),
path: format!("/{name}"),
body_type: String::new(),
execute_flow: "F".to_string(),
output_type: String::new(),
shield_ref: shield_ref.to_string(),
retries: 0,
timeout: String::new(),
compliance: compliance.iter().map(|s| s.to_string()).collect(),
path_params: Vec::new(),
query_params: Vec::new(),
requires_capabilities: Vec::new(),
}
}
fn shield(name: &str, provides: &[&str]) -> IRShield {
IRShield {
node_type: "shield",
source_line: 1,
source_column: 1,
name: name.to_string(),
scan: vec!["pii_leak".to_string()],
strategy: String::new(),
on_breach: "halt".to_string(),
severity: "high".to_string(),
quarantine: String::new(),
max_retries: 0,
confidence_threshold: 0.0,
allow_tools: Vec::new(),
deny_tools: Vec::new(),
sandbox: false,
redact: Vec::new(),
log: String::new(),
deflect_message: String::new(),
taint: String::new(),
compliance: provides.iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn covered_endpoint_proof_verifies() {
let mut ir = empty_ir();
ir.shields.push(shield("PhiGate", &["HIPAA", "GDPR"]));
ir.endpoints
.push(endpoint("ChatEndpoint", &["HIPAA", "GDPR"], "PhiGate"));
let proofs = generate_compliance_coverage_proofs(&ir, VERSION);
assert_eq!(
proofs.len(),
1,
"one compliance-bearing endpoint => one proof"
);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn no_compliance_no_proof() {
let mut ir = empty_ir();
ir.shields.push(shield("PhiGate", &["HIPAA"]));
ir.endpoints.push(endpoint("Plain", &[], "PhiGate"));
assert!(generate_compliance_coverage_proofs(&ir, VERSION).is_empty());
}
#[test]
fn shield_missing_required_class_is_refuted() {
let mut ir = empty_ir();
ir.shields.push(shield("PartialGate", &["HIPAA"]));
ir.endpoints
.push(endpoint("Gap", &["HIPAA", "GDPR"], "PartialGate"));
let proofs = generate_compliance_coverage_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("does not provide"), "got: {reason}");
assert!(reason.contains("GDPR"), "got: {reason}");
}
other => panic!("expected coverage-gap Refuted, got {other:?}"),
}
}
#[test]
fn compliance_without_shield_is_refuted() {
let mut ir = empty_ir();
ir.endpoints.push(endpoint("NoGuard", &["HIPAA"], "")); let proofs = generate_compliance_coverage_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 1);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("no resolvable shield"), "got: {reason}");
}
other => panic!("expected Refuted, got {other:?}"),
}
}
#[test]
fn dangling_shield_ref_is_refuted() {
let mut ir = empty_ir();
ir.endpoints
.push(endpoint("Dangling", &["HIPAA"], "GhostShield"));
let proofs = generate_compliance_coverage_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("no resolvable shield"), "got: {reason}");
}
other => panic!("expected Refuted, got {other:?}"),
}
}
#[test]
fn phantom_regulatory_class_is_refuted() {
let mut ir = empty_ir();
ir.shields.push(shield("PhiGate", &["HIPAA"]));
ir.endpoints.push(endpoint("Typo", &["HIPPA"], "PhiGate")); let proofs = generate_compliance_coverage_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("unknown regulatory class"), "got: {reason}");
assert!(reason.contains("HIPPA"), "got: {reason}");
}
other => panic!("expected Refuted, got {other:?}"),
}
}
#[test]
fn forged_shield_present_rejected() {
let mut ir = empty_ir();
ir.endpoints.push(endpoint("Forge", &["HIPAA"], ""));
let mut proofs = generate_compliance_coverage_proofs(&ir, VERSION);
if let Witness::ComplianceCoverage(ref mut w) = proofs[0].witness {
w.shield_present = true;
}
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("disagrees with artifact"), "got: {reason}");
}
other => panic!("expected forged-witness Refuted, got {other:?}"),
}
}
#[test]
fn forged_hidden_unknown_class_rejected() {
let mut ir = empty_ir();
ir.shields.push(shield("PhiGate", &["HIPAA"]));
ir.endpoints
.push(endpoint("Hide", &["NOTACLASS"], "PhiGate"));
let mut proofs = generate_compliance_coverage_proofs(&ir, VERSION);
if let Witness::ComplianceCoverage(ref mut w) = proofs[0].witness {
w.unknown_classes.clear();
}
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("disagrees with artifact"), "got: {reason}");
}
other => panic!("expected forged-witness Refuted, got {other:?}"),
}
}
#[test]
fn digest_mismatch_rejected() {
let mut ir_a = empty_ir();
ir_a.shields.push(shield("PhiGate", &["HIPAA"]));
ir_a.endpoints
.push(endpoint("ChatEndpoint", &["HIPAA"], "PhiGate"));
let proofs = generate_compliance_coverage_proofs(&ir_a, VERSION);
let mut ir_b = empty_ir();
ir_b.shields.push(shield("PhiGate", &["HIPAA"]));
ir_b.endpoints
.push(endpoint("ChatEndpoint", &["HIPAA"], "PhiGate"));
ir_b.endpoints.push(endpoint("Extra", &["GDPR"], "PhiGate"));
assert_eq!(
check_proof(&proofs[0], &ir_b),
CheckOutcome::DigestMismatch,
"a proof for program A must not verify against program B"
);
}
#[test]
fn proof_for_absent_endpoint_refuted_not_panic() {
let mut ir = empty_ir();
ir.shields.push(shield("PhiGate", &["HIPAA"]));
ir.endpoints.push(endpoint("Real", &["HIPAA"], "PhiGate"));
let mut proofs = generate_compliance_coverage_proofs(&ir, VERSION);
if let Witness::ComplianceCoverage(ref mut w) = proofs[0].witness {
w.endpoint_name = "Ghost".to_string();
}
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("not present in artifact"), "got: {reason}");
}
other => panic!("expected absent-endpoint Refuted, got {other:?}"),
}
}
#[test]
fn proof_term_json_round_trips_and_verifies() {
let mut ir = empty_ir();
ir.shields.push(shield("PhiGate", &["HIPAA"]));
ir.endpoints
.push(endpoint("ChatEndpoint", &["HIPAA"], "PhiGate"));
let proofs = generate_compliance_coverage_proofs(&ir, VERSION);
let json = serde_json::to_string(&proofs[0]).expect("serialize");
let restored: ProofTerm = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored, proofs[0]);
assert_eq!(check_proof(&restored, &ir), CheckOutcome::Verified);
}
#[test]
fn class_ordering_and_dupes_canonicalized() {
let mut ir = empty_ir();
ir.shields.push(shield("PhiGate", &["HIPAA", "GDPR"]));
ir.endpoints.push(endpoint(
"Multi",
&["GDPR", "HIPAA", "GDPR"], "PhiGate",
));
let proofs = generate_compliance_coverage_proofs(&ir, VERSION);
if let Witness::ComplianceCoverage(ref w) = proofs[0].witness {
assert_eq!(
w.required_classes,
vec!["GDPR".to_string(), "HIPAA".to_string()]
);
} else {
panic!("expected ComplianceCoverage witness");
}
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn property_class_slug_stable() {
assert_eq!(
PropertyClass::ComplianceCoverage.slug(),
"compliance_coverage"
);
assert_eq!(
PropertyClass::EffectRowSoundness.slug(),
"effect_row_soundness"
);
assert_eq!(
PropertyClass::CapabilityIsolation.slug(),
"capability_isolation"
);
assert_eq!(PropertyClass::ResourceBounds.slug(), "resource_bounds");
assert_eq!(
PropertyClass::ShieldHaltGuarantee.slug(),
"shield_halt_guarantee"
);
assert_eq!(
PropertyClass::CapabilityContainment.slug(),
"capability_containment"
);
assert_eq!(
PropertyClass::ToolCallSoundness.slug(),
"tool_call_soundness"
);
}
fn tool(name: &str, effects: &[&str]) -> IRToolSpec {
IRToolSpec {
node_type: "tool",
source_line: 1,
source_column: 1,
name: name.to_string(),
provider: "native".to_string(),
max_results: None,
filter_expr: String::new(),
timeout: String::new(),
runtime: String::new(),
sandbox: None,
input_schema: Vec::new(),
output_schema: String::new(),
parameters: Vec::new(),
output_type: None,
effect_row: effects.iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn well_formed_effect_row_verifies() {
let mut ir = empty_ir();
ir.tools.push(tool("Fetch", &["network", "storage"]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 1);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn no_effects_no_proof() {
let mut ir = empty_ir();
ir.tools.push(tool("Plain", &[]));
assert!(generate_effect_row_soundness_proofs(&ir, VERSION).is_empty());
}
#[test]
fn phantom_effect_base_refuted() {
let mut ir = empty_ir();
ir.tools.push(tool("Typo", &["netwrok"]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("unknown base"), "got: {reason}");
assert!(reason.contains("netwrok"), "got: {reason}");
}
other => panic!("expected phantom-effect Refuted, got {other:?}"),
}
}
#[test]
fn bare_stream_missing_qualifier_refuted() {
let mut ir = empty_ir();
ir.tools.push(tool("Streamer", &["stream"]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("without a qualifier"), "got: {reason}");
}
other => panic!("expected missing-qualifier Refuted, got {other:?}"),
}
}
#[test]
fn valid_stream_qualifier_verifies() {
let mut ir = empty_ir();
ir.tools.push(tool("Streamer", &["stream:drop_oldest"]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn invalid_stream_qualifier_refuted() {
let mut ir = empty_ir();
ir.tools.push(tool("Streamer", &["stream:explode"]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(
reason.contains("invalid backpressure policy"),
"got: {reason}"
);
}
other => panic!("expected invalid-qualifier Refuted, got {other:?}"),
}
}
fn effects_ext(name: &str, members: &[&str]) -> crate::ir_nodes::IRExtension {
crate::ir_nodes::IRExtension {
node_type: "extension",
source_line: 1,
source_column: 1,
name: name.to_string(),
category: "effects".to_string(),
members: members
.iter()
.map(|m| crate::ir_nodes::IRExtensionMember {
name: m.to_string(),
semantics: None,
default_confidence: None,
})
.collect(),
}
}
#[test]
fn extension_declared_provenance_base_verifies() {
let mut ir = empty_ir();
ir.extensions
.push(effects_ext("risk_axis", &["risk:elevated"]));
ir.tools.push(tool("Fetch", &["network", "risk:elevated"]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 1);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn undeclared_custom_base_refuted() {
let mut ir = empty_ir();
ir.extensions
.push(effects_ext("risk_axis", &["risk:elevated"]));
ir.tools.push(tool("Fetch", &["risk:guess"]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("unknown base"), "got: {reason}");
assert!(reason.contains("risk:guess"), "got: {reason}");
}
other => panic!("expected undeclared-base Refuted, got {other:?}"),
}
}
#[test]
fn pcc_provenance_set_excludes_canonical_shadow() {
let mut ir = empty_ir();
ir.extensions
.push(effects_ext("mixed", &["io:bypass_shield", "risk:elevated"]));
let set = super::generate::extension_effect_members(&ir);
assert!(
!set.contains("io:bypass_shield"),
"a member shadowing the canonical enforceable base `io` must NOT \
become a provenance member (invariant #2)"
);
assert!(
set.contains("risk:elevated"),
"a genuinely custom base must be a provenance member"
);
}
#[test]
fn builtin_epistemic_level_verifies() {
for level in ["believe", "doubt", "know", "speculate"] {
let mut ir = empty_ir();
ir.tools
.push(tool("Fetch", &["network", &format!("epistemic:{level}")]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 1);
assert_eq!(
check_proof(&proofs[0], &ir),
CheckOutcome::Verified,
"epistemic:{level} must verify (built-in provenance axis)"
);
}
}
#[test]
fn unknown_epistemic_level_refuted() {
let mut ir = empty_ir();
ir.tools.push(tool("X", &["epistemic:guess"]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("unknown base"), "got: {reason}");
assert!(reason.contains("epistemic:guess"), "got: {reason}");
}
other => panic!("expected unknown-epistemic-level Refuted, got {other:?}"),
}
}
#[test]
fn purity_violation_refuted() {
let mut ir = empty_ir();
ir.tools.push(tool("FakePure", &["pure", "network"]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("pure"), "got: {reason}");
assert!(reason.contains("cannot be effectful"), "got: {reason}");
}
other => panic!("expected purity Refuted, got {other:?}"),
}
}
#[test]
fn pure_alone_verifies() {
let mut ir = empty_ir();
ir.tools.push(tool("TrulyPure", &["pure"]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn forged_hidden_unknown_base_rejected() {
let mut ir = empty_ir();
ir.tools.push(tool("Sneaky", &["netwrok"]));
let mut proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
if let Witness::EffectRowSoundness(ref mut w) = proofs[0].witness {
w.unknown_bases.clear(); }
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("disagrees with artifact"), "got: {reason}");
}
other => panic!("expected forged Refuted, got {other:?}"),
}
}
#[test]
fn mismatched_property_witness_is_unknown_property() {
let mut ir = empty_ir();
ir.tools.push(tool("Fetch", &["network"]));
let digest = artifact_digest(&ir);
let bogus = ProofTerm {
property: PropertyClass::ComplianceCoverage, artifact_digest: digest,
witness: Witness::EffectRowSoundness(derive_effect_row_soundness_witness(
"Fetch",
&["network".to_string()],
&std::collections::HashSet::new(),
)),
axon_version: VERSION.to_string(),
};
assert_eq!(check_proof(&bogus, &ir), CheckOutcome::UnknownProperty);
}
#[test]
fn effect_proof_json_round_trips_and_verifies() {
let mut ir = empty_ir();
ir.tools.push(tool("Fetch", &["network", "storage"]));
let proofs = generate_effect_row_soundness_proofs(&ir, VERSION);
let json = serde_json::to_string(&proofs[0]).expect("serialize");
let restored: ProofTerm = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored, proofs[0]);
assert_eq!(check_proof(&restored, &ir), CheckOutcome::Verified);
}
#[test]
fn effect_proof_digest_mismatch_rejected() {
let mut ir_a = empty_ir();
ir_a.tools.push(tool("Fetch", &["network"]));
let proofs = generate_effect_row_soundness_proofs(&ir_a, VERSION);
let mut ir_b = empty_ir();
ir_b.tools.push(tool("Fetch", &["network"]));
ir_b.tools.push(tool("Other", &["storage"]));
assert_eq!(check_proof(&proofs[0], &ir_b), CheckOutcome::DigestMismatch);
}
fn store(name: &str, capability: &str) -> crate::ir_nodes::IRAxonStore {
crate::ir_nodes::IRAxonStore {
node_type: "axonstore",
source_line: 1,
source_column: 1,
name: name.to_string(),
backend: "postgresql".to_string(),
connection: String::new(),
confidence_floor: None,
isolation: String::new(),
on_breach: String::new(),
capability: capability.to_string(),
column_schema: None,
}
}
#[test]
fn well_formed_gate_verifies() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Ledger", "legal.read"));
let proofs = generate_capability_isolation_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 1);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn no_gate_no_proof() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Open", ""));
assert!(generate_capability_isolation_proofs(&ir, VERSION).is_empty());
}
#[test]
fn malformed_gate_refuted() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Broken", "Legal.Read")); let proofs = generate_capability_isolation_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(
reason.contains("malformed capability gate"),
"got: {reason}"
);
assert!(reason.contains("Legal.Read"), "got: {reason}");
}
other => panic!("expected malformed-gate Refuted, got {other:?}"),
}
}
#[test]
fn deep_dotted_gate_verifies() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Phi", "hipaa.phi.read"));
let proofs = generate_capability_isolation_proofs(&ir, VERSION);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn forged_malformed_flag_rejected() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Sneaky", "Bad..Slug"));
let mut proofs = generate_capability_isolation_proofs(&ir, VERSION);
if let Witness::CapabilityIsolation(ref mut w) = proofs[0].witness {
w.malformed = false; }
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("disagrees with artifact"), "got: {reason}");
}
other => panic!("expected forged Refuted, got {other:?}"),
}
}
#[test]
fn capability_proof_digest_mismatch_rejected() {
let mut ir_a = empty_ir();
ir_a.axonstore_specs.push(store("Ledger", "legal.read"));
let proofs = generate_capability_isolation_proofs(&ir_a, VERSION);
let mut ir_b = empty_ir();
ir_b.axonstore_specs.push(store("Ledger", "legal.read"));
ir_b.axonstore_specs.push(store("Extra", "fin.read"));
assert_eq!(check_proof(&proofs[0], &ir_b), CheckOutcome::DigestMismatch);
}
#[test]
fn capability_proof_json_round_trips_and_verifies() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Ledger", "legal.read"));
let proofs = generate_capability_isolation_proofs(&ir, VERSION);
let json = serde_json::to_string(&proofs[0]).expect("serialize");
let restored: ProofTerm = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored, proofs[0]);
assert_eq!(check_proof(&restored, &ir), CheckOutcome::Verified);
}
fn endpoint_retries(name: &str, retries: i64) -> IRAxonEndpoint {
let mut e = endpoint(name, &[], "");
e.retries = retries;
e
}
fn socket(name: &str, credit: Option<i64>) -> crate::ir_nodes::IRSocket {
crate::ir_nodes::IRSocket {
node_type: "socket",
source_line: 1,
source_column: 1,
name: name.to_string(),
protocol: "ChatProtocol".to_string(),
backpressure_credit: credit,
reconnect: false,
legal_basis: None,
}
}
#[test]
fn retry_in_bounds_verifies() {
let mut ir = empty_ir();
ir.endpoints.push(endpoint_retries("Mid", 3));
ir.endpoints.push(endpoint_retries("Zero", 0));
ir.endpoints.push(endpoint_retries("Ceiling", MAX_RETRIES));
let proofs = generate_resource_bounds_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 3);
for p in &proofs {
assert_eq!(check_proof(p, &ir), CheckOutcome::Verified);
}
}
#[test]
fn retry_negative_refuted() {
let mut ir = empty_ir();
ir.endpoints.push(endpoint_retries("Neg", -1));
let proofs = generate_resource_bounds_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("retries=-1"), "got: {reason}");
assert!(reason.contains("outside the bound"), "got: {reason}");
}
other => panic!("expected negative-retry Refuted, got {other:?}"),
}
}
#[test]
fn retry_storm_refuted() {
let mut ir = empty_ir();
ir.endpoints
.push(endpoint_retries("Storm", MAX_RETRIES + 1));
let proofs = generate_resource_bounds_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("retry storm"), "got: {reason}");
}
other => panic!("expected retry-storm Refuted, got {other:?}"),
}
}
#[test]
fn socket_positive_credit_verifies() {
let mut ir = empty_ir();
ir.sockets.push(socket("Chat", Some(8)));
let proofs = generate_resource_bounds_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 1);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn socket_zero_credit_refuted() {
let mut ir = empty_ir();
ir.sockets.push(socket("Dead", Some(0)));
let proofs = generate_resource_bounds_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("deadlock"), "got: {reason}");
}
other => panic!("expected zero-credit Refuted, got {other:?}"),
}
}
#[test]
fn socket_unspecified_credit_no_proof() {
let mut ir = empty_ir();
ir.sockets.push(socket("Unspecified", None));
assert!(generate_resource_bounds_proofs(&ir, VERSION).is_empty());
}
#[test]
fn forged_retry_in_bounds_rejected() {
let mut ir = empty_ir();
ir.endpoints.push(endpoint_retries("Liar", -5));
let mut proofs = generate_resource_bounds_proofs(&ir, VERSION);
if let Witness::ResourceBounds(ResourceBoundsWitness::EndpointRetry {
ref mut in_bounds,
..
}) = proofs[0].witness
{
*in_bounds = true; }
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("disagrees with artifact"), "got: {reason}");
}
other => panic!("expected forged Refuted, got {other:?}"),
}
}
#[test]
fn forged_socket_credit_for_unspecified_rejected() {
let mut ir = empty_ir();
ir.sockets.push(socket("Ghost", None)); let digest = artifact_digest(&ir);
let bogus = ProofTerm {
property: PropertyClass::ResourceBounds,
artifact_digest: digest,
witness: Witness::ResourceBounds(derive_socket_credit_witness("Ghost", 8)),
axon_version: VERSION.to_string(),
};
match check_proof(&bogus, &ir) {
CheckOutcome::Refuted { reason } => {
assert!(
reason.contains("no declared backpressure credit"),
"got: {reason}"
);
}
other => panic!("expected forged-socket Refuted, got {other:?}"),
}
}
#[test]
fn resource_proof_digest_mismatch_rejected() {
let mut ir_a = empty_ir();
ir_a.endpoints.push(endpoint_retries("E", 2));
let proofs = generate_resource_bounds_proofs(&ir_a, VERSION);
let mut ir_b = empty_ir();
ir_b.endpoints.push(endpoint_retries("E", 2));
ir_b.endpoints.push(endpoint_retries("Extra", 1));
assert_eq!(check_proof(&proofs[0], &ir_b), CheckOutcome::DigestMismatch);
}
#[test]
fn resource_proof_json_round_trips_and_verifies() {
let mut ir = empty_ir();
ir.sockets.push(socket("Chat", Some(16)));
let proofs = generate_resource_bounds_proofs(&ir, VERSION);
let json = serde_json::to_string(&proofs[0]).expect("serialize");
let restored: ProofTerm = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored, proofs[0]);
assert_eq!(check_proof(&restored, &ir), CheckOutcome::Verified);
}
fn shield_breach(name: &str, on_breach: &str, scan: &[&str]) -> IRShield {
let mut s = shield(name, &[]); s.on_breach = on_breach.to_string();
s.scan = scan.iter().map(|c| c.to_string()).collect();
s
}
#[test]
fn valid_halt_with_scan_verifies() {
let mut ir = empty_ir();
ir.shields
.push(shield_breach("Guard", "halt", &["pii_leak"]));
let proofs = generate_shield_halt_guarantee_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 1);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn non_halt_policy_with_empty_scan_verifies() {
let mut ir = empty_ir();
ir.shields.push(shield_breach("Soft", "deflect", &[]));
let proofs = generate_shield_halt_guarantee_proofs(&ir, VERSION);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn no_breach_policy_no_proof() {
let mut ir = empty_ir();
ir.shields.push(shield_breach("None", "", &["pii_leak"]));
assert!(generate_shield_halt_guarantee_proofs(&ir, VERSION).is_empty());
}
#[test]
fn unknown_breach_policy_refuted() {
let mut ir = empty_ir();
ir.shields
.push(shield_breach("Typo", "hault", &["pii_leak"]));
let proofs = generate_shield_halt_guarantee_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("unknown on_breach policy"), "got: {reason}");
assert!(reason.contains("hault"), "got: {reason}");
}
other => panic!("expected unknown-policy Refuted, got {other:?}"),
}
}
#[test]
fn vacuous_halt_refuted() {
let mut ir = empty_ir();
ir.shields.push(shield_breach("Theater", "halt", &[]));
let proofs = generate_shield_halt_guarantee_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("vacuous"), "got: {reason}");
assert!(reason.contains("never fire"), "got: {reason}");
}
other => panic!("expected vacuous-halt Refuted, got {other:?}"),
}
}
#[test]
fn forged_vacuous_halt_rejected() {
let mut ir = empty_ir();
ir.shields.push(shield_breach("Sneaky", "halt", &[]));
let mut proofs = generate_shield_halt_guarantee_proofs(&ir, VERSION);
if let Witness::ShieldHaltGuarantee(ref mut w) = proofs[0].witness {
w.vacuous_halt = false; }
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("disagrees with artifact"), "got: {reason}");
}
other => panic!("expected forged Refuted, got {other:?}"),
}
}
#[test]
fn shield_halt_digest_mismatch_rejected() {
let mut ir_a = empty_ir();
ir_a.shields
.push(shield_breach("Guard", "halt", &["pii_leak"]));
let proofs = generate_shield_halt_guarantee_proofs(&ir_a, VERSION);
let mut ir_b = empty_ir();
ir_b.shields
.push(shield_breach("Guard", "halt", &["pii_leak"]));
ir_b.shields
.push(shield_breach("Other", "deflect", &["toxicity"]));
assert_eq!(check_proof(&proofs[0], &ir_b), CheckOutcome::DigestMismatch);
}
#[test]
fn shield_halt_json_round_trips_and_verifies() {
let mut ir = empty_ir();
ir.shields
.push(shield_breach("Guard", "halt", &["pii_leak", "jailbreak"]));
let proofs = generate_shield_halt_guarantee_proofs(&ir, VERSION);
let json = serde_json::to_string(&proofs[0]).expect("serialize");
let restored: ProofTerm = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored, proofs[0]);
assert_eq!(check_proof(&restored, &ir), CheckOutcome::Verified);
}
fn endpoint_requires(name: &str, execute_flow: &str, requires: &[&str]) -> IRAxonEndpoint {
let mut e = endpoint(name, &[], "");
e.execute_flow = execute_flow.to_string();
e.requires_capabilities = requires.iter().map(|s| s.to_string()).collect();
e
}
fn retrieve_step(store_name: &str) -> crate::ir_nodes::IRFlowNode {
crate::ir_nodes::IRFlowNode::Retrieve(crate::ir_nodes::IRRetrieveStep {
node_type: "retrieve",
source_line: 1,
source_column: 1,
store_name: store_name.to_string(),
where_expr: String::new(),
alias: String::new(),
})
}
fn flow_reaching(name: &str, store_names: &[&str]) -> crate::ir_nodes::IRFlow {
crate::ir_nodes::IRFlow {
node_type: "flow",
source_line: 1,
source_column: 1,
name: name.to_string(),
parameters: Vec::new(),
return_type_name: String::new(),
return_type_generic: String::new(),
return_type_optional: false,
steps: store_names.iter().map(|s| retrieve_step(s)).collect(),
edges: Vec::new(),
execution_levels: Vec::new(),
}
}
fn flow_reaching_in_conditional(name: &str, store_name: &str) -> crate::ir_nodes::IRFlow {
let cond = crate::ir_nodes::IRFlowNode::Conditional(crate::ir_nodes::IRConditional {
node_type: "conditional",
source_line: 1,
source_column: 1,
condition: "x".to_string(),
comparison_op: "==".to_string(),
comparison_value: "1".to_string(),
then_body: vec![retrieve_step(store_name)],
else_body: Vec::new(),
conditions: Vec::new(),
conjunctor: String::new(),
});
let mut f = flow_reaching(name, &[]);
f.steps = vec![cond];
f
}
#[test]
fn covered_containment_verifies() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Ledger", "data.read"));
ir.flows.push(flow_reaching("Chat", &["Ledger"]));
ir.endpoints
.push(endpoint_requires("ChatEndpoint", "Chat", &["data.read"]));
let proofs = generate_capability_containment_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 1);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn uncovered_gate_refuted() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Ledger", "data.read"));
ir.flows.push(flow_reaching("Chat", &["Ledger"]));
ir.endpoints.push(endpoint_requires("Leaky", "Chat", &[])); let proofs = generate_capability_containment_proofs(&ir, VERSION);
assert_eq!(
proofs.len(),
1,
"reached-gate-with-no-requires must produce a proof"
);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("capability leak"), "got: {reason}");
assert!(reason.contains("data.read"), "got: {reason}");
}
other => panic!("expected leak Refuted, got {other:?}"),
}
}
#[test]
fn nested_conditional_reach_is_caught() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Secret", "admin.read"));
ir.flows
.push(flow_reaching_in_conditional("Branchy", "Secret"));
ir.endpoints
.push(endpoint_requires("NestedLeak", "Branchy", &[]));
let proofs = generate_capability_containment_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("admin.read"), "got: {reason}");
}
other => panic!("expected nested-reach Refuted, got {other:?}"),
}
}
#[test]
fn unresolved_flow_refuted() {
let mut ir = empty_ir();
ir.endpoints
.push(endpoint_requires("Ghost", "NoSuchFlow", &["x.read"]));
let proofs = generate_capability_containment_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(
reason.contains("not present in the artifact"),
"got: {reason}"
);
}
other => panic!("expected unresolved-flow Refuted, got {other:?}"),
}
}
#[test]
fn over_declared_requires_verifies() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Ledger", "data.read"));
ir.flows.push(flow_reaching("Chat", &["Ledger"]));
ir.endpoints.push(endpoint_requires(
"Strict",
"Chat",
&["data.read", "extra.write"], ));
let proofs = generate_capability_containment_proofs(&ir, VERSION);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn trivial_no_requires_no_gates_no_proof() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Open", "")); ir.flows.push(flow_reaching("Chat", &["Open"]));
ir.endpoints.push(endpoint_requires("Plain", "Chat", &[]));
assert!(generate_capability_containment_proofs(&ir, VERSION).is_empty());
}
#[test]
fn forged_hidden_uncovered_rejected() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Ledger", "data.read"));
ir.flows.push(flow_reaching("Chat", &["Ledger"]));
ir.endpoints.push(endpoint_requires("Sneaky", "Chat", &[]));
let mut proofs = generate_capability_containment_proofs(&ir, VERSION);
if let Witness::CapabilityContainment(ref mut w) = proofs[0].witness {
w.uncovered_gates.clear(); }
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("disagrees with artifact"), "got: {reason}");
}
other => panic!("expected forged Refuted, got {other:?}"),
}
}
#[test]
fn containment_digest_mismatch_rejected() {
let mut ir_a = empty_ir();
ir_a.axonstore_specs.push(store("Ledger", "data.read"));
ir_a.flows.push(flow_reaching("Chat", &["Ledger"]));
ir_a.endpoints
.push(endpoint_requires("E", "Chat", &["data.read"]));
let proofs = generate_capability_containment_proofs(&ir_a, VERSION);
let mut ir_b = empty_ir();
ir_b.axonstore_specs.push(store("Ledger", "data.read"));
ir_b.flows.push(flow_reaching("Chat", &["Ledger"]));
ir_b.endpoints
.push(endpoint_requires("E", "Chat", &["data.read"]));
ir_b.endpoints
.push(endpoint_requires("Extra", "Chat", &["data.read"]));
assert_eq!(check_proof(&proofs[0], &ir_b), CheckOutcome::DigestMismatch);
}
#[test]
fn containment_json_round_trips_and_verifies() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Ledger", "data.read"));
ir.flows.push(flow_reaching("Chat", &["Ledger"]));
ir.endpoints
.push(endpoint_requires("E", "Chat", &["data.read"]));
let proofs = generate_capability_containment_proofs(&ir, VERSION);
let json = serde_json::to_string(&proofs[0]).expect("serialize");
let restored: ProofTerm = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored, proofs[0]);
assert_eq!(check_proof(&restored, &ir), CheckOutcome::Verified);
}
fn bundle_for(ir: &crate::ir_nodes::IRProgram) -> ProofBundle {
ProofBundle {
axon_version: VERSION.to_string(),
artifact_digest: artifact_digest(ir),
proofs: generate_all_proofs(ir, VERSION),
}
}
#[test]
fn bundle_report_all_verified_for_clean_program() {
let mut ir = empty_ir();
ir.shields
.push(shield_breach("Guard", "halt", &["pii_leak"]));
ir.axonstore_specs.push(store("Ledger", "data.read"));
ir.flows.push(flow_reaching("Chat", &["Ledger"]));
ir.endpoints
.push(endpoint_requires("ChatEndpoint", "Chat", &["data.read"]));
let report = check_bundle(&bundle_for(&ir), &ir);
assert!(
!report.results.is_empty(),
"clean program still yields proofs"
);
assert!(
report.all_verified(),
"every proof must verify: {:?}",
report.refutations()
);
assert!(report.refutations().is_empty());
}
#[test]
fn bundle_report_flags_capability_leak() {
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Ledger", "data.read"));
ir.flows.push(flow_reaching("Chat", &["Ledger"]));
ir.endpoints.push(endpoint_requires("Leaky", "Chat", &[]));
let report = check_bundle(&bundle_for(&ir), &ir);
assert!(
!report.all_verified(),
"a capability leak must be non-deployable"
);
let refs = report.refutations();
assert!(!refs.is_empty());
assert!(
refs.iter()
.any(|r| r.property == PropertyClass::CapabilityContainment),
"the leak must surface as a CapabilityContainment refutation"
);
assert!(
refs.iter().any(|r| matches!(&r.outcome, CheckOutcome::Refuted { reason } if reason.contains("data.read"))),
"the refutation must name the leaked capability"
);
}
#[test]
fn bundle_report_matches_per_proof_check() {
let mut ir = empty_ir();
ir.shields
.push(shield_breach("Guard", "halt", &["pii_leak"]));
ir.endpoints.push(endpoint("E", &["HIPAA"], "Guard"));
let bundle = bundle_for(&ir);
let report = check_bundle(&bundle, &ir);
assert_eq!(report.results.len(), bundle.proofs.len());
for (r, proof) in report.results.iter().zip(&bundle.proofs) {
assert_eq!(r.outcome, check_proof(proof, &ir));
assert_eq!(r.property, proof.property);
assert_eq!(r.subject, proof.witness.subject_name());
}
}
#[test]
fn bundle_report_rejects_foreign_bundle() {
let mut ir_a = empty_ir();
ir_a.endpoints.push(endpoint("E", &["HIPAA"], ""));
let bundle = bundle_for(&ir_a);
let mut ir_b = empty_ir();
ir_b.endpoints.push(endpoint("E", &["HIPAA"], ""));
ir_b.endpoints.push(endpoint("Other", &["PCI_DSS"], ""));
let report = check_bundle(&bundle, &ir_b);
assert!(!report.all_verified());
assert!(report
.results
.iter()
.all(|r| r.outcome == CheckOutcome::DigestMismatch));
}
#[test]
fn reachability_walk_has_no_wildcard_arm() {
const GEN_SRC: &str = include_str!("generate.rs");
let start = GEN_SRC
.find("fn collect_store_accesses(")
.expect("collect_store_accesses present");
let rest = &GEN_SRC[start..];
let end = rest
.find("\npub fn derive_capability_containment_witness")
.expect("derive_capability_containment_witness follows");
let body = &rest[..end];
assert!(
!body.contains("_ => {}") && !body.contains("_ =>"),
"collect_store_accesses must stay an EXHAUSTIVE match with no \
`_` wildcard — a wildcard re-opens the §51.x.3 silent-gap risk \
(a future IRFlowNode variant could carry a store ref / nested \
body and be missed)"
);
}
#[test]
fn reachability_walk_documents_transitive_reopen() {
const GEN_SRC: &str = include_str!("generate.rs");
assert!(
GEN_SRC.contains("REOPENED (§51.x.3)") || GEN_SRC.contains("REOPEN"),
"the walk must keep the note on where to reopen transitive \
cross-flow reachability if a flow-invocation node ever joins \
IRFlowNode"
);
}
#[test]
fn leaf_nodes_contribute_no_gates() {
use crate::ir_nodes::{IRBreakStep, IRFlowNode};
let mut ir = empty_ir();
ir.axonstore_specs.push(store("Ledger", "data.read"));
let mut flow = flow_reaching("Chat", &["Ledger"]);
flow.steps.push(IRFlowNode::Break(IRBreakStep {
node_type: "break",
source_line: 1,
source_column: 1,
}));
ir.flows.push(flow);
let w = derive_capability_containment_witness("E", "Chat", &["data.read".to_string()], &ir);
assert_eq!(w.reached_gates, vec!["data.read".to_string()]);
assert!(w.uncovered_gates.is_empty());
}
#[test]
fn empty_bundle_is_vacuously_verified() {
let ir = empty_ir();
let bundle = ProofBundle {
axon_version: VERSION.to_string(),
artifact_digest: artifact_digest(&ir),
proofs: Vec::new(),
};
let report = check_bundle(&bundle, &ir);
assert!(report.all_verified());
assert!(report.refutations().is_empty());
}
fn tool_with_params(name: &str, params: &[(&str, &str, bool)]) -> IRToolSpec {
let mut t = tool(name, &[]); t.provider = "http".to_string();
t.parameters = params
.iter()
.map(|(n, ty, opt)| crate::ir_nodes::IRToolParam {
name: n.to_string(),
type_name: ty.to_string(),
optional: *opt,
})
.collect();
t
}
fn use_named(tool_name: &str, args: &[(&str, &str)]) -> crate::ir_nodes::IRFlowNode {
crate::ir_nodes::IRFlowNode::UseTool(crate::ir_nodes::IRUseToolStep {
node_type: "use_tool",
source_line: 1,
source_column: 1,
tool_name: tool_name.to_string(),
argument: String::new(),
named_args: args
.iter()
.map(|(n, v)| crate::ir_nodes::IRNamedArg {
name: n.to_string(),
value: v.to_string(),
value_kind: "literal".to_string(),
})
.collect(),
})
}
fn use_legacy(tool_name: &str, arg: &str) -> crate::ir_nodes::IRFlowNode {
crate::ir_nodes::IRFlowNode::UseTool(crate::ir_nodes::IRUseToolStep {
node_type: "use_tool",
source_line: 1,
source_column: 1,
tool_name: tool_name.to_string(),
argument: arg.to_string(),
named_args: Vec::new(),
})
}
fn flow_with_steps(name: &str, steps: Vec<crate::ir_nodes::IRFlowNode>) -> crate::ir_nodes::IRFlow {
let mut f = flow_reaching(name, &[]);
f.steps = steps;
f
}
fn crm_tool() -> IRToolSpec {
tool_with_params(
"CrmRadar",
&[
("company", "String", false),
("max_results", "Int", false),
("active", "Bool", true),
],
)
}
#[test]
fn sound_tool_call_verifies() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows.push(flow_with_steps(
"Scan",
vec![use_named("CrmRadar", &[("company", "company"), ("max_results", "5")])],
));
let proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 1, "one schema-full structured call => one proof");
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn unknown_arg_refuted() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows.push(flow_with_steps(
"Scan",
vec![use_named(
"CrmRadar",
&[("company", "x"), ("max_results", "5"), ("bogus", "1")],
)],
));
let proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("does not declare"), "got: {reason}");
assert!(reason.contains("bogus"), "got: {reason}");
}
other => panic!("expected unknown-arg Refuted, got {other:?}"),
}
}
#[test]
fn duplicate_arg_refuted() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows.push(flow_with_steps(
"Scan",
vec![use_named(
"CrmRadar",
&[("company", "a"), ("company", "b"), ("max_results", "5")],
)],
));
let proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("duplicate"), "got: {reason}");
assert!(reason.contains("company"), "got: {reason}");
}
other => panic!("expected duplicate Refuted, got {other:?}"),
}
}
#[test]
fn missing_required_refuted() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows.push(flow_with_steps(
"Scan",
vec![use_named("CrmRadar", &[("company", "x")])],
));
let proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("omits required"), "got: {reason}");
assert!(reason.contains("max_results"), "got: {reason}");
}
other => panic!("expected missing-required Refuted, got {other:?}"),
}
}
#[test]
fn optional_param_omitted_verifies() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows.push(flow_with_steps(
"Scan",
vec![use_named("CrmRadar", &[("company", "x"), ("max_results", "5")])],
));
let proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn literal_type_mismatch_refuted() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows.push(flow_with_steps(
"Scan",
vec![use_named("CrmRadar", &[("company", "5"), ("max_results", "5")])],
));
let proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("type mismatch"), "got: {reason}");
assert!(reason.contains("company:String:Int"), "got: {reason}");
}
other => panic!("expected type-mismatch Refuted, got {other:?}"),
}
}
#[test]
fn int_into_float_param_verifies() {
let mut ir = empty_ir();
ir.tools
.push(tool_with_params("Calc", &[("ratio", "Float", false)]));
ir.flows.push(flow_with_steps(
"Run",
vec![use_named("Calc", &[("ratio", "5")])], ));
let proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
}
#[test]
fn schema_less_tool_no_proof() {
let mut ir = empty_ir();
ir.tools.push(tool("Plain", &["network"])); ir.flows.push(flow_with_steps(
"Scan",
vec![use_named("Plain", &[("anything", "1")])],
));
assert!(generate_tool_call_soundness_proofs(&ir, VERSION).is_empty());
}
#[test]
fn legacy_on_form_no_proof() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows
.push(flow_with_steps("Scan", vec![use_legacy("CrmRadar", "${q}")]));
assert!(generate_tool_call_soundness_proofs(&ir, VERSION).is_empty());
}
#[test]
fn two_calls_same_tool_distinguished() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows.push(flow_with_steps(
"Scan",
vec![
use_named("CrmRadar", &[("company", "x"), ("max_results", "5")]), use_named("CrmRadar", &[("company", "x"), ("bogus", "1")]), ],
));
let proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 2);
assert_eq!(check_proof(&proofs[0], &ir), CheckOutcome::Verified);
match check_proof(&proofs[1], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(
reason.contains("bogus") || reason.contains("max_results"),
"got: {reason}"
);
}
other => panic!("expected Refuted for the defective call, got {other:?}"),
}
}
#[test]
fn nested_conditional_call_is_caught() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
let cond = crate::ir_nodes::IRFlowNode::Conditional(crate::ir_nodes::IRConditional {
node_type: "conditional",
source_line: 1,
source_column: 1,
condition: "x".to_string(),
comparison_op: "==".to_string(),
comparison_value: "1".to_string(),
then_body: vec![use_named("CrmRadar", &[("company", "x"), ("ghost", "1")])],
else_body: Vec::new(),
conditions: Vec::new(),
conjunctor: String::new(),
});
ir.flows.push(flow_with_steps("Branchy", vec![cond]));
let proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
assert_eq!(proofs.len(), 1, "the nested call must be discovered");
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("ghost"), "got: {reason}");
}
other => panic!("expected nested-call Refuted, got {other:?}"),
}
}
#[test]
fn forged_hidden_unknown_arg_rejected() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows.push(flow_with_steps(
"Scan",
vec![use_named(
"CrmRadar",
&[("company", "x"), ("max_results", "5"), ("sneaky", "1")],
)],
));
let mut proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
if let Witness::ToolCallSoundness(ref mut w) = proofs[0].witness {
w.unknown_args.clear(); }
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("disagrees with artifact"), "got: {reason}");
}
other => panic!("expected forged Refuted, got {other:?}"),
}
}
#[test]
fn proof_for_absent_call_index_refuted() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows.push(flow_with_steps(
"Scan",
vec![use_named("CrmRadar", &[("company", "x"), ("max_results", "5")])],
));
let mut proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
if let Witness::ToolCallSoundness(ref mut w) = proofs[0].witness {
w.call_index = 9; }
match check_proof(&proofs[0], &ir) {
CheckOutcome::Refuted { reason } => {
assert!(reason.contains("no structured `use` call at index"), "got: {reason}");
}
other => panic!("expected absent-call Refuted, got {other:?}"),
}
}
#[test]
fn tool_call_proof_digest_mismatch_rejected() {
let mut ir_a = empty_ir();
ir_a.tools.push(crm_tool());
ir_a.flows.push(flow_with_steps(
"Scan",
vec![use_named("CrmRadar", &[("company", "x"), ("max_results", "5")])],
));
let proofs = generate_tool_call_soundness_proofs(&ir_a, VERSION);
let mut ir_b = empty_ir();
ir_b.tools.push(crm_tool());
ir_b.flows.push(flow_with_steps(
"Scan",
vec![use_named("CrmRadar", &[("company", "x"), ("max_results", "5")])],
));
ir_b.tools.push(tool("Extra", &["network"]));
assert_eq!(check_proof(&proofs[0], &ir_b), CheckOutcome::DigestMismatch);
}
#[test]
fn tool_call_proof_json_round_trips_and_verifies() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows.push(flow_with_steps(
"Scan",
vec![use_named(
"CrmRadar",
&[("company", "x"), ("max_results", "5"), ("active", "true")],
)],
));
let proofs = generate_tool_call_soundness_proofs(&ir, VERSION);
let json = serde_json::to_string(&proofs[0]).expect("serialize");
let restored: ProofTerm = serde_json::from_str(&json).expect("deserialize");
assert_eq!(restored, proofs[0]);
assert_eq!(check_proof(&restored, &ir), CheckOutcome::Verified);
}
#[test]
fn all_proofs_includes_tool_call_soundness_and_flags_defect() {
let mut ir = empty_ir();
ir.tools.push(crm_tool());
ir.flows.push(flow_with_steps(
"Scan",
vec![use_named("CrmRadar", &[("company", "5"), ("max_results", "5")])], ));
let bundle = bundle_for(&ir);
assert!(
bundle
.proofs
.iter()
.any(|p| p.property == PropertyClass::ToolCallSoundness),
"generate_all_proofs must include the tool-call-soundness class"
);
let report = check_bundle(&bundle, &ir);
assert!(!report.all_verified(), "a literal type mismatch is non-deployable");
assert!(report
.refutations()
.iter()
.any(|r| r.property == PropertyClass::ToolCallSoundness));
}
}