Validation and QA: what counts as evidence here
This skill defines the evidence bar for claiming work complete, the
coverage and quality thresholds that gate merges, the golden regression
tests that guard settled battles, and the procedure for adding tests.
The one-line thesis: a green check proves spec-satisfaction, not
correctness, so every completion claim needs cited evidence and every
test needs proof it can fail.
The evidence bar
Never claim "should work". Run the thing, capture the output, cite it.
The house discipline comes from Skill(imbue:proof-of-work):
- Number every piece of evidence:
[E1], [E2], each with the exact
command and its captured output.
- Map each acceptance criterion to evidence with a verdict:
Criterion: [E2] -> PASS or -> FAIL.
- Give the overall claim one of three statuses:
COMPLETE (all
criteria passed), PARTIAL (list blockers), BLOCKED (explain
why). A blocked task reported as blocked with evidence is a
successful report. A guessed "done" is not.
- The final response must not contain "should work", "looks right",
or any other unverified confidence phrase.
Verifier integrity: never let the generator judge itself
A passing verifier can mislead two ways (from the prover-verifier
research, codified in commit 29081fda, module
plugins/imbue/skills/proof-of-work/modules/verifier-integrity.md):
| Failure mode |
What it looks like |
| Wrong spec |
The check confirms the code matches the spec, not that the spec matches intent |
| Hollow check |
assert True, a mock returning the expected value, a stubbed service: all green, all worthless |
Rules that follow:
- The agent that wrote the code must not be the sole judge of whether
the code works. Use an independent check: a fresh subagent, the real
test suite, a human, or an end-to-end run the generator cannot
influence.
- Validate the spec separately from the code. A machine-checked pass
against a wrong spec is confident, green, and wrong.
- When reviewing a test, ask what change to the code would make it
fail. If nothing would, it is not a test.
Iron Law TDD (Constitution rule 3)
CONSTITUTION.md rule 3: no implementation without a failing test
first. Scope:
- Applies to every code change in plugin Python sources
(
plugins/*/src/, plugins/*/scripts/, plugins/*/hooks/).
- Skill files, agent files, and prose docs are exempt. Their analogue
is a structural validation test: every new skill needs a
test_skill_<name>.py proving the structure.
A structural validation test asserts observable content in the skill
file: required sections exist, the referenced modules exist on disk,
tables and examples the skill promises are present. Model on
plugins/imbue/tests/unit/skills/test_proof_of_work.py, which checks
SKILL.md sections, module files, and enforcement tables. The test must
fail if someone deletes the section it guards (see the
tautological-test trap below).
Thresholds
| Gate |
Value |
Where defined |
Enforced by |
| Root coverage |
fail_under = 85 |
root pyproject.toml [tool.coverage.report] |
root pytest runs |
| Per-plugin coverage |
coverage_threshold in [tool.nightmarket], 90 for most plugins, 85 for gauntlet |
plugins/<p>/pyproject.toml |
scripts/run-plugin-tests.sh passes --cov-fail-under only when the key is set and > 0 |
| Mutation testing |
weekly Sunday 00:00 UTC cron mutates only sanctum (the matrix falls back to sanctum when the dispatch input is empty); abstract/imbue/attune run only via manual dispatch with plugin=all or a named plugin |
.github/workflows/mutation-testing.yml |
mutmut: exit 0 = no survivors, exit 2 = survivors (allowed), anything else = crash |
| Critical issues |
max_critical_issues: 3, enforce_blocking: true |
.claude/quality_gates.json |
quality-gate tooling |
| File size |
< 20KB and < 5000 tokens per file |
.claude/quality_gates.json |
advisory (block_on_violation: false) |
| Function length |
<= 60 lines, complexity < 12, nesting <= 5, debt ratio < 0.3 |
.claude/quality_gates.json |
advisory, except security dimension which blocks |
Notes:
- The
run-plugin-tests.sh awk parser reads coverage_threshold from
[tool.nightmarket], never from addopts. Use
scripts/fix_coverage_threshold.py to migrate a plugin still using
the old location.
- The script itself sets no minimum floor for
coverage_threshold. If
the key is absent, no --cov-fail-under flag is passed at all, so a
plugin without the key has no coverage gate in the runner.
- imbue's own
addopts force --cov=scripts plus term and HTML
coverage reports on every run, including single-file runs.
The tautological-test trap
PR review here repeatedly catches tests that assert nothing. Evidence
in history: a94240e2 (12 tests of constants tightened to behavioral
coverage), f1cbbcf1 (strengthened tautological assertions),
30e58586 (validation-floor regression test), 42f7ce84 (round-trip
test replacing a structure-only check). The pattern: a test that
restates the code, mocks the unit under test, or asserts a constant
equals itself.
The counter-discipline is the revert test, executed by
sanctum:validate-pr:
- Take the fix the PR claims to make.
- Edit the fixed line back to its broken state (working tree must be
clean first, or the step is skipped as unsafe).
- Run the test that supposedly guards the fix.
- The test must FAIL against the reverted code. If it stays green,
the test is a dead assertion, not a guard.
- Restore the fix.
When writing a new test, apply the same standard preemptively: write
the test so it would catch the bug's return, then confirm it fails
before the fix lands (that is the Iron Law's RED step).
Golden inventory: regression guards worth knowing
These tests lock in lessons from settled incidents. Do not weaken or
delete them without understanding the incident they guard.
| Guard |
Location |
What it locks |
| py39 datetime alias |
plugins/leyline/tests/test_python39_compat.py |
AST-scans leyline source for datetime.UTC (a 3.11+ alias). Hooks run under system Python 3.9, and this alias broke the whole hook import chain three-plus times. Ruff UP017 kept auto-reverting manual fixes, so only this AST invariant test holds the line. |
| Lazy-import blocker |
plugins/gauntlet/tests/unit/test_challenges.py |
Installs a sys.meta_path blocker that raises on any re-import of anthropic, proving gauntlet's heavy deps stay lazily imported. Eager imports made every git commit emit hook ModuleNotFoundError. |
| Hook timeout budget |
plugins/herald/tests/unit/test_double_shot_latte.py (test_llm_timeout_fits_within_hook_timeout) |
Asserts LLM_TIMEOUT_SECONDS is strictly less than the Stop-hook timeout registered in hooks.json. A timeout above the budget once cost the hook its verdict entirely (full record: night-market-failure-archaeology SB7). |
| Hook stdin contract |
plugins/abstract/tests/hooks/test_hook_io.py |
Locks the input contract of shared/hook_io.py: stdin JSON is primary, legacy CLAUDE_TOOL_* env vars are fallback only. Env-reading hooks were once silent no-ops for months (full record: night-market-failure-archaeology SB9). |
When you fix an incident of a similar class, add its guard here in the
same spirit: an invariant test that fails loudly if the lesson is
unlearned.
How to add tests
Layout and isolation
- Each plugin owns
plugins/<plugin>/tests/ with unit/,
integration/ subdirectories and its own conftest.py and pytest
config in the plugin's pyproject.toml.
- Root
pyproject.toml sets norecursedirs = ["plugins/*", ...].
Plugin tests MUST run per-plugin. Running them from the repo root
causes ImportPathMismatchError from duplicate module names (the root
conftest.py documents this).
Commands
# Single test file (fastest loop)
cd plugins/imbue
uv run pytest tests/unit/test_deferred_capture.py -x -q
# Full suite for one plugin
cd plugins/<plugin>
uv run python -m pytest tests/ --tb=short -q
# or, where the plugin has a Makefile target:
make -C plugins/<plugin> test
# Everything (what `make test` at root does)
./scripts/run-plugin-tests.sh --all
# Only plugins with staged changes
./scripts/run-plugin-tests.sh --changed
# Mutation testing for one plugin, locally
cd plugins/<plugin>
uv run mutmut run --paths-to-mutate=scripts/,src/ --tests-dir=tests/
Markers
Root pytest runs with --strict-markers, so only registered markers
are legal. Registered in root pyproject.toml: unit, integration,
e2e, slow, network, plugin, skill, hook, command, bdd,
benchmark (skip benchmarks in CI with -m "not benchmark").
Checklist for new code
Failure-mode detection: silent failure in scanners
The most recurrent bug class in this repo is the swallowed error. In
scanner-shaped code (anything that walks files and reports findings),
the signature is except-continue: a try/except that skips a file on
malformed input and reports success on the remainder. The scanner
looks healthy while ignoring exactly the inputs most likely to be
broken.
The house convention since commit 666171c3 (issue #575): a scanner
that cannot process an input emits an ADVISORY finding for it instead
of skipping silently. Examples from that commit:
check_hook_modernization.py emits findings on malformed
hooks.json, SyntaxError, or OSError instead of dropping the file.
- pensive
harden/scanner.py appends an ADVISORY finding for
unreadable files under --strict.
- minister
dora_metrics warns and sets a partial flag on malformed
tag lines instead of silently classifying the repo Elite.
When reviewing or writing scanner code, grep for the pattern:
rg -n "except .*:\s*$" -A2 plugins/<p>/scripts/ | rg -B1 "continue|pass"
Any hit that discards an error without emitting a finding or an inline
"why it is safe to discard" comment violates Constitution rule 10
(errors are not optional).
When NOT to use
- Running the suites, lint, release, or publish mechanics: use
night-market-operations (command anatomy lives there).
- Classifying and gating a change, PR review flow, non-negotiables:
use
night-market-change-control.
- A test is failing and you need triage for a known repo failure mode:
use
night-market-debugging-playbook.
- History of why a guard exists (incident narratives, reverts): use
night-market-failure-archaeology.
- Setting up pytest config or fixtures for a new plugin:
Skill(leyline:pytest-config) has the templates, and
Skill(leyline:testing-quality-standards) the anti-pattern catalog.
Exit Criteria
Provenance and maintenance
Compiled 2026-07-02 against repo v1.9.15, branch
discussions-fix-1.9.14. Volatile facts and how to re-verify them:
- Root coverage floor:
rg -n "fail_under" pyproject.toml
(85 as of 2026-07-02).
- Per-plugin thresholds:
rg -n "coverage_threshold" plugins/*/pyproject.toml
(90 everywhere except gauntlet at 85, as of 2026-07-02).
- Threshold parser behavior: read the awk block in
scripts/run-plugin-tests.sh (search tool.nightmarket).
- Mutation cadence and plugin list:
rg -n "cron|matrix" .github/workflows/mutation-testing.yml.
- Quality gate numbers:
cat .claude/quality_gates.json.
- Golden guards still present:
ls plugins/leyline/tests/test_python39_compat.py plugins/gauntlet/tests/unit/test_challenges.py plugins/herald/tests/unit/test_double_shot_latte.py plugins/abstract/tests/hooks/test_hook_io.py.
- Cited commits:
git log --oneline -1 <hash> for 29081fda,
a94240e2, f1cbbcf1, 30e58586, 42f7ce84, 666171c3,
268cff89.
- Marker list:
rg -n -A12 "^markers" pyproject.toml.
1---2name: night-market-validation-and-qa-23description: Enforce evidence bar, coverage gates, and regression guards. Use when adding tests or claiming done. Do not use to run suites; use night-market-operations.4---56# Validation and QA: what counts as evidence here78This skill defines the evidence bar for claiming work complete, the9coverage and quality thresholds that gate merges, the golden regression10tests that guard settled battles, and the procedure for adding tests.11The one-line thesis: a green check proves spec-satisfaction, not12correctness, so every completion claim needs cited evidence and every13test needs proof it can fail.1415## The evidence bar1617Never claim "should work". Run the thing, capture the output, cite it.18The house discipline comes from `Skill(imbue:proof-of-work)`:19201. Number every piece of evidence: `[E1]`, `[E2]`, each with the exact21 command and its captured output.222. Map each acceptance criterion to evidence with a verdict:23 `Criterion: [E2] -> PASS` or `-> FAIL`.243. Give the overall claim one of three statuses: `COMPLETE` (all25 criteria passed), `PARTIAL` (list blockers), `BLOCKED` (explain26 why). A blocked task reported as blocked with evidence is a27 successful report. A guessed "done" is not.284. The final response must not contain "should work", "looks right",29 or any other unverified confidence phrase.3031### Verifier integrity: never let the generator judge itself3233A passing verifier can mislead two ways (from the prover-verifier34research, codified in commit `29081fda`, module35`plugins/imbue/skills/proof-of-work/modules/verifier-integrity.md`):3637| Failure mode | What it looks like |38|--------------|--------------------|39| Wrong spec | The check confirms the code matches the spec, not that the spec matches intent |40| Hollow check | `assert True`, a mock returning the expected value, a stubbed service: all green, all worthless |4142Rules that follow:4344- The agent that wrote the code must not be the sole judge of whether45 the code works. Use an independent check: a fresh subagent, the real46 test suite, a human, or an end-to-end run the generator cannot47 influence.48- Validate the spec separately from the code. A machine-checked pass49 against a wrong spec is confident, green, and wrong.50- When reviewing a test, ask what change to the code would make it51 fail. If nothing would, it is not a test.5253## Iron Law TDD (Constitution rule 3)5455CONSTITUTION.md rule 3: no implementation without a failing test56first. Scope:5758- Applies to every code change in plugin Python sources59 (`plugins/*/src/`, `plugins/*/scripts/`, `plugins/*/hooks/`).60- Skill files, agent files, and prose docs are exempt. Their analogue61 is a structural validation test: every new skill needs a62 `test_skill_<name>.py` proving the structure.6364A structural validation test asserts observable content in the skill65file: required sections exist, the referenced modules exist on disk,66tables and examples the skill promises are present. Model on67`plugins/imbue/tests/unit/skills/test_proof_of_work.py`, which checks68SKILL.md sections, module files, and enforcement tables. The test must69fail if someone deletes the section it guards (see the70tautological-test trap below).7172## Thresholds7374| Gate | Value | Where defined | Enforced by |75|------|-------|---------------|-------------|76| Root coverage | `fail_under = 85` | root `pyproject.toml` `[tool.coverage.report]` | root pytest runs |77| Per-plugin coverage | `coverage_threshold` in `[tool.nightmarket]`, 90 for most plugins, 85 for gauntlet | `plugins/<p>/pyproject.toml` | `scripts/run-plugin-tests.sh` passes `--cov-fail-under` only when the key is set and > 0 |78| Mutation testing | weekly Sunday 00:00 UTC cron mutates only sanctum (the matrix falls back to sanctum when the dispatch input is empty); abstract/imbue/attune run only via manual dispatch with `plugin=all` or a named plugin | `.github/workflows/mutation-testing.yml` | mutmut: exit 0 = no survivors, exit 2 = survivors (allowed), anything else = crash |79| Critical issues | `max_critical_issues: 3`, `enforce_blocking: true` | `.claude/quality_gates.json` | quality-gate tooling |80| File size | < 20KB and < 5000 tokens per file | `.claude/quality_gates.json` | advisory (`block_on_violation: false`) |81| Function length | <= 60 lines, complexity < 12, nesting <= 5, debt ratio < 0.3 | `.claude/quality_gates.json` | advisory, except security dimension which blocks |8283Notes:8485- The `run-plugin-tests.sh` awk parser reads `coverage_threshold` from86 `[tool.nightmarket]`, never from `addopts`. Use87 `scripts/fix_coverage_threshold.py` to migrate a plugin still using88 the old location.89- The script itself sets no minimum floor for `coverage_threshold`. If90 the key is absent, no `--cov-fail-under` flag is passed at all, so a91 plugin without the key has no coverage gate in the runner.92- imbue's own `addopts` force `--cov=scripts` plus term and HTML93 coverage reports on every run, including single-file runs.9495## The tautological-test trap9697PR review here repeatedly catches tests that assert nothing. Evidence98in history: `a94240e2` (12 tests of constants tightened to behavioral99coverage), `f1cbbcf1` (strengthened tautological assertions),100`30e58586` (validation-floor regression test), `42f7ce84` (round-trip101test replacing a structure-only check). The pattern: a test that102restates the code, mocks the unit under test, or asserts a constant103equals itself.104105The counter-discipline is the revert test, executed by106`sanctum:validate-pr`:1071081. Take the fix the PR claims to make.1092. Edit the fixed line back to its broken state (working tree must be110 clean first, or the step is skipped as unsafe).1113. Run the test that supposedly guards the fix.1124. The test must FAIL against the reverted code. If it stays green,113 the test is a dead assertion, not a guard.1145. Restore the fix.115116When writing a new test, apply the same standard preemptively: write117the test so it would catch the bug's return, then confirm it fails118before the fix lands (that is the Iron Law's RED step).119120## Golden inventory: regression guards worth knowing121122These tests lock in lessons from settled incidents. Do not weaken or123delete them without understanding the incident they guard.124125| Guard | Location | What it locks |126|-------|----------|---------------|127| py39 datetime alias | `plugins/leyline/tests/test_python39_compat.py` | AST-scans leyline source for `datetime.UTC` (a 3.11+ alias). Hooks run under system Python 3.9, and this alias broke the whole hook import chain three-plus times. Ruff UP017 kept auto-reverting manual fixes, so only this AST invariant test holds the line. |128| Lazy-import blocker | `plugins/gauntlet/tests/unit/test_challenges.py` | Installs a `sys.meta_path` blocker that raises on any re-import of `anthropic`, proving gauntlet's heavy deps stay lazily imported. Eager imports made every git commit emit hook ModuleNotFoundError. |129| Hook timeout budget | `plugins/herald/tests/unit/test_double_shot_latte.py` (`test_llm_timeout_fits_within_hook_timeout`) | Asserts `LLM_TIMEOUT_SECONDS` is strictly less than the Stop-hook timeout registered in hooks.json. A timeout above the budget once cost the hook its verdict entirely (full record: night-market-failure-archaeology SB7). |130| Hook stdin contract | `plugins/abstract/tests/hooks/test_hook_io.py` | Locks the input contract of `shared/hook_io.py`: stdin JSON is primary, legacy `CLAUDE_TOOL_*` env vars are fallback only. Env-reading hooks were once silent no-ops for months (full record: night-market-failure-archaeology SB9). |131132When you fix an incident of a similar class, add its guard here in the133same spirit: an invariant test that fails loudly if the lesson is134unlearned.135136## How to add tests137138### Layout and isolation139140- Each plugin owns `plugins/<plugin>/tests/` with `unit/`,141 `integration/` subdirectories and its own `conftest.py` and pytest142 config in the plugin's `pyproject.toml`.143- Root `pyproject.toml` sets `norecursedirs = ["plugins/*", ...]`.144 Plugin tests MUST run per-plugin. Running them from the repo root145 causes ImportPathMismatchError from duplicate module names (the root146 `conftest.py` documents this).147148### Commands149150```bash151# Single test file (fastest loop)152cd plugins/imbue153uv run pytest tests/unit/test_deferred_capture.py -x -q154155# Full suite for one plugin156cd plugins/<plugin>157uv run python -m pytest tests/ --tb=short -q158# or, where the plugin has a Makefile target:159make -C plugins/<plugin> test160161# Everything (what `make test` at root does)162./scripts/run-plugin-tests.sh --all163164# Only plugins with staged changes165./scripts/run-plugin-tests.sh --changed166167# Mutation testing for one plugin, locally168cd plugins/<plugin>169uv run mutmut run --paths-to-mutate=scripts/,src/ --tests-dir=tests/170```171172### Markers173174Root pytest runs with `--strict-markers`, so only registered markers175are legal. Registered in root `pyproject.toml`: `unit`, `integration`,176`e2e`, `slow`, `network`, `plugin`, `skill`, `hook`, `command`, `bdd`,177`benchmark` (skip benchmarks in CI with `-m "not benchmark"`).178179### Checklist for new code180181- [ ] Failing test written and observed RED before implementation182 (Iron Law).183- [ ] Test would fail if the fix were reverted (revert-test standard).184- [ ] Correct marker applied and test placed in `unit/` or185 `integration/` accordingly.186- [ ] Plugin suite passes locally with its coverage flag:187 `uv run python -m pytest tests/ --cov-fail-under=<threshold>`.188- [ ] New skill files have a `test_skill_<name>.py` structural test.189190## Failure-mode detection: silent failure in scanners191192The most recurrent bug class in this repo is the swallowed error. In193scanner-shaped code (anything that walks files and reports findings),194the signature is except-continue: a `try/except` that skips a file on195malformed input and reports success on the remainder. The scanner196looks healthy while ignoring exactly the inputs most likely to be197broken.198199The house convention since commit `666171c3` (issue #575): a scanner200that cannot process an input emits an ADVISORY finding for it instead201of skipping silently. Examples from that commit:202203- `check_hook_modernization.py` emits findings on malformed204 hooks.json, SyntaxError, or OSError instead of dropping the file.205- pensive `harden/scanner.py` appends an ADVISORY finding for206 unreadable files under `--strict`.207- minister `dora_metrics` warns and sets a partial flag on malformed208 tag lines instead of silently classifying the repo Elite.209210When reviewing or writing scanner code, grep for the pattern:211212```bash213rg -n "except .*:\s*$" -A2 plugins/<p>/scripts/ | rg -B1 "continue|pass"214```215216Any hit that discards an error without emitting a finding or an inline217"why it is safe to discard" comment violates Constitution rule 10218(errors are not optional).219220## When NOT to use221222- Running the suites, lint, release, or publish mechanics: use223 `night-market-operations` (command anatomy lives there).224- Classifying and gating a change, PR review flow, non-negotiables:225 use `night-market-change-control`.226- A test is failing and you need triage for a known repo failure mode:227 use `night-market-debugging-playbook`.228- History of why a guard exists (incident narratives, reverts): use229 `night-market-failure-archaeology`.230- Setting up pytest config or fixtures for a new plugin:231 `Skill(leyline:pytest-config)` has the templates, and232 `Skill(leyline:testing-quality-standards)` the anti-pattern catalog.233234## Exit Criteria235236- [ ] Any completion claim made under this skill cites numbered237 evidence (`[E1]`...) with commands and captured output, and carries238 a COMPLETE, PARTIAL, or BLOCKED status.239- [ ] Every new test added in the session fails when its guarded fix240 is reverted (demonstrated, not assumed).241- [ ] New plugin Python code landed with a test observed failing242 first, and the plugin suite passes at or above its243 `[tool.nightmarket]` coverage_threshold.244- [ ] Any new skill file has a passing `test_skill_<name>.py`245 structural test.246- [ ] No scanner code added or reviewed in the session contains an247 except-continue that drops input without an ADVISORY finding or a248 stated reason.249250## Provenance and maintenance251252Compiled 2026-07-02 against repo v1.9.15, branch253discussions-fix-1.9.14. Volatile facts and how to re-verify them:254255- Root coverage floor: `rg -n "fail_under" pyproject.toml`256 (85 as of 2026-07-02).257- Per-plugin thresholds:258 `rg -n "coverage_threshold" plugins/*/pyproject.toml`259 (90 everywhere except gauntlet at 85, as of 2026-07-02).260- Threshold parser behavior: read the awk block in261 `scripts/run-plugin-tests.sh` (search `tool.nightmarket`).262- Mutation cadence and plugin list:263 `rg -n "cron|matrix" .github/workflows/mutation-testing.yml`.264- Quality gate numbers: `cat .claude/quality_gates.json`.265- Golden guards still present:266 `ls plugins/leyline/tests/test_python39_compat.py267 plugins/gauntlet/tests/unit/test_challenges.py268 plugins/herald/tests/unit/test_double_shot_latte.py269 plugins/abstract/tests/hooks/test_hook_io.py`.270- Cited commits: `git log --oneline -1 <hash>` for `29081fda`,271 `a94240e2`, `f1cbbcf1`, `30e58586`, `42f7ce84`, `666171c3`,272 `268cff89`.273- Marker list: `rg -n -A12 "^markers" pyproject.toml`.