CI hardening v2: drastically stricter gates + massive test-suite expansion
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Summary
Priority: HIGH. The current CI (.github/workflows/ci.yml) is already a solid baseline — ruff, mypy strict + pyright, 100% line+branch coverage, interrogate, radon, vulture, pip-audit, shellcheck, hadolint, docker smoke build, docs --strict, SHA-pinned actions. This task is the next ratchet: close the remaining strictness gaps and massively expand the test suite beyond line coverage into behavior, property, mutation, and environment coverage.
Per CLAUDE.md, 100% line+branch coverage is necessary but not sufficient — feat/configurable-ports shipped a real bug at 100% coverage. The theme of this task: gates that catch bugs coverage can't.
Part 1 — Stricter CI gates
Test-runner strictness
- Warnings as errors: add
filterwarnings = ["error"]to[tool.pytest.ini_options](allowlist specific upstream deprecations individually, with a comment each). Today aDeprecationWarningfrom pydantic/PyYAML sails through silently. - Order-randomized tests: add
pytest-randomlyso inter-test state leaks (registry/tmp-dir bleed-through betweenisolated_registryconsumers) fail loudly. Seed is printed for reproduction. - Per-test timeout: add
pytest-timeoutwith a tight default (e.g. 30s) so a hungsubprocess/flock test fails instead of stalling the job for 6h. -
-p no:cacheproviderin CI so pass/fail never depends on a stale.pytest_cache.
Mutation testing (the big one)
- Add
mutmut(orcosmic-ray) oversrc/beetroot/as a CI job. Start with a survival-rate threshold on the most logic-dense modules (ports.py,config.py,registry.py,snapshot.py), ratchet downward over time. Mutation score is the only automated gate that distinguishes "executed by a test" from "asserted on by a test". Run it on PRs against changed files only (or nightly full-run viaschedule:) to keep wall-time sane.
Toolchain & supply-chain strictness
-
uv lock --checkjob: fail whenpyproject.tomlanduv.lockdrift (today--frozeninstalls would fail confusingly instead of pointing at the lockfile). -
ruff format --checkin CI (formatting is currently "optional; no hard policy" — make it a hard policy so diffs stop churning). - actionlint + zizmor jobs: lint the workflows themselves (expression typos, injection-prone
${{ }}interpolation, missingpermissions:). The workflow files are load-bearing and currently unlinted. - deptry: fail on undeclared/unused dependencies in
pyproject.toml. - codespell over
src/,docs/,README.md,CHANGELOG.md. - Packaging gate:
uv build+twine check dist/*+ install the built wheel into a clean venv and runbeetroot --help. Catches missingtemplates/compose.yamlwheel-data regressions that editable installs hide (paths.bundled_compose_file()viaimportlib.resourcesis exactly the kind of thing that breaks only in the wheel). - yamllint on
src/beetroot/templates/compose.yaml,.github/workflows/*.yml,examples/. - shfmt -d alongside shellcheck for
docker/*.sh; consider raising shellcheck to-S stylewith targeted disables.
Environment matrix
- Run the
testjob on a matrix:ubuntu-latest+macos-latest(researchers run the CLI on macOS;fcntl.flock, path handling, andsubprocessbehavior differ) × Python 3.13 + 3.14-dev (early warning,continue-on-error: truefor the dev lane). - Upload coverage as a job artifact and add a
coverage xml+ diff-cover step so PR review sees exactly which changed lines each test exercises.
Part 2 — Massive test-suite expansion
The suite has ~52 files and 100% coverage; expansion targets composition and adversarial input, per the "Behavior tests, not just line coverage" rule in CLAUDE.md:
- End-to-end verb flows against a fake
docker: a stub executable onPATHthat records argv and replays cannedcompose psJSON. Drivecreate → apply → up → status → down → destroyand assert on the artifacts: the rendered.envdict, the exact compose argv (-p,-f,--project-directory,--env-file), the registry JSON, the freed port index. One test per verb-chain, not per function. - Compose template render validation: for a spread of
beetroot.yamlinputs, render.envand rundocker compose config(or a pure-Python${VAR}substitution check in unit scope) asserting every${VAR}in the template has a binding and no binding is orphaned — both directions, sorender_env()/template drift fails CI (extendstest_compose_template_envs.py). - Property-based expansion (Hypothesis): grow
test_property_ports.py/test_property_registry.py/test_property_render_env.pyinto: arbitrary interleavings of allocate/free on the port allocator (no collisions, lowest-free-reuse invariant); snapshot pack→unpack round-trip equality for arbitrary valid configs; YAML→InstanceConfig→.env→ no port self-collisions for arbitrary partial overrides (the exactfix/ports-resolver-self-collisionclass of bug). - Adversarial config corpus: a
tests/corpus/of hostilebeetroot.yamlfiles — wrong types, api_version 1/2/3/5/garbage, unicode names, path-traversal instance names, 10k-entry denylists, duplicate keys — asserting every one yields a friendlyerror: ...+ exit 1, never a traceback (extendstest_cli_error_contract.py). - Concurrency tests: real multi-process
fcntl.flockcontention on the registry (N processes racingcreate/destroy; assert no lost updates, no duplicate port indexes) — extendstest_registry_race.pyfrom threads to processes. - Boot-script tests: lift the
docker/*.shhelpers into a harness that runs them undershwith a stubbedmagisk/getproponPATH(POSIX-sh compatible, no Docker needed). Assert: bounded-wait behavior, a failing module install doesn't abort the entrypoint underset -e(#13, #14 are exactly this class of bug — these tests would have caught both), env-var override paths (BEETROOT_*) are honored. - Snapshot/restore round-trip suite: pack → mutate nothing → restore under a new name → assert byte-identical
data/, re-rendered.envports, registry row, and manifest schema (pydantic) — across legacy api_version inputs. - CLI help/UX contract tests: snapshot-test
--helpoutput for every verb so flag renames (--as→--name, see #16) can't silently leave stale strings; grep-test that error messages never reference deprecated flags.
Acceptance criteria
- All new CI jobs green on
main; every third-party action SHA-pinned (existing T3 policy). -
filterwarnings = error, random order, and timeouts active with zero blanket suppressions. - Mutation-score gate wired (changed-files on PR or nightly full) with a documented threshold + ratchet plan in
CLAUDE.md. - Test count and assertion depth meaningfully expanded per Part 2 (each checkbox shipped as its own reviewed slice — this issue can be executed as a series of small PRs).
- 100% line+branch coverage still holds; no
# pragma: no coveradded. -
CLAUDE.md"Development workflow"/CI section updated to describe the new gates (docs are part of every feature).
Notes
- Keep CI wall-time in check: new lint-ish jobs are cheap and parallel; mutation testing is the only expensive one — scope it to changed files on PRs and full-run on
schedule:. - Out of scope: booting real redroid in CI (tracked separately in #6, blocked on a KVM-capable runner story).
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Begin with .github/workflows/ci.yml, pyproject.toml, and CLAUDE.md; use the existing suite and named tests such as test_compose_template_envs.py, test_registry_race.py, and test_cli_error_contract.py to map one checkbox into a separate slice. Validate the selected gate or behavior, keep relevant actions SHA-pinned, and update CLAUDE.md. Completing the issue requires all acceptance criteria, including green CI and preserved coverage.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, github-actions, python, shell
- Domain
- build-system, ci-cd, devops, documentation, testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100