use std::sync::Arc;
use serde_json::{json, Value};
use zen_engine::loader::MemoryLoader;
use zen_engine::model::DecisionContent;
use zen_engine::{DecisionEngine, EvaluationError};
use zen_expression::variable::Variable;
fn policy_content() -> DecisionContent {
policy_content_with_threshold(100)
}
fn policy_content_with_threshold(threshold: i64) -> DecisionContent {
let doc = json!({
"blocks": [
{ "id": "dm", "type": "dataModel", "props": { "data": {
"name": "platform",
"scope": "global",
"properties": [
{ "id": "g1", "name": "amount", "type": "number", "array": false, "optional": false }
]
}}},
{ "id": "assert", "type": "assertion", "props": { "data": {
"output": "approved",
"conditions": [
{ "id": "c1", "expression": format!("amount >= {threshold}"), "operator": "and", "depth": 0 }
]
}}}
]
});
serde_json::from_value(doc).unwrap()
}
fn engine_with_policy(precompile: bool) -> DecisionEngine {
let loader = MemoryLoader::default();
loader.add("policy", policy_content());
let engine = DecisionEngine::default().with_loader(Arc::new(loader));
if precompile {
engine.compile();
}
engine
}
fn input(amount: i64) -> Variable {
Variable::from(json!({ "amount": amount }))
}
fn approved(result: &Variable) -> Value {
serde_json::to_value(result).unwrap()["approved"].clone()
}
#[tokio::test]
async fn precompiled_matches_lazy() {
let lazy = engine_with_policy(false)
.evaluate("policy", input(150))
.await
.unwrap();
let eager = engine_with_policy(true)
.evaluate("policy", input(150))
.await
.unwrap();
assert_eq!(
serde_json::to_value(&lazy.result).unwrap(),
serde_json::to_value(&eager.result).unwrap()
);
assert_eq!(approved(&eager.result), json!(true));
}
#[tokio::test]
async fn compiled_resolves_cross_policy_imports() {
let loader = MemoryLoader::default();
loader.add(
"base.json",
serde_json::from_value::<DecisionContent>(json!({
"imports": [],
"blocks": [
{ "id": "dm", "type": "dataModel", "props": { "data": {
"name": "platform", "scope": "global",
"properties": [
{ "id": "g1", "name": "amount", "type": "number", "array": false, "optional": false }
]
}}},
{ "id": "qual", "type": "assertion", "props": { "data": {
"output": "qualified",
"conditions": [
{ "id": "c1", "expression": "amount >= 100", "operator": "and", "depth": 0 }
]
}}}
]
}))
.unwrap(),
);
loader.add(
"main.json",
serde_json::from_value::<DecisionContent>(json!({
"imports": ["base.json"],
"blocks": [
{ "id": "appr", "type": "assertion", "props": { "data": {
"output": "approved",
"conditions": [
{ "id": "c1", "expression": "qualified", "operator": "and", "depth": 0 }
]
}}}
]
}))
.unwrap(),
);
let engine = DecisionEngine::default().with_loader(Arc::new(loader));
engine.compile();
let result = engine.evaluate("main.json", input(150)).await.unwrap();
let json = serde_json::to_value(&result.result).unwrap();
assert_eq!(json["qualified"], serde_json::json!(true));
assert_eq!(json["approved"], serde_json::json!(true));
}
fn parse_error_policy() -> DecisionContent {
serde_json::from_value(json!({
"blocks": [
{ "id": "dm", "type": "dataModel", "props": { "data": {
"name": "platform", "scope": "global",
"properties": [
{ "id": "g1", "name": "amount", "type": "number", "array": false, "optional": false }
]
}}},
{ "id": "a", "type": "assertion", "props": { "data": {
"output": "x",
"conditions": [
{ "id": "c1", "expression": "if (( invalid )) {{", "operator": "and", "depth": 0 }
]
}}}
]
}))
.unwrap()
}
fn invalid_graph() -> DecisionContent {
serde_json::from_value(json!({
"nodes": [
{ "id": "in", "type": "inputNode", "name": "Input", "position": { "x": 0, "y": 0 } }
],
"edges": [
{ "id": "e1", "type": "edge", "sourceId": "in", "targetId": "ghost" }
]
}))
.unwrap()
}
#[test]
fn compile_reports_every_failure() {
let loader = MemoryLoader::default();
loader.add("good.json", policy_content());
loader.add("bad-1.json", parse_error_policy());
loader.add("bad-2.json", parse_error_policy());
let engine = DecisionEngine::default().with_loader(Arc::new(loader));
let failures = engine.compile();
let keys: Vec<&str> = failures.iter().map(|f| f.key.as_ref()).collect();
assert!(
keys.contains(&"bad-1.json"),
"every failure must be listed; got: {keys:?}"
);
assert!(
keys.contains(&"bad-2.json"),
"every failure must be listed; got: {keys:?}"
);
}
#[tokio::test]
async fn compile_evicts_bad_keeps_good() {
let loader = MemoryLoader::default();
loader.add("good.json", policy_content());
loader.add("bad-policy.json", parse_error_policy());
loader.add("bad-graph.json", invalid_graph());
let engine = DecisionEngine::default().with_loader(Arc::new(loader));
let failures = engine.compile();
assert_eq!(failures.len(), 2);
assert!(failures
.iter()
.any(|f| f.kind == "policy" && f.key.as_ref() == "bad-policy.json"));
assert!(failures
.iter()
.any(|f| f.kind == "graph" && f.key.as_ref() == "bad-graph.json"));
assert_eq!(engine.compile_failures().len(), 2);
let result = engine.evaluate("good.json", input(150)).await.unwrap();
assert_eq!(approved(&result.result), json!(true));
}
#[test]
fn compile_surfaces_bundle_errors() {
let loader = MemoryLoader::default();
let broken = json!({
"blocks": [
{ "id": "dm", "type": "dataModel", "props": { "data": {
"name": "platform",
"scope": "global",
"properties": [
{ "id": "g1", "name": "amount", "type": "number", "array": false, "optional": false }
]
}}},
{ "id": "assert", "type": "assertion", "props": { "data": {
"output": "approved",
"conditions": [
{ "id": "c1", "expression": "if (( invalid )) {{", "operator": "and", "depth": 0 }
]
}}}
]
});
loader.add(
"policy",
serde_json::from_value::<DecisionContent>(broken).unwrap(),
);
let engine = DecisionEngine::default().with_loader(Arc::new(loader));
assert!(!engine.compile().is_empty());
}
#[tokio::test]
async fn clone_with_loader_does_not_share_compiled_set() {
let loader_a = MemoryLoader::default();
loader_a.add("policy", policy_content_with_threshold(100));
let engine_a = DecisionEngine::default().with_loader(Arc::new(loader_a));
engine_a.compile();
let loader_b = MemoryLoader::default();
loader_b.add("policy", policy_content_with_threshold(1000));
let engine_b = engine_a.clone().with_loader(Arc::new(loader_b));
engine_b.compile();
let from_a = engine_a.evaluate("policy", input(150)).await.unwrap();
assert_eq!(approved(&from_a.result), json!(true));
let from_b = engine_b.evaluate("policy", input(150)).await.unwrap();
assert_eq!(approved(&from_b.result), json!(false));
}
#[tokio::test]
async fn compile_reports_policies_with_broken_imports() {
let loader = MemoryLoader::default();
loader.add("base.json", parse_error_policy());
loader.add(
"main.json",
serde_json::from_value::<DecisionContent>(json!({
"imports": ["base.json"],
"blocks": [
{ "id": "appr", "type": "assertion", "props": { "data": {
"output": "approved",
"conditions": [
{ "id": "c1", "expression": "amount >= 100", "operator": "and", "depth": 0 }
]
}}}
]
}))
.unwrap(),
);
let engine = DecisionEngine::default().with_loader(Arc::new(loader));
let failures = engine.compile();
assert!(failures
.iter()
.any(|f| f.kind == "policy" && f.key.as_ref() == "base.json"));
let main_failure = failures
.iter()
.find(|f| f.key.as_ref() == "main.json")
.expect("main.json must fail compilation when its import is broken");
assert_eq!(main_failure.kind, "policy");
assert!(!main_failure.diagnostics.is_empty());
let err = engine
.evaluate("main.json", input(150))
.await
.expect_err("main.json must not evaluate through a broken fast path");
assert!(
matches!(
*err,
EvaluationError::Policy(zen_engine::policy::EvaluationError::CompilationErrors { .. })
),
"expected CompilationErrors, got: {err:?}"
);
}
#[tokio::test]
async fn reload_recompiles_from_loader() {
let engine = engine_with_policy(true);
let denied = engine.evaluate("policy", input(50)).await.unwrap();
assert_eq!(approved(&denied.result), json!(false));
engine.compile();
let allowed = engine.evaluate("policy", input(150)).await.unwrap();
assert_eq!(approved(&allowed.result), json!(true));
}