Skip to content

Running Tests in Git Worktrees

TL;DR

just wt-doctor          # from any worktree — tells you if it is testing its own code

If that passes, run tests normally. There is nothing else to configure.

The one thing that breaks this

Never export a PYTHONPATH that points at a specific checkout.

An absolute PYTHONPATH exported for one checkout precedes the venv's site-packages on sys.path. Inside any other worktree, uv run — and therefore just check and just test — then imports tradai.* from that first checkout. Tests pass or fail on code you did not change, and nothing warns you:

$ cd .claude/worktrees/my-feature
$ uv run python -c "import tradai.common as m; print(m.__file__)"
/Users/you/tradai-uv/libs/tradai-common/src/tradai/common/__init__.py   # ← the WRONG tree

The fix is to remove it, not to set it to something else:

unset PYTHONPATH

Then relaunch your shell, editor, or agent so the child processes stop inheriting it.

just recipes are already immune — the justfile exports PYTHONPATH := '' at the top — but a bare uv run, pytest, editor test-runner, or agent invocation is not.

What is allowed

The invariant is no PYTHONPATH entry may resolve outside the current git toplevel, not "never use PYTHONPATH". These are fine:

${workspaceFolder} is not a safe way to write one. It resolves to the folder VS Code has open, not the worktree your terminal is standing in — so it becomes an absolute path into the wrong checkout the moment you cd. That is why terminal.integrated.env.* was removed from .vscode/settings.json. python.analysis.extraPaths still uses ${workspaceFolder} safely, because it never reaches a subprocess. - Container-local values baked into a Dockerfile. - Anything under the worktree you are currently in.

Why the venvs are fine on their own

Each worktree needs its own environment:

uv sync --all-packages          # inside the worktree, once

uv then writes per-venv _editable_impl_tradai_*.pth files scoped to that worktree, and imports resolve correctly with no environment variables at all. Confirm with:

ls .venv/lib/python3.11/site-packages/_editable_impl_tradai_common.pth

A worktree missing that file has a half-built venv — uv venv ran but uv sync never finished. It will either fail on a missing third-party import or fall through to another checkout. Re-sync it, or delete .venv and let the next uv run rebuild it.

Historical note. Earlier versions of this document blamed .pth aggregation — the claim that the tradai namespace package merges paths from every checkout with the main repo winning. That does not reproduce with current uv, and the wt-test helper it recommended set PYTHONPATH, which is what causes the problem it was meant to solve. Both have been removed. Audited 2026-08-23; see reports/worktree-branch-health-audit-2026-08-22.md §2.

Creating and retiring worktrees

just wt-new feat/my-thing       # .claude/worktrees/, uv sync, gt track, wt-doctor
just wt-prune                   # what has landed and is safe to remove
just wt-prune apply             # remove only what that dry run approved

wt-prune is deliberately conservative. It keeps anything dirty, anything holding ignored local state (.env, mlflow.db, Pulumi state — invisible to git status, deleted by git worktree remove), anything with commits absent from origin/dev, any Graphite stack parent, and any locked worktree. Read the KEEP reasons, not just the removable list.

Guardrails

Guard What it catches
just wt-doctor ambient PYTHONPATH, half-built venv, imports resolving outside this worktree
tests/unit/test_worktree_import_isolation.py the same invariant, under pytest and in CI
justfile export PYTHONPATH := '' protects every just recipe regardless of your shell
tests/scripts/worktree_prune_test.sh 27 fixture-repo assertions on the cleanup tool

Running services from a worktree

just up, just up-ministack and friends work from any worktree simultaneously. Each checkout gets its own Compose project, so containers, networks and named volumes are namespaced, and the host ports are shifted into a per-worktree band:

just compose-env        # which project and ports THIS checkout uses

The main checkout keeps project tradai and the original ports (8000, 5433, 4566, …), so nothing about the established workflow changes there — only additional worktrees move. Override either explicitly when you need a fixed value:

COMPOSE_PROJECT_NAME=my-stack just up
TRADAI_PORT_OFFSET=300 just up

Two things to know:

  • just down -v only destroys this checkout's volumes. Before #1350 the volumes were shared, so tearing down from one worktree deleted another's Postgres and MLflow data.
  • A bare docker compose up does not get this. The derivation lives in the justfile, and .env still pins COMPOSE_PROJECT_NAME=tradai. Use just, or export the variables yourself.