Files
bot-bottle/tests
didericis-claude 909029085e
test / unit (pull_request) Successful in 21s
test / integration (pull_request) Successful in 41s
feat(sidecars): egress binds 127.0.0.1 when EGRESS_LISTEN_HOST is set (PRD 0023 chunk 3)
Egress's bind address is now env-driven via EGRESS_LISTEN_HOST.
Unset → mitmdump's default (all interfaces) — the docker
backend's behavior, unchanged. Set to `127.0.0.1` → mitmdump
binds localhost only.

The smolmachines launch sets EGRESS_LISTEN_HOST=127.0.0.1 in
the bundle's env unconditionally. TSI's allowlist is
`<bundle-ip>/32` (IP-only, not port-granular), which would
otherwise let the agent dial `<bundle-ip>:9099` and bypass
pipelock's DLP by talking to egress directly. Binding egress
to localhost inside the bundle closes that gap at the socket
level — the agent still reaches the IP (TSI permits it) but
egress refuses the connect because it's not listening on the
docker bridge interface.

The docker backend doesn't set the env var because its agent
dials egress directly via the docker network alias — egress
MUST be reachable from outside the bundle there. The
asymmetry is documented in the entrypoint script's comment.

Changes:
- egress_entrypoint.sh: read EGRESS_LISTEN_HOST, conditionally
  pass `--listen-host <host>` to mitmdump.
- smolmachines/launch.py: BundleLaunchSpec.environment now
  includes `EGRESS_LISTEN_HOST=127.0.0.1`.
- New unit tests (5): the entrypoint script's argv shape under
  various env combinations, verified via a fake mitmdump shim
  that prints its argv.

545 unit + 3 integration tests passing. The egress-port-bypass
probe from chunk 2d still passes (chunk 2d ran with daemons_csv=""
so no egress was up; chunk 3 makes the probe preserve its
property once egress IS up in chunk 4).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 04:49:22 -04:00
..

Tests

Plain-Python test suite using stdlib unittest. No external dependencies. Unit tests run anywhere Python 3 is present; integration tests need Docker and skip cleanly otherwise.

Layout

tests/
  fixtures.py                       # JSON manifest builders (shared)
  _docker.py                        # docker-availability skip helper (shared)
  unit/
    test_pipelock_classify.py
    test_pipelock_allowlist.py
    test_pipelock_yaml.py
    test_manifest_runtime.py
  integration/
    test_pipelock_sidecar_smoke.py
    test_dry_run_plan.py
    test_orphan_cleanup.py
  canaries/
    test_pipelock_image.py          # opt-in; see below

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_pipelock_yaml          # one file

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

What the integration tests cover

  • test_dry_run_plan.pycli.py start --dry-run --format=json emits a structured plan that contains the resolved egress allowlist and the bottle's runtime, and creates zero Docker resources.
  • test_orphan_cleanup.pynetwork_remove is idempotent against missing resources, so the EXIT trap can call it unconditionally.
  • test_sidecar_bundle_image.py — builds Dockerfile.sidecars and probes that pipelock / gitleaks / mitmdump / supervise are all reachable inside the bundle.
  • test_sidecar_bundle_compose.py — end-to-end compose-up of an agent + bundle pair; verifies the agent reaches the bundle via the legacy network aliases.

Canaries

tests/canaries/ holds upstream-regression checks (e.g. the pinned pipelock digest's binary still runs). These are gated on CLAUDE_BOTTLE_RUN_CANARIES=1 and not part of the per-push suite. They're invoked by the scheduled canaries workflow.

CLAUDE_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries -v

What's NOT covered

  • claude_bottle/ssh.py end-to-end (would need a fake SSH host inside the container).
  • A live SSH-through-pipelock 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 Docker.
  2. Filename: test_<topic>.py.
  3. Boilerplate:
    import unittest
    
    from claude_bottle.<module> import <symbol>
    
    class TestThing(unittest.TestCase):
        def test_x(self):
            ...
    
    if __name__ == "__main__":
        unittest.main()
    
  4. For Docker-dependent tests, decorate the class with @skip_unless_docker() from tests._docker.