> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zespan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing a policy before you enforce it

> Backtest a policy against your own recorded traffic to see what it would have caught and what it would have broken, before promoting it to deny.

The hard part of a guardrail is not writing it. It is knowing whether turning it
on will break real conversations.

`zespan policy test` runs a policy over your project's own recorded traffic and
tells you both sides:

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
zespan policy test --against issues
```

```text theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
Corpus: issues

hipaa-phi-egress
  ✓ fires on 14/14 known-bad samples
  ✗ fires on 3/91 samples that were fine
  by rule:
    [0] pii      +14 caught   -0 false
    [1] regex     +9 caught   -3 false
      trace 9f2a1c04b8e1…  "patient asked about their appointment time on the 3rd"
  → net: catches 14 real, would break 3 good conversation(s)
```

Two things that matter in that output. The false-positive count is broken down
**per rule**, so you can see that rule `[1]` is the noisy one and fix it rather
than abandoning the whole policy. And each false positive comes with the
**actual conversation**, because we hold the trace — not an estimated rate.

## Corpora

| `--against`    | What it is                                 | Which side it measures                                               |
| -------------- | ------------------------------------------ | -------------------------------------------------------------------- |
| `issues`       | Traffic a guardrail already flagged        | **True positives** — would this policy have caught what got through? |
| `last:7d`      | Recent traffic nothing flagged (1–90 days) | **False positives** — would it break things that were fine?          |
| `dataset:<id>` | A curated dataset                          | Whichever you built the dataset to represent                         |

Test against both sides. A policy tested only against `last:7d` will look
perfect and tell you nothing about what it catches; a policy tested only against
`issues` tells you nothing about what it breaks. The report says so explicitly
when one side is missing rather than letting a one-sided result look clean.

<Note>
  Prompts and completions are separate samples. A rule scoped to `pre` is never
  evaluated against text the model produced — it would never see that in
  production, and counting it would invent false positives that cannot happen.
</Note>

## The `deny` gate

Promoting a policy to `enforcement: deny` requires either a test result or an
explicit opt-out:

```text theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
Apply failed (409): 1 policy/policies would enforce at "deny" without a backtest: hipaa-phi-egress.
Run `zespan policy test` to see what they would have blocked, or re-run with --untested.
```

This is deliberately **not** a hard block. A platform engineer mid-incident has
to be able to ship, and a gate that cannot be bypassed just gets worked around
permanently. But the default path makes you look at the false positives first.

A test result is keyed to the policy's exact content, so **editing a rule
invalidates it**. That is what stops the gate becoming a formality where an old
passing test vouches for a policy that has since changed.

<Tip>
  In CI, run `zespan policy test --against issues` on every pull request that
  touches `policies/`. The output is what a reviewer needs in order to approve a
  change to what gets blocked in production.
</Tip>

## What a backtest cannot tell you

<Warning>
  A backtest only catches what is in the corpus. It cannot catch attack patterns
  that have not happened to you yet.
</Warning>

That is exactly why the enforcement ladder does not end at a green test.
`warn` against live traffic remains the second line of defence: it sees novel
patterns a historical corpus by definition does not contain. Every report
carries this note, because a green test is easy to over-read.

Rule types that need agent context — `loop_detection`, `tool_misuse`,
`agent_rate_limit` and the other agent-scoped types — are not evaluated in a
backtest, and the report says which were skipped rather than counting them as
clean.

## Next steps

<CardGroup cols={2}>
  <Card title="The enforcement ladder" icon="stairs" href="/policies/as-code#the-enforcement-ladder">
    dryrun → warn → deny, and why to start at the bottom.
  </Card>

  <Card title="zespan policy" icon="terminal" href="/cli/policy">
    Every verb and flag.
  </Card>

  <Card title="Local evaluation" icon="bolt" href="/policies/local-evaluation">
    Evaluate policies in-process instead of over the network.
  </Card>

  <Card title="Policy as code" icon="file-code" href="/policies/as-code">
    The authoring loop and ownership model.
  </Card>
</CardGroup>
