behavior-contracts 0.1.2

Language-neutral IR runtime core (expression evaluation, template rendering, execution plan, canonical serialization) shared across DSL implementations. Passes the dsl-contracts conformance vectors byte-for-byte.
Documentation
//! Unit tests over the public COMMON API + a conformance-runner smoke test.

use behavior_contracts::{
    assert_portable, canonical_json, canonical_value, decode_value, deep_equals,
    evaluate_expression, py_float_repr, render_template, run_plan, validate_envelope, ExecOutcome,
    OpSpec, Value,
};
use serde_json::json;

fn scope(pairs: &[(&str, Value)]) -> Vec<(String, Value)> {
    pairs
        .iter()
        .map(|(k, v)| (k.to_string(), v.clone()))
        .collect()
}

// ── expression ───────────────────────────────────────────────────────────────
#[test]
fn expr_int_add_checked() {
    let r = evaluate_expression(&json!({"add": [1, 2]}), &[]).unwrap();
    assert!(matches!(r, Value::Int(3)));
}

#[test]
fn expr_i64_overflow_is_failure_not_panic() {
    // i64::MAX + 1 via {int:"…"} literals
    let e = evaluate_expression(
        &json!({"add": [{"int": "9223372036854775807"}, {"int": "1"}]}),
        &[],
    )
    .unwrap_err();
    assert_eq!(e.code.as_str(), "INT_OVERFLOW");
}

#[test]
fn expr_int_literal_beyond_i64_is_failure() {
    let e = evaluate_expression(&json!({"int": "99999999999999999999999"}), &[]).unwrap_err();
    assert_eq!(e.code.as_str(), "INT_OVERFLOW");
}

#[test]
fn expr_mod_sign_follows_dividend() {
    // -7 % 2 == -1 (truncated). Confirms Rust `%` semantics match the spec.
    let r = evaluate_expression(&json!({"mod": [{"int": "-7"}, {"int": "2"}]}), &[]).unwrap();
    assert!(matches!(r, Value::Int(-1)), "got {r:?}");
    let r2 = evaluate_expression(&json!({"mod": [{"int": "7"}, {"int": "-2"}]}), &[]).unwrap();
    assert!(matches!(r2, Value::Int(1)), "got {r2:?}");
}

#[test]
fn expr_div_always_float() {
    let r = evaluate_expression(&json!({"div": [6, 2]}), &[]).unwrap();
    match r {
        Value::Float(f) => assert_eq!(f, 3.0),
        other => panic!("expected float, got {other:?}"),
    }
}

#[test]
fn expr_div_by_zero_is_nan_or_inf() {
    let e = evaluate_expression(&json!({"div": [1, 0]}), &[]).unwrap_err();
    assert_eq!(e.code.as_str(), "NAN_OR_INF");
}

#[test]
fn expr_precision_loss_widening() {
    // |int| > 2^53 widened to float in div → PRECISION_LOSS
    let e =
        evaluate_expression(&json!({"div": [{"int": "9007199254740993"}, 1]}), &[]).unwrap_err();
    assert_eq!(e.code.as_str(), "PRECISION_LOSS");
}

#[test]
fn expr_string_compare_code_point_order() {
    // 'Z'(0x5A) < 'a'(0x61)
    let r = evaluate_expression(&json!({"lt": ["Z", "a"]}), &[]).unwrap();
    assert!(matches!(r, Value::Bool(true)));
}

#[test]
fn expr_ref_and_short_circuit() {
    let s = scope(&[("x", Value::Obj(vec![("n".into(), Value::Int(5))]))]);
    let r = evaluate_expression(&json!({"ref": ["x", "n"]}), &s).unwrap();
    assert!(matches!(r, Value::Int(5)));
    // and short-circuits: right side would be UNKNOWN_BINDING but is not evaluated
    let r2 = evaluate_expression(&json!({"and": [false, {"ref": ["nope"]}]}), &[]).unwrap();
    assert!(matches!(r2, Value::Bool(false)));
}

#[test]
fn expr_unknown_op_fail_closed() {
    let e = evaluate_expression(&json!({"frobnicate": [1]}), &[]).unwrap_err();
    assert_eq!(e.code.as_str(), "UNKNOWN_OP");
}

// ── template ─────────────────────────────────────────────────────────────────
#[test]
fn template_strict_and_stringify() {
    let p = scope(&[("n", Value::Int(42)), ("flag", Value::Bool(true))]);
    assert_eq!(render_template("N#{n}", &p).unwrap(), "N#42");
    assert_eq!(render_template("F#{flag}", &p).unwrap(), "F#True");
}

#[test]
fn template_missing_key_is_failure_not_empty() {
    let e = render_template("USER#{userId}", &[]).unwrap_err();
    assert_eq!(e.code.as_str(), "UNBOUND_PARAM");
}

#[test]
fn template_null_param_is_failure() {
    let p = scope(&[("userId", Value::Null)]);
    let e = render_template("USER#{userId}", &p).unwrap_err();
    assert_eq!(e.code.as_str(), "UNBOUND_PARAM");
}

#[test]
fn template_empty_braces_are_literal() {
    assert_eq!(render_template("a{}b", &[]).unwrap(), "a{}b");
}

// ── canonical ────────────────────────────────────────────────────────────────
#[test]
fn canonical_top_level_sort_only() {
    let v = decode_value(&json!({"b": {"y": "1", "x": "2"}, "a": "0"})).unwrap();
    assert_eq!(
        canonical_value(&v).unwrap(),
        "{\"a\":\"0\",\"b\":{\"y\":\"1\",\"x\":\"2\"}}"
    );
}

#[test]
fn canonical_json_all_levels_sort() {
    let v = decode_value(&json!({"outer": {"y": "1", "x": "2"}})).unwrap();
    assert_eq!(
        canonical_json(&v).unwrap(),
        "{\"outer\":{\"x\":\"2\",\"y\":\"1\"}}"
    );
}

#[test]
fn canonical_astral_key_sort_code_point() {
    // U+E000 () must sort before U+10000 (𐀀) by code point.
    let v = decode_value(&json!({"\u{10000}": "astral", "\u{E000}": "bmp"})).unwrap();
    assert_eq!(
        canonical_value(&v).unwrap(),
        "{\"\u{E000}\":\"bmp\",\"\u{10000}\":\"astral\"}"
    );
}

#[test]
fn canonical_int_float_distinct() {
    assert_eq!(canonical_json(&Value::Int(1)).unwrap(), "1");
    assert_eq!(canonical_json(&Value::Float(1.0)).unwrap(), "1.0");
}

#[test]
fn canonical_nan_inf_failure() {
    assert_eq!(
        py_float_repr(f64::NAN).unwrap_err().code.as_str(),
        "NAN_OR_INF"
    );
    assert_eq!(
        py_float_repr(f64::INFINITY).unwrap_err().code.as_str(),
        "NAN_OR_INF"
    );
}

// ── py_float_repr edge cases (pinned to CPython repr) ─────────────────────────
#[test]
fn py_float_repr_matches_cpython() {
    let cases: &[(f64, &str)] = &[
        (0.1, "0.1"),
        (1e16, "1e+16"),
        (-0.0, "-0.0"),
        (0.0, "0.0"),
        (9999000000000000.0, "9999000000000000.0"),
        (5e-324, "5e-324"),
        (1.7976931348623157e308, "1.7976931348623157e+308"),
        (1.0, "1.0"),
        (1e-5, "1e-05"),
        (0.0001, "0.0001"),
        (3.5048, "3.5048"),
        (1.25e30, "1.25e+30"),
    ];
    for (f, want) in cases {
        assert_eq!(py_float_repr(*f).unwrap(), *want, "py_float_repr({f})");
    }
}

// ── plan ─────────────────────────────────────────────────────────────────────
#[test]
fn plan_null_binding_skip_propagates() {
    let ops = vec![
        OpSpec {
            id: "root".into(),
            parent: None,
            bind_field: None,
            relation_kind: None,
            policy: None,
        },
        OpSpec {
            id: "child".into(),
            parent: Some(0),
            bind_field: Some("next".into()),
            relation_kind: None,
            policy: None,
        },
    ];
    // root returns an object with no "next" field → child skipped
    let res = run_plan(None, &ops, |op, _| {
        if op.id == "root" {
            ExecOutcome::Ok(Value::Obj(vec![("x".into(), Value::Int(1))]))
        } else {
            ExecOutcome::Ok(Value::Null)
        }
    })
    .unwrap();
    assert_eq!(res.executed, vec!["root".to_string()]);
    assert_eq!(res.skipped, vec!["child".to_string()]);
}

#[test]
fn plan_fail_policy_raises() {
    let ops = vec![OpSpec {
        id: "a".into(),
        parent: None,
        bind_field: None,
        relation_kind: None,
        policy: None,
    }];
    let e = run_plan(None, &ops, |_, _| ExecOutcome::Error("boom".into())).unwrap_err();
    assert_eq!(e.code.as_str(), "OP_FAILED");
}

#[test]
fn plan_unknown_policy_fail_closed() {
    let ops = vec![OpSpec {
        id: "a".into(),
        parent: None,
        bind_field: None,
        relation_kind: None,
        policy: Some("wat".into()),
    }];
    let e = run_plan(None, &ops, |_, _| ExecOutcome::Ok(Value::Null)).unwrap_err();
    assert_eq!(e.code.as_str(), "UNKNOWN_POLICY");
}

// ── envelope ─────────────────────────────────────────────────────────────────
#[test]
fn envelope_version_checks() {
    assert!(validate_envelope(&json!({"specVersion": "1.0"}), "1.1", None, None).is_ok());
    assert!(validate_envelope(&json!({"specVersion": "1.1"}), "1.1", None, None).is_ok());
    assert_eq!(
        validate_envelope(&json!({"specVersion": "2.0"}), "1.1", None, None)
            .unwrap_err()
            .code
            .as_str(),
        "UNSUPPORTED_MAJOR"
    );
    assert_eq!(
        validate_envelope(&json!({"specVersion": "1.2"}), "1.1", None, None)
            .unwrap_err()
            .code
            .as_str(),
        "UNSUPPORTED_MINOR"
    );
    assert_eq!(
        validate_envelope(&json!({}), "1.1", None, None)
            .unwrap_err()
            .code
            .as_str(),
        "MISSING_VERSION"
    );
    assert_eq!(
        validate_envelope(&json!({"specVersion": "x"}), "1.1", None, None)
            .unwrap_err()
            .code
            .as_str(),
        "MALFORMED_VERSION"
    );
}

// ── guard ────────────────────────────────────────────────────────────────────
#[test]
fn guard_rejects_non_finite() {
    assert!(assert_portable(&Value::Obj(vec![("x".into(), Value::Float(f64::NAN))])).is_err());
    assert!(assert_portable(&Value::Arr(vec![Value::Int(1), Value::Str("ok".into())])).is_ok());
}

// ── codec / deep_equals ──────────────────────────────────────────────────────
#[test]
fn codec_int_float_distinction() {
    let a = decode_value(&json!(1)).unwrap();
    let b = decode_value(&json!({"float": 1})).unwrap();
    assert!(matches!(a, Value::Int(1)));
    assert!(matches!(b, Value::Float(_)));
    assert!(!deep_equals(&a, &b), "int 1 must not equal float 1.0");
}