Testing
August 24, 2026 ยท View on GitHub
Base uses three test layers. Prefer the narrowest layer that proves the behavior, then broaden when a change crosses command or runtime boundaries.
The Tests GitHub Actions workflow runs for every pull request and for pushes
to main. Feature-branch pushes do not start a second copy of the pull-request
workflow. Concurrency is scoped to the pull-request number, or to the Git ref
for default-branch runs, so a newer commit cancels only the superseded run for
the same change.
Regression And Completion Evidence
Bug fixes should start with a failing test, fixture, or reproduction whenever practical. The useful proof is not just that the final test passes, but that the test or reproduction failed for the expected reason before the fix.
Use this order for behavior changes and bug fixes:
- Reproduce the symptom with the narrowest command or test.
- Identify the root cause before changing code.
- Add or update the focused test, fixture, or reproduction.
- Verify the test fails for the expected reason.
- Implement the smallest fix that addresses the root cause.
- Rerun the focused verification, then broaden only when shared behavior is touched.
If an automated regression test is not practical, record the manual reproduction and the final verification command in the PR. Do not claim a bug is fixed, tests pass, or a contract is preserved without fresh output from the current checkout or worktree.
Diagnostic Workflow
When a failure crosses Base boundaries, inspect current state before running commands that intentionally change the checkout or machine. Start with narrow diagnostics in this order:
basectl check <project>
basectl doctor <project>
basectl test <project>
basectl check and basectl doctor are non-mutating diagnostics. They should
not install dependencies, rewrite shell profiles, change manifests, or mutate
repositories.
Map each symptom to its likely ownership before changing code:
- Shell startup/profile changes:
lib/shell/andcli/bash/commands/basectl/subcommands/update_profile.sh. - Runtime shell behavior:
lib/bash/runtime/. - Public command dispatch and Bash command behavior:
bin/basectl,cli/bash/commands/basectl/, and nearby BATS tests. - Manifest and project-discovery behavior:
base_manifest.yaml,cli/python/,lib/python/, and integration tests when multiple commands interact. - Python CLI and helper behavior:
cli/python/andlib/python/.
Python Unit Tests
Python engine and helper behavior lives under cli/python/**/tests/ and
lib/python/**/tests/. These tests should cover parsing, manifest merging,
artifact decisions, JSON output, and error handling without launching public
shell commands. Monorepo-wide contract checks that inspect STANDARDS.md,
scan all production packages, or invoke shell files live under the top-level
tests/ directory so they are not mistaken for tests of an installed Python
distribution.
From a Base source checkout with the standalone base-cli checkout next to it,
run the Python suite with the same source roots used by CI:
BASE_CLI_SOURCE_DIR=../base-cli/lib/python \
PYTHONPATH=../base-cli/lib/python:lib/python:cli/python \
python -m pytest
If base-cli is not at ../base-cli, replace that path in both assignments
with the absolute path to its lib/python directory. BASE_CLI_SOURCE_DIR
must point at a directory containing base_cli/__init__.py. The pinned
base-cli package installed from requirements-dev.txt is useful for package
metadata and tooling, but it is not a substitute for this source checkout:
the documented suite imports source-level APIs that the package does not
provide.
The Python CI job also measures coverage for production code under
cli/python:
BASE_CLI_SOURCE_DIR=../base-cli/lib/python \
PYTHONPATH=../base-cli/lib/python:lib/python:cli/python \
python -m pytest \
--cov=cli/python \
--cov-report=term-missing \
--cov-report=json:coverage.json
python -m tests.coverage_gate coverage.json
The shared coverage configuration excludes Python test
modules, package initializers, and __main__.py entrypoints from the measured
source set and enables branch measurement. The ratchet checks the JSON report
separately at 85% statements, 76% branches, and 84% combined coverage. These
whole-point floors preserve the existing statement gate and leave a small
cross-version margin below the measured 87.51%, 76.99%, and 84.94% baseline.
The terminal report keeps per-file missing lines and partial branches visible,
while the ratchet prints every metric and names any failed threshold.
This is a regression guard, not a claim that every module has ideal behavioral
coverage. Raise a floor deliberately as lower-covered areas gain focused tests;
do not lower one merely to make a change pass. To prove the failure behavior
locally without changing production code, run
python -m pytest tests/test_coverage_gate.py -k fails.
Bash Command And Runtime Tests
BATS tests next to Bash commands and Base runtime helpers cover shell parsing,
dispatch, small command contracts, and failure messages. Keep these focused on
one command or helper at a time. Reusable Bash library tests live in the
standalone base-bash-libs repository.
Before running source-checkout BATS tests for the first time, clone the reusable Bash library and shared Python framework checkouts next to Base:
git clone https://github.com/basefoundry/base-bash-libs.git ~/work/base-bash-libs
git clone https://github.com/basefoundry/base-cli.git ~/work/base-cli
From a source checkout, run the full command/library suite through:
env -u BASE_HOME ./bin/base-test
bin/base-test preflights the Bash dependency before starting the
source-checkout BATS suite. The full suite expects Base to resolve the external
reusable Bash libraries and the standalone base-cli package. A normal
~/work/base checkout uses sibling ~/work/base-bash-libs and
~/work/base-cli
automatically. A linked issue worktree under ~/work/base-worktrees/<slug> is a
nonstandard layout because the sibling lookup would search
~/work/base-worktrees/base-bash-libs and ~/work/base-worktrees/base-cli.
For the standard contributor checkout shape, point Base at the reusable Bash
library explicitly and either place base-cli at the expected sibling path or
set BASE_CLI_SOURCE_DIR:
BASE_BASH_LIBS_DIR=~/work/base-bash-libs/lib/bash \
BASE_CLI_SOURCE_DIR=~/work/base-cli/lib/python \
env -u BASE_HOME ./bin/base-test
basectl test base delegates to the same runner when the base project
resolves to a source checkout. In a packaged install such as Homebrew,
basectl test base is package-aware: it runs the packaged Python test layer and
skips source-checkout-only BATS and integration tests with a message pointing
back to the source checkout command above.
Base deliberately uses command-focused BATS regression coverage plus ShellCheck
as the Bash equivalent instead of a line percentage. Contributors changing a
public command or runtime branch must add or update a focused BATS assertion,
then run that test and the full bin/base-test suite. Linux CI runs the complete
BATS command matrix and ShellCheck on tracked shell files, so missing behavioral
coverage or unsafe shell changes block the pull request.
This policy avoids adding kcov or bashcov, whose tracing and platform
requirements would make the macOS/Linux signal inconsistent and whose sourced
shell-line percentages do not establish command behavior. Revisit the decision
if a portable tool can report command-path coverage without changing Base's
runtime behavior; until then, command-focused tests are the reviewed coverage
unit and the required CI signal.
Integration Tests
Integration tests live under tests/integration/. They run real basectl
launchers against a temporary HOME, temporary workspace, copied Base runtime,
and fake project repositories. External platform tools such as brew and
xcode-select are stubbed so the suite stays deterministic, network-free, and
safe for local machines and CI.
Add integration coverage when a change affects:
- workspace discovery across Base and project repositories
basectl setup,check,doctor, ortestworking together- shell profile update behavior
- installation layout assumptions, including Homebrew-style Base homes
- public command behavior that cannot be proven by a single command unit test
The default integration suite should not install real Homebrew packages, edit real shell startup files, depend on network access, or mutate repositories outside the temporary BATS workspace.
External Base Demo E2E
The Base Demo E2E workflow runs the
macOS product loop against the real basefoundry/base-demo repository on a
nightly schedule and through workflow_dispatch. It sets up and checks the
demo, approves its reviewed manifest, then runs the declared test and
non-interactive demo commands. Use the workflow input to validate a specific
base-demo branch, tag, or commit.
This job is intentionally separate from Base's own test matrix because it
validates an external manifest and project contract. Its grouped CI output
labels the likely owner as either Base bug or base-demo manifest needs updating, while keeping a per-command temporary log path available during the
runner for triage.
Run only integration tests with:
BASE_INTEGRATION_PYTHON="$HOME/.base.d/base/.venv/bin/python" \
bats tests/integration/base_workflows.bats
Contract Checks
Contract checks are the narrow review-hardening layer for documented behavior that has drifted in past reviews: workflow policy, workspace manifest URL policy, project installer integrity, and CLI docs/help/completion alignment. They are mapped in Base Contracts.
Run the current contract slice with:
tests/contracts/run.sh
Use this runner before or during large review-batch triage, and for PRs that edit the mapped contract areas. It composes existing focused tests and should stay smaller than the full source-checkout suite.