Files
bot-bottle/tests
didericis c62aa7b805
prd-number-check / require-numbered-prds (pull_request) Successful in 10s
tracker-policy-pr / check-pr (pull_request) Successful in 13s
test / image-input-builds (pull_request) Successful in 43s
test / unit (pull_request) Successful in 51s
test / integration-docker (pull_request) Successful in 59s
test / coverage (pull_request) Successful in 15s
lint / lint (push) Failing after 14m25s
fix(install): find a usable python instead of dead-ending on PATH's
The macOS install harness caught this on its first real run: a throwaway
account gets `bot-bottle install: error: python3 3.11 or newer is required`
and stops. A fresh account's PATH is just /etc/paths, which excludes
/opt/homebrew/bin, so `python3` resolves to the Command Line Tools stub —
still 3.9.6 on macOS 26. Homebrew's shellenv line lives in the *installing*
user's ~/.zprofile and is inherited by nobody. The documented `curl … | sh`
path therefore dead-ends for anyone whose profile isn't already set up, which
is every new user, launchd job, and CI runner.

So look past PATH before giving up: try `python3`, then python3.11-3.14, then
/opt/homebrew/bin, /usr/local/bin, ~/.local/bin, and python.org framework
builds, and say which one was picked when it isn't the obvious one. On this
host that turns the failure into a successful install.

The chosen interpreter is now threaded through everything downstream — the pip
probe, the PEP 668 check, the pip --user fallback, and the user-scripts-dir
lookup — which were all still hardcoding `python3` and would otherwise have
run against the 3.9 stub we just rejected. `pipx install` gains `--python`,
since pipx otherwise builds the venv with whichever interpreter pipx itself
was installed with, not the one that passed the version check.

When nothing usable is found the error is now actionable: what was found and
why it's insufficient, where else it looked, a platform-appropriate install
command, and BOT_BOTTLE_PYTHON to point at an interpreter directly. An
explicit BOT_BOTTLE_PYTHON that is too old or unusable is an error rather than
a silent fallback to a different interpreter than the caller asked for.

Three existing tests asserted on incidental literals rather than the behaviour
they describe, and are narrowed to their actual intent:

* `never_uses_sudo` matched the word anywhere, including the new "sudo apt
  install python3.12" remediation *advice*. It now strips string literals and
  comments first, so it still catches sudo as a bare command, in a pipeline,
  and in a command substitution — verified by mutation — while allowing the
  script to print the word.
* `checks_pip_usable_before_fallback` pinned the literal `python3 -m pip`.
* `resolves_user_scripts_dir_not_hardcoded` banned ".local/bin" script-wide;
  it's now scoped to the USER_SCRIPTS assignment it exists to guard, since
  ~/.local/bin is a legitimate place to *find an interpreter*.

README grows the install command and a Requirements section it never had,
leading with the Python floor and why a working `python3` in your own shell
says nothing about what a fresh account sees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEfZZhakx13bxTfXcZCoS5
2026-07-27 00:50:21 -04:00
..

Tests

Plain-Python test suite using stdlib unittest. No external dependencies. Unit tests run anywhere Python 3 is present; integration tests run through the backend named by BOT_BOTTLE_BACKEND (default docker) and skip cleanly when that backend isn't available on the host.

Layout

tests/
  fixtures.py                       # JSON manifest builders (shared)
  _backend.py                       # backend selection + skip guards (shared)
  unit/
    test_egress.py
    test_egress_addon_core.py
    test_manifest_egress.py
    test_dlp_detectors.py
    test_manifest_runtime.py
    ...                             # many others; see unit/ directory
  integration/
    test_gateway_image.py
    test_sandbox_escape.py
    test_orphan_cleanup.py
    ...
  canaries/
    test_gitleaks_release.py        # opt-in upstream artifact check

Classification falls out of the directory — no hand-maintained list to keep in sync.

Running

python -m unittest discover -t . -s tests/unit -v         # unit only
python -m unittest discover -t . -s tests/integration -v  # integration only
python -m unittest discover -t . -s tests -v              # both (recursive)
python -m unittest tests.unit.test_manifest_egress        # one file

Discovery is invoked with -t . (top-level dir = repo root) so the bot_bottle package on sys.path resolves correctly.

What the integration tests cover

  • test_orphan_cleanup.pynetwork_remove is idempotent against missing resources, so the EXIT trap can call it unconditionally.
  • test_gateway_image.py — builds Dockerfile.gateway and probes that gitleaks / mitmdump / supervise are all reachable inside the gateway image.
  • test_orchestrator_docker_auth.py — drives the real control-plane container and verifies role-scoped authentication.
  • test_multitenant_isolation.py and test_sandbox_escape.py — exercise token/allowlist separation and end-to-end escape attempts.

Canaries

tests/canaries/ holds upstream-regression checks gated on BOT_BOTTLE_RUN_CANARIES=1 and not part of the per-push suite. They're invoked by the scheduled canaries workflow. The gitleaks canary downloads the exact release archive pinned by Dockerfile.gateway, verifies its architecture-specific checksum, and executes the binary.

BOT_BOTTLE_RUN_CANARIES=1 python -m scripts.unittest_gate \
  -t . -s tests/canaries -v --minimum-executed 1 --fail-on-skip

What's NOT covered

  • bot_bottle/ssh.py end-to-end (would need a fake SSH host inside the container).
  • A live SSH-through-git-gate tunnel against a real Tailscale-style IP.
  • DLP false-positive measurements.
  • TLS handling / cert pinning behavior.

Adding a test

  1. Pick the directory: tests/unit/ for a pure unit test, tests/integration/ for one that needs a backend.

  2. Filename: test_<topic>.py.

  3. Boilerplate:

    import unittest
    
    from bot_bottle.<module> import <symbol>
    
    class TestThing(unittest.TestCase):
        def test_x(self):
            ...
    
    if __name__ == "__main__":
        unittest.main()
    
  4. Skip guards live in tests._backend and gate on the backend's own readiness check, bot_bottle.backend.has_backend — the same probe behind ./cli.py backend status:

    • Backend-agnostic tests (go through get_bottle_backend()) decorate the class with @skip_unless_selected_backend_available() — the test runs against whichever backend BOT_BOTTLE_BACKEND selects and skips unless that backend is available (checking, e.g., Linux + /dev/kvm for Firecracker rather than unrelated Docker availability).
    • Backend-specific tests (exercise DockerBroker, DockerGateway, backend.docker.*, …) decorate with @skip_unless_backend("docker") so they no-op under a run targeting a different backend.

    Each CI integration job runs ./cli.py backend status --backend=<name> as a preflight, which prints a clear per-check summary and exits non-zero when the backend is missing — so absent infrastructure fails the job instead of hiding among per-test unittest.skip lines.