use lemma::{DateTimeValue, Engine, SourceType};
use std::collections::HashMap;
fn date(year: i32, month: u32, day: u32) -> DateTimeValue {
DateTimeValue {
year,
month,
day,
hour: 0,
minute: 0,
second: 0,
microsecond: 0,
timezone: None,
}
}
fn eval(engine: &Engine, spec_name: &str, effective: &DateTimeValue) -> lemma::Response {
engine
.run(spec_name, Some(effective), HashMap::new(), false)
.unwrap()
}
fn assert_rule_value(response: &lemma::Response, rule: &str, expected: &str) {
let result = response
.results
.get(rule)
.unwrap_or_else(|| panic!("rule '{}' not in results", rule));
let val = result
.result
.value()
.unwrap_or_else(|| panic!("rule '{}' is Veto, expected Value", rule));
assert_eq!(
val.to_string(),
expected,
"rule '{}': expected {}, got {}",
rule,
expected,
val
);
}
#[test]
fn unqualified_dep_must_cover_consumer_temporal_range_gap_errors() {
let mut engine = Engine::new();
let err = engine
.load(
r#"
spec consumer 2025-01-01
with d: dep
rule out: d.v
spec dep 2025-08-01
rule v: 42
"#,
SourceType::Labeled("t.lemma"),
)
.expect_err("unqualified dep with coverage gap must fail planning");
let joined = err
.errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(" ");
assert!(
joined.contains("consumer") && joined.contains("dep"),
"expected coverage error naming specs; got: {joined}"
);
assert!(
joined.contains("no version") || joined.contains("active"),
"expected temporal coverage wording; got: {joined}"
);
}
#[test]
fn qualified_dep_allows_consumer_starting_before_dep_exists() {
let mut engine = Engine::new();
engine
.load(
r#"
spec consumer 2025-01-01
with d: dep 2025-08-01
rule out: d.v
spec dep 2025-08-01
rule v: 42
"#,
SourceType::Labeled("t.lemma"),
)
.expect("qualified dep at T should not require dep to cover entire consumer range");
assert_rule_value(&eval(&engine, "consumer", &date(2025, 3, 1)), "out", "42");
}
#[test]
fn qualified_dep_nested_unqualified_child_resolves_at_qualifier_instant() {
let mut engine = Engine::new();
engine
.load(
r#"
spec consumer 2025-01-01
with d: dep 2025-10-01
rule out: d.nested_val
spec dep 2025-01-01
with nested: child
rule nested_val: nested.x
spec dep 2025-12-01
with nested: child
rule nested_val: nested.x
spec child 2025-01-01
rule x: 1
spec child 2025-06-01
rule x: 2
"#,
SourceType::Labeled("t.lemma"),
)
.unwrap();
assert_rule_value(&eval(&engine, "consumer", &date(2025, 3, 1)), "out", "2");
}
#[test]
fn qualified_dep_nested_resolution_independent_of_run_effective() {
let mut engine = Engine::new();
engine
.load(
r#"
spec consumer 2025-01-01
with d: dep 2025-10-01
rule out: d.nested_val
spec dep 2025-01-01
with nested: child
rule nested_val: nested.x
spec dep 2025-12-01
with nested: child
rule nested_val: nested.x
spec child 2025-01-01
rule x: 1
spec child 2025-06-01
rule x: 2
"#,
SourceType::Labeled("t.lemma"),
)
.unwrap();
assert_rule_value(&eval(&engine, "consumer", &date(2025, 11, 1)), "out", "2");
}
#[test]
fn qualified_only_dep_reference_does_not_split_consumer_temporal_slices() {
let mut engine = Engine::new();
engine
.load(
r#"
spec consumer 2025-01-01
with d: dep 2025-06-15
rule out: d.v
spec dep 2025-03-01
rule v: 10
spec dep 2025-09-01
rule v: 20
"#,
SourceType::Labeled("t.lemma"),
)
.unwrap();
assert_rule_value(&eval(&engine, "consumer", &date(2025, 2, 1)), "out", "10");
}
#[test]
fn mixed_unqualified_and_qualified_deps_slice_count_from_unqualified_only() {
let mut engine = Engine::new();
engine
.load(
r#"
spec consumer 2025-01-01
with a: dep_a
with b: dep_b 2025-04-01
rule out_a: a.v
rule out_b: b.v
spec dep_a 2025-01-01
rule v: 1
spec dep_a 2025-06-01
rule v: 2
spec dep_b 2025-01-01
rule v: 10
spec dep_b 2025-08-01
rule v: 20
"#,
SourceType::Labeled("t.lemma"),
)
.unwrap();
let r1 = eval(&engine, "consumer", &date(2025, 3, 1));
assert_rule_value(&r1, "out_a", "1");
assert_rule_value(&r1, "out_b", "10");
let r2 = eval(&engine, "consumer", &date(2025, 9, 1));
assert_rule_value(&r2, "out_a", "2");
assert_rule_value(&r2, "out_b", "10");
}
#[test]
fn qualified_dep_type_import_from_child_uses_qualifier_not_slice_start() {
let mut engine = Engine::new();
engine
.load(
r#"
spec consumer 2025-01-01
with d: dep 2025-10-01
rule out: d.doubled
spec dep 2025-01-01
data money from child 2025-06-01
data p: 5 usd
rule doubled: p * 2
spec child 2025-01-01
data money: scale
-> unit eur 1.00
-> decimals 2
spec child 2025-06-01
data money: scale
-> unit eur 1.00
-> unit usd 1.10
-> decimals 2
"#,
SourceType::Labeled("t.lemma"),
)
.expect("type import `from child` under qualified dep must resolve child at qualifier instant so `usd` exists");
assert_rule_value(
&eval(&engine, "consumer", &date(2025, 3, 1)),
"out",
"10.00 usd",
);
}