0899a898e0
Phase 2 of PRD 0016. New module claude_bottle/backend/docker/capability_apply.py: - apply_capability_change(slug, new_dockerfile): snapshot transcript → push working tree → write per-bottle Dockerfile → teardown. Returns (before, after) for the dashboard's audit/diff render. - fetch_current_dockerfile(slug): per-bottle Dockerfile if set, else the repo's Dockerfile. - Internal helpers _snapshot_transcript, _push_working_tree are best-effort (log + return on failure); _teardown_bottle is idempotent (force-rm + network rm silently ignore missing names). Fire-and-forget from the agent's perspective: by the time the dashboard writes the response file the supervise sidecar is already gone (it was torn down), so the agent's tool call connection drops without receiving the response. The replacement agent (next manual `cli.py start <agent>`) sees the new per-bottle Dockerfile and the transcript snapshot for resume. v1 does not auto-relaunch. Tests cover sequencing (snapshot → push → teardown order), the per-bottle vs repo Dockerfile fallback chain, empty-input rejection, and the per-bottle-Dockerfile write. The docker exec / cp / rm plumbing is covered by the Phase 4 integration test. 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_pipelock_sidecar_smoke.py— drivesDockerPipelockProxy.prepare.start(the production code path) against a real Docker daemon and probes the sidecar's/healthfrom an in-network curl container.
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_removeandPipelockProxy.stopare idempotent against missing resources, so the EXIT trap can call them unconditionally.
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.