test(acl): ACL mutation-score baseline with Stryker.NET (closes #47) #52

Merged
not merged 7 commits from feat/47-acl-mutation-baseline into main 2026-06-29 09:00:30 +00:00
Contributor

What & why

S-04b finishes the last acceptance criterion split off S-04 (#5): a captured, enforced mutation-score baseline for the ACL. CLAUDE.md §3/§5 make "mutation (ratchet)" a Definition-of-Done gate for every slice, but no baseline existed — so this establishes the repo-wide one.

  • Stryker.NET pinned as a local dotnet tool (.config/dotnet-tools.json, v4.15.0) — reproducible locally and in CI, no global install.
  • Solution mode on Acl.slnx (services/acl/stryker-config.json): mutates Acl.Application + Acl.Infrastructure; the untested Acl.Api is skipped.
  • make mutation target, wired into make ci and a parallel Gitea Actions mutation job.
  • Baseline: Stryker exposed thin ACL tests (35%). Strengthened them honestly — CRS headers, null guards, error/empty-body paths, JWT header+claims decode — raising the score to 95%. Enforced break threshold 90% (one-mutant headroom).
  • ADR-0005 records the decision (proposed in #51).

Closes #47
Closes #51 (the adr-proposal — ADR-0005 lands in this PR)

Definition of Done

  • Linked Gitea issue (above).
  • Failing test committed before the implementation. (See notes — TDD nuance for a tooling slice.)
  • Implementation makes the test pass; refactor commit if structure improved.
  • Conventional Commits referencing the issue (refs #NN).
  • CI green — make ci (lint, build, unit, mutation) green locally; compose-smoke unchanged by this slice and CI-gated.
  • docker compose up from a fresh clone reaches green health checks within 3 minutes. (Unchanged — no infra touched.)
  • Docs updated if behaviour, contracts, or operations changed. (ADR-0005, docs/runbooks/ci.md, docs nav.)
  • ADR added in docs/architecture/ if a non-obvious decision was made. (ADR-0005.)
  • Demo note in docs/demo-script.md if user-visible. (N/A — not user-visible.)

Notes for reviewers

  • TDD nuance: a CI/tooling slice has no natural xUnit red→green. The "red" was the mutation run itself — 35% with 12 survivors; the "green" is killing them to 95% (commit 10816f5) and the ratchet (break 90%) failing CI on regression. Tests were strengthened, not padded.
  • Ratchet proven to bite: break 90% → Stryker exits 0 at 95%; a temp floor of 96% exits non-zero with "score below threshold break". Later slices ratchet up, never down (§5).
  • One accepted survivor: a mutation of the empty-response exception message string — an equivalent mutant (message-text assertions are brittle; type + control flow unchanged).
  • Sibling follow-up: #46 (S-04a, real-OpenZaak Testcontainers integration test) remains open.

🤖 Generated with Claude Code

## What & why S-04b finishes the last acceptance criterion split off S-04 (#5): a captured, enforced **mutation-score baseline** for the ACL. CLAUDE.md §3/§5 make "mutation (ratchet)" a Definition-of-Done gate for every slice, but no baseline existed — so this establishes the repo-wide one. - **Stryker.NET** pinned as a local dotnet tool (`.config/dotnet-tools.json`, v4.15.0) — reproducible locally and in CI, no global install. - **Solution mode** on `Acl.slnx` (`services/acl/stryker-config.json`): mutates `Acl.Application` + `Acl.Infrastructure`; the untested `Acl.Api` is skipped. - `make mutation` target, wired into `make ci` **and** a parallel Gitea Actions `mutation` job. - **Baseline:** Stryker exposed thin ACL tests (35%). Strengthened them honestly — CRS headers, null guards, error/empty-body paths, JWT header+claims decode — raising the score to **95%**. Enforced `break` threshold **90%** (one-mutant headroom). - **ADR-0005** records the decision (proposed in #51). Closes #47 Closes #51 (the adr-proposal — ADR-0005 lands in this PR) ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation. *(See notes — TDD nuance for a tooling slice.)* - [x] Implementation makes the test pass; refactor commit if structure improved. - [x] Conventional Commits referencing the issue (`refs #NN`). - [x] CI green — `make ci` (lint, build, unit, mutation) green locally; `compose-smoke` unchanged by this slice and CI-gated. - [x] `docker compose up` from a fresh clone reaches green health checks within 3 minutes. *(Unchanged — no infra touched.)* - [x] Docs updated if behaviour, contracts, or operations changed. *(ADR-0005, `docs/runbooks/ci.md`, docs nav.)* - [x] ADR added in `docs/architecture/` if a non-obvious decision was made. *(ADR-0005.)* - [ ] Demo note in `docs/demo-script.md` if user-visible. *(N/A — not user-visible.)* ## Notes for reviewers - **TDD nuance:** a CI/tooling slice has no natural xUnit red→green. The "red" was the mutation run itself — 35% with 12 survivors; the "green" is killing them to 95% (commit `10816f5`) and the ratchet (`break` 90%) failing CI on regression. Tests were strengthened, not padded. - **Ratchet proven to bite:** `break` 90% → Stryker exits 0 at 95%; a temp floor of 96% exits non-zero with "score below threshold break". Later slices ratchet **up**, never down (§5). - **One accepted survivor:** a mutation of the empty-response *exception message string* — an equivalent mutant (message-text assertions are brittle; type + control flow unchanged). - **Sibling follow-up:** #46 (S-04a, real-OpenZaak Testcontainers integration test) remains open. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
not added 5 commits 2026-06-25 13:01:24 +00:00
Add a tool manifest pinning dotnet-stryker 4.15.0 so `make mutation` runs
the same mutation tester locally and in CI from a fresh clone (`dotnet tool
restore`), with no global install. Ignore the generated StrykerOutput/ report
directory.

Refs #47.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stryker exposed thin ACL tests (35% mutation score): the suite never asserted
the geo CRS headers, the ArgumentNullException guards, the non-success and
empty-body error paths, or the structure of the minted ZGW JWT — so mutating
any of those survived.

Strengthen the unit tests to kill those mutants:
- assert Accept-Crs / Content-Crs are EPSG:4326,
- assert OpenZaakAsync rejects a null request/registration without calling out,
- assert a non-2xx response throws and an empty body throws InvalidOperationException,
- decode the Bearer token and assert the HS256 header + acl identity claims.

Raises the ACL mutation score to 95%. The one remaining survivor mutates only
the exception *message* text (an equivalent mutant — message strings are not
worth a brittle assertion).

Refs #47.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Configure Stryker.NET for the ACL in solution mode (Acl.slnx), so both
Acl.Application and Acl.Infrastructure — the two projects under test — are
mutated while Acl.Api (untested) is skipped. Record the repo-wide mutation
baseline as the ratchet (CLAUDE.md §5): observed score 95%, enforced break
threshold 90% (one-mutant headroom over the ~20-mutant surface). The ACL is the
first service with branching logic, so it sets the baseline; later slices
ratchet it up deliberately, never down.

Add a `mutation` make target (`dotnet tool restore` + `dotnet stryker`) and wire
it into the `make ci` aggregate, keeping `make ci` an exact mirror of the
pipeline.

Refs #47.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `mutation` job mirroring the unit job (checkout + pinned setup-dotnet,
then `make mutation`). It runs in parallel with lint/build/unit/compose-smoke
and gates merges on the ACL mutation baseline (CLAUDE.md §5/§15). The job calls
the same make target developers run, so the pipeline stays a mirror of `make ci`.

Refs #47.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
arch(acl): ADR-0005 adopt Stryker.NET for mutation testing (refs #47)
All checks were successful
CI / lint (pull_request) Successful in 51s
CI / build (pull_request) Successful in 42s
CI / unit (pull_request) Successful in 45s
CI / mutation (pull_request) Successful in 1m24s
CI / compose-smoke (pull_request) Successful in 4m1s
7ecc184111
Record the decision to adopt Stryker.NET (pinned local tool, solution mode on
Acl.slnx) and to set the first repo-wide mutation baseline on the ACL: observed
95%, enforced break threshold 90%. Document the ratchet, local run, and report
location in the CI runbook; add the ADR to the docs nav.

Proposed in #51 (adr-proposal). Refs #47.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added this to the Iteration 1 — Walking Skeleton milestone 2026-06-25 13:01:37 +00:00
not added the type:slicearea:acl labels 2026-06-25 13:01:48 +00:00
not added 1 commit 2026-06-25 13:21:29 +00:00
ci(acl): publish the Stryker HTML report as a CI artifact (refs #47)
Some checks failed
CI / lint (pull_request) Successful in 52s
CI / build (pull_request) Successful in 41s
CI / unit (pull_request) Successful in 47s
CI / mutation (pull_request) Failing after 1m50s
CI / compose-smoke (pull_request) Successful in 3m55s
347713766e
Add an upload-artifact step to the mutation job so the ACL mutation report is
downloadable from the run summary. `if: always()` uploads it even when the
ratchet fails — exactly when the survivors matter. A glob handles Stryker's
timestamped output directory. First use of actions/upload-artifact (@v4, pinned);
Gitea 1.25.x supports it. Document it in the CI runbook.

Refs #47.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not added 1 commit 2026-06-25 13:28:10 +00:00
fix(ci): pin upload-artifact to @v3 — @v4 refuses to run on Gitea (refs #47)
All checks were successful
CI / lint (pull_request) Successful in 50s
CI / build (pull_request) Successful in 46s
CI / unit (pull_request) Successful in 43s
CI / mutation (pull_request) Successful in 1m49s
CI / compose-smoke (pull_request) Successful in 4m2s
5f3dd31925
The artifact step failed the mutation job: upload-artifact@v4 bundles
@actions/artifact v2, which hard-aborts on any non-github.com server ("not
supported on GHES"), even though Gitea 1.25 stores artifacts fine. @v3 uses the
older protocol Gitea speaks and has no GHES guard — a drop-in swap (same inputs).
Document it as gotcha §4 and correct the CI runbook note.

Refs #47.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
not merged commit fd5fa5ac3c into main 2026-06-29 09:00:30 +00:00
Sign in to join this conversation.