Check policy paths
For: Plugin authors testing agents and flows.
Check which policy branches your cases exercise.
Policy-path coverage
Whether the answers were right is one question; which of the plugin's own policy paths the cases went through is another. A plugin can pass every case while no case ever reaches the refusal of a rule, the denial of an approval, the skipped branch of a flow or a spent budget, the paths that matter when something goes wrong. --coverage measures them:
oz0 plugin test ./support --coverage
oz0 plugin test ./support --min-coverage 80 --coverage-out coverage.json # either one implies --coverage
Policy-path coverage: 5 of 7 branches (71.4%), threshold 60%: ok
KIND POINT BRANCH CASES
rule rule-ask-e2e true covered/approved, covered/denied then quiet
false covered/denied then quiet
rule rule-run decided covered/denied then quiet
approval greeter.hello approved covered/approved
denied covered/denied then quiet
loop greeter continued MISSED
stopped MISSED
A decision point is a place where the plugin's own policy decides, named <kind>:<subject>, with a fixed list of branches. Coverage is the share of all their branches that at least one case reached:
| Kind | Point | Branches | Declared by |
|---|---|---|---|
rule | a tool-policy rule, by its ID | decided, or true and false for a rule with a when: | the tool policy an eval ships (below) |
approval | a function (support.refund), or a flow's approval step (flow:<flow>/<step>) | approved, denied, and edited when the tool sets approval: {edit: true} | requires_approval, the functions the default policy sends to a person by their annotations (destructive, or declaring nothing), the shipped rules that approve, and the functions the taint, origin and sequence rules below may send to a person |
step | a flow's step (<flow>/<step>) | ran, and skipped for a step with when: | flows |
loop | an agent's loop rule | by its on_loop: reported, or continued and stopped, or stopped | agents with tools |
budget | an agent's budget | within and reached | agents with limits.max_cost_usd or limits.tree |
outcome | an agent's outcome check | passed (first round), revised (a later round), unmet | agents with outcome: |
taint.default | an outbound function | asked (or refused, for a floor that refuses) when the job is tainted, clear when it is not | the taint-default rule, when an eval's shipped policy keeps it on |
origin.check | a sensitive argument (<function>/<argument>) | passed or below its floor | arguments with x-oz0-role that the schema's x-oz0-origin or a shipped origin rule gives a floor (where arguments came from) |
origin.unknown | an origin rule, by its ID (rule-origin-default for the built-in one) | asked (or refused) when an argument is below its floor, met when none is | the built-in origin rule and the shipped origin rules, for the functions they set floors for |
sequence.match | a sequence rule, by its ID | asked (or refused) after a call that matched its after, clear when none came before | the acting sequence rules of a shipped policy |
tool | a function an agent may call (<agent>/<tool>.<function>) | called | each agent's tools; a pattern such as crm.* stands for the functions the plugin lists |
skill | a skill an agent may read (<agent>/<plugin>/<skill>) | read | each agent's skills and its tools' manuals |
The tool and skill points are the model's choices rather than the policy's: they show which of an agent's tools no case ever called and which of its skills no case read, and they are counted apart, as tools and skills chosen: X of Y after the figure. The policy-path figure, and --min-coverage, count the other kinds only, so a threshold a plugin set keeps its meaning. They are read from the cases' steps, as tools_called is.
A point a run reached that the plugin does not declare is listed as observed, outside the figure. So is a branch no case can reach in plugin test: an approval's expired (every approval is answered at once), and taint.join, which says whether each tool's result was trusted or tainted the job (what a result carries is its server's to say, not something the plugin's files declare). The edge's intent entry before each call is no decision point: every call that runs has one.
Only the plugin's own policy counts: a tenant's rules are the tenant's, not the plugin's. A plugin that wants its rules tested ships them, as a tool policy an eval names, in the JSON oz0 tenant tool-policy show --json prints:
agent: support
policy: evals/policy.json # set on the test tenant while this eval's cases run; the next eval without one gets the default
cases: [...]
{"rules": [
{"id": "rule-big-refund", "match": "support.refund", "action": "approve", "when": "args.amount > 100"},
{"match": "support.*", "action": "allow"}
]}
A rule without an id gets one derived from its place and what it says, so it keeps its name from run to run. A rule whose expires_at has passed decides nothing, so it is left out, of the policy and of the points. The file must be inside the plugin.
The points come from the plugin as the run leaves it: when tools.listing.json is missing, plugin test writes the listing its node measured first, and the functions' annotations and schemas are read from that, so a fresh checkout and the next run give the same figure.
How it is recorded: plugin test --coverage starts each case's job with an ID that begins with cov., and such a job, with every child job below it, reports each decision point it reaches as a no-op step in its history, the point's name and branch and nothing of what the job read or wrote. The job's trace (GetJobTrace) gives them back. Any other job reports nothing.
What it writes:
- The table, after the evals' lines, and the whole report under
coveragein--json. evals/coverage.jsonin the plugin's folder, every point with the cases that reached each branch, which the studio shows. It is left out of the digest a measured listing records, liketools.listing.json.--coverage-out FILE, the same report, for a CI to keep.- A Markdown summary appended to the file
$GITHUB_STEP_SUMMARYnames, when that variable is set: the figure, the counts per kind, the missed branches, and every point in a fold. Any CI that sets the variable gets it; nothing else is assumed. - With
--report, the test report a listing carries holds the figure and the counts per kind, never the points.
--min-coverage N fails the run below N percent and names what no case reached, as the tool-size budget fails it.