c73d717f71
First sub-PR of chunk 2: rewrite the renderer chunk 1 shipped to match smolvm 0.8.0's actual Smolfile shape, delete the dead gvproxy renderer + its tests, simplify the prepare flow now that there's no gvproxy socket + no loopback-port allocation. Smolfile renderer: - Old shape (under the abandoned gvproxy design): name = ..., command = [...], [[net]] attachment = "unixgram", socket = "...". - New shape (smolvm 0.8.0): env = [...] (sorted K=V pairs), [network] allow_cidrs = ["<bundle-ip>/32"]. Nothing else. image / entrypoint / cmd come from the .smolmachine artifact built in chunk 2b; cpus / memory left at smolvm defaults. - Tests assert no leakage of TSI's --outbound-localhost-only or the old gvproxy/unixgram keys. util.py: - smolmachines_gvproxy_subnet → smolmachines_bundle_subnet, returning (subnet, gateway, bundle_ip). bundle_ip is always at .2 (gateway .1); subnet is /24, third octet derived from the slug hash, skipping the docker-default 17 to avoid the common 192.168.17.x collision. - allocate_loopback_port: deleted. The bundle gets a pinned docker IP now; the agent dials that IP directly through TSI. - smolmachines_preflight: dropped the gvproxy check; only smolvm is required. prepare.py: - Drops the gvproxy.yaml render + the loopback port allocation + the gvproxy_socket field on the plan. - Derives subnet / gateway / bundle_ip from the slug and populates the new SmolmachinesBottlePlan fields. - Agent env now uses IP-literal URLs (http://<bundle-ip>:8888 etc) since the guest will have no DNS resolver inside TSI's allowlist. bottle_plan.py: - Old fields: gvproxy_config_path, gvproxy_socket, gvproxy_subnet, gvproxy_gateway, host_port_map. - New fields: bundle_subnet, bundle_gateway, bundle_ip, smolfile_path. (smolmachine artifact path lands in chunk 2b.) Net: -410 lines. Full unit suite: 516 passing. The VM lifecycle + bundle bringup + launch wiring + smoke tests land in chunk 2b. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.py—cli.py start --dry-run --format=jsonemits a structured plan that contains the resolved egress allowlist and the bottle's runtime, and creates zero Docker resources.test_orphan_cleanup.py—network_removeis 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.pyend-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
- Pick the directory:
tests/unit/for a pure unit test,tests/integration/for one that needs Docker. - Filename:
test_<topic>.py. - Boilerplate:
import unittest from claude_bottle.<module> import <symbol> class TestThing(unittest.TestCase): def test_x(self): ... if __name__ == "__main__": unittest.main() - For Docker-dependent tests, decorate the class with
@skip_unless_docker()fromtests._docker.