Repository Testing
This is the canonical maintainer guide to the repository's tests, validators, suites, and benchmark interfaces.
For pipeline architecture, exact-SHA debugging, platform-specific pitfalls, historical failure lessons, and performance baselines, see the Continuous Integration Handbook.
Binding test-code requirements, smells, and remedies live in the Code Test Design and Performance Standard. For performance work, use the code-test optimization playbook. Historical campaign measurements and rejected approaches are preserved in the 2026-08 cleanup retrospective.
Tests and validators
Tests and validators are both evidence about nodes, but they answer different questions. Any node may be covered by either or both. A test or validator's location follows its ownership scope, not the node type.
- A test runs a scenario and asserts an observable result. Tests cover behavior, integrations, and the behavior of validators themselves.
- A validator deterministically checks a repository or node artifact for conformance with a rule and reports each finding. The rule belongs to its standard or contract; the validator enforces that rule but does not define it.
Validators also need focused tests. For example,
validators/portable_dates.py is a validator, while
tests/validate_portable_dates.py tests its accepted and rejected cases. The
validate_*.py test filename does not make a file a repository validator.
The current repository runner discovers validator implementations from two central locations:
| Location | Scope | Canonical ID |
|---|---|---|
validators/<name>.py |
Repository-wide conformance | repo/<name> |
validators/skill/<name>.py |
Shared skill-system conformance | skill-maker/<name> |
Tests live with the narrowest scope that owns the scenario:
| Location | Scope |
|---|---|
tests/ |
Repository-wide, integration, and validator tests |
hooks/tests/ |
Git and assistant lifecycle hooks |
skills/<skill>/tests/ |
A skill's instruction gateway and declared interface contract |
skills/<skill>/_rtx/tests/ |
A skill's private runtime behavior |
src/officina/rutter/tests/ |
Officina Rutter behavior |
These paths describe current collection. They are not a claim that tests or validators belong only to particular node types.
Commands
Run the staged local gate used by the pre-commit hook:
python3 repo_checks.py --suite precommit
Run every validator and functional test, with Chrome-backed tests and performance thresholds isolated from the pooled phase:
python3 repo_checks.py --suite full --verbose
Other public suites are:
python3 repo_checks.py --suite validators
python3 repo_checks.py --suite tests
python3 repo_checks.py --suite pre-push
python3 repo_checks.py --suite portability
Use --jobs N to choose the pytest-xdist worker count. The default is two
thirds of the machine's logical CPUs, with a minimum of one. Requests above one
require pytest-xdist.
Collection and execution
repo_checks.py is the only repository-check entry point.
src/officina/repository/checks/runner.py owns suite policy, repository views, pytest
arguments, and validator integration. pytest.ini owns ordinary discovery:
- roots:
tests/,hooks/tests/,skills/,src/officina/rutter/tests/, andvalidators/; - file names:
test_*.pyandvalidate_*.py; - import mode: pytest
importlibmode; - excluded template:
skills/initialize-tdd/assets/python/tests/.
The custom plugin adapts repository validators into pytest function items for scheduling, reporting, and fixture injection. This does not make them tests. Pytest's default collector contributes the test items. When a suite includes both, validator and test items enter the same xdist queue and consume one worker budget. The runner does not maintain a second inventory of test directories.
Suites
| Suite | Repository view by default | Contents |
|---|---|---|
validators |
working | All selected repository validators. |
tests |
working | Browser-free functional tests, performance thresholds serially, then Chrome-backed tests serially. |
precommit |
staged | Validators and the fast functional selection in one pytest invocation. |
pre-push |
working | Validators and functional tests except docstring and performance tests. |
portability |
working | Seven cross-platform boundary sentinels. |
full |
working | Performance thresholds serially, then validators and browser-free functional tests together, then Chrome-backed tests serially. |
The precommit selection excludes Chrome tests, docstring tests, performance thresholds, the docstring validator, and reviewed expensive integration tests that do not belong in the fast staged gate.
The full suite runs tests/test_dispatcher_performance.py first and keeps
Chrome-backed modules in a later separate single-worker invocation. Prior
repository load invalidates the calibrated performance thresholds, while
Chrome's virtual-time completion is unreliable under pooled repository load.
No suite uses global pytest fail-fast. A failure does not cancel already queued
items or later declared phases. The isolated browser task uses --maxfail=1
because later Chrome cases are not useful evidence after its first failure.
Repository views
Every run containing validators uses one internally consistent source tree for its validator and test items:
precommituses an exact temporary mirror of the Git index;- other validator-bearing suites use the working tree by default;
--repository-view stagedand--repository-view workingoverride that default for runs containing validators;- test-only suites and tasks always execute from the working tree;
- CI's clean checkout already represents the commit under test.
Unstaged and untracked files are absent from the staged mirror. This means a new implementation or test must be staged before the canonical precommit command can exercise it. Manual working-view validators may report untracked logs, build artifacts, or scratch files.
The runner places Python bytecode and pytest caches outside the execution tree. This permits normal bytecode reuse without modifying the staged mirror.
Parallel execution
Pytest-xdist is the only worker pool. The repository runner does not schedule a second layer of test processes.
- Browser-free parallel phases use
--dist worksteal. - Chrome-backed tests run in their own one-worker phase; no second lock or xdist grouping layer is used.
- A one-worker run omits xdist arguments entirely.
The hidden --sequential option is a deprecated compatibility alias. It does
not select a different runner implementation. CI retains it temporarily while
the simplified route is certified on Linux, macOS, and Windows.
Selection and timing interfaces
Maintainers may repeat --validator ID or --exclude-validator ID for suites
that contain validators. The private stable phase identifiers used by the
benchmark harness are validators, tests:shared, tests:performance, and
tests:browser. CI runs the complete browser behavior suite on Ubuntu and in
a dedicated one-worker Windows shard. macOS gates validators, shared tests,
performance invariants, portability, keyring, and scheduler behavior, but not
Chrome rendering: the hosted macOS Chrome CLI renders correctly and then fails
to terminate reliably, so treating its timeout as success is not an acceptable
browser gate.
--timing-output PATH writes schema-version-1 JSON containing task wall time
and pytest's per-file setup, call, and teardown totals. These totals do not
include collection, controller startup, or unattributed scheduler overhead.
Local hook
The pre-commit hook runs the staged precommit suite after its generation and
secret-scanning steps. The pre-push hook runs the working-tree pre-push
suite. See Repository Git Hooks for activation,
exact ordering, generated-file side effects, targeted validator commands, and
failure handling.
CI
.github/workflows/python-tests.yml runs on pushes and pull requests to
master and main using Linux, macOS, and Windows. Each matrix job installs
the Python test environment from requirements-ci.txt, then runs:
- the full repository suite on Ubuntu;
- explicit validator, shared, and performance shards on macOS and Windows;
- the complete browser suite in a separate Windows shard;
- the portability sentinel after a failed combined or shared check on every supported OS;
- on macOS and Windows, the native keyring smoke;
- on macOS and Windows, the native recurring-scheduler smoke.
The native smokes use always() so their platform evidence is still collected
after an unrelated full-suite failure.
requirements-ci.txt is the GitHub Actions dependency manifest for pytest,
pytest-xdist, and every imported test/validator dependency. Update it only
after the proposed versions pass the full repository suite; runtime dependency
declarations remain governed separately by the blueprint inventory.
Platform skips
Skips are repository-level coverage decisions. Each pytest.skip,
pytest.mark.skipif, unittest.SkipTest, unittest.skip, or self.skipTest
under a test root needs a nearby famulus-skip comment with:
category: an accepted skip category;reason: why the condition is part of the supported contract;alternate: where equivalent or nearest coverage exists.
Do not skip a product failure merely because it appears on one host. Use an explicit platform contract and preserve alternate coverage.
Adding tests
Place repository-wide tests under tests/, hook tests under hooks/tests/, and
Rutter tests under src/officina/rutter/tests/. A skill has two test locations,
and which one a test belongs in follows from what it asserts about. Runtime
behavior—the Python code executed by a machine interface—goes under
skills/<skill>/_rtx/tests/, beside the code it covers. The module's own
gateway contract goes under skills/<skill>/tests/: instruction wording the
skill promises, routing between its interfaces, and the shape of its declared
exports. Both are collected, because pytest.ini lists bare skills in
testpaths. Update pytest.ini only when a discovery boundary changes. Update
src/officina/repository/checks/runner.py only when suite policy changes, and
update this guide whenever either contract changes.
Prefer normal pytest fixtures at the narrowest correct scope for immutable or resettable preparation. Keep real subprocess, filesystem, browser, and platform boundaries when they are the behavior under test. A faster test is not an improvement if it weakens the assertion or changes isolation semantics.
Preventing setup inflation
For every expensive test, record its entry point, initial state, action or mutation, exact observable, retained evidence owner, and required physical boundary. Setup should stop at the lowest stable layer owning that observable. Share only immutable preparation, isolate mutating consumers, and remember that pytest fixture scope is per xdist worker.
The binding details and remedies are in the code-testing standard. Use the optimization playbook when auditing or reducing existing cost. Do not copy those rules into this guide.
Adding validators
Place a repository-wide validator in validators/ and a shared skill-system
validator in validators/skill/. Each exposes the validator protocol expected
by the repository runner and has focused tests of accepted and rejected cases
under tests/. Do not add a second test that merely invokes the validator and
expects an empty finding list: the validator suite already owns that live
conformance result.