Files
bot-bottle/tests/unit/test_bottle_state.py
T
didericis e996f72532
test / unit (pull_request) Successful in 16s
test / integration (pull_request) Successful in 1m30s
fix(bottle): identity-key all per-bottle resources by (agent, cwd)
The single point that computed `slug = slugify(agent_name)` in
prepare.py is now `slug = bottle_identity(agent_name, cwd)`. With
--cwd the identity has a sha256(resolved-cwd)[:12] suffix, so the
same agent against different projects gets distinct container
names, network names, queue dir, audit log paths, and per-bottle
state (Dockerfile + transcript). Without --cwd the identity is
just slugify(agent_name), unchanged from before — no-cwd bottles
look the same as today.

The downstream `slug` field on DockerBottlePlan keeps its name —
every module already threads it under "slug" and the value flowing
through is now the bottle's full identity. A comment in prepare.py
flags the change.

Fixes the bug surfaced in PR #22 review: running the same agent
against project-A's cwd then project-B's would silently share
project-A's per-bottle Dockerfile + transcript snapshot, container
name (forcing serialized runs), and queue/audit history.

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

117 lines
4.2 KiB
Python

"""Unit: per-bottle state helpers (PRD 0016 Phase 1) + identity."""
import re
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"))
class TestBottleIdentity(unittest.TestCase):
"""bottle_identity(agent_name, cwd) — PRD 0016 follow-up.
Without --cwd, identity == slugify(agent_name) so existing
no-cwd bottles look unchanged. With --cwd, identity has a
cwd-hash suffix so the same agent against different projects
gets distinct container / queue / audit / state dirs."""
def test_no_cwd_returns_slug(self):
self.assertEqual("dev", bottle_state.bottle_identity("dev", None))
self.assertEqual("api-foo", bottle_state.bottle_identity("Api Foo", None))
def test_cwd_appends_hash_suffix(self):
identity = bottle_state.bottle_identity("dev", Path("/proj/A"))
self.assertTrue(identity.startswith("dev-"))
suffix = identity[len("dev-"):]
self.assertEqual(12, len(suffix))
self.assertTrue(re.fullmatch(r"[0-9a-f]+", suffix), suffix)
def test_same_cwd_same_identity(self):
a = bottle_state.bottle_identity("dev", Path("/proj/A"))
b = bottle_state.bottle_identity("dev", Path("/proj/A"))
self.assertEqual(a, b)
def test_different_cwds_differ(self):
a = bottle_state.bottle_identity("dev", Path("/proj/A"))
b = bottle_state.bottle_identity("dev", Path("/proj/B"))
self.assertNotEqual(a, b)
def test_different_agents_same_cwd_differ(self):
a = bottle_state.bottle_identity("dev", Path("/proj/A"))
b = bottle_state.bottle_identity("api", Path("/proj/A"))
self.assertNotEqual(a, b)
def test_agent_name_slugified(self):
# Identity's agent-name prefix is slugify(name), not the raw
# name — same rule the rest of the codebase has always used.
self.assertEqual(
"my-agent",
bottle_state.bottle_identity("My Agent", None),
)
if __name__ == "__main__":
unittest.main()