use lemma::parsing::ast::DateTimeValue;
use lemma::{Engine, Error};
use std::collections::HashMap;
#[test]
fn test_duplicate_fact_definition_error() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec test
fact salary: 50000
fact salary: 60000
"#,
lemma::SourceType::Labeled("test.lemma"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
let msg = &details.message;
assert!(
msg.to_lowercase().contains("duplicate") && msg.to_lowercase().contains("fact"),
"Error should mention duplicate fact, got: {msg}"
);
assert!(
msg.contains("salary"),
"Error should mention fact name, got: {msg}"
);
}
#[test]
fn test_duplicate_rule_definition_error() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec test
fact x: 10
rule total: x * 2
rule total: x * 3
"#,
lemma::SourceType::Labeled("test.lemma"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
let msg = &details.message;
assert!(
msg.to_lowercase().contains("duplicate") && msg.to_lowercase().contains("rule"),
"Error should mention duplicate rule, got: {msg}"
);
assert!(
msg.contains("total"),
"Error should mention rule name, got: {msg}"
);
}
#[test]
fn test_duplicate_fact_shows_name() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec test
fact name: "Alice"
fact age: 30
fact name: "Bob"
"#,
lemma::SourceType::Labeled("test.lemma"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
let msg = &details.message;
assert!(
msg.contains("Duplicate"),
"Error should mention duplicate, got: {msg}"
);
assert!(
msg.contains("name"),
"Error should mention fact name, got: {msg}"
);
}
#[test]
fn test_runtime_error_division_by_zero() {
let mut engine = Engine::new();
engine
.load(
r#"
spec test
fact numerator: 100
fact denominator: 0
rule result: numerator / denominator
"#,
lemma::SourceType::Labeled("test.lemma"),
)
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run("test", Some(&now), HashMap::new(), false)
.expect("Division by zero should return Veto, not Error");
let result_rule = response
.results
.values()
.find(|r| r.rule.name == "result")
.expect("result rule should exist");
assert!(
result_rule.result.vetoed(),
"Division by zero should return Veto, got: {:?}",
result_rule.result
);
if let lemma::OperationResult::Veto(Some(msg)) = &result_rule.result {
assert!(
msg.to_lowercase().contains("division") || msg.to_lowercase().contains("zero"),
"Veto message should mention division or zero, got: {}",
msg
);
}
}
#[test]
fn test_runtime_error_division_by_zero_with_cli_facts() {
let mut engine = Engine::new();
engine
.load(
r#"
spec test
fact hours_worked: [number]
fact salary: 50000
rule hourly_rate: salary / hours_worked
"#,
lemma::SourceType::Labeled("test.lemma"),
)
.unwrap();
let mut facts = std::collections::HashMap::new();
facts.insert("hours_worked".to_string(), "0".to_string());
let now = DateTimeValue::now();
let response = engine
.run("test", Some(&now), facts, false)
.expect("Division by zero should return Veto, not Error");
let hourly_rate = response
.results
.values()
.find(|r| r.rule.name == "hourly_rate")
.expect("hourly_rate rule should exist");
assert!(
hourly_rate.result.vetoed(),
"Division by zero should return Veto, got: {:?}",
hourly_rate.result
);
}
#[test]
fn test_transpile_error_self_referencing_rule() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec test
rule x: x + 1
"#,
lemma::SourceType::Labeled("test.lemma"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
let msg = &details.message;
assert!(msg.to_lowercase().contains("circular") || msg.to_lowercase().contains("itself"));
assert!(msg.contains("x"));
}
#[test]
fn test_validation_error_type_mismatch_text_in_arithmetic() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec test
fact name: "Alice"
fact salary: 50000
rule result: salary + name
"#,
lemma::SourceType::Labeled("test.lemma"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
assert!(details.message.contains("Cannot apply"));
}
#[test]
fn test_validation_error_boolean_in_arithmetic() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec test
fact is_active: true
fact count: 10
rule result: count * is_active
"#,
lemma::SourceType::Labeled("test.lemma"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
assert!(details.message.contains("Cannot apply"));
}
#[test]
fn test_duplicate_error_contains_fact_name() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec my_spec
fact price: 100
fact price: 200
"#,
lemma::SourceType::Labeled("my_file.lemma"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
assert!(details.message.contains("Duplicate"));
assert!(details.message.contains("price"));
}
#[test]
fn test_duplicate_error_is_reported() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec test
fact x: 10
fact x: 20
"#,
lemma::SourceType::Labeled("test.lemma"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
assert!(details.message.contains("Duplicate"));
assert!(details.message.contains("x"));
}
#[test]
fn test_duplicate_in_second_spec_is_caught() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec first_spec
fact a: 1
spec second_spec
fact b: 2
fact b: 3
"#,
lemma::SourceType::Labeled("multi.lemma"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
assert!(details.message.contains("Duplicate"));
assert!(details.message.contains("b"));
}
#[test]
fn test_error_display_contains_duplicate_info() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec test
fact value: 100
fact value: 200
"#,
lemma::SourceType::Labeled("test.lemma"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
assert!(details.message.contains("Duplicate"));
assert!(details.message.contains("value"));
}
#[test]
fn test_division_by_zero_returns_veto_with_message() {
let mut engine = Engine::new();
engine
.load(
r#"
spec test
fact x: 100
fact y: 0
rule result: x / y
"#,
lemma::SourceType::Labeled("test.lemma"),
)
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run("test", Some(&now), HashMap::new(), false)
.expect("Should return Veto, not Error");
let result_rule = response
.results
.values()
.find(|r| r.rule.name == "result")
.expect("result rule should exist");
match &result_rule.result {
lemma::OperationResult::Veto(Some(msg)) => {
assert!(
msg.to_lowercase().contains("zero") || msg.to_lowercase().contains("division"),
"Veto message should mention zero or division, got: {}",
msg
);
}
lemma::OperationResult::Veto(None) => {
panic!("Expected Veto with message");
}
other => panic!("Expected Veto, got: {:?}", other),
}
}
#[test]
fn test_circular_dependency_has_helpful_suggestion() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec test
rule x: y
rule y: x
"#,
lemma::SourceType::Labeled("test.lemma"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
let msg = &details.message;
assert!(msg.to_lowercase().contains("circular") || msg.to_lowercase().contains("cycle"));
assert!(msg.contains("x") && msg.contains("y"));
}
#[test]
fn test_duplicate_fact_is_detected() {
let mut engine = Engine::new();
let lemma_code = r#"spec test
fact line2: 1
fact line3: 2
fact line4: 3
fact line4: 4"#;
let result = engine.load(lemma_code, lemma::SourceType::Labeled("test.lemma"));
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
assert!(details.message.contains("Duplicate"));
assert!(details.message.contains("line4"));
}
#[test]
fn test_division_by_zero_returns_veto() {
let mut engine = Engine::new();
engine
.load(
r#"
spec test
fact numerator: 42
fact denominator: 0
rule division_result: numerator / denominator
"#,
lemma::SourceType::Labeled("test.lemma"),
)
.unwrap();
let now = DateTimeValue::now();
let response = engine
.run("test", Some(&now), HashMap::new(), false)
.expect("Should return Veto, not Error");
let division_result = response
.results
.values()
.find(|r| r.rule.name == "division_result")
.expect("division_result rule should exist");
assert!(
division_result.result.vetoed(),
"Division by zero should return Veto, got: {:?}",
division_result.result
);
}
#[test]
fn test_duplicate_detected_from_database_source() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec contract
fact amount: 1000
fact amount: 2000
"#,
lemma::SourceType::Labeled("db://contracts/123"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
assert!(details.message.contains("Duplicate"));
assert!(details.message.contains("amount"));
}
#[test]
fn test_duplicate_detected_from_api_source() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec policy
rule rate: 1.5
rule rate: 2.0
"#,
lemma::SourceType::Labeled("api://policies/endpoint"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
assert!(details.message.contains("Duplicate"));
assert!(details.message.contains("rate"));
}
#[test]
fn test_duplicate_detected_from_runtime_source() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec runtime_spec
fact x: 5
fact x: 10
"#,
lemma::SourceType::Labeled("<runtime>"),
);
let errs = result.unwrap_err();
let details = errs
.iter()
.find_map(|e| match e {
Error::Validation(d) => Some(d),
_ => None,
})
.expect("expected at least one Validation error");
assert!(details.message.contains("Duplicate"));
assert!(details.message.contains("x"));
}
#[test]
fn test_multiple_error_phases_reported_together() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec pricing
type money: scale
-> unit eur 1
-> unit usd 1.19
fact price : [money]
fact quantity : [number -> minimum 0]
fact is_member: false
rule discount: 0%
unless quantity >= 10 then 10%
unless quantity >= 50 then 20%
unless is_member then 15
rule total: price * quantity - non_existent_rule
unless price > 100 usd then veto "This price is too high."
"#,
lemma::SourceType::Labeled("pricing.lemma"),
);
let errs = result.unwrap_err();
let messages: Vec<String> = errs.iter().map(|e| e.to_string()).collect();
let has_rule_ref_error = messages
.iter()
.any(|m| m.contains("non_existent_rule") && m.contains("not found"));
let has_type_mismatch = messages
.iter()
.any(|m| m.contains("Type mismatch") || m.contains("type mismatch"));
assert!(
has_rule_ref_error,
"Should report missing reference. Got: {messages:?}"
);
assert!(
has_type_mismatch,
"Should report type mismatch (15 is number, not ratio). Got: {messages:?}"
);
}