
## Testing Strategy

- **Per-tier Rust unit tests**: each tier adds tests for new token types, parser paths, IR output
- **Rust integration tests**: compile `.ops` snippets and assert JSON output
- **Bats acceptance suite**: `test/compile.api.bats` and `test/resolve.api.bats` are the gold standard. At Tier 22, update helpers to call `ops-engine` and verify all pass.
- **Snapshot tests**: capture bash output for every `.ops` file, assert Rust produces identical output

### POSIX Compliance Validation

Emitted scripts must be strictly POSIX sh. Three layers of validation, starting at Tier 16:

1. **`shellcheck --shell=sh`** — static analysis on all emitted `.op.sh` files and standalone scripts. Flags bash-isms (`local`, `[[ ]]`, `(( ))`, arrays, process substitution). Fast enough to run on every test invocation.
2. **`dash` execution** — emitted scripts are executed under dash (strict POSIX sh) alongside bash, with output comparison. Catches runtime behavior differences that static analysis misses (echo flag handling, word-splitting edge cases, arithmetic evaluation).
3. **`busybox sh` execution** — validates the Alpine/minimal container target. busybox sh has its own quirks vs dash; running both catches implementation-specific assumptions.

All three run in CI from Tier 16 onward. A POSIX regression is a blocking failure.

### Fuzz Testing

`cargo-fuzz` targets grow with each tier. Corpus persists between runs. Crash cases become regression tests.

#### Fuzz targets by tier

| Tier | Target | Assertion |
|------|--------|-----------|
| 0 | Lexer | Random bytes → no panics/hangs |
| 2 | Parser (empty registry) | Lex → parse → no panics |
| 5 | Parser + type validation | Fixture registry with typed ops |
| 11 | Parser + full registry | Fixture registry with block ops, infix |
| 13 | Resolver | Compiled IR → resolve → no panics on malformed graphs |
| 16 | Emitter + shellcheck | Resolved plan → emitted POSIX sh → `shellcheck --shell=sh` passes |

#### CI integration

Three cadences, each with a distinct role:

1. **Nightly (20–30 minutes per target):** novel input exploration. If a crash is found, CI creates a branch with the crash artifact as a regression test, opens a PR, and assigns it for fixing.
2. **PR (regression only):** runs the existing corpus of crash cases and regression tests against the current branch. Fast — just replays known inputs. Failures block merge.
3. **On merge (regression only):** replays the existing corpus after merge so main gets the same known-crash coverage without running novel-input exploration on every push.

### Coverage

90% branch coverage required before moving to next tier.

- **Rust**: `cargo-llvm-cov` or `cargo-tarpaulin` (branch mode) on all `src/`
- **Bash**: `kcov` via `./ops bash coverage` on surviving runtime code (`lib/*.api.sh`, `@operation`, load_ops, `@out`, `@export`)
- No regressions allowed
