use lemma::DateTimeValue;
use lemma::Engine;
use std::collections::HashMap;
#[test]
fn test_missing_data_propagation_through_rule_reference() {
let mut engine = Engine::new();
let private_spec = r#"
spec private_rules
data base_price: number
data quantity: number
rule total_before_discount: base_price * quantity
rule final_total: total_before_discount
"#;
let main_spec = r#"
spec rules_and_unless
uses rules: private_rules
with rules.base_price: 500
rule total: rules.final_total
"#;
engine
.load(
private_spec,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"private.lemma",
))),
)
.unwrap();
engine
.load(
main_spec,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("main.lemma"))),
)
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run(None, "rules_and_unless", Some(&now), HashMap::new(), false)
.unwrap();
let total_rule = response
.results
.values()
.find(|r| r.rule.name == "total")
.expect("total rule should be in results");
assert!(total_rule.vetoed, "total should be vetoed");
let msg_str = total_rule.veto_reason.as_deref().expect("veto reason");
assert!(
msg_str.contains("Missing data"),
"Error message should contain 'Missing data', but got: {}",
msg_str
);
assert!(
!msg_str.contains("not found"),
"Error message should NOT contain 'not found', but got: {}",
msg_str
);
}
#[test]
fn test_rules_without_missing_data_still_evaluate() {
let mut engine = Engine::new();
let spec = r#"
spec test_spec
data price: number
data quantity: number
rule subtotal: price * quantity
rule message: "Order processed"
"#;
engine.load(spec, lemma::SourceType::Volatile).unwrap();
let mut data = std::collections::HashMap::new();
data.insert("price".to_string(), "10".to_string());
let now = DateTimeValue::now();
let response = engine
.run(None, "test_spec", Some(&now), data, false)
.unwrap();
let subtotal_rule = response
.results
.values()
.find(|r| r.rule.name == "subtotal")
.expect("subtotal rule should be in results");
assert!(
subtotal_rule.vetoed,
"subtotal should be Veto due to missing quantity"
);
let message_rule = response
.results
.values()
.find(|r| r.rule.name == "message")
.expect("message rule should be in results");
assert!(
!message_rule.vetoed,
"message rule should evaluate successfully"
);
assert_eq!(message_rule.text.as_deref(), Some("Order processed"));
}
#[test]
fn reference_with_missing_target_vetoes_as_missing_data() {
let code = r#"
spec inner
data slot: number
spec outer
uses i: inner
rule r: i.slot
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"missing.lemma",
))),
)
.unwrap();
let now = DateTimeValue::now();
let resp = engine
.run(None, "outer", Some(&now), HashMap::new(), false)
.expect("evaluates");
let rr = resp.results.get("r").expect("rule 'r'");
assert!(rr.vetoed, "expected MissingData veto");
match rr.veto_detail.as_ref().expect("veto detail") {
lemma::VetoType::MissingData { data } => {
let shown = data.to_string();
assert!(
shown.contains("slot") || shown.contains("i.slot"),
"missing-data veto should name the missing data path; got: {shown}"
);
}
other => panic!("expected MissingData veto, got: {:?}", other),
}
}
#[test]
fn rule_target_reference_veto_propagates_to_consumer() {
let code = r#"
spec inner
data denom: number -> default 0
rule divided: 10 / denom
spec top
uses i: inner
rule out: i.divided
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"missing.lemma",
))),
)
.expect("rule-target reference must be accepted at plan time");
let now = DateTimeValue::now();
let resp = engine
.run(None, "top", Some(&now), HashMap::new(), false)
.expect("evaluator must run; veto is a domain result, not an error");
let rr = resp.results.get("out").expect("rule 'out'");
assert!(rr.vetoed, "expected propagated veto");
let s = rr.veto_reason.as_deref().expect("veto reason");
assert!(
s.contains("Division by zero"),
"rule-target reference must propagate the exact veto reason of the target rule, \
got: {s}"
);
}