refactor!: rename project to bot-bottle
Assisted-by: Codex
This commit is contained in:
+5
-5
@@ -36,7 +36,7 @@ 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.
|
||||
`bot_bottle` package on `sys.path` resolves correctly.
|
||||
|
||||
## What the integration tests cover
|
||||
|
||||
@@ -56,16 +56,16 @@ Discovery is invoked with `-t .` (top-level dir = repo root) so the
|
||||
|
||||
`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.
|
||||
`BOT_BOTTLE_RUN_CANARIES=1` and not part of the per-push suite.
|
||||
They're invoked by the scheduled `canaries` workflow.
|
||||
|
||||
```bash
|
||||
CLAUDE_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries -v
|
||||
BOT_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
|
||||
- `bot_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.
|
||||
@@ -80,7 +80,7 @@ CLAUDE_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries
|
||||
```python
|
||||
import unittest
|
||||
|
||||
from claude_bottle.<module> import <symbol>
|
||||
from bot_bottle.<module> import <symbol>
|
||||
|
||||
class TestThing(unittest.TestCase):
|
||||
def test_x(self):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
This test exists to catch a broken upstream packaging at the pinned
|
||||
digest. It is NOT part of the per-push suite — that would couple every
|
||||
dev push to upstream registry availability. Set
|
||||
CLAUDE_BOTTLE_RUN_CANARIES=1 to opt in (a scheduled CI workflow does
|
||||
BOT_BOTTLE_RUN_CANARIES=1 to opt in (a scheduled CI workflow does
|
||||
this; humans can run it ad-hoc the same way).
|
||||
"""
|
||||
|
||||
@@ -11,13 +11,13 @@ import os
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from claude_bottle.backend.docker.pipelock import PIPELOCK_IMAGE
|
||||
from bot_bottle.backend.docker.pipelock import PIPELOCK_IMAGE
|
||||
from tests._docker import skip_unless_docker
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
os.environ.get("CLAUDE_BOTTLE_RUN_CANARIES") == "1",
|
||||
"canary suite is opt-in; set CLAUDE_BOTTLE_RUN_CANARIES=1 to run",
|
||||
os.environ.get("BOT_BOTTLE_RUN_CANARIES") == "1",
|
||||
"canary suite is opt-in; set BOT_BOTTLE_RUN_CANARIES=1 to run",
|
||||
)
|
||||
@skip_unless_docker()
|
||||
class TestPipelockImage(unittest.TestCase):
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.manifest import Manifest
|
||||
|
||||
|
||||
def fixture_minimal_dict() -> dict[str, Any]:
|
||||
@@ -45,8 +45,8 @@ def fixture_with_git_dict() -> dict[str, Any]:
|
||||
"git": {
|
||||
"remotes": {
|
||||
"gitea.dideric.is": {
|
||||
"Name": "claude-bottle",
|
||||
"Upstream": "ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git",
|
||||
"Name": "bot-bottle",
|
||||
"Upstream": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git",
|
||||
"IdentityFile": "/dev/null",
|
||||
"KnownHostKey": "ssh-ed25519 AAAA...",
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ an interactive claude session). Instead, this test stages the
|
||||
minimum the orchestrator interacts with:
|
||||
|
||||
- A lightweight `alpine:latest sleep infinity` container named
|
||||
`claude-bottle-<slug>` (matches the agent container name pattern)
|
||||
`bot-bottle-<slug>` (matches the agent container name pattern)
|
||||
on the per-bottle internal network.
|
||||
- A marker file under `/home/node/.claude/` so we can assert the
|
||||
transcript snapshot path actually transferred bytes.
|
||||
@@ -31,15 +31,15 @@ import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle import supervise
|
||||
from claude_bottle.backend.docker import bottle_state, capability_apply
|
||||
from claude_bottle.backend.docker.capability_apply import apply_capability_change
|
||||
from claude_bottle.backend.docker.network import (
|
||||
from bot_bottle import supervise
|
||||
from bot_bottle.backend.docker import bottle_state, capability_apply
|
||||
from bot_bottle.backend.docker.capability_apply import apply_capability_change
|
||||
from bot_bottle.backend.docker.network import (
|
||||
network_create_egress,
|
||||
network_create_internal,
|
||||
network_remove,
|
||||
)
|
||||
from claude_bottle.backend.docker.sidecar_bundle import (
|
||||
from bot_bottle.backend.docker.sidecar_bundle import (
|
||||
sidecar_bundle_container_name,
|
||||
)
|
||||
from tests._docker import skip_unless_docker
|
||||
@@ -61,21 +61,21 @@ class TestCapabilityApply(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.slug = f"cb-test-cap-{os.getpid()}-{int(time.time())}"
|
||||
self.agent_name = f"claude-bottle-{self.slug}"
|
||||
self.agent_name = f"bot-bottle-{self.slug}"
|
||||
self.sidecar_names: list[str] = []
|
||||
self.internal_net = ""
|
||||
self.egress_net = ""
|
||||
# Fake home so tests don't touch ~/.claude-bottle/.
|
||||
# Fake home so tests don't touch ~/.bot-bottle/.
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="cap-apply-int.")
|
||||
self._original_root = supervise.claude_bottle_root
|
||||
self._original_root = supervise.bot_bottle_root
|
||||
|
||||
def fake_root() -> Path:
|
||||
return Path(self._tmp.name) / ".claude-bottle"
|
||||
return Path(self._tmp.name) / ".bot-bottle"
|
||||
|
||||
supervise.claude_bottle_root = fake_root # type: ignore[assignment]
|
||||
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
|
||||
|
||||
def tearDown(self):
|
||||
supervise.claude_bottle_root = self._original_root # type: ignore[assignment]
|
||||
supervise.bot_bottle_root = self._original_root # type: ignore[assignment]
|
||||
for name in [self.agent_name, *self.sidecar_names]:
|
||||
subprocess.run(
|
||||
["docker", "rm", "-f", name],
|
||||
|
||||
@@ -13,7 +13,7 @@ import os
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from claude_bottle.backend.docker.network import (
|
||||
from bot_bottle.backend.docker.network import (
|
||||
network_create_egress,
|
||||
network_create_internal,
|
||||
network_remove,
|
||||
@@ -40,7 +40,7 @@ class TestOrphanCleanup(unittest.TestCase):
|
||||
|
||||
def test_remove_missing_is_noop(self):
|
||||
# Returning True == idempotent success.
|
||||
self.assertTrue(network_remove(f"claude-bottle-net-{self.slug}-does-not-exist"))
|
||||
self.assertTrue(network_remove(f"bot-bottle-net-{self.slug}-does-not-exist"))
|
||||
|
||||
@unittest.skipIf(
|
||||
os.environ.get("GITEA_ACTIONS") == "true",
|
||||
|
||||
@@ -20,7 +20,7 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from bot_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from tests._docker import skip_unless_docker
|
||||
from tests.fixtures import fixture_minimal
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from bot_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from tests._docker import skip_unless_docker
|
||||
from tests.fixtures import fixture_minimal
|
||||
|
||||
|
||||
@@ -26,29 +26,29 @@ import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.backend.docker.bottle_state import pipelock_state_dir
|
||||
from claude_bottle.backend.docker.network import (
|
||||
from bot_bottle.backend.docker.bottle_state import pipelock_state_dir
|
||||
from bot_bottle.backend.docker.network import (
|
||||
network_create_egress,
|
||||
network_create_internal,
|
||||
network_remove,
|
||||
)
|
||||
from claude_bottle.backend.docker.pipelock import (
|
||||
from bot_bottle.backend.docker.pipelock import (
|
||||
PIPELOCK_CA_CERT_IN_CONTAINER,
|
||||
PIPELOCK_CA_KEY_IN_CONTAINER,
|
||||
pipelock_tls_init,
|
||||
)
|
||||
from claude_bottle.pipelock import PipelockProxy
|
||||
from claude_bottle.backend.docker.pipelock_apply import (
|
||||
from bot_bottle.pipelock import PipelockProxy
|
||||
from bot_bottle.backend.docker.pipelock_apply import (
|
||||
PipelockApplyError,
|
||||
apply_allowlist_change,
|
||||
fetch_current_allowlist,
|
||||
fetch_current_yaml,
|
||||
)
|
||||
from claude_bottle.backend.docker.sidecar_bundle import (
|
||||
from bot_bottle.backend.docker.sidecar_bundle import (
|
||||
SIDECAR_BUNDLE_IMAGE,
|
||||
sidecar_bundle_container_name,
|
||||
)
|
||||
from claude_bottle.yaml_subset import parse_yaml_subset
|
||||
from bot_bottle.yaml_subset import parse_yaml_subset
|
||||
from tests._docker import skip_unless_docker
|
||||
from tests.fixtures import fixture_minimal
|
||||
|
||||
@@ -77,7 +77,7 @@ class TestPipelockApply(unittest.TestCase):
|
||||
if n:
|
||||
network_remove(n)
|
||||
shutil.rmtree(self.work_dir, ignore_errors=True)
|
||||
# Clean up the per-slug state dir under ~/.claude-bottle/state/
|
||||
# Clean up the per-slug state dir under ~/.bot-bottle/state/
|
||||
# (apply_allowlist_change writes there; _bring_up calls
|
||||
# proxy.prepare with the same path so the bind-mount and the
|
||||
# hot-reload write target stay coherent).
|
||||
@@ -123,7 +123,7 @@ class TestPipelockApply(unittest.TestCase):
|
||||
["docker", "create",
|
||||
"--name", self.sidecar_name,
|
||||
"--network", self.internal_net,
|
||||
"-e", "CLAUDE_BOTTLE_SIDECAR_DAEMONS=pipelock",
|
||||
"-e", "BOT_BOTTLE_SIDECAR_DAEMONS=pipelock",
|
||||
"-v", f"{prep.yaml_path}:/etc/pipelock.yaml:ro",
|
||||
"-v", f"{ca_cert_host}:{PIPELOCK_CA_CERT_IN_CONTAINER}:ro",
|
||||
"-v", f"{ca_key_host}:{PIPELOCK_CA_KEY_IN_CONTAINER}:ro",
|
||||
|
||||
@@ -17,7 +17,7 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from bot_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from tests._docker import skip_unless_docker
|
||||
from tests.fixtures import fixture_minimal
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from bot_bottle.manifest import Manifest
|
||||
from tests._docker import skip_unless_docker
|
||||
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from bot_bottle.manifest import Manifest
|
||||
from tests._docker import skip_unless_docker
|
||||
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from bot_bottle.manifest import Manifest
|
||||
from tests._docker import skip_unless_docker
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ asserts each one is blocked:
|
||||
|
||||
The suite is backend-agnostic — it goes through `get_bottle_backend()`
|
||||
so a future smolmachines backend can be tested by setting
|
||||
`CLAUDE_BOTTLE_BACKEND=smolmachines` without touching this file.
|
||||
`BOT_BOTTLE_BACKEND=smolmachines` without touching this file.
|
||||
|
||||
PRD 0022 chunk 1 (this commit): fixture + setUpClass +
|
||||
tearDownClass + preflight tool check. Attack tests land in
|
||||
@@ -28,9 +28,9 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from claude_bottle.backend.docker.bottle_state import cleanup_state
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from bot_bottle.backend.docker.bottle_state import cleanup_state
|
||||
from bot_bottle.manifest import Manifest
|
||||
from tests._docker import skip_unless_docker
|
||||
|
||||
|
||||
@@ -78,16 +78,16 @@ class TestSandboxEscape(unittest.TestCase):
|
||||
# already covers that. Smolmachines additionally needs smolvm on
|
||||
# PATH and is macOS-only in v1 (libkrun/TSI). Skip cleanly when
|
||||
# those are missing rather than die-ing inside backend.prepare.
|
||||
backend_name = os.environ.get("CLAUDE_BOTTLE_BACKEND", "docker")
|
||||
backend_name = os.environ.get("BOT_BOTTLE_BACKEND", "docker")
|
||||
if backend_name == "smolmachines":
|
||||
if sys.platform != "darwin":
|
||||
raise unittest.SkipTest(
|
||||
"CLAUDE_BOTTLE_BACKEND=smolmachines is macOS-only in "
|
||||
"BOT_BOTTLE_BACKEND=smolmachines is macOS-only in "
|
||||
"v1 (libkrun TSI)"
|
||||
)
|
||||
if shutil.which("smolvm") is None:
|
||||
raise unittest.SkipTest(
|
||||
"CLAUDE_BOTTLE_BACKEND=smolmachines requires `smolvm` "
|
||||
"BOT_BOTTLE_BACKEND=smolmachines requires `smolvm` "
|
||||
"on PATH: curl -sSL https://smolmachines.com/install.sh | sh"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Integration: end-to-end smoke for the PRD 0024 bundle shape.
|
||||
|
||||
Verifies that flipping `CLAUDE_BOTTLE_SIDECAR_BUNDLE=1` produces a
|
||||
Verifies that flipping `BOT_BOTTLE_SIDECAR_BUNDLE=1` produces a
|
||||
working bottle: `docker compose up` brings the agent + bundle pair
|
||||
online, the four daemons inside the bundle bind their ports, and
|
||||
the agent can reach pipelock + supervise via the bundle's network
|
||||
@@ -21,8 +21,8 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from bot_bottle.manifest import Manifest
|
||||
from tests._docker import skip_unless_docker
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class TestSidecarBundleCompose(unittest.TestCase):
|
||||
def test_bottle_up_with_bundle_flag_on(self):
|
||||
stage_dir = Path(tempfile.mkdtemp(prefix="cb-bundle-smoke."))
|
||||
try:
|
||||
with patch.dict(os.environ, {"CLAUDE_BOTTLE_SIDECAR_BUNDLE": "1"}):
|
||||
with patch.dict(os.environ, {"BOT_BOTTLE_SIDECAR_BUNDLE": "1"}):
|
||||
backend = get_bottle_backend()
|
||||
spec = BottleSpec(
|
||||
manifest=_manifest(),
|
||||
|
||||
@@ -28,7 +28,7 @@ import unittest
|
||||
from tests._docker import skip_unless_docker
|
||||
|
||||
|
||||
_IMAGE = "claude-bottle-sidecars-test:chunk1"
|
||||
_IMAGE = "bot-bottle-sidecars-test:chunk1"
|
||||
_DOCKERFILE = "Dockerfile.sidecars"
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ class TestSidecarBundleImage(unittest.TestCase):
|
||||
# ENTRYPOINT wiring works.
|
||||
proc = subprocess.run(
|
||||
["docker", "run", "--rm",
|
||||
"-e", "CLAUDE_BOTTLE_SIDECAR_DAEMONS=nothing",
|
||||
"-e", "BOT_BOTTLE_SIDECAR_DAEMONS=nothing",
|
||||
_IMAGE],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
timeout=10.0,
|
||||
|
||||
@@ -18,7 +18,7 @@ import subprocess
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from claude_bottle.backend.smolmachines.sidecar_bundle import (
|
||||
from bot_bottle.backend.smolmachines.sidecar_bundle import (
|
||||
BundleLaunchSpec,
|
||||
bundle_container_name,
|
||||
bundle_network_name,
|
||||
@@ -47,13 +47,13 @@ class TestBundleBringup(unittest.TestCase):
|
||||
remove_bundle_network(self.network)
|
||||
|
||||
def _bundle_image_built(self) -> bool:
|
||||
"""The bundle image (`claude-bottle-sidecars:latest`) is
|
||||
"""The bundle image (`bot-bottle-sidecars:latest`) is
|
||||
built lazily by the docker backend's compose. If a
|
||||
smolmachines-only operator hasn't run the docker backend
|
||||
first, the image won't exist locally. Skip rather than
|
||||
fail."""
|
||||
r = subprocess.run(
|
||||
["docker", "image", "inspect", "claude-bottle-sidecars:latest"],
|
||||
["docker", "image", "inspect", "bot-bottle-sidecars:latest"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
@@ -62,7 +62,7 @@ class TestBundleBringup(unittest.TestCase):
|
||||
def test_create_network_then_start_bundle_pins_ip(self):
|
||||
if not self._bundle_image_built():
|
||||
self.skipTest(
|
||||
"claude-bottle-sidecars:latest not built; run a docker "
|
||||
"bot-bottle-sidecars:latest not built; run a docker "
|
||||
"bottle first or `docker build -f Dockerfile.sidecars .`"
|
||||
)
|
||||
|
||||
@@ -85,7 +85,7 @@ class TestBundleBringup(unittest.TestCase):
|
||||
# Only run the pipelock daemon for this smoke — it's
|
||||
# the lightest of the four and doesn't need bind
|
||||
# mounts beyond what we'd skip without
|
||||
# CLAUDE_BOTTLE_SIDECAR_DAEMONS. (The init
|
||||
# BOT_BOTTLE_SIDECAR_DAEMONS. (The init
|
||||
# supervisor will exit if pipelock fails to find its
|
||||
# yaml — that's expected here; we just need the
|
||||
# container to land on the network at the right IP.)
|
||||
|
||||
@@ -32,9 +32,9 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from claude_bottle.backend.smolmachines.smolvm import is_available as _smolvm_available
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.backend import BottleSpec, get_bottle_backend
|
||||
from bot_bottle.backend.smolmachines.smolvm import is_available as _smolvm_available
|
||||
from bot_bottle.manifest import Manifest
|
||||
from tests._docker import skip_unless_docker
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class TestSmolmachinesLaunch(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.stage = Path(tempfile.mkdtemp(prefix="cb-smol-launch."))
|
||||
os.environ["CLAUDE_BOTTLE_BACKEND"] = "smolmachines"
|
||||
os.environ["BOT_BOTTLE_BACKEND"] = "smolmachines"
|
||||
backend = get_bottle_backend()
|
||||
spec = BottleSpec(
|
||||
manifest=_minimal_manifest(),
|
||||
@@ -94,7 +94,7 @@ class TestSmolmachinesLaunch(unittest.TestCase):
|
||||
cls._launch.__exit__(None, None, None)
|
||||
finally:
|
||||
shutil.rmtree(cls.stage, ignore_errors=True)
|
||||
os.environ.pop("CLAUDE_BOTTLE_BACKEND", None)
|
||||
os.environ.pop("BOT_BOTTLE_BACKEND", None)
|
||||
|
||||
def test_smoke_exec_echo(self):
|
||||
# The plumbing-verifies-end-to-end smoke: a shell command
|
||||
@@ -152,10 +152,10 @@ class TestSmolmachinesLaunch(unittest.TestCase):
|
||||
|
||||
def test_prompt_file_lands_in_guest(self):
|
||||
# provision_prompt copies the host-side prompt.txt into the
|
||||
# guest at /root/.claude-bottle-prompt.txt. The content
|
||||
# guest at /root/.bot-bottle-prompt.txt. The content
|
||||
# must match what the manifest declared so claude-code's
|
||||
# --append-system-prompt-file reads the right text.
|
||||
r = self.bottle.exec("cat /root/.claude-bottle-prompt.txt")
|
||||
r = self.bottle.exec("cat /root/.bot-bottle-prompt.txt")
|
||||
self.assertEqual(0, r.returncode, msg=r.stderr)
|
||||
self.assertEqual(_AGENT_PROMPT, r.stdout.rstrip("\n"))
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import platform
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from claude_bottle.backend.smolmachines.smolvm import is_available
|
||||
from bot_bottle.backend.smolmachines.smolvm import is_available
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
|
||||
@@ -12,8 +12,8 @@ import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle import backend as backend_mod
|
||||
from claude_bottle.backend import (
|
||||
from bot_bottle import backend as backend_mod
|
||||
from bot_bottle.backend import (
|
||||
ActiveAgent,
|
||||
enumerate_active_agents,
|
||||
get_bottle_backend,
|
||||
@@ -23,12 +23,12 @@ from claude_bottle.backend import (
|
||||
|
||||
class TestGetBottleBackend(unittest.TestCase):
|
||||
def test_explicit_name_wins_over_env(self):
|
||||
with patch.dict(os.environ, {"CLAUDE_BOTTLE_BACKEND": "smolmachines"}):
|
||||
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "smolmachines"}):
|
||||
b = get_bottle_backend("docker")
|
||||
self.assertEqual("docker", b.name)
|
||||
|
||||
def test_env_var_fallback(self):
|
||||
with patch.dict(os.environ, {"CLAUDE_BOTTLE_BACKEND": "smolmachines"}):
|
||||
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "smolmachines"}):
|
||||
b = get_bottle_backend()
|
||||
self.assertEqual("smolmachines", b.name)
|
||||
|
||||
@@ -139,11 +139,11 @@ class TestHasBackend(unittest.TestCase):
|
||||
with patch.object(
|
||||
backend_mod, "_BACKENDS", {"docker": _FakeBackend()},
|
||||
):
|
||||
from claude_bottle.backend import has_backend
|
||||
from bot_bottle.backend import has_backend
|
||||
self.assertFalse(has_backend("docker"))
|
||||
|
||||
def test_unknown_backend_returns_false(self):
|
||||
from claude_bottle.backend import has_backend
|
||||
from bot_bottle.backend import has_backend
|
||||
self.assertFalse(has_backend("nonexistent"))
|
||||
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle import supervise
|
||||
from claude_bottle.backend.docker import bottle_state
|
||||
from claude_bottle.backend.docker.bottle_state import (
|
||||
from bot_bottle import supervise
|
||||
from bot_bottle.backend.docker import bottle_state
|
||||
from bot_bottle.backend.docker.bottle_state import (
|
||||
BottleMetadata,
|
||||
read_metadata,
|
||||
write_metadata,
|
||||
@@ -18,13 +18,13 @@ from claude_bottle.backend.docker.bottle_state import (
|
||||
class _FakeHomeMixin:
|
||||
def _setup_fake_home(self):
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="bottle-state-test.")
|
||||
original = supervise.claude_bottle_root
|
||||
original = supervise.bot_bottle_root
|
||||
|
||||
def fake_root() -> Path:
|
||||
return Path(self._tmp.name) / ".claude-bottle"
|
||||
return Path(self._tmp.name) / ".bot-bottle"
|
||||
|
||||
supervise.claude_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore = lambda: setattr(supervise, "claude_bottle_root", original)
|
||||
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore = lambda: setattr(supervise, "bot_bottle_root", original)
|
||||
|
||||
def _teardown_fake_home(self):
|
||||
self._restore()
|
||||
@@ -58,11 +58,11 @@ class TestPerBottleDockerfile(_FakeHomeMixin, unittest.TestCase):
|
||||
|
||||
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"))
|
||||
self.assertTrue(str(path).endswith("/.bot-bottle/state/dev/Dockerfile"))
|
||||
|
||||
def test_image_tag_unique_per_slug(self):
|
||||
self.assertEqual(
|
||||
"claude-bottle-rebuilt-dev:latest",
|
||||
"bot-bottle-rebuilt-dev:latest",
|
||||
bottle_state.per_bottle_image_tag("dev"),
|
||||
)
|
||||
self.assertNotEqual(
|
||||
@@ -72,7 +72,7 @@ class TestPerBottleDockerfile(_FakeHomeMixin, unittest.TestCase):
|
||||
|
||||
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"))
|
||||
self.assertTrue(str(path).endswith("/.bot-bottle/state/dev/transcript"))
|
||||
|
||||
|
||||
class TestBottleIdentity(unittest.TestCase):
|
||||
@@ -143,7 +143,7 @@ class TestPreserveMarker(_FakeHomeMixin, unittest.TestCase):
|
||||
|
||||
def test_marker_path_under_state_dir(self):
|
||||
path = bottle_state.preserve_marker_path("dev-x")
|
||||
self.assertTrue(str(path).endswith("/.claude-bottle/state/dev-x/.preserve"))
|
||||
self.assertTrue(str(path).endswith("/.bot-bottle/state/dev-x/.preserve"))
|
||||
|
||||
|
||||
class TestCleanupState(_FakeHomeMixin, unittest.TestCase):
|
||||
@@ -197,7 +197,7 @@ class TestBottleMetadata(_FakeHomeMixin, unittest.TestCase):
|
||||
)
|
||||
path = write_metadata(meta)
|
||||
self.assertTrue(
|
||||
str(path).endswith("/.claude-bottle/state/dev-x/metadata.json"),
|
||||
str(path).endswith("/.bot-bottle/state/dev-x/metadata.json"),
|
||||
)
|
||||
|
||||
def test_overwriting_metadata_updates_timestamp(self):
|
||||
|
||||
@@ -12,9 +12,9 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle import supervise
|
||||
from claude_bottle.backend.docker import bottle_state, capability_apply
|
||||
from claude_bottle.backend.docker.capability_apply import (
|
||||
from bot_bottle import supervise
|
||||
from bot_bottle.backend.docker import bottle_state, capability_apply
|
||||
from bot_bottle.backend.docker.capability_apply import (
|
||||
CapabilityApplyError,
|
||||
apply_capability_change,
|
||||
fetch_current_dockerfile,
|
||||
@@ -24,13 +24,13 @@ from claude_bottle.backend.docker.capability_apply import (
|
||||
class _FakeHomeMixin:
|
||||
def _setup_fake_home(self):
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="cap-apply-test.")
|
||||
original = supervise.claude_bottle_root
|
||||
original = supervise.bot_bottle_root
|
||||
|
||||
def fake_root() -> Path:
|
||||
return Path(self._tmp.name) / ".claude-bottle"
|
||||
return Path(self._tmp.name) / ".bot-bottle"
|
||||
|
||||
supervise.claude_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore = lambda: setattr(supervise, "claude_bottle_root", original)
|
||||
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore = lambda: setattr(supervise, "bot_bottle_root", original)
|
||||
|
||||
def _teardown_fake_home(self):
|
||||
self._restore()
|
||||
|
||||
@@ -10,7 +10,7 @@ import sys
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from claude_bottle.cli import cleanup as cmd
|
||||
from bot_bottle.cli import cleanup as cmd
|
||||
|
||||
|
||||
def _make_backend(empty: bool = True):
|
||||
|
||||
@@ -13,7 +13,7 @@ import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
from claude_bottle.backend import known_backend_names
|
||||
from bot_bottle.backend import known_backend_names
|
||||
|
||||
|
||||
class TestStartBackendFlag(unittest.TestCase):
|
||||
@@ -51,9 +51,9 @@ class TestStartBackendFlag(unittest.TestCase):
|
||||
def test_resolution_priority_explicit_over_env(self):
|
||||
# Independent assertion that get_bottle_backend (where
|
||||
# `--backend` ultimately threads to) prefers the explicit
|
||||
# name over CLAUDE_BOTTLE_BACKEND.
|
||||
from claude_bottle.backend import get_bottle_backend
|
||||
with patch.dict(os.environ, {"CLAUDE_BOTTLE_BACKEND": "smolmachines"}):
|
||||
# name over BOT_BOTTLE_BACKEND.
|
||||
from bot_bottle.backend import get_bottle_backend
|
||||
with patch.dict(os.environ, {"BOT_BOTTLE_BACKEND": "smolmachines"}):
|
||||
self.assertEqual("docker", get_bottle_backend("docker").name)
|
||||
|
||||
|
||||
|
||||
@@ -8,23 +8,23 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle import supervise
|
||||
from claude_bottle.backend.docker import bottle_state
|
||||
from claude_bottle.cli import start as start_mod
|
||||
from bot_bottle import supervise
|
||||
from bot_bottle.backend.docker import bottle_state
|
||||
from bot_bottle.cli import start as start_mod
|
||||
|
||||
|
||||
class _FakeHomeMixin:
|
||||
def _setup_fake_home(self):
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="cli-start-settle.")
|
||||
self._original = supervise.claude_bottle_root
|
||||
self._original = supervise.bot_bottle_root
|
||||
|
||||
def fake_root() -> Path:
|
||||
return Path(self._tmp.name) / ".claude-bottle"
|
||||
return Path(self._tmp.name) / ".bot-bottle"
|
||||
|
||||
supervise.claude_bottle_root = fake_root # type: ignore[assignment]
|
||||
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
|
||||
|
||||
def _teardown_fake_home(self):
|
||||
supervise.claude_bottle_root = self._original # type: ignore[assignment]
|
||||
supervise.bot_bottle_root = self._original # type: ignore[assignment]
|
||||
self._tmp.cleanup()
|
||||
|
||||
|
||||
|
||||
+28
-28
@@ -12,22 +12,22 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.backend import BottleSpec
|
||||
from claude_bottle.backend.docker.bottle_plan import DockerBottlePlan
|
||||
from claude_bottle.backend.docker.compose import (
|
||||
from bot_bottle.backend import BottleSpec
|
||||
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
|
||||
from bot_bottle.backend.docker.compose import (
|
||||
COMPOSE_PROJECT_PREFIX,
|
||||
bottle_plan_to_compose,
|
||||
compose_project_name,
|
||||
slug_from_compose_project,
|
||||
)
|
||||
from claude_bottle.egress import (
|
||||
from bot_bottle.egress import (
|
||||
EgressPlan,
|
||||
EgressRoute,
|
||||
)
|
||||
from claude_bottle.git_gate import GitGatePlan, GitGateUpstream
|
||||
from claude_bottle.manifest import Manifest
|
||||
from claude_bottle.pipelock import PipelockProxyPlan
|
||||
from claude_bottle.supervise import SupervisePlan
|
||||
from bot_bottle.git_gate import GitGatePlan, GitGateUpstream
|
||||
from bot_bottle.manifest import Manifest
|
||||
from bot_bottle.pipelock import PipelockProxyPlan
|
||||
from bot_bottle.supervise import SupervisePlan
|
||||
|
||||
|
||||
SLUG = "demo-abc12"
|
||||
@@ -78,9 +78,9 @@ def _proxy_plan() -> PipelockProxyPlan:
|
||||
return PipelockProxyPlan(
|
||||
yaml_path=STATE / "pipelock.yaml",
|
||||
slug=SLUG,
|
||||
internal_network=f"claude-bottle-net-{SLUG}",
|
||||
internal_network=f"bot-bottle-net-{SLUG}",
|
||||
internal_network_cidr="10.1.2.0/24",
|
||||
egress_network=f"claude-bottle-egress-{SLUG}",
|
||||
egress_network=f"bot-bottle-egress-{SLUG}",
|
||||
ca_cert_host_path=STATE / "pipelock-ca" / "ca.pem",
|
||||
ca_key_host_path=STATE / "pipelock-ca" / "ca-key.pem",
|
||||
)
|
||||
@@ -93,8 +93,8 @@ def _git_gate_plan(upstreams: tuple[GitGateUpstream, ...] = ()) -> GitGatePlan:
|
||||
hook_script=STATE / "git-gate" / "pre-receive",
|
||||
access_hook_script=STATE / "git-gate" / "access-hook",
|
||||
upstreams=upstreams,
|
||||
internal_network=f"claude-bottle-net-{SLUG}",
|
||||
egress_network=f"claude-bottle-egress-{SLUG}",
|
||||
internal_network=f"bot-bottle-net-{SLUG}",
|
||||
egress_network=f"bot-bottle-egress-{SLUG}",
|
||||
)
|
||||
|
||||
|
||||
@@ -109,8 +109,8 @@ def _egress_plan(routes: tuple[EgressRoute, ...] = ()) -> EgressPlan:
|
||||
routes_path=STATE / "egress" / "routes.yaml",
|
||||
routes=routes,
|
||||
token_env_map=token_env_map,
|
||||
internal_network=f"claude-bottle-net-{SLUG}",
|
||||
egress_network=f"claude-bottle-egress-{SLUG}",
|
||||
internal_network=f"bot-bottle-net-{SLUG}",
|
||||
egress_network=f"bot-bottle-egress-{SLUG}",
|
||||
mitmproxy_ca_host_path=STATE / "egress-ca" / "mitmproxy-ca.pem",
|
||||
mitmproxy_ca_cert_only_host_path=STATE / "egress-ca" / "ca.pem",
|
||||
pipelock_ca_host_path=STATE / "pipelock-ca" / "ca.pem",
|
||||
@@ -123,7 +123,7 @@ def _supervise_plan() -> SupervisePlan:
|
||||
slug=SLUG,
|
||||
queue_dir=STATE / "supervise" / "queue",
|
||||
current_config_dir=STATE / "supervise" / "current-config",
|
||||
internal_network=f"claude-bottle-net-{SLUG}",
|
||||
internal_network=f"bot-bottle-net-{SLUG}",
|
||||
)
|
||||
|
||||
|
||||
@@ -161,11 +161,11 @@ def _plan(
|
||||
spec=_spec(supervise=supervise, with_git=with_git, with_egress=with_egress),
|
||||
stage_dir=STAGE,
|
||||
slug=SLUG,
|
||||
container_name=f"claude-bottle-{SLUG}",
|
||||
container_name=f"bot-bottle-{SLUG}",
|
||||
container_name_pinned=False,
|
||||
image="claude-bottle:latest",
|
||||
image="bot-bottle-claude:latest",
|
||||
derived_image="",
|
||||
runtime_image="claude-bottle:latest",
|
||||
runtime_image="bot-bottle-claude:latest",
|
||||
dockerfile_path="",
|
||||
env_file=Path("/dev/null"), # exists, size 0 → renderer skips env_file
|
||||
forwarded_env={"CLAUDE_CODE_OAUTH_TOKEN": "x"},
|
||||
@@ -181,18 +181,18 @@ def _plan(
|
||||
class TestProjectAndNetworks(unittest.TestCase):
|
||||
def test_project_name(self):
|
||||
spec = bottle_plan_to_compose(_plan())
|
||||
self.assertEqual(f"claude-bottle-{SLUG}", spec["name"])
|
||||
self.assertEqual(f"bot-bottle-{SLUG}", spec["name"])
|
||||
|
||||
def test_internal_network_is_internal(self):
|
||||
spec = bottle_plan_to_compose(_plan())
|
||||
net = spec["networks"]["internal"]
|
||||
self.assertEqual(f"claude-bottle-net-{SLUG}", net["name"])
|
||||
self.assertEqual(f"bot-bottle-net-{SLUG}", net["name"])
|
||||
self.assertTrue(net["internal"])
|
||||
|
||||
def test_egress_network_is_external_bridge(self):
|
||||
spec = bottle_plan_to_compose(_plan())
|
||||
net = spec["networks"]["egress"]
|
||||
self.assertEqual(f"claude-bottle-egress-{SLUG}", net["name"])
|
||||
self.assertEqual(f"bot-bottle-egress-{SLUG}", net["name"])
|
||||
# No `internal:` key on the egress network — defaults to a
|
||||
# normal user-defined bridge.
|
||||
self.assertNotIn("internal", net)
|
||||
@@ -262,13 +262,13 @@ class TestAgentAlwaysPresent(unittest.TestCase):
|
||||
def test_agent_current_config_mount_only_with_supervise(self):
|
||||
with_sv = bottle_plan_to_compose(_plan(supervise=True))["services"]["agent"]
|
||||
self.assertTrue(any(
|
||||
v["target"] == "/etc/claude-bottle/current-config"
|
||||
v["target"] == "/etc/bot-bottle/current-config"
|
||||
for v in with_sv.get("volumes", [])
|
||||
))
|
||||
without_sv = bottle_plan_to_compose(_plan(supervise=False))["services"]["agent"]
|
||||
# Either no volumes key at all, or no current-config target.
|
||||
self.assertFalse(any(
|
||||
v["target"] == "/etc/claude-bottle/current-config"
|
||||
v["target"] == "/etc/bot-bottle/current-config"
|
||||
for v in without_sv.get("volumes", [])
|
||||
))
|
||||
|
||||
@@ -293,12 +293,12 @@ class TestSidecarBundleShape(unittest.TestCase):
|
||||
|
||||
def test_bundle_uses_bundle_image_and_dockerfile(self):
|
||||
sc = self._render()["services"]["sidecars"]
|
||||
self.assertEqual("claude-bottle-sidecars:latest", sc["image"])
|
||||
self.assertEqual("bot-bottle-sidecars:latest", sc["image"])
|
||||
self.assertEqual("Dockerfile.sidecars", sc["build"]["dockerfile"])
|
||||
|
||||
def test_bundle_container_name_uses_sidecars_prefix(self):
|
||||
sc = self._render()["services"]["sidecars"]
|
||||
self.assertEqual(f"claude-bottle-sidecars-{SLUG}", sc["container_name"])
|
||||
self.assertEqual(f"bot-bottle-sidecars-{SLUG}", sc["container_name"])
|
||||
|
||||
def test_bundle_joins_both_networks(self):
|
||||
sc = self._render()["services"]["sidecars"]
|
||||
@@ -335,18 +335,18 @@ class TestSidecarBundleShape(unittest.TestCase):
|
||||
daemons = {
|
||||
line.split("=", 1)[1]
|
||||
for line in sc["environment"]
|
||||
if line.startswith("CLAUDE_BOTTLE_SIDECAR_DAEMONS=")
|
||||
if line.startswith("BOT_BOTTLE_SIDECAR_DAEMONS=")
|
||||
}
|
||||
self.assertEqual({"egress,pipelock"}, daemons)
|
||||
|
||||
def test_daemons_csv_expands_with_optional_sidecars(self):
|
||||
sc = self._render(with_git=True, supervise=True)["services"]["sidecars"]
|
||||
for line in sc["environment"]:
|
||||
if line.startswith("CLAUDE_BOTTLE_SIDECAR_DAEMONS="):
|
||||
if line.startswith("BOT_BOTTLE_SIDECAR_DAEMONS="):
|
||||
csv = line.split("=", 1)[1]
|
||||
break
|
||||
else:
|
||||
self.fail("CLAUDE_BOTTLE_SIDECAR_DAEMONS not in env")
|
||||
self.fail("BOT_BOTTLE_SIDECAR_DAEMONS not in env")
|
||||
self.assertEqual(
|
||||
["egress", "pipelock", "git-gate", "supervise"],
|
||||
csv.split(","),
|
||||
|
||||
@@ -15,12 +15,12 @@ import unittest
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle import supervise
|
||||
from claude_bottle.backend.docker.capability_apply import CapabilityApplyError
|
||||
from claude_bottle.backend.docker.egress_apply import EgressApplyError
|
||||
from claude_bottle.backend.docker.pipelock_apply import PipelockApplyError
|
||||
from claude_bottle.cli import dashboard
|
||||
from claude_bottle.supervise import (
|
||||
from bot_bottle import supervise
|
||||
from bot_bottle.backend.docker.capability_apply import CapabilityApplyError
|
||||
from bot_bottle.backend.docker.egress_apply import EgressApplyError
|
||||
from bot_bottle.backend.docker.pipelock_apply import PipelockApplyError
|
||||
from bot_bottle.cli import dashboard
|
||||
from bot_bottle.supervise import (
|
||||
Proposal,
|
||||
STATUS_APPROVED,
|
||||
STATUS_MODIFIED,
|
||||
@@ -58,17 +58,17 @@ def _proposal(slug: str = "dev", tool: str = TOOL_EGRESS_BLOCK) -> Proposal:
|
||||
|
||||
|
||||
class _FakeHomeMixin:
|
||||
"""Patch supervise.claude_bottle_root to a temp dir for the test."""
|
||||
"""Patch supervise.bot_bottle_root to a temp dir for the test."""
|
||||
|
||||
def _setup_fake_home(self):
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="dashboard-test.")
|
||||
original = supervise.claude_bottle_root
|
||||
original = supervise.bot_bottle_root
|
||||
|
||||
def fake_root() -> Path:
|
||||
return Path(self._tmp.name) / ".claude-bottle"
|
||||
return Path(self._tmp.name) / ".bot-bottle"
|
||||
|
||||
supervise.claude_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore_home = lambda: setattr(supervise, "claude_bottle_root", original)
|
||||
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore_home = lambda: setattr(supervise, "bot_bottle_root", original)
|
||||
|
||||
def _teardown_fake_home(self):
|
||||
self._restore_home()
|
||||
|
||||
@@ -6,20 +6,20 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle import supervise
|
||||
from claude_bottle.cli import dashboard
|
||||
from bot_bottle import supervise
|
||||
from bot_bottle.cli import dashboard
|
||||
|
||||
|
||||
class _FakeHomeMixin:
|
||||
def _setup_fake_home(self) -> None:
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="dashboard-aa-test.")
|
||||
original = supervise.claude_bottle_root
|
||||
original = supervise.bot_bottle_root
|
||||
|
||||
def fake_root() -> Path:
|
||||
return Path(self._tmp.name) / ".claude-bottle"
|
||||
return Path(self._tmp.name) / ".bot-bottle"
|
||||
|
||||
supervise.claude_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore_home = lambda: setattr(supervise, "claude_bottle_root", original)
|
||||
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore_home = lambda: setattr(supervise, "bot_bottle_root", original)
|
||||
|
||||
def _teardown_fake_home(self) -> None:
|
||||
self._restore_home()
|
||||
@@ -230,7 +230,7 @@ class TestBottleForSlug(unittest.TestCase):
|
||||
def test_unowned_synthesizes_docker_bottle(self):
|
||||
bottle, _ = dashboard._bottle_for_slug("dev-xyz", {}, None)
|
||||
# The synth wraps the slug-derived container name.
|
||||
self.assertEqual("claude-bottle-dev-xyz", bottle.name)
|
||||
self.assertEqual("bot-bottle-dev-xyz", bottle.name)
|
||||
|
||||
def test_unowned_without_manifest_omits_prompt_path(self):
|
||||
bottle, hint = dashboard._bottle_for_slug("dev-xyz", {}, None)
|
||||
@@ -295,7 +295,7 @@ class TestTmuxPaneArgvBuilders(unittest.TestCase):
|
||||
|
||||
DOCKER_ARGV = [
|
||||
"docker", "exec", "-it",
|
||||
"claude-bottle-dev-abc",
|
||||
"bot-bottle-dev-abc",
|
||||
"claude", "--dangerously-skip-permissions", "--continue",
|
||||
]
|
||||
|
||||
@@ -329,9 +329,9 @@ class TestResumeArgvWithFallback(unittest.TestCase):
|
||||
--continue has no session to resume."""
|
||||
|
||||
def _bottle(self, prompt_path: str | None = None):
|
||||
from claude_bottle.backend.docker.bottle import DockerBottle
|
||||
from bot_bottle.backend.docker.bottle import DockerBottle
|
||||
return DockerBottle(
|
||||
container="claude-bottle-dev-abc",
|
||||
container="bot-bottle-dev-abc",
|
||||
teardown=lambda: None,
|
||||
prompt_path_in_container=prompt_path,
|
||||
)
|
||||
@@ -340,7 +340,7 @@ class TestResumeArgvWithFallback(unittest.TestCase):
|
||||
argv = dashboard._build_resume_argv_with_fallback(self._bottle())
|
||||
# Must end with `sh -c '<cmd> --continue || <cmd>'`.
|
||||
self.assertEqual(
|
||||
["docker", "exec", "-it", "claude-bottle-dev-abc", "sh", "-c"],
|
||||
["docker", "exec", "-it", "bot-bottle-dev-abc", "sh", "-c"],
|
||||
argv[:6],
|
||||
)
|
||||
inner = argv[6]
|
||||
@@ -362,10 +362,10 @@ class TestResumeArgvWithFallback(unittest.TestCase):
|
||||
self.assertIn("--dangerously-skip-permissions", argv[-1])
|
||||
|
||||
def test_includes_prompt_file_flag_when_set(self):
|
||||
bottle = self._bottle("/home/node/.claude-bottle-prompt.txt")
|
||||
bottle = self._bottle("/home/node/.bot-bottle-prompt.txt")
|
||||
argv = dashboard._build_resume_argv_with_fallback(bottle)
|
||||
self.assertIn("--append-system-prompt-file", argv[-1])
|
||||
self.assertIn("/home/node/.claude-bottle-prompt.txt", argv[-1])
|
||||
self.assertIn("/home/node/.bot-bottle-prompt.txt", argv[-1])
|
||||
|
||||
|
||||
class TestClaudeRuntimeArgs(unittest.TestCase):
|
||||
|
||||
@@ -7,9 +7,9 @@ which hostname will land in pipelock's allowlist on approval."""
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle import supervise
|
||||
from claude_bottle.cli import dashboard
|
||||
from claude_bottle.supervise import (
|
||||
from bot_bottle import supervise
|
||||
from bot_bottle.cli import dashboard
|
||||
from bot_bottle.supervise import (
|
||||
Proposal,
|
||||
TOOL_CAPABILITY_BLOCK,
|
||||
TOOL_EGRESS_BLOCK,
|
||||
|
||||
@@ -6,7 +6,7 @@ highlight window?`"""
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle.cli import dashboard
|
||||
from bot_bottle.cli import dashboard
|
||||
|
||||
|
||||
class TestIsRecent(unittest.TestCase):
|
||||
|
||||
@@ -11,12 +11,12 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle.backend.docker.bottle import DockerBottle
|
||||
from bot_bottle.backend.docker.bottle import DockerBottle
|
||||
|
||||
|
||||
def _bottle(prompt_path: str | None = None) -> DockerBottle:
|
||||
return DockerBottle(
|
||||
container="claude-bottle-dev-abc",
|
||||
container="bot-bottle-dev-abc",
|
||||
teardown=lambda: None,
|
||||
prompt_path_in_container=prompt_path,
|
||||
)
|
||||
@@ -24,7 +24,7 @@ def _bottle(prompt_path: str | None = None) -> DockerBottle:
|
||||
|
||||
def _codex_bottle(prompt_path: str | None = None) -> DockerBottle:
|
||||
return DockerBottle(
|
||||
container="claude-bottle-dev-abc",
|
||||
container="bot-bottle-dev-abc",
|
||||
teardown=lambda: None,
|
||||
prompt_path_in_container=prompt_path,
|
||||
agent_command="codex",
|
||||
@@ -36,7 +36,7 @@ class TestClaudeArgv(unittest.TestCase):
|
||||
def test_minimal_argv_no_prompt(self):
|
||||
argv = _bottle().claude_argv([])
|
||||
self.assertEqual(
|
||||
["docker", "exec", "-it", "claude-bottle-dev-abc", "claude"],
|
||||
["docker", "exec", "-it", "bot-bottle-dev-abc", "claude"],
|
||||
argv,
|
||||
)
|
||||
|
||||
@@ -45,20 +45,20 @@ class TestClaudeArgv(unittest.TestCase):
|
||||
["--dangerously-skip-permissions", "--continue"],
|
||||
)
|
||||
self.assertEqual(
|
||||
["docker", "exec", "-it", "claude-bottle-dev-abc", "claude",
|
||||
["docker", "exec", "-it", "bot-bottle-dev-abc", "claude",
|
||||
"--dangerously-skip-permissions", "--continue"],
|
||||
argv,
|
||||
)
|
||||
|
||||
def test_appends_prompt_file_flag_when_set(self):
|
||||
argv = _bottle("/home/node/.claude-bottle-prompt.txt").claude_argv(
|
||||
argv = _bottle("/home/node/.bot-bottle-prompt.txt").claude_argv(
|
||||
["--dangerously-skip-permissions"],
|
||||
)
|
||||
self.assertEqual(
|
||||
["docker", "exec", "-it", "claude-bottle-dev-abc", "claude",
|
||||
["docker", "exec", "-it", "bot-bottle-dev-abc", "claude",
|
||||
"--dangerously-skip-permissions",
|
||||
"--append-system-prompt-file",
|
||||
"/home/node/.claude-bottle-prompt.txt"],
|
||||
"/home/node/.bot-bottle-prompt.txt"],
|
||||
argv,
|
||||
)
|
||||
|
||||
@@ -76,7 +76,7 @@ class TestClaudeArgv(unittest.TestCase):
|
||||
def test_tty_false_drops_it_flag(self):
|
||||
argv = _bottle().claude_argv([], tty=False)
|
||||
self.assertEqual(
|
||||
["docker", "exec", "claude-bottle-dev-abc", "claude"],
|
||||
["docker", "exec", "bot-bottle-dev-abc", "claude"],
|
||||
argv,
|
||||
)
|
||||
|
||||
@@ -94,26 +94,26 @@ class TestClaudeArgv(unittest.TestCase):
|
||||
["--dangerously-bypass-approvals-and-sandbox"],
|
||||
)
|
||||
self.assertEqual(
|
||||
["docker", "exec", "-it", "claude-bottle-dev-abc", "codex",
|
||||
["docker", "exec", "-it", "bot-bottle-dev-abc", "codex",
|
||||
"--dangerously-bypass-approvals-and-sandbox"],
|
||||
argv,
|
||||
)
|
||||
|
||||
def test_codex_provider_passes_prompt_reference_as_initial_prompt(self):
|
||||
argv = _codex_bottle("/home/node/.claude-bottle-prompt.txt").claude_argv([])
|
||||
argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").claude_argv([])
|
||||
self.assertEqual(
|
||||
["docker", "exec", "-it", "claude-bottle-dev-abc", "codex",
|
||||
["docker", "exec", "-it", "bot-bottle-dev-abc", "codex",
|
||||
"Read and follow the instructions in "
|
||||
"/home/node/.claude-bottle-prompt.txt."],
|
||||
"/home/node/.bot-bottle-prompt.txt."],
|
||||
argv,
|
||||
)
|
||||
|
||||
def test_codex_resume_does_not_append_initial_prompt(self):
|
||||
argv = _codex_bottle("/home/node/.claude-bottle-prompt.txt").claude_argv(
|
||||
argv = _codex_bottle("/home/node/.bot-bottle-prompt.txt").claude_argv(
|
||||
["--dangerously-bypass-approvals-and-sandbox", "resume", "--last"],
|
||||
)
|
||||
self.assertEqual(
|
||||
["docker", "exec", "-it", "claude-bottle-dev-abc", "codex",
|
||||
["docker", "exec", "-it", "bot-bottle-dev-abc", "codex",
|
||||
"--dangerously-bypass-approvals-and-sandbox", "resume", "--last"],
|
||||
argv,
|
||||
)
|
||||
|
||||
@@ -15,21 +15,21 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle import supervise
|
||||
from claude_bottle.backend.docker import bottle_state
|
||||
from claude_bottle.backend.docker.cleanup import _list_orphan_state_dirs
|
||||
from bot_bottle import supervise
|
||||
from bot_bottle.backend.docker import bottle_state
|
||||
from bot_bottle.backend.docker.cleanup import _list_orphan_state_dirs
|
||||
|
||||
|
||||
class _FakeHomeMixin:
|
||||
def _setup_fake_home(self) -> None:
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="docker-cleanup-test.")
|
||||
original = supervise.claude_bottle_root
|
||||
original = supervise.bot_bottle_root
|
||||
|
||||
def fake_root() -> Path:
|
||||
return Path(self._tmp.name) / ".claude-bottle"
|
||||
return Path(self._tmp.name) / ".bot-bottle"
|
||||
|
||||
supervise.claude_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore = lambda: setattr(supervise, "claude_bottle_root", original)
|
||||
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore = lambda: setattr(supervise, "bot_bottle_root", original)
|
||||
|
||||
def _teardown_fake_home(self) -> None:
|
||||
self._restore()
|
||||
@@ -61,7 +61,7 @@ class TestOrphanStateDirs(_FakeHomeMixin, unittest.TestCase):
|
||||
bottle_state.write_per_bottle_dockerfile("live-bbb", "FROM x\n")
|
||||
self.assertEqual(
|
||||
[],
|
||||
_list_orphan_state_dirs({"claude-bottle-live-bbb"}, set()),
|
||||
_list_orphan_state_dirs({"bot-bottle-live-bbb"}, set()),
|
||||
)
|
||||
|
||||
def test_preserve_marker_skips_dir(self):
|
||||
@@ -86,7 +86,7 @@ class TestOrphanStateDirs(_FakeHomeMixin, unittest.TestCase):
|
||||
bottle_state.write_per_bottle_dockerfile("kept-ggg", "FROM z\n")
|
||||
bottle_state.mark_preserved("kept-ggg")
|
||||
|
||||
result = _list_orphan_state_dirs({"claude-bottle-live-fff"}, set())
|
||||
result = _list_orphan_state_dirs({"bot-bottle-live-fff"}, set())
|
||||
self.assertEqual(["orphan-eee"], result)
|
||||
|
||||
def test_sorted_output(self):
|
||||
@@ -116,7 +116,7 @@ class TestOrphanStateDirs(_FakeHomeMixin, unittest.TestCase):
|
||||
self.assertEqual(
|
||||
[],
|
||||
_list_orphan_state_dirs(
|
||||
{"claude-bottle-something-else"}, # different project up
|
||||
{"bot-bottle-something-else"}, # different project up
|
||||
{"smol-iii"}, # but smol-iii is live
|
||||
),
|
||||
)
|
||||
|
||||
@@ -23,8 +23,8 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle import supervise
|
||||
from claude_bottle.backend.docker import bottle_state, enumerate as _enumerate
|
||||
from bot_bottle import supervise
|
||||
from bot_bottle.backend.docker import bottle_state, enumerate as _enumerate
|
||||
|
||||
|
||||
class TestParseServicesByProject(unittest.TestCase):
|
||||
@@ -33,18 +33,18 @@ class TestParseServicesByProject(unittest.TestCase):
|
||||
|
||||
def test_one_container(self):
|
||||
out = _enumerate._parse_services_by_project(
|
||||
"claude-bottle-dev-abc\tegress\n"
|
||||
"bot-bottle-dev-abc\tegress\n"
|
||||
)
|
||||
self.assertEqual({"claude-bottle-dev-abc": {"egress"}}, out)
|
||||
self.assertEqual({"bot-bottle-dev-abc": {"egress"}}, out)
|
||||
|
||||
def test_multiple_services_per_project(self):
|
||||
out = _enumerate._parse_services_by_project(
|
||||
"claude-bottle-dev-abc\tegress\n"
|
||||
"claude-bottle-dev-abc\tpipelock\n"
|
||||
"claude-bottle-dev-abc\tsupervise\n"
|
||||
"bot-bottle-dev-abc\tegress\n"
|
||||
"bot-bottle-dev-abc\tpipelock\n"
|
||||
"bot-bottle-dev-abc\tsupervise\n"
|
||||
)
|
||||
self.assertEqual(
|
||||
{"claude-bottle-dev-abc": {"egress", "pipelock", "supervise"}},
|
||||
{"bot-bottle-dev-abc": {"egress", "pipelock", "supervise"}},
|
||||
out,
|
||||
)
|
||||
|
||||
@@ -63,24 +63,24 @@ class TestParseServicesByProject(unittest.TestCase):
|
||||
# Defends against unlabeled containers slipping into the
|
||||
# output (the filter should prevent it, but be robust).
|
||||
out = _enumerate._parse_services_by_project(
|
||||
"claude-bottle-dev-abc\tegress\n"
|
||||
"bot-bottle-dev-abc\tegress\n"
|
||||
"no-tab-here\n"
|
||||
"\tmissing-project\n"
|
||||
"missing-service\t\n"
|
||||
)
|
||||
self.assertEqual({"claude-bottle-dev-abc": {"egress"}}, out)
|
||||
self.assertEqual({"bot-bottle-dev-abc": {"egress"}}, out)
|
||||
|
||||
|
||||
class _FakeHomeMixin:
|
||||
def _setup_fake_home(self) -> None:
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="enum-active.")
|
||||
original = supervise.claude_bottle_root
|
||||
original = supervise.bot_bottle_root
|
||||
|
||||
def fake_root() -> Path:
|
||||
return Path(self._tmp.name) / ".claude-bottle"
|
||||
return Path(self._tmp.name) / ".bot-bottle"
|
||||
|
||||
supervise.claude_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore_home = lambda: setattr(supervise, "claude_bottle_root", original)
|
||||
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
|
||||
self._restore_home = lambda: setattr(supervise, "bot_bottle_root", original)
|
||||
|
||||
def _teardown_fake_home(self) -> None:
|
||||
self._restore_home()
|
||||
@@ -113,11 +113,11 @@ class TestEnumerateActive(_FakeHomeMixin, unittest.TestCase):
|
||||
cwd="",
|
||||
copy_cwd=False,
|
||||
started_at="2026-05-26T03:00:00+00:00",
|
||||
compose_project="claude-bottle-dev-abc",
|
||||
compose_project="bot-bottle-dev-abc",
|
||||
))
|
||||
self._stub(
|
||||
["dev-abc"],
|
||||
{"claude-bottle-dev-abc": {"pipelock", "egress", "supervise"}},
|
||||
{"bot-bottle-dev-abc": {"pipelock", "egress", "supervise"}},
|
||||
)
|
||||
active = _enumerate.enumerate_active()
|
||||
self.assertEqual(1, len(active))
|
||||
@@ -131,7 +131,7 @@ class TestEnumerateActive(_FakeHomeMixin, unittest.TestCase):
|
||||
def test_missing_metadata_renders_question_mark(self):
|
||||
# State dir doesn't exist for this slug — agent_name falls
|
||||
# back to "?" rather than dropping the row.
|
||||
self._stub(["mystery-zzz"], {"claude-bottle-mystery-zzz": {"pipelock"}})
|
||||
self._stub(["mystery-zzz"], {"bot-bottle-mystery-zzz": {"pipelock"}})
|
||||
active = _enumerate.enumerate_active()
|
||||
self.assertEqual(1, len(active))
|
||||
self.assertEqual("?", active[0].agent_name)
|
||||
@@ -148,7 +148,7 @@ class TestEnumerateActive(_FakeHomeMixin, unittest.TestCase):
|
||||
cwd="",
|
||||
copy_cwd=False,
|
||||
started_at="2026-05-26T03:05:00+00:00",
|
||||
compose_project="claude-bottle-warming-up",
|
||||
compose_project="bot-bottle-warming-up",
|
||||
))
|
||||
self._stub(["warming-up"], {})
|
||||
active = _enumerate.enumerate_active()
|
||||
@@ -162,7 +162,7 @@ class TestEnumerateActive(_FakeHomeMixin, unittest.TestCase):
|
||||
cwd="",
|
||||
copy_cwd=False,
|
||||
started_at="t",
|
||||
compose_project=f"claude-bottle-{slug}",
|
||||
compose_project=f"bot-bottle-{slug}",
|
||||
))
|
||||
# list_active_slugs returns sorted; preserve that order in
|
||||
# the output.
|
||||
|
||||
@@ -13,13 +13,13 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.backend import BottleSpec
|
||||
from claude_bottle.backend.docker.bottle_plan import DockerBottlePlan
|
||||
from claude_bottle.backend.docker.provision import git as _git
|
||||
from claude_bottle.egress import EgressPlan
|
||||
from claude_bottle.git_gate import GitGatePlan
|
||||
from claude_bottle.manifest import Manifest
|
||||
from claude_bottle.pipelock import PipelockProxyPlan
|
||||
from bot_bottle.backend import BottleSpec
|
||||
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
|
||||
from bot_bottle.backend.docker.provision import git as _git
|
||||
from bot_bottle.egress import EgressPlan
|
||||
from bot_bottle.git_gate import GitGatePlan
|
||||
from bot_bottle.manifest import Manifest
|
||||
from bot_bottle.pipelock import PipelockProxyPlan
|
||||
|
||||
|
||||
def _plan(*, git_user: dict | None = None,
|
||||
@@ -39,11 +39,11 @@ def _plan(*, git_user: dict | None = None,
|
||||
spec=spec,
|
||||
stage_dir=stage_dir or Path("/tmp/stage"),
|
||||
slug="demo-abc12",
|
||||
container_name="claude-bottle-demo-abc12",
|
||||
container_name="bot-bottle-demo-abc12",
|
||||
container_name_pinned=False,
|
||||
image="claude-bottle:latest",
|
||||
image="bot-bottle-claude:latest",
|
||||
derived_image="",
|
||||
runtime_image="claude-bottle:latest",
|
||||
runtime_image="bot-bottle-claude:latest",
|
||||
dockerfile_path="",
|
||||
env_file=Path("/tmp/agent.env"),
|
||||
forwarded_env={},
|
||||
@@ -93,7 +93,7 @@ class TestProvisionGitUser(unittest.TestCase):
|
||||
def test_noop_when_no_git_user(self):
|
||||
with patch.object(_git.subprocess, "run") as run:
|
||||
_git._provision_git_user(
|
||||
_plan(stage_dir=self.stage), "claude-bottle-demo-abc12",
|
||||
_plan(stage_dir=self.stage), "bot-bottle-demo-abc12",
|
||||
)
|
||||
self.assertEqual([], _git_config_calls(run))
|
||||
|
||||
@@ -103,14 +103,14 @@ class TestProvisionGitUser(unittest.TestCase):
|
||||
stage_dir=self.stage,
|
||||
)
|
||||
with patch.object(_git.subprocess, "run") as run:
|
||||
_git._provision_git_user(plan, "claude-bottle-demo-abc12")
|
||||
_git._provision_git_user(plan, "bot-bottle-demo-abc12")
|
||||
calls = _git_config_calls(run)
|
||||
self.assertEqual(2, len(calls))
|
||||
# All `docker exec` invocations run as `-u node` so the
|
||||
# --global config lands in /home/node/.gitconfig.
|
||||
for argv in calls:
|
||||
self.assertEqual(
|
||||
["docker", "exec", "-u", "node", "claude-bottle-demo-abc12",
|
||||
["docker", "exec", "-u", "node", "bot-bottle-demo-abc12",
|
||||
"git", "config", "--global"],
|
||||
argv[:8],
|
||||
)
|
||||
@@ -120,7 +120,7 @@ class TestProvisionGitUser(unittest.TestCase):
|
||||
def test_name_only_sets_only_name(self):
|
||||
plan = _plan(git_user={"name": "Bot"}, stage_dir=self.stage)
|
||||
with patch.object(_git.subprocess, "run") as run:
|
||||
_git._provision_git_user(plan, "claude-bottle-demo-abc12")
|
||||
_git._provision_git_user(plan, "bot-bottle-demo-abc12")
|
||||
calls = _git_config_calls(run)
|
||||
self.assertEqual(1, len(calls))
|
||||
self.assertEqual(["user.name", "Bot"], calls[0][8:])
|
||||
@@ -130,7 +130,7 @@ class TestProvisionGitUser(unittest.TestCase):
|
||||
git_user={"email": "bot@example.com"}, stage_dir=self.stage,
|
||||
)
|
||||
with patch.object(_git.subprocess, "run") as run:
|
||||
_git._provision_git_user(plan, "claude-bottle-demo-abc12")
|
||||
_git._provision_git_user(plan, "bot-bottle-demo-abc12")
|
||||
calls = _git_config_calls(run)
|
||||
self.assertEqual(1, len(calls))
|
||||
self.assertEqual(["user.email", "bot@example.com"], calls[0][8:])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Unit: image_id / tag / push helpers in
|
||||
claude_bottle.backend.docker.util (PRD 0023 chunk 4c additions).
|
||||
bot_bottle.backend.docker.util (PRD 0023 chunk 4c additions).
|
||||
|
||||
Tests mock `subprocess.run` and assert on argv shape + parsing.
|
||||
The actual docker round-trip is covered by the chunk 4c
|
||||
@@ -11,7 +11,7 @@ import subprocess
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.backend.docker import util as docker_mod
|
||||
from bot_bottle.backend.docker import util as docker_mod
|
||||
|
||||
|
||||
def _ok(stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess:
|
||||
@@ -34,11 +34,11 @@ class TestImageId(unittest.TestCase):
|
||||
return_value=_ok(stdout="sha256:abcdef\n"),
|
||||
) as run:
|
||||
self.assertEqual(
|
||||
"sha256:abcdef", docker_mod.image_id("claude-bottle:latest")
|
||||
"sha256:abcdef", docker_mod.image_id("bot-bottle-claude:latest")
|
||||
)
|
||||
argv = run.call_args.args[0]
|
||||
self.assertEqual(
|
||||
["docker", "image", "inspect", "--format", "{{.Id}}", "claude-bottle:latest"],
|
||||
["docker", "image", "inspect", "--format", "{{.Id}}", "bot-bottle-claude:latest"],
|
||||
argv,
|
||||
)
|
||||
|
||||
@@ -59,10 +59,10 @@ class TestSave(unittest.TestCase):
|
||||
with patch.object(
|
||||
docker_mod.subprocess, "run", return_value=_ok(),
|
||||
) as run:
|
||||
docker_mod.save("claude-bottle:latest", "/tmp/img.tar")
|
||||
docker_mod.save("bot-bottle-claude:latest", "/tmp/img.tar")
|
||||
argv = run.call_args.args[0]
|
||||
self.assertEqual(
|
||||
["docker", "save", "claude-bottle:latest", "-o", "/tmp/img.tar"],
|
||||
["docker", "save", "bot-bottle-claude:latest", "-o", "/tmp/img.tar"],
|
||||
argv,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ resolution (PRD 0017)."""
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle.egress import (
|
||||
from bot_bottle.egress import (
|
||||
DEFAULT_ALLOWLIST,
|
||||
egress_manifest_routes,
|
||||
egress_render_routes,
|
||||
@@ -11,9 +11,9 @@ from claude_bottle.egress import (
|
||||
egress_routes_for_bottle,
|
||||
egress_token_env_map,
|
||||
)
|
||||
from claude_bottle.log import Die
|
||||
from claude_bottle.manifest import Manifest
|
||||
from claude_bottle.yaml_subset import parse_yaml_subset
|
||||
from bot_bottle.log import Die
|
||||
from bot_bottle.manifest import Manifest
|
||||
from bot_bottle.yaml_subset import parse_yaml_subset
|
||||
|
||||
|
||||
def _bottle(routes):
|
||||
@@ -188,7 +188,7 @@ class TestRenderRoutes(unittest.TestCase):
|
||||
def test_round_trip_through_addon_core(self):
|
||||
# Render here → parse in the addon must succeed for every
|
||||
# combination the manifest can produce.
|
||||
from claude_bottle.egress_addon_core import load_routes
|
||||
from bot_bottle.egress_addon_core import load_routes
|
||||
b = _bottle([
|
||||
{"host": "api.github.com",
|
||||
"auth": {"scheme": "Bearer", "token_ref": "GH_PAT"},
|
||||
|
||||
@@ -6,7 +6,7 @@ half of the addon. The mitmproxy hook wrapper in
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle.egress_addon_core import (
|
||||
from bot_bottle.egress_addon_core import (
|
||||
Decision,
|
||||
Route,
|
||||
decide,
|
||||
|
||||
@@ -4,14 +4,14 @@ integration test."""
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle.backend.docker.egress_apply import (
|
||||
from bot_bottle.backend.docker.egress_apply import (
|
||||
EgressApplyError,
|
||||
_hosts_in_routes,
|
||||
_merge_single_route,
|
||||
_pipelock_safe_hosts,
|
||||
validate_routes_content,
|
||||
)
|
||||
from claude_bottle.yaml_subset import parse_yaml_subset
|
||||
from bot_bottle.yaml_subset import parse_yaml_subset
|
||||
|
||||
|
||||
# YAML fixtures matching the hand-rolled `_render_routes_payload`
|
||||
|
||||
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
|
||||
_SCRIPT = (
|
||||
Path(__file__).resolve().parent.parent.parent
|
||||
/ "claude_bottle" / "egress_entrypoint.sh"
|
||||
/ "bot_bottle" / "egress_entrypoint.sh"
|
||||
)
|
||||
|
||||
|
||||
|
||||
+10
-10
@@ -5,7 +5,7 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.git_gate import (
|
||||
from bot_bottle.git_gate import (
|
||||
GitGate,
|
||||
GitGatePlan,
|
||||
GitGateUpstream,
|
||||
@@ -16,8 +16,8 @@ from claude_bottle.git_gate import (
|
||||
git_gate_render_hook,
|
||||
git_gate_upstreams_for_bottle,
|
||||
)
|
||||
from claude_bottle.log import Die
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.log import Die
|
||||
from bot_bottle.manifest import Manifest
|
||||
from tests.fixtures import fixture_minimal, fixture_with_git
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class TestUpstreamsForBottle(unittest.TestCase):
|
||||
bottle = fixture_with_git().bottles["dev"]
|
||||
ups = git_gate_upstreams_for_bottle(bottle)
|
||||
self.assertEqual(2, len(ups))
|
||||
self.assertEqual("claude-bottle", ups[0].name)
|
||||
self.assertEqual("bot-bottle", ups[0].name)
|
||||
self.assertEqual("gitea.dideric.is", ups[0].upstream_host)
|
||||
self.assertEqual("30009", ups[0].upstream_port)
|
||||
self.assertEqual("foo", ups[1].name)
|
||||
@@ -53,8 +53,8 @@ class TestExtraHostsPlumbing(unittest.TestCase):
|
||||
"dev": {
|
||||
"git": {"remotes": {
|
||||
"gitea.dideric.is": {
|
||||
"Name": "claude-bottle",
|
||||
"Upstream": "ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git",
|
||||
"Name": "bot-bottle",
|
||||
"Upstream": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git",
|
||||
"IdentityFile": "/dev/null",
|
||||
"ExtraHosts": {"gitea.dideric.is": "100.78.141.42"},
|
||||
},
|
||||
@@ -140,8 +140,8 @@ class TestEntrypointRender(unittest.TestCase):
|
||||
def test_one_init_repo_call_per_upstream(self):
|
||||
ups = (
|
||||
GitGateUpstream(
|
||||
name="claude-bottle",
|
||||
upstream_url="ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git",
|
||||
name="bot-bottle",
|
||||
upstream_url="ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git",
|
||||
upstream_host="gitea.dideric.is",
|
||||
upstream_port="30009",
|
||||
identity_file="/host/path/key",
|
||||
@@ -159,8 +159,8 @@ class TestEntrypointRender(unittest.TestCase):
|
||||
script = git_gate_render_entrypoint(ups)
|
||||
self.assertIn("#!/bin/sh", script)
|
||||
self.assertIn(
|
||||
"init_repo 'claude-bottle' "
|
||||
"'ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git'",
|
||||
"init_repo 'bot-bottle' "
|
||||
"'ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git'",
|
||||
script,
|
||||
)
|
||||
self.assertIn(
|
||||
|
||||
@@ -7,8 +7,8 @@ auth omission means unauthenticated."""
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle.log import Die
|
||||
from claude_bottle.manifest import EgressRoute, Manifest
|
||||
from bot_bottle.log import Die
|
||||
from bot_bottle.manifest import EgressRoute, Manifest
|
||||
|
||||
|
||||
def _bottle(routes):
|
||||
|
||||
@@ -14,8 +14,8 @@ import contextlib
|
||||
import io
|
||||
import unittest
|
||||
|
||||
from claude_bottle.log import Die
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.log import Die
|
||||
from bot_bottle.manifest import Manifest
|
||||
|
||||
|
||||
def _die_message(callable_, *args, **kwargs) -> str:
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle.log import Die
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.log import Die
|
||||
from bot_bottle.manifest import Manifest
|
||||
|
||||
|
||||
def _manifest(git_entries):
|
||||
@@ -31,18 +31,18 @@ def _host_for(entry):
|
||||
class TestGitEntryParsing(unittest.TestCase):
|
||||
def test_parses_minimal_entry(self):
|
||||
m = Manifest.from_json_obj(_manifest([{
|
||||
"Name": "claude-bottle",
|
||||
"Upstream": "ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git",
|
||||
"Name": "bot-bottle",
|
||||
"Upstream": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git",
|
||||
"IdentityFile": "/dev/null",
|
||||
}]))
|
||||
entries = m.bottles["dev"].git
|
||||
self.assertEqual(1, len(entries))
|
||||
e = entries[0]
|
||||
self.assertEqual("claude-bottle", e.Name)
|
||||
self.assertEqual("bot-bottle", e.Name)
|
||||
self.assertEqual("git", e.UpstreamUser)
|
||||
self.assertEqual("gitea.dideric.is", e.UpstreamHost)
|
||||
self.assertEqual("30009", e.UpstreamPort)
|
||||
self.assertEqual("didericis/claude-bottle.git", e.UpstreamPath)
|
||||
self.assertEqual("didericis/bot-bottle.git", e.UpstreamPath)
|
||||
|
||||
def test_default_port_is_22(self):
|
||||
m = Manifest.from_json_obj(_manifest([{
|
||||
@@ -137,8 +137,8 @@ class TestGitEntryExtraHosts(unittest.TestCase):
|
||||
|
||||
def test_extra_hosts_parses_host_to_ip_map(self):
|
||||
m = Manifest.from_json_obj(_manifest([{
|
||||
"Name": "claude-bottle",
|
||||
"Upstream": "ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git",
|
||||
"Name": "bot-bottle",
|
||||
"Upstream": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git",
|
||||
"IdentityFile": "/dev/null",
|
||||
"ExtraHosts": {"gitea.dideric.is": "100.78.141.42"},
|
||||
}]))
|
||||
|
||||
@@ -4,8 +4,8 @@ import contextlib
|
||||
import io
|
||||
import unittest
|
||||
|
||||
from claude_bottle.log import Die
|
||||
from claude_bottle.manifest import GitUser, Manifest
|
||||
from bot_bottle.log import Die
|
||||
from bot_bottle.manifest import GitUser, Manifest
|
||||
|
||||
|
||||
def _die_message(callable_, *args, **kwargs) -> str:
|
||||
|
||||
@@ -11,8 +11,8 @@ import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle.log import Die
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.log import Die
|
||||
from bot_bottle.manifest import Manifest
|
||||
|
||||
|
||||
def _write(p: Path, text: str) -> None:
|
||||
@@ -63,21 +63,21 @@ class _ResolveCase(unittest.TestCase):
|
||||
shutil.rmtree(self.home_root, ignore_errors=True)
|
||||
shutil.rmtree(self.cwd_root, ignore_errors=True)
|
||||
|
||||
# Convenience: paths under home/cwd .claude-bottle dirs.
|
||||
# Convenience: paths under home/cwd .bot-bottle dirs.
|
||||
@property
|
||||
def home_cb(self) -> Path:
|
||||
return self.home_root / ".claude-bottle"
|
||||
return self.home_root / ".bot-bottle"
|
||||
|
||||
@property
|
||||
def cwd_cb(self) -> Path:
|
||||
return self.cwd_root / ".claude-bottle"
|
||||
return self.cwd_root / ".bot-bottle"
|
||||
|
||||
def resolve(self) -> Manifest:
|
||||
return Manifest.resolve(str(self.cwd_root))
|
||||
|
||||
|
||||
class TestBottleFileParses(_ResolveCase):
|
||||
"""SC #1: a bottle file under $HOME/.claude-bottle/bottles/
|
||||
"""SC #1: a bottle file under $HOME/.bot-bottle/bottles/
|
||||
parses into the expected Bottle shape."""
|
||||
|
||||
def test_loads(self):
|
||||
@@ -94,7 +94,7 @@ class TestBottleFileParses(_ResolveCase):
|
||||
|
||||
|
||||
class TestAgentFileParses(_ResolveCase):
|
||||
"""SC #2: an agent file under $HOME/.claude-bottle/agents/
|
||||
"""SC #2: an agent file under $HOME/.bot-bottle/agents/
|
||||
parses, the body becomes the prompt, the frontmatter fields
|
||||
map to Agent fields."""
|
||||
|
||||
@@ -171,7 +171,7 @@ class TestStdlibOnly(unittest.TestCase):
|
||||
existence of an `import yaml`-free file is the assertion."""
|
||||
|
||||
def test_no_pyyaml(self):
|
||||
src = Path("claude_bottle/yaml_subset.py").read_text()
|
||||
src = Path("bot_bottle/yaml_subset.py").read_text()
|
||||
self.assertNotIn("import yaml", src)
|
||||
self.assertNotIn("from yaml", src)
|
||||
|
||||
@@ -262,12 +262,12 @@ class TestUnknownBottleKeyDies(_ResolveCase):
|
||||
|
||||
|
||||
class TestStaleJsonDies(_ResolveCase):
|
||||
"""If `claude-bottle.json` exists in $HOME alongside no
|
||||
`.claude-bottle/` dir, die with a clear pointer at the README's
|
||||
"""If `bot-bottle.json` exists in $HOME alongside no
|
||||
`.bot-bottle/` dir, die with a clear pointer at the README's
|
||||
new manifest section. Don't silently ignore the JSON content."""
|
||||
|
||||
def test_dies(self):
|
||||
(self.home_root / "claude-bottle.json").write_text('{"bottles": {}}')
|
||||
(self.home_root / "bot-bottle.json").write_text('{"bottles": {}}')
|
||||
with self.assertRaises(Die):
|
||||
self.resolve()
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ silently ignoring."""
|
||||
import unittest
|
||||
from typing import Any
|
||||
|
||||
from claude_bottle.log import Die
|
||||
from claude_bottle.manifest import Bottle, Manifest
|
||||
from bot_bottle.log import Die
|
||||
from bot_bottle.manifest import Bottle, Manifest
|
||||
|
||||
|
||||
def _manifest_with_runtime(value: object) -> dict[str, Any]:
|
||||
|
||||
@@ -5,8 +5,8 @@ contribute; they flow through the per-agent git-gate (PRD 0008)."""
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle.manifest import Manifest
|
||||
from claude_bottle.pipelock import (
|
||||
from bot_bottle.manifest import Manifest
|
||||
from bot_bottle.pipelock import (
|
||||
pipelock_effective_allowlist,
|
||||
pipelock_effective_tls_passthrough,
|
||||
)
|
||||
|
||||
@@ -6,13 +6,13 @@ test in Phase 4. Here we cover the host-side parsing + yaml roundtrip.
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle.backend.docker.pipelock_apply import (
|
||||
from bot_bottle.backend.docker.pipelock_apply import (
|
||||
PipelockApplyError,
|
||||
parse_allowlist_content,
|
||||
render_allowlist_content,
|
||||
)
|
||||
from claude_bottle.pipelock import pipelock_render_yaml
|
||||
from claude_bottle.yaml_subset import parse_yaml_subset
|
||||
from bot_bottle.pipelock import pipelock_render_yaml
|
||||
from bot_bottle.yaml_subset import parse_yaml_subset
|
||||
|
||||
|
||||
class TestParseAllowlistContent(unittest.TestCase):
|
||||
|
||||
@@ -12,8 +12,8 @@ import unittest
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
from claude_bottle.manifest import Manifest
|
||||
from claude_bottle.pipelock import (
|
||||
from bot_bottle.manifest import Manifest
|
||||
from bot_bottle.pipelock import (
|
||||
DEFAULT_TLS_PASSTHROUGH,
|
||||
PipelockProxy,
|
||||
pipelock_build_config,
|
||||
@@ -114,7 +114,7 @@ class TestBuildConfig(unittest.TestCase):
|
||||
# fires. The only knob that actually skips the block is the
|
||||
# global on/off, so we flip it off whenever the bottle is set
|
||||
# up to route claude through pipelock.
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.manifest import Manifest
|
||||
bottle = Manifest.from_json_obj({
|
||||
"bottles": {"dev": {"egress": {"routes": [
|
||||
{"host": "api.anthropic.com",
|
||||
@@ -209,7 +209,7 @@ class TestRenderAndWrite(unittest.TestCase):
|
||||
self.assertIn('- "172.20.0.0/16"', text)
|
||||
|
||||
def test_render_emits_seed_phrase_off_for_anthropic_route(self):
|
||||
from claude_bottle.manifest import Manifest
|
||||
from bot_bottle.manifest import Manifest
|
||||
bottle = Manifest.from_json_obj({
|
||||
"bottles": {"dev": {"egress": {"routes": [
|
||||
{"host": "api.anthropic.com",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Unit: render of ~/.gitconfig insteadOf rules (PRD 0008).
|
||||
|
||||
The render moved to `claude_bottle.git_gate` so both backends
|
||||
The render moved to `bot_bottle.git_gate` so both backends
|
||||
share it; tests live here because docker's provision_git is the
|
||||
original consumer."""
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle.git_gate import (
|
||||
from bot_bottle.git_gate import (
|
||||
GIT_GATE_HOSTNAME,
|
||||
git_gate_render_gitconfig,
|
||||
)
|
||||
@@ -26,12 +26,12 @@ class TestGitGateGitconfigRender(unittest.TestCase):
|
||||
# Both entries map to a [url ...] block keyed on the gate's
|
||||
# short network alias (`git-gate`) inside the sidecar bundle.
|
||||
self.assertIn(
|
||||
'[url "git://git-gate/claude-bottle.git"]',
|
||||
'[url "git://git-gate/bot-bottle.git"]',
|
||||
out,
|
||||
)
|
||||
self.assertIn(
|
||||
"\tinsteadOf = "
|
||||
"ssh://git@gitea.dideric.is:30009/didericis/claude-bottle.git",
|
||||
"ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git",
|
||||
out,
|
||||
)
|
||||
self.assertIn('[url "git://git-gate/foo.git"]', out)
|
||||
@@ -56,7 +56,7 @@ class TestGitGateGitconfigRender(unittest.TestCase):
|
||||
bottle = fixture_with_git().bottles["dev"]
|
||||
out = git_gate_render_gitconfig(bottle.git, "192.168.20.2:9418")
|
||||
self.assertIn(
|
||||
'[url "git://192.168.20.2:9418/claude-bottle.git"]', out,
|
||||
'[url "git://192.168.20.2:9418/bot-bottle.git"]', out,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ plumbing surfaces in unit CI."""
|
||||
|
||||
import unittest
|
||||
|
||||
from claude_bottle.backend.docker.provision.supervise import supervise_mcp_url
|
||||
from claude_bottle.supervise import SUPERVISE_HOSTNAME, SUPERVISE_PORT
|
||||
from bot_bottle.backend.docker.provision.supervise import supervise_mcp_url
|
||||
from bot_bottle.supervise import SUPERVISE_HOSTNAME, SUPERVISE_PORT
|
||||
|
||||
|
||||
class TestSuperviseMcpUrl(unittest.TestCase):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Unit: sidecar bundle init supervisor (PRD 0024 chunk 1).
|
||||
|
||||
Tests both the helper functions in `claude_bottle.sidecar_init`
|
||||
Tests both the helper functions in `bot_bottle.sidecar_init`
|
||||
and the supervisor's end-to-end signal / exit-code behavior. The
|
||||
end-to-end tests use real subprocesses (`/bin/sleep`,
|
||||
`/bin/sh -c '...'`) — short-lived, no docker required — so they
|
||||
@@ -17,7 +17,7 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.sidecar_init import (
|
||||
from bot_bottle.sidecar_init import (
|
||||
_DaemonSpec,
|
||||
_Supervisor,
|
||||
_env_for_daemon,
|
||||
@@ -88,18 +88,18 @@ class TestSelectedDaemons(unittest.TestCase):
|
||||
["egress", "pipelock", "git-gate", "supervise"])
|
||||
|
||||
def test_empty_returns_all(self):
|
||||
got = _selected_daemons({"CLAUDE_BOTTLE_SIDECAR_DAEMONS": ""},
|
||||
got = _selected_daemons({"BOT_BOTTLE_SIDECAR_DAEMONS": ""},
|
||||
all_daemons=self._DAEMONS)
|
||||
self.assertEqual(4, len(got))
|
||||
|
||||
def test_whitespace_only_returns_all(self):
|
||||
got = _selected_daemons({"CLAUDE_BOTTLE_SIDECAR_DAEMONS": " "},
|
||||
got = _selected_daemons({"BOT_BOTTLE_SIDECAR_DAEMONS": " "},
|
||||
all_daemons=self._DAEMONS)
|
||||
self.assertEqual(4, len(got))
|
||||
|
||||
def test_explicit_subset(self):
|
||||
got = _selected_daemons(
|
||||
{"CLAUDE_BOTTLE_SIDECAR_DAEMONS": "egress,pipelock"},
|
||||
{"BOT_BOTTLE_SIDECAR_DAEMONS": "egress,pipelock"},
|
||||
all_daemons=self._DAEMONS,
|
||||
)
|
||||
self.assertEqual([d.name for d in got], ["egress", "pipelock"])
|
||||
@@ -109,7 +109,7 @@ class TestSelectedDaemons(unittest.TestCase):
|
||||
# the canonical _DAEMONS order so egress starts before
|
||||
# pipelock (race-window reason).
|
||||
got = _selected_daemons(
|
||||
{"CLAUDE_BOTTLE_SIDECAR_DAEMONS": "supervise,pipelock,egress"},
|
||||
{"BOT_BOTTLE_SIDECAR_DAEMONS": "supervise,pipelock,egress"},
|
||||
all_daemons=self._DAEMONS,
|
||||
)
|
||||
self.assertEqual([d.name for d in got],
|
||||
@@ -117,14 +117,14 @@ class TestSelectedDaemons(unittest.TestCase):
|
||||
|
||||
def test_unknown_names_ignored(self):
|
||||
got = _selected_daemons(
|
||||
{"CLAUDE_BOTTLE_SIDECAR_DAEMONS": "egress,bogus"},
|
||||
{"BOT_BOTTLE_SIDECAR_DAEMONS": "egress,bogus"},
|
||||
all_daemons=self._DAEMONS,
|
||||
)
|
||||
self.assertEqual([d.name for d in got], ["egress"])
|
||||
|
||||
def test_whitespace_in_names_stripped(self):
|
||||
got = _selected_daemons(
|
||||
{"CLAUDE_BOTTLE_SIDECAR_DAEMONS": " egress , pipelock "},
|
||||
{"BOT_BOTTLE_SIDECAR_DAEMONS": " egress , pipelock "},
|
||||
all_daemons=self._DAEMONS,
|
||||
)
|
||||
self.assertEqual([d.name for d in got], ["egress", "pipelock"])
|
||||
@@ -355,7 +355,7 @@ class TestSupervisor(unittest.TestCase):
|
||||
time.sleep(0.3) # let `trap` register
|
||||
sup.request_shutdown(reason="test")
|
||||
|
||||
with patch("claude_bottle.sidecar_init._GRACE_SECONDS", 0.3):
|
||||
with patch("bot_bottle.sidecar_init._GRACE_SECONDS", 0.3):
|
||||
rc = self._drive(sup, max_wait_s=4.0)
|
||||
|
||||
# Process was SIGKILL'd → returncode -9 on POSIX.
|
||||
@@ -397,14 +397,14 @@ class TestMainEndToEnd(unittest.TestCase):
|
||||
|
||||
helper = (
|
||||
"import os, runpy, sys\n"
|
||||
"from claude_bottle import sidecar_init as si\n"
|
||||
"from bot_bottle import sidecar_init as si\n"
|
||||
"si._DAEMONS = (\n"
|
||||
" si._DaemonSpec('alpha', ('/bin/sleep','30')),\n"
|
||||
" si._DaemonSpec('beta', ('/bin/sleep','30')),\n"
|
||||
")\n"
|
||||
"sys.exit(si.main([]))\n"
|
||||
)
|
||||
env = {**os.environ, "CLAUDE_BOTTLE_SIDECAR_DAEMONS": daemons_csv}
|
||||
env = {**os.environ, "BOT_BOTTLE_SIDECAR_DAEMONS": daemons_csv}
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-c", helper],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
|
||||
@@ -16,13 +16,13 @@ from __future__ import annotations
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from claude_bottle.backend.smolmachines import pty_resize as _pty_resize
|
||||
from claude_bottle.backend.smolmachines.bottle import SmolmachinesBottle
|
||||
from bot_bottle.backend.smolmachines import pty_resize as _pty_resize
|
||||
from bot_bottle.backend.smolmachines.bottle import SmolmachinesBottle
|
||||
|
||||
|
||||
def _bottle(prompt_path: str | None = None, **env: str) -> SmolmachinesBottle:
|
||||
return SmolmachinesBottle(
|
||||
"claude-bottle-dev-abc",
|
||||
"bot-bottle-dev-abc",
|
||||
prompt_path=prompt_path,
|
||||
guest_env=env,
|
||||
)
|
||||
@@ -47,7 +47,7 @@ class TestClaudeArgvWrapped(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
[
|
||||
sys.executable, _pty_resize.__file__,
|
||||
"claude-bottle-dev-abc", "--",
|
||||
"bot-bottle-dev-abc", "--",
|
||||
],
|
||||
argv[:4],
|
||||
)
|
||||
@@ -57,7 +57,7 @@ class TestClaudeArgvWrapped(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
[
|
||||
"smolvm", "machine", "exec", "--name",
|
||||
"claude-bottle-dev-abc",
|
||||
"bot-bottle-dev-abc",
|
||||
"-i", "-t",
|
||||
"-e", "HOME=/home/node",
|
||||
"-e", "USER=node",
|
||||
@@ -79,7 +79,7 @@ class TestClaudeArgvWrapped(unittest.TestCase):
|
||||
|
||||
def test_appends_prompt_file_flag_when_set(self):
|
||||
argv = _unwrap(
|
||||
_bottle("/home/node/.claude-bottle-prompt.txt").claude_argv(
|
||||
_bottle("/home/node/.bot-bottle-prompt.txt").claude_argv(
|
||||
["--dangerously-skip-permissions"],
|
||||
)
|
||||
)
|
||||
@@ -87,7 +87,7 @@ class TestClaudeArgvWrapped(unittest.TestCase):
|
||||
[
|
||||
"claude",
|
||||
"--append-system-prompt-file",
|
||||
"/home/node/.claude-bottle-prompt.txt",
|
||||
"/home/node/.bot-bottle-prompt.txt",
|
||||
"--dangerously-skip-permissions",
|
||||
],
|
||||
argv[argv.index("claude"):],
|
||||
|
||||
@@ -11,9 +11,9 @@ import subprocess
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle import backend as backend_mod
|
||||
from claude_bottle.backend.smolmachines import cleanup
|
||||
from claude_bottle.backend.smolmachines.bottle_cleanup_plan import (
|
||||
from bot_bottle import backend as backend_mod
|
||||
from bot_bottle.backend.smolmachines import cleanup
|
||||
from bot_bottle.backend.smolmachines.bottle_cleanup_plan import (
|
||||
SmolmachinesBottleCleanupPlan,
|
||||
)
|
||||
|
||||
@@ -38,19 +38,19 @@ class TestPrepareCleanup(unittest.TestCase):
|
||||
def fake_run(argv, *a, **kw):
|
||||
if argv[:3] == ["smolvm", "machine", "ls"]:
|
||||
return _ok(stdout=(
|
||||
'[{"name":"claude-bottle-a-1","state":"running"},'
|
||||
' {"name":"claude-bottle-b-2","state":"created"},'
|
||||
'[{"name":"bot-bottle-a-1","state":"running"},'
|
||||
' {"name":"bot-bottle-b-2","state":"created"},'
|
||||
' {"name":"unrelated","state":"running"}]'
|
||||
))
|
||||
if argv[:2] == ["docker", "ps"]:
|
||||
return _ok(stdout=(
|
||||
"claude-bottle-sidecars-a-1\n"
|
||||
"claude-bottle-sidecars-b-2\n"
|
||||
"bot-bottle-sidecars-a-1\n"
|
||||
"bot-bottle-sidecars-b-2\n"
|
||||
))
|
||||
if argv[:3] == ["docker", "network", "ls"]:
|
||||
return _ok(stdout=(
|
||||
"claude-bottle-bundle-a-1\n"
|
||||
"claude-bottle-bundle-b-2\n"
|
||||
"bot-bottle-bundle-a-1\n"
|
||||
"bot-bottle-bundle-b-2\n"
|
||||
))
|
||||
return _ok()
|
||||
|
||||
@@ -60,17 +60,17 @@ class TestPrepareCleanup(unittest.TestCase):
|
||||
smolvm.is_available.return_value = True
|
||||
plan = cleanup.prepare_cleanup()
|
||||
|
||||
# `unrelated` filtered out (no claude-bottle- prefix).
|
||||
# `unrelated` filtered out (no bot-bottle- prefix).
|
||||
self.assertEqual(
|
||||
("claude-bottle-a-1", "claude-bottle-b-2"),
|
||||
("bot-bottle-a-1", "bot-bottle-b-2"),
|
||||
plan.machines,
|
||||
)
|
||||
self.assertEqual(
|
||||
("claude-bottle-sidecars-a-1", "claude-bottle-sidecars-b-2"),
|
||||
("bot-bottle-sidecars-a-1", "bot-bottle-sidecars-b-2"),
|
||||
plan.bundles,
|
||||
)
|
||||
self.assertEqual(
|
||||
("claude-bottle-bundle-a-1", "claude-bottle-bundle-b-2"),
|
||||
("bot-bottle-bundle-a-1", "bot-bottle-bundle-b-2"),
|
||||
plan.networks,
|
||||
)
|
||||
|
||||
@@ -86,9 +86,9 @@ class TestPrepareCleanup(unittest.TestCase):
|
||||
class TestCleanup(unittest.TestCase):
|
||||
def test_machines_stopped_then_deleted_then_bundles_then_networks(self):
|
||||
plan = SmolmachinesBottleCleanupPlan(
|
||||
machines=("claude-bottle-a-1",),
|
||||
bundles=("claude-bottle-sidecars-a-1",),
|
||||
networks=("claude-bottle-bundle-a-1",),
|
||||
machines=("bot-bottle-a-1",),
|
||||
bundles=("bot-bottle-sidecars-a-1",),
|
||||
networks=("bot-bottle-bundle-a-1",),
|
||||
)
|
||||
calls: list[list[str]] = []
|
||||
|
||||
@@ -107,10 +107,10 @@ class TestCleanup(unittest.TestCase):
|
||||
["smolvm", "machine", "delete", "-f"], calls[1],
|
||||
)
|
||||
self.assertEqual(
|
||||
["docker", "rm", "-f", "claude-bottle-sidecars-a-1"], calls[2],
|
||||
["docker", "rm", "-f", "bot-bottle-sidecars-a-1"], calls[2],
|
||||
)
|
||||
self.assertEqual(
|
||||
["docker", "network", "rm", "claude-bottle-bundle-a-1"], calls[3],
|
||||
["docker", "network", "rm", "bot-bottle-bundle-a-1"], calls[3],
|
||||
)
|
||||
|
||||
def test_failures_are_warnings_not_fatal(self):
|
||||
@@ -118,8 +118,8 @@ class TestCleanup(unittest.TestCase):
|
||||
# but continue with bundles + networks. The cleanup is
|
||||
# idempotent on success and tries to remove every resource.
|
||||
plan = SmolmachinesBottleCleanupPlan(
|
||||
machines=("claude-bottle-a-1",),
|
||||
bundles=("claude-bottle-sidecars-a-1",),
|
||||
machines=("bot-bottle-a-1",),
|
||||
bundles=("bot-bottle-sidecars-a-1",),
|
||||
networks=(),
|
||||
)
|
||||
results = iter([
|
||||
|
||||
@@ -18,7 +18,7 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.backend.smolmachines import launch as _launch_mod
|
||||
from bot_bottle.backend.smolmachines import launch as _launch_mod
|
||||
|
||||
|
||||
class TestEnsureSmolmachine(unittest.TestCase):
|
||||
@@ -53,7 +53,7 @@ class TestEnsureSmolmachine(unittest.TestCase):
|
||||
) as push, patch.object(
|
||||
_launch_mod._smolvm, "pack_create",
|
||||
) as pack:
|
||||
result = _launch_mod._ensure_smolmachine("claude-bottle:latest")
|
||||
result = _launch_mod._ensure_smolmachine("bot-bottle-claude:latest")
|
||||
|
||||
self.assertEqual(sidecar, result)
|
||||
# build still runs (Dockerfile edits land without manual rmi).
|
||||
@@ -71,7 +71,7 @@ class TestEnsureSmolmachine(unittest.TestCase):
|
||||
# ephemeral_registry yields a RegistryHandle with the
|
||||
# docker network + a push endpoint (container DNS) and
|
||||
# pull endpoint (host port-forward).
|
||||
from claude_bottle.backend.smolmachines.local_registry import (
|
||||
from bot_bottle.backend.smolmachines.local_registry import (
|
||||
RegistryHandle,
|
||||
)
|
||||
|
||||
@@ -111,7 +111,7 @@ class TestEnsureSmolmachine(unittest.TestCase):
|
||||
_launch_mod._smolvm, "pack_create",
|
||||
side_effect=record("pack"),
|
||||
) as pack:
|
||||
_launch_mod._ensure_smolmachine("claude-bottle:latest")
|
||||
_launch_mod._ensure_smolmachine("bot-bottle-claude:latest")
|
||||
|
||||
# Build → save → push → pack in that order. No `docker
|
||||
# push` (the daemon's HTTPS-by-default path is what we're
|
||||
@@ -121,14 +121,14 @@ class TestEnsureSmolmachine(unittest.TestCase):
|
||||
# docker save targets a per-digest tarball alongside the
|
||||
# cached sidecar.
|
||||
save_args = save.call_args.args
|
||||
self.assertEqual("claude-bottle:latest", save_args[0])
|
||||
self.assertEqual("bot-bottle-claude:latest", save_args[0])
|
||||
self.assertTrue(save_args[1].endswith(f"{digest}.image.tar"))
|
||||
|
||||
# crane push runs against the push_endpoint (container DNS
|
||||
# on the registry network) with the digest as the tag.
|
||||
push_args = push.call_args.args
|
||||
self.assertEqual(
|
||||
f"cb-registry-xyz:5000/claude-bottle:{digest}", push_args[2],
|
||||
f"cb-registry-xyz:5000/bot-bottle:{digest}", push_args[2],
|
||||
)
|
||||
|
||||
# pack_create reads from the pull_endpoint (host port-
|
||||
@@ -136,7 +136,7 @@ class TestEnsureSmolmachine(unittest.TestCase):
|
||||
# different routing hostname — the registry stores one blob.
|
||||
pack_args = pack.call_args.args
|
||||
self.assertEqual(
|
||||
f"localhost:54321/claude-bottle:{digest}", pack_args[0],
|
||||
f"localhost:54321/bot-bottle:{digest}", pack_args[0],
|
||||
)
|
||||
self.assertTrue(str(pack_args[1]).endswith(f"{digest}.smolmachine"))
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import subprocess
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.backend.smolmachines import local_registry
|
||||
from bot_bottle.backend.smolmachines import local_registry
|
||||
|
||||
|
||||
def _ok(stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess:
|
||||
@@ -57,7 +57,7 @@ class TestEphemeralRegistry(unittest.TestCase):
|
||||
# its docker-network name on its container port.
|
||||
self.assertTrue(
|
||||
handle.push_endpoint.startswith(
|
||||
"claude-bottle-registry-"
|
||||
"bot-bottle-registry-"
|
||||
)
|
||||
)
|
||||
self.assertTrue(handle.push_endpoint.endswith(":5000"))
|
||||
@@ -65,7 +65,7 @@ class TestEphemeralRegistry(unittest.TestCase):
|
||||
self.assertEqual("localhost:54321", handle.pull_endpoint)
|
||||
# network name is the per-session bridge crane joins.
|
||||
self.assertTrue(
|
||||
handle.network.startswith("claude-bottle-registry-net-")
|
||||
handle.network.startswith("bot-bottle-registry-net-")
|
||||
)
|
||||
# docker network create + docker run + docker port + rm -f + network rm
|
||||
self.assertEqual(5, run.call_count)
|
||||
|
||||
@@ -15,7 +15,7 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.backend.smolmachines import loopback_alias
|
||||
from bot_bottle.backend.smolmachines import loopback_alias
|
||||
|
||||
|
||||
def _ok(stdout: str = "") -> subprocess.CompletedProcess:
|
||||
@@ -161,7 +161,7 @@ class TestAliasInUseDetection(unittest.TestCase):
|
||||
# First call: docker ps -> two bundle names.
|
||||
# Then docker inspect each, returning a port-bindings JSON
|
||||
# blob with a HostIp on the per-bottle alias.
|
||||
ps_out = "claude-bottle-sidecars-a\nclaude-bottle-sidecars-b\n"
|
||||
ps_out = "bot-bottle-sidecars-a\nbot-bottle-sidecars-b\n"
|
||||
inspect_a = (
|
||||
'{"8888/tcp":[{"HostIp":"127.0.0.16","HostPort":"54000"}]}'
|
||||
)
|
||||
@@ -183,7 +183,7 @@ class TestAliasInUseDetection(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_inspect_failures_are_skipped(self):
|
||||
ps_out = "claude-bottle-sidecars-c\n"
|
||||
ps_out = "bot-bottle-sidecars-c\n"
|
||||
with patch.object(
|
||||
loopback_alias.subprocess, "run",
|
||||
side_effect=[_ok(stdout=ps_out), _fail("inspect failed")],
|
||||
|
||||
@@ -12,23 +12,23 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.backend import BottleSpec
|
||||
from claude_bottle.backend.smolmachines.bottle_plan import (
|
||||
from bot_bottle.backend import BottleSpec
|
||||
from bot_bottle.backend.smolmachines.bottle_plan import (
|
||||
SmolmachinesBottlePlan,
|
||||
)
|
||||
from claude_bottle.backend.smolmachines.provision import (
|
||||
from bot_bottle.backend.smolmachines.provision import (
|
||||
ca as _ca,
|
||||
git as _git,
|
||||
prompt as _prompt,
|
||||
skills as _skills,
|
||||
supervise as _supervise,
|
||||
)
|
||||
from claude_bottle.backend.smolmachines.smolvm import SmolvmRunResult
|
||||
from claude_bottle.egress import EgressPlan, EgressRoute
|
||||
from claude_bottle.git_gate import GitGatePlan
|
||||
from claude_bottle.manifest import GitEntry, Manifest
|
||||
from claude_bottle.pipelock import PipelockProxyPlan
|
||||
from claude_bottle.supervise import SupervisePlan
|
||||
from bot_bottle.backend.smolmachines.smolvm import SmolvmRunResult
|
||||
from bot_bottle.egress import EgressPlan, EgressRoute
|
||||
from bot_bottle.git_gate import GitGatePlan
|
||||
from bot_bottle.manifest import GitEntry, Manifest
|
||||
from bot_bottle.pipelock import PipelockProxyPlan
|
||||
from bot_bottle.supervise import SupervisePlan
|
||||
|
||||
|
||||
def _remote_host(g: GitEntry) -> str:
|
||||
@@ -101,8 +101,8 @@ def _plan(
|
||||
bundle_subnet="192.168.50.0/24",
|
||||
bundle_gateway="192.168.50.1",
|
||||
bundle_ip=bundle_ip,
|
||||
machine_name="claude-bottle-demo-abc12",
|
||||
agent_image_ref="claude-bottle:latest",
|
||||
machine_name="bot-bottle-demo-abc12",
|
||||
agent_image_ref="bot-bottle-claude:latest",
|
||||
guest_env={},
|
||||
prompt_file=Path("/tmp/state/demo-abc12/agent/prompt.txt"),
|
||||
proxy_plan=PipelockProxyPlan(
|
||||
@@ -133,37 +133,37 @@ def _plan(
|
||||
class TestProvisionPrompt(unittest.TestCase):
|
||||
def test_cp_uses_smolvm_machine_cp_with_machine_path_syntax(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp"
|
||||
) as cp, patch(
|
||||
"claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec"
|
||||
):
|
||||
_prompt.provision_prompt(_plan(), "claude-bottle-demo-abc12")
|
||||
_prompt.provision_prompt(_plan(), "bot-bottle-demo-abc12")
|
||||
cp.assert_called_once_with(
|
||||
"/tmp/state/demo-abc12/agent/prompt.txt",
|
||||
"claude-bottle-demo-abc12:/home/node/.claude-bottle-prompt.txt",
|
||||
"bot-bottle-demo-abc12:/home/node/.bot-bottle-prompt.txt",
|
||||
)
|
||||
|
||||
def test_returns_path_when_agent_has_prompt(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp"
|
||||
), patch(
|
||||
"claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec"
|
||||
):
|
||||
r = _prompt.provision_prompt(
|
||||
_plan(agent_prompt="You are a helpful assistant."),
|
||||
"claude-bottle-demo-abc12",
|
||||
"bot-bottle-demo-abc12",
|
||||
)
|
||||
self.assertEqual("/home/node/.claude-bottle-prompt.txt", r)
|
||||
self.assertEqual("/home/node/.bot-bottle-prompt.txt", r)
|
||||
|
||||
def test_returns_none_when_agent_has_no_prompt(self):
|
||||
# The file is still copied (path-must-exist contract);
|
||||
# only the return value differs.
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp"
|
||||
) as cp, patch(
|
||||
"claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec"
|
||||
):
|
||||
r = _prompt.provision_prompt(_plan(agent_prompt=""), "claude-bottle-demo-abc12")
|
||||
r = _prompt.provision_prompt(_plan(agent_prompt=""), "bot-bottle-demo-abc12")
|
||||
self.assertIsNone(r)
|
||||
cp.assert_called_once()
|
||||
|
||||
@@ -171,18 +171,18 @@ class TestProvisionPrompt(unittest.TestCase):
|
||||
# machine cp lands as root; without the chown, the node user
|
||||
# can't read its own mode-600 prompt.
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_cp"
|
||||
), patch(
|
||||
"claude_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.prompt._smolvm.machine_exec"
|
||||
) as ex:
|
||||
_prompt.provision_prompt(_plan(), "claude-bottle-demo-abc12")
|
||||
_prompt.provision_prompt(_plan(), "bot-bottle-demo-abc12")
|
||||
argv_seen = [call.args[1] for call in ex.call_args_list]
|
||||
self.assertIn(
|
||||
["chown", "node:node", "/home/node/.claude-bottle-prompt.txt"],
|
||||
["chown", "node:node", "/home/node/.bot-bottle-prompt.txt"],
|
||||
argv_seen,
|
||||
)
|
||||
self.assertIn(
|
||||
["chmod", "600", "/home/node/.claude-bottle-prompt.txt"],
|
||||
["chmod", "600", "/home/node/.bot-bottle-prompt.txt"],
|
||||
argv_seen,
|
||||
)
|
||||
|
||||
@@ -190,17 +190,17 @@ class TestProvisionPrompt(unittest.TestCase):
|
||||
class TestProvisionSkills(unittest.TestCase):
|
||||
def _patch_host_skill_dir(self, returns: dict[str, str]):
|
||||
return patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills.host_skill_dir",
|
||||
"bot_bottle.backend.smolmachines.provision.skills.host_skill_dir",
|
||||
side_effect=lambda n: returns.get(n, f"/nope/{n}"),
|
||||
)
|
||||
|
||||
def test_no_op_when_agent_has_no_skills(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp"
|
||||
) as cp, patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec"
|
||||
) as ex:
|
||||
_skills.provision_skills(_plan(skills=[]), "claude-bottle-demo-abc12")
|
||||
_skills.provision_skills(_plan(skills=[]), "bot-bottle-demo-abc12")
|
||||
self.assertEqual(0, cp.call_count)
|
||||
self.assertEqual(0, ex.call_count)
|
||||
|
||||
@@ -209,23 +209,23 @@ class TestProvisionSkills(unittest.TestCase):
|
||||
"init-prd": "/host/skills/init-prd",
|
||||
"verify": "/host/skills/verify",
|
||||
}), patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills.os.path.isdir",
|
||||
"bot_bottle.backend.smolmachines.provision.skills.os.path.isdir",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp"
|
||||
) as cp, patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec"
|
||||
) as ex:
|
||||
_skills.provision_skills(
|
||||
_plan(skills=["init-prd", "verify"]),
|
||||
"claude-bottle-demo-abc12",
|
||||
"bot-bottle-demo-abc12",
|
||||
)
|
||||
|
||||
# mkdir -p once + (rm -rf + chown) per skill = 5 exec calls.
|
||||
self.assertEqual(5, ex.call_count)
|
||||
mkdir_call = ex.call_args_list[0]
|
||||
self.assertEqual(
|
||||
("claude-bottle-demo-abc12", ["mkdir", "-p", "/home/node/.claude/skills"]),
|
||||
("bot-bottle-demo-abc12", ["mkdir", "-p", "/home/node/.claude/skills"]),
|
||||
mkdir_call.args,
|
||||
)
|
||||
# Two cp calls, one per skill, into the per-skill subdir.
|
||||
@@ -233,8 +233,8 @@ class TestProvisionSkills(unittest.TestCase):
|
||||
cp_targets = {call.args[1] for call in cp.call_args_list}
|
||||
self.assertEqual(
|
||||
{
|
||||
"claude-bottle-demo-abc12:/home/node/.claude/skills/init-prd",
|
||||
"claude-bottle-demo-abc12:/home/node/.claude/skills/verify",
|
||||
"bot-bottle-demo-abc12:/home/node/.claude/skills/init-prd",
|
||||
"bot-bottle-demo-abc12:/home/node/.claude/skills/verify",
|
||||
},
|
||||
cp_targets,
|
||||
)
|
||||
@@ -257,36 +257,36 @@ class TestProvisionSkills(unittest.TestCase):
|
||||
import os
|
||||
with self._patch_host_skill_dir({"init-prd": "/host/skills/init-prd"}), \
|
||||
patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills.os.path.isdir",
|
||||
"bot_bottle.backend.smolmachines.provision.skills.os.path.isdir",
|
||||
return_value=True,
|
||||
), \
|
||||
patch.dict(os.environ, {"CLAUDE_BOTTLE_GUEST_SKILLS_DIR": "/home/node/.claude/skills"}), \
|
||||
patch.dict(os.environ, {"BOT_BOTTLE_GUEST_SKILLS_DIR": "/home/node/.claude/skills"}), \
|
||||
patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp"
|
||||
) as cp, \
|
||||
patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec"
|
||||
):
|
||||
_skills.provision_skills(_plan(skills=["init-prd"]), "claude-bottle-demo-abc12")
|
||||
_skills.provision_skills(_plan(skills=["init-prd"]), "bot-bottle-demo-abc12")
|
||||
self.assertEqual(
|
||||
"claude-bottle-demo-abc12:/home/node/.claude/skills/init-prd",
|
||||
"bot-bottle-demo-abc12:/home/node/.claude/skills/init-prd",
|
||||
cp.call_args.args[1],
|
||||
)
|
||||
|
||||
def test_missing_skill_dies(self):
|
||||
with self._patch_host_skill_dir({"init-prd": "/host/skills/init-prd"}), \
|
||||
patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills.os.path.isdir",
|
||||
"bot_bottle.backend.smolmachines.provision.skills.os.path.isdir",
|
||||
return_value=False,
|
||||
), \
|
||||
patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_cp"
|
||||
), \
|
||||
patch(
|
||||
"claude_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.skills._smolvm.machine_exec"
|
||||
):
|
||||
with self.assertRaises(SystemExit):
|
||||
_skills.provision_skills(_plan(skills=["init-prd"]), "claude-bottle-demo-abc12")
|
||||
_skills.provision_skills(_plan(skills=["init-prd"]), "bot-bottle-demo-abc12")
|
||||
|
||||
|
||||
def _write_self_signed_cert(path: Path) -> None:
|
||||
@@ -331,15 +331,15 @@ class TestProvisionCA(unittest.TestCase):
|
||||
def test_pipelock_path_when_no_routes(self):
|
||||
plan = _plan(pipelock_ca_path=self.pipelock_ca)
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp"
|
||||
) as cp, patch(
|
||||
"claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec",
|
||||
"bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec",
|
||||
return_value=self._UPDATE_OK,
|
||||
) as ex:
|
||||
_ca.provision_ca(plan, "claude-bottle-demo-abc12")
|
||||
_ca.provision_ca(plan, "bot-bottle-demo-abc12")
|
||||
cp.assert_called_once_with(
|
||||
str(self.pipelock_ca),
|
||||
"claude-bottle-demo-abc12:" + _ca.AGENT_CA_PATH,
|
||||
"bot-bottle-demo-abc12:" + _ca.AGENT_CA_PATH,
|
||||
)
|
||||
# chmod + chown + update-ca-certificates are now folded
|
||||
# into one `sh -c` invocation (working around a smolvm
|
||||
@@ -359,17 +359,17 @@ class TestProvisionCA(unittest.TestCase):
|
||||
pipelock_ca_path=self.pipelock_ca,
|
||||
)
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp"
|
||||
) as cp, patch(
|
||||
"claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec",
|
||||
"bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec",
|
||||
return_value=self._UPDATE_OK,
|
||||
):
|
||||
_ca.provision_ca(plan, "claude-bottle-demo-abc12")
|
||||
_ca.provision_ca(plan, "bot-bottle-demo-abc12")
|
||||
# When routes are declared, egress is the agent's first hop,
|
||||
# so egress's CA is the one that gets installed.
|
||||
cp.assert_called_once_with(
|
||||
str(self.egress_ca),
|
||||
"claude-bottle-demo-abc12:" + _ca.AGENT_CA_PATH,
|
||||
"bot-bottle-demo-abc12:" + _ca.AGENT_CA_PATH,
|
||||
)
|
||||
|
||||
def test_dies_when_selected_cert_missing(self):
|
||||
@@ -377,12 +377,12 @@ class TestProvisionCA(unittest.TestCase):
|
||||
# something went wrong in launch's pipelock_tls_init.
|
||||
plan = _plan(pipelock_ca_path=self.tmp / "does-not-exist.pem")
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_cp"
|
||||
), patch(
|
||||
"claude_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.ca._smolvm.machine_exec"
|
||||
):
|
||||
with self.assertRaises(SystemExit):
|
||||
_ca.provision_ca(plan, "claude-bottle-demo-abc12")
|
||||
_ca.provision_ca(plan, "bot-bottle-demo-abc12")
|
||||
|
||||
|
||||
class TestProvisionGit(unittest.TestCase):
|
||||
@@ -399,12 +399,12 @@ class TestProvisionGit(unittest.TestCase):
|
||||
|
||||
def test_noop_when_no_cwd_and_no_git_entries(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_cp"
|
||||
) as cp, patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
) as ex:
|
||||
_git.provision_git(
|
||||
_plan(stage_dir=self.stage), "claude-bottle-demo-abc12",
|
||||
_plan(stage_dir=self.stage), "bot-bottle-demo-abc12",
|
||||
)
|
||||
cp.assert_not_called()
|
||||
ex.assert_not_called()
|
||||
@@ -418,14 +418,14 @@ class TestProvisionGit(unittest.TestCase):
|
||||
copy_cwd=True, user_cwd=str(cwd), stage_dir=self.stage,
|
||||
)
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_cp"
|
||||
) as cp, patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
) as ex:
|
||||
_git.provision_git(plan, "claude-bottle-demo-abc12")
|
||||
_git.provision_git(plan, "bot-bottle-demo-abc12")
|
||||
cp.assert_called_once_with(
|
||||
f"{cwd}/.git",
|
||||
"claude-bottle-demo-abc12:/home/node/workspace/.git",
|
||||
"bot-bottle-demo-abc12:/home/node/workspace/.git",
|
||||
)
|
||||
argvs = [c.args[1] for c in ex.call_args_list]
|
||||
self.assertIn(["mkdir", "-p", "/home/node/workspace"], argvs)
|
||||
@@ -438,11 +438,11 @@ class TestProvisionGit(unittest.TestCase):
|
||||
def test_skips_cwd_when_copy_cwd_false(self):
|
||||
plan = _plan(copy_cwd=False, stage_dir=self.stage)
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_cp"
|
||||
) as cp, patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
):
|
||||
_git.provision_git(plan, "claude-bottle-demo-abc12")
|
||||
_git.provision_git(plan, "bot-bottle-demo-abc12")
|
||||
cp.assert_not_called()
|
||||
|
||||
def test_writes_gitconfig_with_ip_port_form_for_smolmachines(self):
|
||||
@@ -452,7 +452,7 @@ class TestProvisionGit(unittest.TestCase):
|
||||
# carries the discovered host port (here mocked to 9418).
|
||||
plan = _plan(
|
||||
git=[GitEntry(
|
||||
Name="claude-bottle",
|
||||
Name="bot-bottle",
|
||||
Upstream="ssh://git@host/repo.git",
|
||||
IdentityFile="~/.ssh/id_ed25519",
|
||||
)],
|
||||
@@ -460,11 +460,11 @@ class TestProvisionGit(unittest.TestCase):
|
||||
agent_git_gate_host="127.0.0.1:9418",
|
||||
)
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_cp"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_cp"
|
||||
) as cp, patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
):
|
||||
_git.provision_git(plan, "claude-bottle-demo-abc12")
|
||||
_git.provision_git(plan, "bot-bottle-demo-abc12")
|
||||
# The staged gitconfig path is whatever NamedTemporaryFile
|
||||
# picked; we read its contents.
|
||||
cp_call = cp.call_args
|
||||
@@ -472,7 +472,7 @@ class TestProvisionGit(unittest.TestCase):
|
||||
self.assertEqual(self.stage, staged_path.parent)
|
||||
content = staged_path.read_text()
|
||||
self.assertIn(
|
||||
'[url "git://127.0.0.1:9418/claude-bottle.git"]', content,
|
||||
'[url "git://127.0.0.1:9418/bot-bottle.git"]', content,
|
||||
)
|
||||
self.assertIn(
|
||||
"\tinsteadOf = ssh://git@host/repo.git", content,
|
||||
@@ -497,9 +497,9 @@ class TestProvisionGitUser(unittest.TestCase):
|
||||
|
||||
def test_noop_when_no_git_user(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
) as ex:
|
||||
_git._provision_git_user(_plan(), "claude-bottle-demo-abc12")
|
||||
_git._provision_git_user(_plan(), "bot-bottle-demo-abc12")
|
||||
self.assertEqual([], self._git_config_calls(ex))
|
||||
|
||||
def test_sets_name_and_email_as_node(self):
|
||||
@@ -508,9 +508,9 @@ class TestProvisionGitUser(unittest.TestCase):
|
||||
"email": "eric@dideric.is",
|
||||
})
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
) as ex:
|
||||
_git._provision_git_user(plan, "claude-bottle-demo-abc12")
|
||||
_git._provision_git_user(plan, "bot-bottle-demo-abc12")
|
||||
calls = self._git_config_calls(ex)
|
||||
self.assertEqual(2, len(calls))
|
||||
# Both go through `runuser -u node --` so they run as node;
|
||||
@@ -530,9 +530,9 @@ class TestProvisionGitUser(unittest.TestCase):
|
||||
def test_name_only(self):
|
||||
plan = _plan(git_user={"name": "Bot"})
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
) as ex:
|
||||
_git._provision_git_user(plan, "claude-bottle-demo-abc12")
|
||||
_git._provision_git_user(plan, "bot-bottle-demo-abc12")
|
||||
calls = self._git_config_calls(ex)
|
||||
self.assertEqual(1, len(calls))
|
||||
self.assertEqual(["user.name", "Bot"], calls[0][0][7:])
|
||||
@@ -540,9 +540,9 @@ class TestProvisionGitUser(unittest.TestCase):
|
||||
def test_email_only(self):
|
||||
plan = _plan(git_user={"email": "bot@example.com"})
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.git._smolvm.machine_exec"
|
||||
) as ex:
|
||||
_git._provision_git_user(plan, "claude-bottle-demo-abc12")
|
||||
_git._provision_git_user(plan, "bot-bottle-demo-abc12")
|
||||
calls = self._git_config_calls(ex)
|
||||
self.assertEqual(1, len(calls))
|
||||
self.assertEqual(["user.email", "bot@example.com"], calls[0][0][7:])
|
||||
@@ -551,9 +551,9 @@ class TestProvisionGitUser(unittest.TestCase):
|
||||
class TestProvisionSupervise(unittest.TestCase):
|
||||
def test_noop_when_supervise_not_enabled(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec"
|
||||
"bot_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec"
|
||||
) as ex:
|
||||
_supervise.provision_supervise(_plan(), "claude-bottle-demo-abc12")
|
||||
_supervise.provision_supervise(_plan(), "bot-bottle-demo-abc12")
|
||||
ex.assert_not_called()
|
||||
|
||||
def test_calls_claude_mcp_add_when_supervise_enabled(self):
|
||||
@@ -562,10 +562,10 @@ class TestProvisionSupervise(unittest.TestCase):
|
||||
agent_supervise_url="http://127.0.0.1:9100/",
|
||||
)
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec",
|
||||
"bot_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec",
|
||||
return_value=SmolvmRunResult(returncode=0, stdout="", stderr=""),
|
||||
) as ex:
|
||||
_supervise.provision_supervise(plan, "claude-bottle-demo-abc12")
|
||||
_supervise.provision_supervise(plan, "bot-bottle-demo-abc12")
|
||||
ex.assert_called_once()
|
||||
argv = ex.call_args.args[1]
|
||||
# `claude mcp add --scope user` writes to ~/.claude.json,
|
||||
@@ -589,14 +589,14 @@ class TestProvisionSupervise(unittest.TestCase):
|
||||
def test_non_zero_exit_logs_warning_but_does_not_raise(self):
|
||||
plan = _plan(supervise=True)
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec",
|
||||
"bot_bottle.backend.smolmachines.provision.supervise._smolvm.machine_exec",
|
||||
return_value=SmolvmRunResult(
|
||||
returncode=1, stdout="", stderr="boom",
|
||||
),
|
||||
):
|
||||
# No raise — the bottle still works without the MCP
|
||||
# entry, so we log and move on.
|
||||
_supervise.provision_supervise(plan, "claude-bottle-demo-abc12")
|
||||
_supervise.provision_supervise(plan, "bot-bottle-demo-abc12")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -13,7 +13,7 @@ import unittest
|
||||
import unittest.mock
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.backend.smolmachines import pty_resize
|
||||
from bot_bottle.backend.smolmachines import pty_resize
|
||||
|
||||
|
||||
class TestPushSize(unittest.TestCase):
|
||||
@@ -23,11 +23,11 @@ class TestPushSize(unittest.TestCase):
|
||||
# Per-PTY `stty -F ... 2>/dev/null` swallows EBADF when a
|
||||
# session has already exited.
|
||||
with patch.object(pty_resize.subprocess, "run") as run:
|
||||
pty_resize._push_size("claude-bottle-m", 50, 200)
|
||||
pty_resize._push_size("bot-bottle-m", 50, 200)
|
||||
argv = run.call_args.args[0]
|
||||
self.assertEqual(
|
||||
["smolvm", "machine", "exec", "--name",
|
||||
"claude-bottle-m", "--", "sh", "-c"],
|
||||
"bot-bottle-m", "--", "sh", "-c"],
|
||||
argv[:8],
|
||||
)
|
||||
# cols / rows land in the order stty wants them.
|
||||
@@ -42,7 +42,7 @@ class TestPushSize(unittest.TestCase):
|
||||
# PTY's FG-PG / input plumbing). DEVNULL stdin sidesteps
|
||||
# the interaction.
|
||||
with patch.object(pty_resize.subprocess, "run") as run:
|
||||
pty_resize._push_size("claude-bottle-m", 24, 80)
|
||||
pty_resize._push_size("bot-bottle-m", 24, 80)
|
||||
self.assertEqual(
|
||||
pty_resize.subprocess.DEVNULL,
|
||||
run.call_args.kwargs.get("stdin"),
|
||||
@@ -116,7 +116,7 @@ class TestMainArgvParsing(unittest.TestCase):
|
||||
def test_missing_separator_returns_error_exit_code(self):
|
||||
# No `--` between machine name and inner argv.
|
||||
with patch.object(pty_resize.sys, "stderr", new=io.StringIO()) as err:
|
||||
rc = pty_resize.main(["claude-bottle-m", "smolvm", "machine"])
|
||||
rc = pty_resize.main(["bot-bottle-m", "smolvm", "machine"])
|
||||
self.assertEqual(2, rc)
|
||||
self.assertIn("usage:", err.getvalue())
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import subprocess
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.backend.smolmachines.sidecar_bundle import (
|
||||
from bot_bottle.backend.smolmachines.sidecar_bundle import (
|
||||
BundleLaunchSpec,
|
||||
bundle_container_name,
|
||||
bundle_network_name,
|
||||
@@ -37,7 +37,7 @@ def _fail(stderr: str = "boom") -> subprocess.CompletedProcess:
|
||||
def _spec(**kwargs) -> BundleLaunchSpec:
|
||||
defaults = dict(
|
||||
slug="demo-abc12",
|
||||
network_name="claude-bottle-bundle-demo-abc12",
|
||||
network_name="bot-bottle-bundle-demo-abc12",
|
||||
subnet="192.168.50.0/24",
|
||||
gateway="192.168.50.1",
|
||||
bundle_ip="192.168.50.2",
|
||||
@@ -49,10 +49,10 @@ def _spec(**kwargs) -> BundleLaunchSpec:
|
||||
class TestNamingHelpers(unittest.TestCase):
|
||||
def test_network_name_uses_bundle_prefix(self):
|
||||
# Distinct from the docker backend's
|
||||
# `claude-bottle-net-<slug>` so two backends running the
|
||||
# `bot-bottle-net-<slug>` so two backends running the
|
||||
# same agent slug don't collide.
|
||||
self.assertEqual(
|
||||
"claude-bottle-bundle-myagent-xyz",
|
||||
"bot-bottle-bundle-myagent-xyz",
|
||||
bundle_network_name("myagent-xyz"),
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ class TestNamingHelpers(unittest.TestCase):
|
||||
# bundle container — dashboard prefix-discovery covers
|
||||
# both backends with one filter.
|
||||
self.assertEqual(
|
||||
"claude-bottle-sidecars-myagent-xyz",
|
||||
"bot-bottle-sidecars-myagent-xyz",
|
||||
bundle_container_name("myagent-xyz"),
|
||||
)
|
||||
|
||||
@@ -69,7 +69,7 @@ class TestNamingHelpers(unittest.TestCase):
|
||||
class TestNetworkLifecycle(unittest.TestCase):
|
||||
def _patch_run(self, **kwargs):
|
||||
return patch(
|
||||
"claude_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
|
||||
"bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -108,7 +108,7 @@ class TestNetworkLifecycle(unittest.TestCase):
|
||||
class TestStartBundle(unittest.TestCase):
|
||||
def _patch_run(self):
|
||||
return patch(
|
||||
"claude_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
|
||||
"bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
|
||||
return_value=_ok(),
|
||||
)
|
||||
|
||||
@@ -118,7 +118,7 @@ class TestStartBundle(unittest.TestCase):
|
||||
argv = m.call_args.args[0]
|
||||
# --network NETNAME --ip <bundle-ip> on the docker run.
|
||||
self.assertIn("--network", argv)
|
||||
self.assertIn("claude-bottle-bundle-demo-abc12", argv)
|
||||
self.assertIn("bot-bottle-bundle-demo-abc12", argv)
|
||||
self.assertIn("--ip", argv)
|
||||
self.assertIn("192.168.50.2", argv)
|
||||
# Detached and auto-removed.
|
||||
@@ -126,9 +126,9 @@ class TestStartBundle(unittest.TestCase):
|
||||
self.assertIn("--rm", argv)
|
||||
# Container name uses the per-slug bundle prefix.
|
||||
i = argv.index("--name")
|
||||
self.assertEqual("claude-bottle-sidecars-demo-abc12", argv[i + 1])
|
||||
self.assertEqual("bot-bottle-sidecars-demo-abc12", argv[i + 1])
|
||||
# Image at the end.
|
||||
self.assertEqual("claude-bottle-sidecars:latest", argv[-1])
|
||||
self.assertEqual("bot-bottle-sidecars:latest", argv[-1])
|
||||
|
||||
def test_daemons_env_passed_in(self):
|
||||
with self._patch_run() as m:
|
||||
@@ -136,7 +136,7 @@ class TestStartBundle(unittest.TestCase):
|
||||
argv = m.call_args.args[0]
|
||||
self.assertIn("-e", argv)
|
||||
self.assertIn(
|
||||
"CLAUDE_BOTTLE_SIDECAR_DAEMONS=egress,pipelock,supervise",
|
||||
"BOT_BOTTLE_SIDECAR_DAEMONS=egress,pipelock,supervise",
|
||||
argv,
|
||||
)
|
||||
|
||||
@@ -164,7 +164,7 @@ class TestStartBundle(unittest.TestCase):
|
||||
|
||||
def test_failure_dies(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
|
||||
"bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
|
||||
return_value=_fail("invalid mount"),
|
||||
):
|
||||
with self.assertRaises(SystemExit):
|
||||
@@ -175,7 +175,7 @@ class TestStartBundle(unittest.TestCase):
|
||||
# subprocess being run with the host env. Confirm `env=`
|
||||
# threads through.
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
|
||||
"bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
|
||||
return_value=_ok(),
|
||||
) as m:
|
||||
start_bundle(_spec(), env={"FOO": "bar"})
|
||||
@@ -185,7 +185,7 @@ class TestStartBundle(unittest.TestCase):
|
||||
class TestStopBundle(unittest.TestCase):
|
||||
def _patch_run(self, **kwargs):
|
||||
return patch(
|
||||
"claude_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
|
||||
"bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -193,13 +193,13 @@ class TestStopBundle(unittest.TestCase):
|
||||
with self._patch_run(return_value=_ok()) as m:
|
||||
stop_bundle("demo-abc12")
|
||||
self.assertEqual(
|
||||
["docker", "rm", "-f", "claude-bottle-sidecars-demo-abc12"],
|
||||
["docker", "rm", "-f", "bot-bottle-sidecars-demo-abc12"],
|
||||
m.call_args.args[0],
|
||||
)
|
||||
|
||||
def test_missing_container_is_idempotent(self):
|
||||
with self._patch_run(return_value=_fail(
|
||||
"Error: No such container: claude-bottle-sidecars-demo-abc12"
|
||||
"Error: No such container: bot-bottle-sidecars-demo-abc12"
|
||||
)):
|
||||
stop_bundle("demo-abc12") # no raise
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.backend.smolmachines.smolvm import (
|
||||
from bot_bottle.backend.smolmachines.smolvm import (
|
||||
SmolvmError,
|
||||
SmolvmRunResult,
|
||||
is_available,
|
||||
@@ -46,17 +46,17 @@ class TestArgvShapes(unittest.TestCase):
|
||||
|
||||
def _patch_run(self):
|
||||
return patch(
|
||||
"claude_bottle.backend.smolmachines.smolvm.subprocess.run",
|
||||
"bot_bottle.backend.smolmachines.smolvm.subprocess.run",
|
||||
return_value=_ok(),
|
||||
)
|
||||
|
||||
def test_pack_create_argv(self):
|
||||
with self._patch_run() as m:
|
||||
pack_create("claude-bottle:latest", Path("/tmp/agent.smolmachine"))
|
||||
pack_create("bot-bottle-claude:latest", Path("/tmp/agent.smolmachine"))
|
||||
argv = m.call_args.args[0]
|
||||
self.assertEqual(
|
||||
["smolvm", "pack", "create",
|
||||
"--image", "claude-bottle:latest",
|
||||
"--image", "bot-bottle-claude:latest",
|
||||
"-o", "/tmp/agent.smolmachine"],
|
||||
argv,
|
||||
)
|
||||
@@ -175,7 +175,7 @@ class TestErrorPath(unittest.TestCase):
|
||||
|
||||
def test_create_failure_raises(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.smolvm.subprocess.run",
|
||||
"bot_bottle.backend.smolmachines.smolvm.subprocess.run",
|
||||
return_value=_fail("no such image"),
|
||||
):
|
||||
with self.assertRaises(SmolvmError) as cm:
|
||||
@@ -185,7 +185,7 @@ class TestErrorPath(unittest.TestCase):
|
||||
|
||||
def test_pack_create_failure_raises(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.smolvm.subprocess.run",
|
||||
"bot_bottle.backend.smolmachines.smolvm.subprocess.run",
|
||||
return_value=_fail("pack failed"),
|
||||
):
|
||||
with self.assertRaises(SmolvmError):
|
||||
@@ -195,7 +195,7 @@ class TestErrorPath(unittest.TestCase):
|
||||
# The in-VM command's exit code is what Bottle.exec sees;
|
||||
# `false` exiting non-zero is not a smolvm failure.
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.smolvm.subprocess.run",
|
||||
"bot_bottle.backend.smolmachines.smolvm.subprocess.run",
|
||||
return_value=subprocess.CompletedProcess(
|
||||
args=[], returncode=42, stdout="", stderr="nope",
|
||||
),
|
||||
@@ -207,14 +207,14 @@ class TestErrorPath(unittest.TestCase):
|
||||
class TestIsAvailable(unittest.TestCase):
|
||||
def test_true_when_on_path(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.smolvm.shutil.which",
|
||||
"bot_bottle.backend.smolmachines.smolvm.shutil.which",
|
||||
return_value="/usr/local/bin/smolvm",
|
||||
):
|
||||
self.assertTrue(is_available())
|
||||
|
||||
def test_false_when_missing(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.smolvm.shutil.which",
|
||||
"bot_bottle.backend.smolmachines.smolvm.shutil.which",
|
||||
return_value=None,
|
||||
):
|
||||
self.assertFalse(is_available())
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from claude_bottle.backend.smolmachines.util import (
|
||||
from bot_bottle.backend.smolmachines.util import (
|
||||
smolmachines_bundle_subnet,
|
||||
smolmachines_preflight,
|
||||
)
|
||||
@@ -57,14 +57,14 @@ class TestBundleSubnet(unittest.TestCase):
|
||||
class TestPreflight(unittest.TestCase):
|
||||
def test_smolvm_present_returns_none(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.util.shutil.which",
|
||||
"bot_bottle.backend.smolmachines.util.shutil.which",
|
||||
return_value="/usr/local/bin/smolvm",
|
||||
):
|
||||
self.assertIsNone(smolmachines_preflight())
|
||||
|
||||
def test_missing_smolvm_dies(self):
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.util.shutil.which",
|
||||
"bot_bottle.backend.smolmachines.util.shutil.which",
|
||||
return_value=None,
|
||||
):
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
@@ -75,7 +75,7 @@ class TestPreflight(unittest.TestCase):
|
||||
import io
|
||||
import sys
|
||||
with patch(
|
||||
"claude_bottle.backend.smolmachines.util.shutil.which",
|
||||
"bot_bottle.backend.smolmachines.util.shutil.which",
|
||||
return_value=None,
|
||||
):
|
||||
captured = io.StringIO()
|
||||
|
||||
@@ -8,8 +8,8 @@ import unittest
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from claude_bottle import supervise
|
||||
from claude_bottle.supervise import (
|
||||
from bot_bottle import supervise
|
||||
from bot_bottle.supervise import (
|
||||
AuditEntry,
|
||||
Proposal,
|
||||
Response,
|
||||
@@ -108,7 +108,7 @@ class TestResponseRoundtrip(unittest.TestCase):
|
||||
|
||||
class TestQueueIO(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="claude-bottle-supervise-test.")
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="bot-bottle-supervise-test.")
|
||||
self.queue_dir = Path(self._tmp.name)
|
||||
|
||||
def tearDown(self):
|
||||
@@ -207,7 +207,7 @@ class TestQueueIO(unittest.TestCase):
|
||||
|
||||
class TestAuditLog(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="claude-bottle-supervise-audit.")
|
||||
self._tmp = tempfile.TemporaryDirectory(prefix="bot-bottle-supervise-audit.")
|
||||
self._home_patch = self._patch_home(Path(self._tmp.name))
|
||||
|
||||
def tearDown(self):
|
||||
@@ -215,13 +215,13 @@ class TestAuditLog(unittest.TestCase):
|
||||
self._tmp.cleanup()
|
||||
|
||||
def _patch_home(self, fake_home: Path):
|
||||
original = supervise.claude_bottle_root
|
||||
original = supervise.bot_bottle_root
|
||||
|
||||
def fake_root() -> Path:
|
||||
return fake_home / ".claude-bottle"
|
||||
return fake_home / ".bot-bottle"
|
||||
|
||||
supervise.claude_bottle_root = fake_root # type: ignore[assignment]
|
||||
return lambda: setattr(supervise, "claude_bottle_root", original)
|
||||
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
|
||||
return lambda: setattr(supervise, "bot_bottle_root", original)
|
||||
|
||||
def test_write_then_read_single_entry(self):
|
||||
e = AuditEntry(
|
||||
@@ -351,13 +351,13 @@ class TestSupervisePrepare(unittest.TestCase):
|
||||
self._tmp.cleanup()
|
||||
|
||||
def _patch_home(self, fake_home: Path):
|
||||
original = supervise.claude_bottle_root
|
||||
original = supervise.bot_bottle_root
|
||||
|
||||
def fake_root() -> Path:
|
||||
return fake_home / ".claude-bottle"
|
||||
return fake_home / ".bot-bottle"
|
||||
|
||||
supervise.claude_bottle_root = fake_root # type: ignore[assignment]
|
||||
return lambda: setattr(supervise, "claude_bottle_root", original)
|
||||
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
|
||||
return lambda: setattr(supervise, "bot_bottle_root", original)
|
||||
|
||||
def test_prepare_creates_queue_and_current_config(self):
|
||||
plan = _StubSupervise().prepare(
|
||||
|
||||
@@ -12,13 +12,13 @@ from pathlib import Path
|
||||
|
||||
# The server module loads `supervise` via same-directory import inside
|
||||
# the container (Dockerfile.supervise WORKDIRs into /app). For tests
|
||||
# we mirror that by injecting claude_bottle/ onto sys.path under the
|
||||
# we mirror that by injecting bot_bottle/ onto sys.path under the
|
||||
# bare name `supervise`.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "claude_bottle"))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "bot_bottle"))
|
||||
import supervise as _sv # noqa: E402
|
||||
|
||||
from claude_bottle import supervise_server # noqa: E402
|
||||
from claude_bottle.supervise_server import (
|
||||
from bot_bottle import supervise_server # noqa: E402
|
||||
from bot_bottle.supervise_server import (
|
||||
ERR_INVALID_PARAMS,
|
||||
ERR_INVALID_REQUEST,
|
||||
ERR_METHOD_NOT_FOUND,
|
||||
@@ -152,7 +152,7 @@ class TestHandleInitialize(unittest.TestCase):
|
||||
self.assertEqual("2024-11-05", result["protocolVersion"])
|
||||
self.assertIn("tools", result["capabilities"]) # type: ignore[index]
|
||||
self.assertEqual(
|
||||
"claude-bottle-supervise",
|
||||
"bot-bottle-supervise",
|
||||
result["serverInfo"]["name"], # type: ignore[index]
|
||||
)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ actually use, and every rejection case the PRD enumerates."""
|
||||
import textwrap
|
||||
import unittest
|
||||
|
||||
from claude_bottle.yaml_subset import YamlSubsetError
|
||||
from claude_bottle.yaml_subset import parse_frontmatter, parse_yaml_subset
|
||||
from bot_bottle.yaml_subset import YamlSubsetError
|
||||
from bot_bottle.yaml_subset import parse_frontmatter, parse_yaml_subset
|
||||
|
||||
|
||||
def _y(s: str):
|
||||
@@ -174,7 +174,7 @@ class TestBlockList(unittest.TestCase):
|
||||
|
||||
def test_list_item_with_inline_list_value(self):
|
||||
# role: [git-insteadof, tea-login] — the exact shape in the
|
||||
# claude-bottle manifest.
|
||||
# bot-bottle manifest.
|
||||
out = _y("""
|
||||
routes:
|
||||
- path: /x/
|
||||
@@ -267,7 +267,7 @@ class TestRealisticBottleFile(unittest.TestCase):
|
||||
git:
|
||||
remotes:
|
||||
gitea.dideric.is:
|
||||
Name: claude-bottle
|
||||
Name: bot-bottle
|
||||
Upstream: ssh://git@gitea.dideric.is:30009/x/y.git
|
||||
IdentityFile: ~/.ssh/gitea.pem
|
||||
ExtraHosts:
|
||||
|
||||
Reference in New Issue
Block a user