-
Notifications
You must be signed in to change notification settings - Fork 0
TESTING
Analysis Date: 2026-08-15
-
pytest
>=8, configured inpyproject.toml[tool.pytest.ini_options] - All dev dependencies declared under
[project.optional-dependencies].dev— never rely on a developer's PATH -
pytest-randomlyrandomises test order every run; every count the suite reports rests on it being deterministic, and order-dependence stays visible this way -
hypothesis >=6for property-based tests -
pytest-covfor coverage;diff-coverfor branch-restricted coverage -
mutmutfor mutation testing
tests/
├── conftest.py # shared fixtures, live-verification accounting, FORCE_COLOR pop
├── support.py # shared helpers: podman gate decorator, patch_all()
├── fixtures/ # static fixture files used by multiple test modules
├── test_schema.py # schema validators, parse logic
├── test_launcher_*.py # launcher sub-systems (build, install, init, scan, …)
├── test_emit.py # profile assembly output
├── test_paths.py # path resolution
└── … # ~100 test files total, >2200 tests
tests/ is added to pythonpath in pyproject.toml, making support and conftest importable as top-level modules by the suite. Pyright also receives this path via extraPaths = ["tests"].
Always use the wrapper script. Never hand-compose mise/uv/pytest.
tools/run-tests.sh # whole suite, quiet
tools/run-tests.sh tests/test_schema.py # one file
tools/run-tests.sh -k install -x # filter, stop on first failureThe script exists because three independent traps make the suite fail locally while CI stays green:
-
Per-branch venvs.
mise.tomlsetsUV_PROJECT_ENVIRONMENTto~/.local/share/harnessed/venvs/<branch>/.venv— outside the repo, unreachable from a fresh worktree.run-tests.shrunsuv sync --extra devfirst, which is a no-op when already in sync. -
--extra devis mandatory. A plainuv syncomits pytest.uv run pytestthen silently falls back to a system pytest on a different Python, where every test errorsModuleNotFoundError: No module named 'harnessed'. -
mise trustin fresh worktrees. The script runsmise trust(a no-op once trusted) before any command.
Record the baseline test count before making changes. A count drop is a regression even if new tests pass.
A green run is not end-to-end proof. The suite runs no podman build and no harnessed container-run; those require the live layer.
Tests that exercise real podman or external binaries are gated behind HARNESSED_PODMAN=1. Use the support.podman decorator — do not write the skipif inline:
# tests/support.py
def podman(func):
"""Gate a test on `HARNESSED_PODMAN=1` and mark it as one the gate governs."""
gated = pytest.mark.skipif(
not PODMAN_REQUESTED, reason="set HARNESSED_PODMAN=1 for live podman tests"
)(func)
return pytest.mark.live_podman(gated)The live_podman marker is what allows the run's accounting in conftest.py to be honest. When HARNESSED_PODMAN=1 is set but the gate-governed tests still skip (e.g., image not built), conftest.pytest_sessionfinish sets exitstatus = 1 — asking for live verification and silently delivering none is treated as a failure.
HARNESSED_PODMAN=1 tools/run-tests.sh # run with live podman layerCI runs the two layers in separate workflows — do not read the hermetic one as "CI never runs podman":
-
.github/workflows/test.ymlis hermetic by design. No podman on the runner, so everyHARNESSED_PODMAN-gated test skips. -
.github/workflows/live.ymlis the live layer and does setHARNESSED_PODMAN: "1"(live.yml:154). It runs on push tomain, on a nightly0 4 * * *cron, and on manual dispatch — but deliberately not onpull_request, becausepodman buildminutes on every PR would get the job muted, and a disabled check verifies as little as a skipped one.
The terminal summary always reports how many live tests did NOT run.
Provides:
-
pytest_configure: registersliveandlive_podmanmarkers -
pytest_terminal_summary: prints which live tests were skipped and why -
pytest_sessionfinish: fail-closed guard whenHARNESSED_PODMAN=1was set -
catalog_local_restored(checkout): context manager that snapshots and restorescatalog-local/symlinks after a test; lives here (notsupport.py) becausetest_live_gate_accountingcopies conftest verbatim into apytestersandbox wheresupportis not importable - Module-level
os.environ.pop("FORCE_COLOR", None): must run before any module that constructs arich.Console; a fixture runs too late
# Shared helpers
def podman(func) -> func: # gate + mark decorator for live tests
def patch_all(monkeypatch, name, value) -> None: # patch a name in ALL loaded harnessed.* modulespatch_all is the correct way to monkeypatch any helper called from more than one harnessed module. A from .x import y binds y into the importing module's globals; patching only one module leaves the real binding live in every other. patch_all raises if no loaded module binds the name, so a typo or rename fails loudly.
# tests/test_launcher_*.py — typical usage
support.patch_all(monkeypatch, "_host_os", lambda: "darwin")Static files (YAML manifests, Dockerfiles, config seeds) consumed by multiple test modules.
Tests are grouped into classes when they share a subject or setup:
# tests/test_schema.py
class TestValidateNoRawNpm:
def test_clean_recipe_passes(self):
r = _make_recipe()
validate_no_raw_npm(r) # must not raise
def test_npm_command_raises(self):
r = _make_recipe(servers=[McpServer(name="s", command="npm", args=["install"])])
with pytest.raises(RecipeLintError, match="pnpm"):
validate_no_raw_npm(r)
def test_npx_command_raises(self):
r = _make_recipe(servers=[McpServer(name="s", command="npx", args=["some-pkg"])])
with pytest.raises(RecipeLintError, match="pnpm dlx"):
validate_no_raw_npm(r)Standalone tests (not in a class) are used for single-concern checks that do not benefit from grouping.
-
monkeypatch(pytest built-in) for attribute and env-var patching -
support.patch_all(monkeypatch, name, value)when a helper is imported across multiple modules - No dedicated mock library by convention; prefer direct substitution via monkeypatch. One deliberate exception:
tests/test_persist_mounts.pyusesunittest.mock
Use the pytest tmp_path fixture to build minimal catalog trees for schema and path tests. The catalog lookup functions accept an explicit root argument precisely to enable this:
# tests/test_schema.py
def test_minimal_stack_loads(self, tmp_path):
(tmp_path / "stacks" / "mystack").mkdir(parents=True)
(tmp_path / "stacks" / "mystack" / "stack.yaml").write_text("name: mystack\n")
stack = load_stack("mystack", root=tmp_path)
assert stack.name == "mystack"mutmut is configured in pyproject.toml [tool.mutmut]:
paths_to_mutate = ["src/harnessed/"]
tests_dir = ["tests/"]
also_copy = ["tests/", ".github/", "catalog/", "tools/", "schemas/", "mise.toml", "README.md"]also_copy is wide because many tests assert against repo assets (workflow YAML, catalog files, shell scripts under tools/). Two known limitations:
- The
src/harnessed/catalogsymlink does not exist in mutmut's tree — exportHARNESSED_DIR=$PWDwhen running mutmut. - mutmut dereferences symlinks, so tests asserting a path IS a symlink fail in the mutants tree — narrow the run to the relevant test files.
Targeted invocation:
HARNESSED_DIR=$PWD mise exec -- uv run --extra dev mutmut run "*<function>*"-
pytest-covcollects coverage during the normal test run -
diff-coverrestricts the coverage report to lines changed on the current branch — the relevant number for a PR, not the whole-codebase percentage
tests/conftest.py pops FORCE_COLOR at module import time (before any rich.Console is constructed). Never "fix" a plain-text-vs-ANSI assertion failure by editing the assertion — it means the environment is wrong. The test must run in an environment where FORCE_COLOR is unset, which run-tests.sh guarantees.
-
podman build— no container images are built -
harnessed container-run— no end-to-end launches - These require
HARNESSED_PODMAN=1and a usable podman installation. The hermetic workflow (test.yml) deliberately does not set it;live.ymldoes, post-merge and nightly — so they are unexercised per-PR, not unexercised outright
Start Here
Guides
- Recipe authoring
- Service authoring
- Stacks
- Extending stacks (proposed)
- Recipe catalog
- System prompt & rules (proposed)
- Secrets
- AWS SSO
- Pulumi (host login forwarding)
- Egress & exposing services
- Container filesystem
- Git hooks
- Troubleshooting
- Pin management (harnessed update)
Codebase Map
Planning & Roadmap
- open work: GitHub Issues
Research & Prompts
- research/ (home-folder requirements per harness, browse in-repo)
- prompts/ (reusable prompt templates, browse in-repo)