use lemma::DateTimeValue;
use lemma::Engine;
use std::collections::HashMap;
fn assert_rule_vetoed(
result: Result<lemma::Response, lemma::Error>,
rule_name: &str,
reason_contains: &str,
) -> String {
let resp = result.unwrap_or_else(|err| {
panic!("run must complete with veto, not abort with Error — got: {err}")
});
let rr = resp
.results
.get(rule_name)
.unwrap_or_else(|| panic!("rule '{rule_name}' not found"));
assert!(
rr.vetoed,
"rule '{rule_name}' must veto on invalid override, got {:?}",
rr.display
);
let reason = rr.veto_reason.clone().expect("veto reason");
if !reason_contains.is_empty() {
assert!(
reason.contains(reason_contains),
"expected '{reason_contains}' in veto reason, got: {reason}"
);
}
reason
}
fn rule_value(result: &lemma::Response, name: &str) -> String {
let rr = result
.results
.get(name)
.unwrap_or_else(|| panic!("rule '{}' not found", name));
if rr.vetoed {
return format!("VETO({})", rr.veto_reason.as_deref().unwrap_or("Vetoed"));
}
rr.display.clone().expect("display")
}
#[test]
fn unknown_key_is_rejected() {
let code = r#"
spec s
data x: number
rule r: x
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
)
.unwrap();
let mut data = HashMap::new();
data.insert("x".to_string(), "1".to_string());
data.insert("does_not_exist".to_string(), "42".to_string());
let now = DateTimeValue::now();
let err = engine
.run(None, "s", Some(&now), data, false, None)
.expect_err("unknown key must fail");
let s = err.to_string();
assert!(
s.contains("does_not_exist") || s.contains("not found"),
"unknown key error must name the key, got: {s}"
);
}
#[test]
fn override_spec_reference_is_rejected() {
let code = r#"
spec inner
data x: number -> default 1
spec outer
uses i: inner
rule r: i.x
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
)
.unwrap();
let mut data = HashMap::new();
data.insert("i".to_string(), "42".to_string());
let now = DateTimeValue::now();
let err = engine
.run(None, "outer", Some(&now), data, false, None)
.expect_err("spec-ref override must fail");
let s = err.to_string();
assert!(
s.contains("spec reference") && s.contains("cannot provide"),
"override on SpecRef must have the exact error pattern, got: {s}"
);
}
#[test]
fn override_of_schema_declaration_succeeds() {
let code = r#"
spec s
data x: number
rule r: x
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
)
.unwrap();
let mut data = HashMap::new();
data.insert("x".to_string(), "42".to_string());
let now = DateTimeValue::now();
let resp = engine
.run(None, "s", Some(&now), data, false, None)
.expect("evaluates");
assert_eq!(rule_value(&resp, "r"), "42");
}
#[test]
fn override_of_literal_value_replaces() {
let code = r#"
spec s
data x: 10
rule r: x
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
)
.unwrap();
let mut data = HashMap::new();
data.insert("x".to_string(), "99".to_string());
let now = DateTimeValue::now();
let resp = engine
.run(None, "s", Some(&now), data, false, None)
.expect("evaluates");
assert_eq!(rule_value(&resp, "r"), "99");
}
#[test]
fn override_wrong_primitive_kind_fails_with_related_data() {
let code = r#"
spec s
data age: number
rule r: age
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
)
.unwrap();
let mut data = HashMap::new();
data.insert("age".to_string(), "thirty".to_string());
let now = DateTimeValue::now();
assert_rule_vetoed(
engine.run(None, "s", Some(&now), data, false, None),
"r",
"number",
);
}
#[test]
fn override_violating_minimum_fails() {
let code = r#"
spec s
data n: number -> minimum 10
rule r: n
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
)
.unwrap();
let mut data = HashMap::new();
data.insert("n".to_string(), "5".to_string());
let now = DateTimeValue::now();
assert_rule_vetoed(
engine.run(None, "s", Some(&now), data, false, None),
"r",
"minimum",
);
}
#[test]
fn override_violating_maximum_fails() {
let code = r#"
spec s
data n: number -> maximum 5
rule r: n
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
)
.unwrap();
let mut data = HashMap::new();
data.insert("n".to_string(), "10".to_string());
let now = DateTimeValue::now();
assert_rule_vetoed(
engine.run(None, "s", Some(&now), data, false, None),
"r",
"maximum",
);
}
#[test]
fn override_violating_length_fails() {
let code = r#"
spec s
data msg: text -> length 3
rule r: msg
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
)
.unwrap();
let mut data = HashMap::new();
data.insert("msg".to_string(), "way too long".to_string());
let now = DateTimeValue::now();
assert_rule_vetoed(
engine.run(None, "s", Some(&now), data, false, None),
"r",
"length",
);
}
#[test]
fn override_violating_options_fails() {
let code = r#"
spec s
data color: text -> options red green blue
rule r: color
"#;
let mut engine = Engine::new();
let load_result = engine.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
);
if let Err(errors) = &load_result {
panic!(
"`text -> options ...` must be supported or rejected with a clear error at load; \
got load errors: {}",
errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n")
);
}
let mut data = HashMap::new();
data.insert("color".to_string(), "purple".to_string());
let now = DateTimeValue::now();
assert_rule_vetoed(
engine.run(None, "s", Some(&now), data, false, None),
"r",
"option",
);
}
#[test]
fn empty_override_map_is_noop() {
let code = r#"
spec s
data x: 10
rule r: x
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
)
.unwrap();
let now = DateTimeValue::now();
let resp = engine
.run(None, "s", Some(&now), HashMap::new(), false, None)
.expect("evaluates");
assert_eq!(rule_value(&resp, "r"), "10");
}
#[test]
fn override_on_reference_replaces_and_wins_over_target() {
let code = r#"
spec inner
data v: number -> default 1
spec outer
uses i: inner
with i.v: 42
rule r: i.v
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
)
.unwrap();
let mut data = HashMap::new();
data.insert("i.v".to_string(), "500".to_string());
let now = DateTimeValue::now();
let resp = engine
.run(None, "outer", Some(&now), data, false, None)
.expect("evaluates");
assert_eq!(rule_value(&resp, "r"), "500");
}
#[test]
fn override_on_reference_still_validates_against_merged_type() {
let code = r#"
spec inner
data n: number -> maximum 5
data v: number
spec outer
uses i: inner
with i.n: i.v
rule r: i.n
"#;
let mut engine = Engine::new();
let load_result = engine.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from("w.lemma"))),
);
load_result.expect("binding with i.n must plan");
let mut data = HashMap::new();
data.insert("i.n".to_string(), "10".to_string());
let now = DateTimeValue::now();
assert_rule_vetoed(
engine.run(None, "outer", Some(&now), data, false, None),
"r",
"maximum",
);
}