You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 a DeprecationWarning from pydantic/PyYAML sails through silently.
Order-randomized tests: add pytest-randomly so inter-test state leaks (registry/tmp-dir bleed-through between isolated_registry consumers) fail loudly. Seed is printed for reproduction.
Per-test timeout: add pytest-timeout with a tight default (e.g. 30s) so a hung subprocess/flock test fails instead of stalling the job for 6h.
-p no:cacheprovider in CI so pass/fail never depends on a stale .pytest_cache.
Mutation testing (the big one)
Add mutmut (or cosmic-ray) over src/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 via schedule:) to keep wall-time sane.
Toolchain & supply-chain strictness
uv lock --check job: fail when pyproject.toml and uv.lock drift (today --frozen installs would fail confusingly instead of pointing at the lockfile).
ruff format --check in 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, missing permissions:). 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 run beetroot --help. Catches missing templates/compose.yaml wheel-data regressions that editable installs hide (paths.bundled_compose_file() via importlib.resources is 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 style with targeted disables.
Environment matrix
Run the test job on a matrix: ubuntu-latest + macos-latest (researchers run the CLI on macOS; fcntl.flock, path handling, and subprocess behavior differ) × Python 3.13 + 3.14-dev (early warning, continue-on-error: true for 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 on PATH that records argv and replays canned compose ps JSON. Drive create → apply → up → status → down → destroy and assert on the artifacts: the rendered .env dict, 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.yaml inputs, render .env and run docker 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, so render_env()/template drift fails CI (extends test_compose_template_envs.py).
Property-based expansion (Hypothesis): grow test_property_ports.py / test_property_registry.py / test_property_render_env.py into: 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 exact fix/ports-resolver-self-collision class of bug).
Adversarial config corpus: a tests/corpus/ of hostile beetroot.yaml files — 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 (extends test_cli_error_contract.py).
Concurrency tests: real multi-process fcntl.flock contention on the registry (N processes racing create/destroy; assert no lost updates, no duplicate port indexes) — extends test_registry_race.py from threads to processes.
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 cover added.
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:.
Summary
Priority: HIGH. The current CI (
.github/workflows/ci.yml) is already a solid baseline — ruff, mypystrict+ 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-portsshipped 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
filterwarnings = ["error"]to[tool.pytest.ini_options](allowlist specific upstream deprecations individually, with a comment each). Today aDeprecationWarningfrom pydantic/PyYAML sails through silently.pytest-randomlyso inter-test state leaks (registry/tmp-dir bleed-through betweenisolated_registryconsumers) fail loudly. Seed is printed for reproduction.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)
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).${{ }}interpolation, missingpermissions:). The workflow files are load-bearing and currently unlinted.pyproject.toml.src/,docs/,README.md,CHANGELOG.md.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).src/beetroot/templates/compose.yaml,.github/workflows/*.yml,examples/.docker/*.sh; consider raising shellcheck to-S stylewith targeted disables.Environment matrix
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).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: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.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).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).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).fcntl.flockcontention on the registry (N processes racingcreate/destroy; assert no lost updates, no duplicate port indexes) — extendstest_registry_race.pyfrom threads to processes.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(A single failingmagisk --install-moduleaborts boot underset -e, so the container exits without launching Frida #13,magisk-config.shwaits for the Magisk daemon in an unbounded loop with no timeout #14 are exactly this class of bug — these tests would have caught both), env-var override paths (BEETROOT_*) are honored.data/, re-rendered.envports, registry row, and manifest schema (pydantic) — across legacy api_version inputs.--helpoutput for every verb so flag renames (--as→--name, seesnapshot restoreerror messages tell the user to use the deprecated--asflag instead of--name#16) can't silently leave stale strings; grep-test that error messages never reference deprecated flags.Acceptance criteria
main; every third-party action SHA-pinned (existing T3 policy).filterwarnings = error, random order, and timeouts active with zero blanket suppressions.CLAUDE.md.# pragma: no coveradded.CLAUDE.md"Development workflow"/CI section updated to describe the new gates (docs are part of every feature).Notes
schedule:.