diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 78c1205..9b43dbd 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -474,6 +474,68 @@ If a workflow's behaviour genuinely depends on a feature only present from a particular commit onwards, express that as a comment or a changelog note, not as a test assertion on the SHA string. +## Mutation-testing workflow contract tests + +This repository runs scheduled, informational mutation testing through a thin +caller workflow, [`.github/workflows/mutation-testing.yml`](../.github/workflows/mutation-testing.yml), +which delegates to the shared reusable workflow +`leynos/shared-actions/.github/workflows/mutation-mutmut.yml`. The heavy +lifting — running `mutmut` and summarizing survivors — lives in +`shared-actions`; this repository carries only declarative configuration. The +run is **informational only**: it never gates a pull request. Survivors are +reported through the job summary and downloadable artefacts so they can be +triaged into tests, not enforced as a blocking check. The mutation targets and +test selection themselves are configured in `[tool.mutmut]` in +`pyproject.toml` (`source_paths`, `do_not_mutate`, +`pytest_add_cli_args_test_selection`). + +The workflow runs in two modes. A **daily schedule** fires a change-scoped run +that mutates only the source files touched within the detection window, so +quiet days are cheap no-ops. A **manual dispatch** (the Actions "Run workflow" +control) mutates the whole package; select a branch in that control to +exercise a feature branch. + +The caller passes two configuration inputs: + +- `paths` — set to `hooks/`, the change-detection glob bounding scheduled runs + to the repository's only importable product code (the Stop-hook script and + its co-located tests). +- `module-prefix-strip` — set to `""`, because the flat `hooks/` layout means + changed-file paths already map to module globs unaltered, with no package + prefix to strip. + +The repository does not set `exclude-globs` or `extra-args`; both default in +the shared workflow. + +The `uses:` reference pins the shared workflow to a full 40-character commit +SHA rather than a branch or tag, so a force-push upstream cannot silently +change what runs here. The contract test asserts only that the pin is a full +commit SHA, not a particular value, so Dependabot bumps it automatically +without any accompanying test edit. + +Because the caller is configuration rather than code, `tests/test_workflow_contract.py` +pins the shape it must uphold, failing the pull request when the caller drifts +— repointing the pin at a branch, widening the token scope, or dropping a +configuration input — rather than letting the breakage surface only in a +scheduled run. The test module self-skips when the workflow file is absent +(`pytestmark = pytest.mark.skipif(not WORKFLOW_PATH.exists(), ...)`), because +`mutmut` copies sources into a `mutants/` sandbox that omits `.github/`, and +the contract test would otherwise fail there for the wrong reason. Run it +locally with +`uv run --group dev python -m pytest tests/test_workflow_contract.py -v`, or +as part of the full suite via `make test`. The test validates: + +- the `uses:` reference targets `mutation-mutmut.yml` pinned to a full commit + SHA; +- the `with:` block carries exactly `paths: hooks/` and + `module-prefix-strip: ""`, nothing more and nothing less; +- job permissions are least-privilege (`contents: read`, `id-token: write`) + and the workflow-level default token scope is empty; +- `concurrency` serializes runs per ref (`mutation-testing-${{ github.ref }}`) + without cancelling one in progress; and +- the triggers keep the daily schedule (`50 12 * * *`) and a plain + `workflow_dispatch` with no inputs. + ## Validation expectations When changing bootstrap behaviour in this repository, replay the usual