Testing your policies
A policy is the only thing between an agent and a real action. Unit-test it.
from watchlight import govern
govern.load("watchlight.policy.json")
assert govern.has_policies, "no policies loaded — every call would be denied"
report = govern.test([
{"name": "under limit allows", "action": "book",
"context": {"amount": 200, "limit": 500, "refundable": True}, "expect": "Allow"},
{"name": "over limit denies", "action": "book",
"context": {"amount": 800, "limit": 500, "refundable": True}, "expect": "Deny"},
{"name": "big wire needs a human", "action": "wire",
"context": {"amount": 5000}, "expect": "NeedsApproval"},
])
assert report["failed"] == 0, report
Each fixture asserts the verdict for a (principal, action, resource, context).
A wrong expectation fails the suite; a fixture missing action or expect
raises.
const report = await govern.test([
{ name: "over limit denies", action: "book",
context: { amount: 800, limit: 500, refundable: true }, expect: "Deny" },
]);
if (report.failed) throw new Error(`${report.failed} policy tests failed`);
Those fixtures exercise these policies:
[
{ "name": "funded-book",
"code": "permit(principal, action == Action::\"book\", resource) when { context.amount <= context.limit && context.refundable };" },
{ "name": "big-wire-needs-human",
"code": "@enforcement_effect(\"require_approval\")\npermit(principal, action == Action::\"wire\", resource) when { context.amount > 1000 };" }
]
govern.test(...) drives the engine's decision core directly, so it holds no
decision logic of its own and writes nothing to the audit trail. A thousand
fixtures in CI leave no residue in .watchlight/.
Each result is { name, expected, actual, ok, reason } — reason carries the
engine's explanation, so an unexpected Deny tells you why.
Two more fixture keys
Approving a hold
"approved": true mints a single-use token and asserts the
NeedsApproval → Allow downgrade, so you can prove both halves of an approval
gate:
{"name": "the wire proceeds once approved", "action": "wire",
"context": {"amount": 5000}, "approved": True, "expect": "Allow"}
Asserting obligations
"obligations" asserts the obligations an Allow carries:
{"name": "readable with the SSN redacted", "action": "read",
"context": {"record_type": "customer"},
"expect": "Allow", "obligations": {"redact": ["ssn"]}}
The match is exact — a key you leave out must be absent, and {} asserts no
obligations at all. Suite files accept both spellings (maxItems /
max_items), so one JSON file runs under both CLIs.
Run it in CI
Put policies and fixtures in one suite.json —
{ policyFile?, policies?, tests: [...] } — and run it:
watchlight policy test suite.json # Python
npx --package @watchlight/sdk watchlight policy test suite.json # Node
watchlight policy test — suite.json
✓ under limit allows → Allow
✓ over limit denies → Deny
2 passed, 0 failed (2 total)
The CLI exits 0 when every fixture passes, and 1 on a verdict mismatch.
It exits 2 on a malformed suite: bad JSON, a missing file, a fixture missing
action or expect, or an unimplemented @enforcement_effect.
- run: pip install watchlight # or: npm i @watchlight/sdk
- run: watchlight policy test policy/suite.json
Add a fixture for the behaviour you are changing and for the behaviour you are not, so a fix cannot quietly widen something else.
Worth knowing
- An unrecognised
@enforcement_effectfails at load. Anything outsideattenuate,escalate,observe,quarantine,require_approval,revoke,sever_subtree,terminateraisesPolicyError, andloadadds nothing from that file. A typo in the annotation name only warns. govern.load(path)is idempotent per source, keyed on the resolved path, not on content: edit a loaded file and passforce=Trueto load it again.govern.allow(code)is always additive. The same code twice is two policies.- A test that calls
authorize()or a governed tool is a governed call like any other and appends a record. SetWATCHLIGHT_AUDIT_FILE=0for the test run to keep those verdicts out of the application's trail.
See also
showcase/policy-tests-ci/— a suite, a workflow that runs it in both lanes, and a widened policy that turns the run red.- Policy recipes — every pattern ships the suite that proves its verdicts.
- Enforcement effects — where
NeedsApprovalcomes from.