The ./Test convention
- What it does
- The environment model
- The isolation model
- Key patterns
- Language-agnostic
- When to reach for it
- When not to
- Agent integration
- Related
Put a top-level executable shell script named Test (capital T, no extension) at the root of every repo. Running ./Test on a fresh clone goes from zero to pass/fail with no manual setup — no README step, no “make sure you have X installed first.” It works or it doesn’t, and if it doesn’t, the output says why.
This is the convention that makes repos safe for agents. An agent that can run ./Test doesn’t need to understand your stack. It runs the script, reads the output, and knows whether the change broke something.
What it does
Three phases, in order:
- Bootstrap — create an isolated environment: virtualenv, toolchain, build directory. Everything goes into
.build/, which is gitignored and disposable. - Install — pull in all dependencies needed to build and test.
- Test — run the full verification suite: lint, unit tests, integration tests, build checks.
The script is idempotent. Deleting .build/ and re-running produces the same result as running it fresh. The -C flag does the delete for you; -t runs a single named phase:
./Test # normal run (incremental if .build/ exists)
./Test -C # clean run (rm -rf .build/ first, then full rebuild)
./Test -t lint # run a single phase, e.g. lint
The first two must produce the same outcome.
The environment model
The project runs in one of three environments. ./Test always runs in test, never in devl.
| Environment | Set by | What it does |
|---|---|---|
devl |
Default when the program runs directly | Developer’s local state, hot reload, relaxed constraints |
test |
./Test, automatically |
Clean build, full verification: lint, unit, integration |
prod |
ENV=prod ./Test, or path contains prod |
Read-only smoke tests — never creates, never writes |
The script picks RUNTIME_ENV from the first rule that matches, highest priority first:
ENV=prod ./Testis set →prod- The checkout path contains
prod→prod - Running via
./Test→test - Program running normally (no
./Test) →devl
It then exports RUNTIME_ENV so the program knows which config set to load. Config files follow the same pattern: .env.devl, .env.test, .env.prod and config/devl.yaml, config/test.yaml, config/prod.yaml. ./Test controls the environment; the program responds to it.
The isolation model
Isolation is the checkout, not the user. Every developer has their own clone; .build/ lives inside it and is never shared:
/home/alice/projects/myapp/.build/ ← Alice's artifacts
/home/bob/projects/myapp/.build/ ← Bob's artifacts
Two people running ./Test simultaneously in different checkouts never conflict. CI gets its own checkout too. .build/ is safe to delete at any time — the next ./Test rebuilds it from source.
Key patterns
Trap-based pass/fail — use this in every Test script:
PASSED=false
trap 'ec=$?; $PASSED || echo 1>&2 "FAILED (exitcode=$ec)"; exit $ec' 0
# ... all work ...
PASSED=true
echo "OK ($(elapsed)s)"
The trap fires on set -e exits, signals, and unexpected termination. Nothing passes silently.
Elapsed timing — so the closing OK line tells you how long the run took:
elapsed_start=$(date +%s)
elapsed() { echo $(( $(date +%s) - $elapsed_start )); }
Non-fatal error accumulation — continue testing after soft failures, report everything at the end:
errorlist=()
adderror() { echo 1>&2 '***** ERROR:' "$@"; errorlist+=("$*"); }
some_check || adderror "check failed for $item"
[[ ${#errorlist[@]} -gt 0 ]] && { printf '%s\n' "${errorlist[@]}"; exit 1; }
Language-agnostic
The structure is the same regardless of stack. Only the bootstrap and test commands change:
| Stack | Bootstrap + test |
|---|---|
| Python | . ./pactivate -q && pytest -qq |
| Go | go test ./... |
| Rust | cargo test |
| Node.js | npm ci && npm test |
| Haskell | stack build --test |
When to reach for it
- New project. Scaffold
Testbefore writing the first line of application code. Having it forces you to define “done” up front. - Inheriting a repo. If there’s no
Test, add one. A repo without it is a repo where you can’t verify anything quickly. - CI onboarding. If your CI script and your local test command have diverged,
Testcloses the gap. CI runs./Test -C; so do you.
When not to
- Partial phase needed. Use
./Test -t lintto run one phase rather than skipping the script entirely. - Required tooling is genuinely unavailable. If there’s no graceful fallback,
Testshould print a clear warning and skip the affected phase, not fail silently. An agent needs to know what ran and what didn’t.
Agent integration
An agent in a repo with a Test script should run:
cd /path/to/repo && ./Test -C 2>&1 | tee test-output.log
tail -1 test-output.log | grep -q '^OK (' && echo "PASS" || echo "FAIL"
For prod smoke tests:
ENV=prod ./Test 2>&1 | tee smoke-output.log
When an agent creates a new project, scaffold Test as part of initialization:
- Create
Testat repo root,chmod +x - Add
set -euo pipefailand the trap-based pass/fail boilerplate - Add environment detection (prod path/ENV var, else test), export
RUNTIME_ENV - Set
PROJDIRandcd "$PROJDIR"at the top - Add isolation:
mkdir -p .build/, support-Cfor clean rebuilds - Add bootstrap step (venv, toolchain, dep install)
- Add test phases with
echo "===== Phase Name"headers - Add prod branch for read-only smoke tests
- End with
PASSED=trueandecho "OK ($(elapsed)s)"
Related
- The XP workflow — where
./Testfits in the red-green-refactor loop - Parallel XP with token budgets — the N-agent variant that depends on
./Testas its verification primitive - Supporting skills — other skills that compose with the XP loop