Files
bot-bottle/tests/unit/test_bottle_state.py
T
didericis 02811e0417 feat(bottle): per-bottle Dockerfile state + image build hook (PRD 0016)
Phase 1 of PRD 0016. Lays the per-bottle state plumbing that
capability-block remediation will write into:

- claude_bottle/backend/docker/bottle_state.py: bottle_state_dir,
  per_bottle_dockerfile (read), write_per_bottle_dockerfile,
  per_bottle_image_tag (unique per slug), transcript_snapshot_dir.
  Stores under ~/.claude-bottle/state/<slug>/.
- prepare.py: when a per-bottle Dockerfile exists, use
  per_bottle_image_tag(slug) as the base image and pass the
  per-bottle Dockerfile path through DockerBottlePlan.dockerfile_path.
  --cwd still layers a derived image on top.
- launch.py: passes plan.dockerfile_path to build_image so the
  per-bottle Dockerfile is what docker build reads.
- DockerBottlePlan gains dockerfile_path field; print() surfaces it
  in the preflight summary so the operator can see at-a-glance that
  this bottle is running on a rebuilt image.

Phase 2 will write to write_per_bottle_dockerfile (capability-block
approval); Phase 3 wires it into the dashboard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 05:23:31 -04:00

73 lines
2.4 KiB
Python

"""Unit: per-bottle state helpers (PRD 0016 Phase 1)."""
import tempfile
import unittest
from pathlib import Path
from claude_bottle import supervise
from claude_bottle.backend.docker import bottle_state
class _FakeHomeMixin:
def _setup_fake_home(self):
self._tmp = tempfile.TemporaryDirectory(prefix="bottle-state-test.")
original = supervise.claude_bottle_root
def fake_root() -> Path:
return Path(self._tmp.name) / ".claude-bottle"
supervise.claude_bottle_root = fake_root # type: ignore[assignment]
self._restore = lambda: setattr(supervise, "claude_bottle_root", original)
def _teardown_fake_home(self):
self._restore()
self._tmp.cleanup()
class TestPerBottleDockerfile(_FakeHomeMixin, unittest.TestCase):
def setUp(self):
self._setup_fake_home()
def tearDown(self):
self._teardown_fake_home()
def test_returns_none_when_absent(self):
self.assertIsNone(bottle_state.per_bottle_dockerfile("dev"))
def test_write_then_read_roundtrip(self):
bottle_state.write_per_bottle_dockerfile(
"dev", "FROM python:3.13\nRUN apk add ripgrep\n",
)
self.assertEqual(
"FROM python:3.13\nRUN apk add ripgrep\n",
bottle_state.per_bottle_dockerfile("dev"),
)
def test_isolated_per_slug(self):
bottle_state.write_per_bottle_dockerfile("dev", "FROM dev\n")
bottle_state.write_per_bottle_dockerfile("api", "FROM api\n")
self.assertEqual("FROM dev\n", bottle_state.per_bottle_dockerfile("dev"))
self.assertEqual("FROM api\n", bottle_state.per_bottle_dockerfile("api"))
def test_dockerfile_path_under_state_dir(self):
path = bottle_state.per_bottle_dockerfile_path("dev")
self.assertTrue(str(path).endswith("/.claude-bottle/state/dev/Dockerfile"))
def test_image_tag_unique_per_slug(self):
self.assertEqual(
"claude-bottle-rebuilt-dev:latest",
bottle_state.per_bottle_image_tag("dev"),
)
self.assertNotEqual(
bottle_state.per_bottle_image_tag("dev"),
bottle_state.per_bottle_image_tag("api"),
)
def test_transcript_dir_under_state_dir(self):
path = bottle_state.transcript_snapshot_dir("dev")
self.assertTrue(str(path).endswith("/.claude-bottle/state/dev/transcript"))
if __name__ == "__main__":
unittest.main()