Skip to content

Testing a package

A scenario file is how a package states what it promises. Each scenario gives the facts, asks one question and says what answer is expected; the runner evaluates the package on those facts and compares. This page walks through the parts of a .lawtest file, shows a scenario whose expected answer is “not established”, and runs the whole directory.

Open this package in the playground → — edit the rules and the scenarios of this page and run them in your browser.

language "law.core" version "0.2";
package demo.parking version "0.1.0";
namespace "urn:law:demo:parking";
entity Applicant;
relation resident(a: Applicant) kind institutional;
relation vehicle_registered(a: Applicant) kind institutional;
relation permit_eligible(a: Applicant) kind institutional;
const MONTHLY_RATE: Money = 10 EUR;
function permit_fee(months: Integer) -> Money = months * MONTHLY_RATE;
rule PermitEligibility strict {
for a: Applicant;
when resident(a) and vehicle_registered(a);
then permit_eligible(a);
}

A scenario file begins with the same three header lines as the package it tests, then any number of test blocks. Each block has three parts.

  • given { … } — the case: a context with the dates the question is asked at, and the facts. Every fact names its id and origin.
  • evaluate …; — the question: truth(…), collect …, a term, or positions().
  • expect …; — one or more expectations about the answer.
test "given, evaluate, expect" {
given {
context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; }
assert resident(entity_ref("urn:demo:parking:ann")) { id "ann-resident"; origin case_input; }
assert vehicle_registered(entity_ref("urn:demo:parking:ann")) { id "ann-vehicle"; origin case_input; }
}
evaluate truth(permit_eligible(entity_ref("urn:demo:parking:ann")));
expect truth_status == TRUE_ONLY;
expect evaluation_status == COMPUTED;
expect applied(PermitEligibility);
}

Expectations you will use most:

Expectation Checks
expect truth_status == TRUE_ONLY; the answer to a truth question (TRUE_ONLY, FALSE_ONLY, NEITHER, BOTH)
expect evaluation_status == COMPUTED; that the engine could compute at all; other values name what was missing, such as MISSING_POLICY
expect applied(Rule); / expect not applied(Rule); whether a named rule took part in the answer
expect value == 30 EUR; the value of a calculation
expect collected_count(1); / expect collected(entity_ref("…")); the size and members of a collection
expect position(Duty, ACTIVE); the state of a duty, permission or power

The most important scenario in any package is the one where the rule must stay silent. Only residence is known; nothing says Ann has no vehicle, so the expected answer is NEITHER, and the rule must not be applied. Had the package quietly treated a missing fact as false, this scenario would catch it.

test "residence alone is not enough" {
given {
context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; }
assert resident(entity_ref("urn:demo:parking:ann")) { id "ann-resident"; origin case_input; }
}
evaluate truth(permit_eligible(entity_ref("urn:demo:parking:ann")));
expect truth_status == NEITHER;
expect not applied(PermitEligibility);
}

A value expectation for the fee function:

test "the fee for a year" {
given {
context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; }
}
evaluate permit_fee(12);
expect value == 120 EUR;
}
Terminal window
law engine test tests/scenarios.lawtest --program parking.law
test PASS: given, evaluate, expect
test PASS: residence alone is not enough
test PASS: the fee for a year

The runner exits with code 0 when every scenario passes and 1 when any fails, so the same command works in continuous integration.

Change the rule so that residence alone suffices — delete and vehicle_registered(a) from its when line — and run the scenarios again. The first two still describe the old promise, and the second one fails:

test PASS: given, evaluate, expect
test FAIL: residence alone is not enough
truth_status == NEITHER: в документе TRUE_ONLY
not applied(urn:law:demo:parking#PermitEligibility): в документе есть
test PASS: the fee for a year

That is the point of the “not established” scenario: a change that widens a rule is caught by the case that expected silence.

Documentation for Arxo. Writings — blog.arxo.io.

Anonymous visit counts on stats.arxo.io, no cookies.