use indexmap::IndexMap;
use lemma::DateTimeValue;
use lemma::Engine;
#[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 base_price: 500
rule total: rules.final_total
"#;
engine
.load([(
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"private.lemma",
))),
private_spec.to_string(),
)])
.unwrap();
engine
.load([(
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("main.lemma"))),
main_spec.to_string(),
)])
.unwrap();
let now = DateTimeValue::now();
let response = engine
.evaluate(
None,
"rules_and_unless",
Some(&now),
IndexMap::new(),
None,
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");
assert_eq!(
total_rule.missing_data(),
&["rules.quantity"],
"with binds base_price; quantity stays unbound under uses alias"
);
assert_eq!(
total_rule.veto_reason.as_deref(),
Some("Missing data: rules.quantity"),
"got: {:?}",
total_rule.veto_reason
);
}
#[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([(lemma::SourceType::Volatile, spec.to_string())])
.unwrap();
let mut data = IndexMap::new();
data.insert("price".to_string(), "10".into());
let now = DateTimeValue::now();
let response = engine
.evaluate(None, "test_spec", Some(&now), data, None, 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
.result
.as_ref()
.expect("rule result value")
.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([(
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"missing.lemma",
))),
code.to_string(),
)])
.unwrap();
let now = DateTimeValue::now();
let resp = engine
.evaluate(None, "outer", Some(&now), IndexMap::new(), None, 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, .. } => {
assert_eq!(
data.input_key(),
"i.slot",
"missing-data veto must name the slot under the import alias"
);
}
other => panic!("expected MissingData veto, got: {:?}", other),
}
}
#[test]
fn rule_target_reference_veto_propagates_to_consumer() {
let code = r#"
spec inner
data denom: 0
rule divided: 10 / denom
spec root_spec
uses i: inner
rule out: i.divided
"#;
let mut engine = Engine::new();
engine
.load([(
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"missing.lemma",
))),
code.to_string(),
)])
.expect("rule-target reference must be accepted at plan time");
let now = DateTimeValue::now();
let resp = engine
.evaluate(None, "root_spec", Some(&now), IndexMap::new(), None, 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}"
);
}
#[test]
fn unary_minus_and_reciprocal_over_unbound_input_veto_with_missing_data() {
let code = r#"
spec unary
data b: number
rule negated: -b
rule inverted: 1 / b
rule negated_explained: -b
"#;
let mut engine = Engine::new();
engine
.load([(
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("unary.lemma"))),
code.to_string(),
)])
.expect("spec must load");
let now = DateTimeValue::now();
for explain in [false, true] {
let response = engine
.evaluate(None, "unary", Some(&now), IndexMap::new(), None, explain)
.expect("evaluate must not error");
for rule in ["negated", "inverted", "negated_explained"] {
let result = response.results.get(rule).expect("rule result");
assert!(
result.vetoed,
"{rule} must veto without b (explain={explain})"
);
assert_eq!(
result.missing_data(),
["b".to_string()],
"{rule} must list b as missing (explain={explain})"
);
}
}
}