Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions bindings/nodejs/test/decision.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,37 @@ const loader = async (key: string) => fs.readFile(path.join(testDataRoot, key))

jest.useRealTimers();

interface PropertyMatcher {
[key: string]: any;
}

const defaultMatchers: PropertyMatcher = {
timestamp: expect.any(Number),
estimatedArrival: expect.any(Number),
approvalDate: expect.any(Number),
};

function addJestMatchers(obj: any, matchers: PropertyMatcher = defaultMatchers): any {
if (obj === null || typeof obj !== 'object') {
return obj;
}

if (Array.isArray(obj)) {
return obj.map((item: any) => addJestMatchers(item, matchers));
}

const result: Record<string, any> = {};
for (const [key, value] of Object.entries(obj)) {
if (matchers[key]) {
result[key] = matchers[key];
} else {
result[key] = addJestMatchers(value, matchers);
}
}

return result;
}

describe('ZenEngine', () => {
it('Evaluates decisions using loader', async () => {
const engine = new ZenEngine({
Expand Down Expand Up @@ -128,9 +159,10 @@ describe('ZenEngine', () => {

assert.ok(engineResponse.success, 'Engine response must be ok');
assert.ok(decisionResponse.success, 'Decision response must be ok');
const expectedObject = addJestMatchers(testCase.output);

expect(engineResponse.data.result).toMatchObject(testCase.output);
expect(decisionResponse.data.result).toMatchObject(testCase.output);
expect(engineResponse.data.result).toMatchObject(expectedObject);
expect(decisionResponse.data.result).toMatchObject(expectedObject);
}
}

Expand Down
3 changes: 3 additions & 0 deletions core/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ zen-expression = { path = "../expression", version = "0.49.1" }
zen-tmpl = { path = "../template", version = "0.49.1" }

[dev-dependencies]
chrono = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
criterion = { workspace = true, features = ["async_tokio"] }
insta = { version = "1.43", features = ["yaml", "redactions"] }
zen-expression = { path = "../expression", version = "0.49.1", features = ["time-override"] }

[[bench]]
harness = false
Expand Down
4 changes: 2 additions & 2 deletions core/engine/src/handler/expression/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub struct ExpressionHandler {

#[derive(Debug, Serialize)]
struct ExpressionTrace {
result: String,
result: serde_json::Value,
}

impl ExpressionHandler {
Expand Down Expand Up @@ -82,7 +82,7 @@ impl<'a> ExpressionHandlerInner<'a> {
tmap.insert(
&expression.key,
ExpressionTrace {
result: serde_json::to_string(&value).unwrap_or("Error".to_owned()),
result: value.to_value(),
},
);
}
Expand Down
68 changes: 68 additions & 0 deletions core/engine/tests/engine.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::support::{create_fs_loader, load_raw_test_data, load_test_data, test_data_root};
use chrono::{TimeZone, Utc};
use serde::Deserialize;
use serde_json::json;
use std::fs;
Expand All @@ -11,6 +12,7 @@ use zen_engine::loader::{LoaderError, MemoryLoader};
use zen_engine::model::{DecisionContent, DecisionNode, DecisionNodeKind, FunctionNodeContent};
use zen_engine::Variable;
use zen_engine::{DecisionEngine, EvaluationError, EvaluationOptions};
use zen_expression::vm::UTC_OVERRIDE;

mod support;

Expand Down Expand Up @@ -223,6 +225,8 @@ async fn engine_switch_node() {
#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn engine_graph_tests() {
mock_datetime();

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct TestCase {
Expand Down Expand Up @@ -264,6 +268,70 @@ async fn engine_graph_tests() {
}
}

fn mock_datetime() {
*UTC_OVERRIDE.write().unwrap() = Some("2025-08-19T16:55:02.078Z".parse().unwrap());
}

#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn engine_snapshot_tests() {
mock_datetime();

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct TestCase {
input: Variable,
output: Variable,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct TestData {
tests: Vec<TestCase>,
#[serde(flatten)]
decision_content: DecisionContent,
}

let engine = DecisionEngine::default();

let graphs_path = Path::new(test_data_root().as_str()).join("graphs");
let file_list = fs::read_dir(graphs_path).unwrap();
for maybe_file in file_list {
let Ok(file) = maybe_file else {
panic!("Failed to read DirEntry {maybe_file:?}");
};

let file_name = file.file_name().to_str().map(|s| s.to_string()).unwrap();
let file_name = if let Some(pos) = file_name.rfind('.') {
file_name[..pos].to_string()
} else {
file_name
};
let file_contents = fs::read_to_string(file.path()).expect("valid file data");
let test_data: TestData = serde_json::from_str(&file_contents).expect("Valid JSON");

let decision = engine.create_decision(test_data.decision_content.into());
for (index, test_case) in test_data.tests.iter().enumerate() {
let input = test_case.input.clone();
let result = decision
.evaluate_with_opts(
input.clone(),
EvaluationOptions {
trace: Some(true),
max_depth: None,
},
)
.await
.unwrap();
let serialized_result = serde_json::to_value(&result).unwrap();
insta::assert_yaml_snapshot!(format!("{}_{}", file_name, index), serialized_result, {
".performance" => "[perf]",
".trace.*.performance" => "[perf]"
});
}
}
}

#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn engine_function_v2() {
Expand Down
1 change: 1 addition & 0 deletions core/engine/tests/snapshots/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.new
221 changes: 221 additions & 0 deletions core/engine/tests/snapshots/engine__account-dormancy-management_0.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
---
source: core/engine/tests/engine.rs
expression: serialized_result
---
performance: "[perf]"
result:
actionPriority: high
recommendedAction: fee_waiver
trace:
dt1:
id: dt1
input:
accountBalance:
"$serde_json::private::Number": "25750.45"
accountId: ACC98765432
accountType: savings
contactPreference: email
currency: USD
customerEmail: customer@example.com
customerPhone: "+15551234567"
customerTier: premium
daysSinceLastActivity:
"$serde_json::private::Number": "23993702"
daysToThreshold:
"$serde_json::private::Number": "-23993522"
dormancyThreshold:
"$serde_json::private::Number": "180"
lastActivityDate: 2024-11-15
region: NORTH_AMERICA
regulatoryJurisdiction: US-NY
name: dormancy_status
order:
"$serde_json::private::Number": "2"
output:
accountBalance:
"$serde_json::private::Number": "25750.45"
accountId: ACC98765432
accountType: savings
contactPreference: email
currency: USD
customerEmail: customer@example.com
customerPhone: "+15551234567"
customerTier: premium
daysSinceLastActivity:
"$serde_json::private::Number": "23993702"
daysToThreshold:
"$serde_json::private::Number": "-23993522"
dormancyStatus: dormant
dormancyThreshold:
"$serde_json::private::Number": "180"
lastActivityDate: 2024-11-15
region: NORTH_AMERICA
regulatoryJurisdiction: US-NY
performance: "[perf]"
traceData:
index:
"$serde_json::private::Number": "0"
reference_map:
daysToThreshold:
"$serde_json::private::Number": "-23993522"
rule:
_id: r1-1
"daysToThreshold[i1-1]": "< 0"
dt2:
id: dt2
input:
accountBalance:
"$serde_json::private::Number": "25750.45"
accountId: ACC98765432
accountType: savings
contactPreference: email
currency: USD
customerEmail: customer@example.com
customerPhone: "+15551234567"
customerTier: premium
daysSinceLastActivity:
"$serde_json::private::Number": "23993702"
daysToThreshold:
"$serde_json::private::Number": "-23993522"
dormancyStatus: dormant
dormancyThreshold:
"$serde_json::private::Number": "180"
lastActivityDate: 2024-11-15
region: NORTH_AMERICA
regulatoryJurisdiction: US-NY
name: determine_action
order:
"$serde_json::private::Number": "4"
output:
actionPriority: high
recommendedAction: fee_waiver
performance: "[perf]"
traceData:
index:
"$serde_json::private::Number": "0"
reference_map:
customerTier: premium
dormancyStatus: dormant
rule:
_id: r2-1
"customerTier[i2-2]": "'premium'"
"dormancyStatus[i2-1]": "'dormant'"
ex1:
id: ex1
input:
accountBalance:
"$serde_json::private::Number": "25750.45"
accountId: ACC98765432
accountType: savings
contactPreference: email
currency: USD
customerEmail: customer@example.com
customerPhone: "+15551234567"
customerTier: premium
dormancyThreshold:
"$serde_json::private::Number": "180"
lastActivityDate: 2024-11-15
region: NORTH_AMERICA
regulatoryJurisdiction: US-NY
name: calculate_dormancy
order:
"$serde_json::private::Number": "1"
output:
accountBalance:
"$serde_json::private::Number": "25750.45"
accountId: ACC98765432
accountType: savings
contactPreference: email
currency: USD
customerEmail: customer@example.com
customerPhone: "+15551234567"
customerTier: premium
daysSinceLastActivity:
"$serde_json::private::Number": "23993702"
daysToThreshold:
"$serde_json::private::Number": "-23993522"
dormancyThreshold:
"$serde_json::private::Number": "180"
lastActivityDate: 2024-11-15
region: NORTH_AMERICA
regulatoryJurisdiction: US-NY
performance: "[perf]"
traceData:
daysSinceLastActivity:
result:
"$serde_json::private::Number": "23993702"
daysToThreshold:
result:
"$serde_json::private::Number": "-23993522"
ip1:
id: ip1
input: ~
name: request
order:
"$serde_json::private::Number": "0"
output:
accountBalance:
"$serde_json::private::Number": "25750.45"
accountId: ACC98765432
accountType: savings
contactPreference: email
currency: USD
customerEmail: customer@example.com
customerPhone: "+15551234567"
customerTier: premium
dormancyThreshold:
"$serde_json::private::Number": "180"
lastActivityDate: 2024-11-15
region: NORTH_AMERICA
regulatoryJurisdiction: US-NY
performance: "[perf]"
traceData: ~
sw1:
id: sw1
input:
accountBalance:
"$serde_json::private::Number": "25750.45"
accountId: ACC98765432
accountType: savings
contactPreference: email
currency: USD
customerEmail: customer@example.com
customerPhone: "+15551234567"
customerTier: premium
daysSinceLastActivity:
"$serde_json::private::Number": "23993702"
daysToThreshold:
"$serde_json::private::Number": "-23993522"
dormancyStatus: dormant
dormancyThreshold:
"$serde_json::private::Number": "180"
lastActivityDate: 2024-11-15
region: NORTH_AMERICA
regulatoryJurisdiction: US-NY
name: route_by_status
order:
"$serde_json::private::Number": "3"
output:
accountBalance:
"$serde_json::private::Number": "25750.45"
accountId: ACC98765432
accountType: savings
contactPreference: email
currency: USD
customerEmail: customer@example.com
customerPhone: "+15551234567"
customerTier: premium
daysSinceLastActivity:
"$serde_json::private::Number": "23993702"
daysToThreshold:
"$serde_json::private::Number": "-23993522"
dormancyStatus: dormant
dormancyThreshold:
"$serde_json::private::Number": "180"
lastActivityDate: 2024-11-15
region: NORTH_AMERICA
regulatoryJurisdiction: US-NY
performance: "[perf]"
traceData:
statements:
- id: s1-1
Loading
Loading