# tests/

Unit and integration tests for the deterministic core (`scripts/`). Every test
file is **self-contained** — it bootstraps `sys.path` to import directly from
`scripts/` (there is no shared `conftest.py`). All time- and filesystem-dependent
behaviour is driven by injected frozen clocks and `tmp_path`, so no test touches
the real clock, a real session, or the network.

## Contents

| File | Covers | Key assertions |
|------|--------|----------------|
| `test_scanner_exclusions.py` | Scanner exclusion pipeline (invariant #9) | A `hygiene: frozen` file, a `.dochygiene-ignore`-matched file, and append-only logs are absent from the shortlist; `.cc-os/` and `.dochygiene/` and excluded dirs are never scanned. |
| `test_scanner_signals.py` | Scanner per-path signals | One fixture per signal type asserts the expected signal on the right path, with no classification attached (objective facts only). |
| `test_state_store.py` | `resolve_project_root` + `StateStore` (invariants #3, #4; ADR-027) | Root resolution (git-root / cwd-fallback); writes confined to `.cc-os/dochygiene/`; `.gitignore` never edited; atomic torn-write safety; report rollover keeps one pair; timestamp round-trip with injected clock; legacy `.dochygiene/` fallback reads and auto-migration on first write. |
| `test_reminder.py` | `Reminder` decision + `ReminderRunner` (invariants #1, #2) | Pure decision: fresh→silent, stale→banner, same-day→snoozed, prior-day→re-banner, **calendar-day boundary across midnight** (20-min delta, different day → banner). Runner: only `last_reminded` mutated and only on banner; missing/corrupt state → never-checked; always exits 0. |
| `test_validate_report.py` | `validate_report.py` (invariants #10, #11) | Golden fixtures end-to-end (PASS exit 0 / FAIL exit 1, not mutated); crafted-dict branch coverage for `raw_tokens` requirement, op_type↔exact_edit biconditional, and safety_tier derivation truth table. |
| `test_cross_seam.py` | scanner + state_store + reminder together (Group 5; invariant #6) | End-to-end on a tmp doc tree: scan produces a shortlist and self-excludes `.cc-os/`/`.dochygiene/`; rollover keeps one report pair; reminder snooze behaves across simulated sessions with an injected clock. No model, no network. |

## Fixtures

`fixtures/scanner/` (one subtree per exclusion/signal case) and
`fixtures/state_store/sample_project/` hold static fixture doc trees. The
cross-seam and reminder tests build their trees in `tmp_path` at runtime instead.
