feat(bottles): implement bottle factory abstraction per PRD 0003
test / run tests/run_tests.py (pull_request) Successful in 16s
test / run tests/run_tests.py (pull_request) Successful in 16s
Introduce claude_bottle/bottles/ with a Bottle Protocol and a get_bottle_factory() that dispatches on CLAUDE_BOTTLE_PLATFORM (default "docker"). Move every Docker-specific subprocess.run call from cli/start.py, plus the orchestration of build, networks, the pipelock sidecar, container launch, and per-container provisioning (prompt, skills, ssh, .git), into create_docker_bottle. Drop bottles[].runtime from the manifest schema. Auto-detect whether gVisor is registered with the daemon and pass --runtime=runsc when it is; the preflight shows the resolved runtime so the choice is visible. Manifests still carrying 'runtime' get a clear error pointing at the auto-detect behavior, rather than silent ignore. Out of scope: cli/cleanup.py and cli/list.py still call docker directly. They enumerate active bottles across the host, which is a separate concern from "create a bottle" and is left for a follow-up that introduces a list_active/cleanup primitive on the factory.
This commit is contained in:
@@ -1,16 +1,21 @@
|
||||
"""Unit: bottle runtime — Manifest.from_json_obj defaults runtime to runc,
|
||||
accepts runsc, and rejects unknown values, non-strings, and empty strings."""
|
||||
"""Unit: bottle 'runtime' field is no longer supported (PRD 0003).
|
||||
|
||||
gVisor is now auto-detected by the Docker factory. A manifest carrying
|
||||
the legacy 'runtime' field must fail loudly with a message pointing the
|
||||
user at the auto-detect behavior, rather than silently ignoring."""
|
||||
|
||||
import io
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from claude_bottle.log import Die
|
||||
from claude_bottle.manifest import Manifest
|
||||
from claude_bottle.manifest import Bottle, Manifest
|
||||
|
||||
|
||||
_ABSENT = object()
|
||||
|
||||
|
||||
def _bottle(runtime_value: object) -> dict:
|
||||
def _manifest(runtime_value: object) -> dict:
|
||||
"""Build a minimal manifest JSON shape with one bottle whose runtime
|
||||
field is set (or absent if `runtime_value is _ABSENT`)."""
|
||||
bottle: dict = {}
|
||||
@@ -22,30 +27,41 @@ def _bottle(runtime_value: object) -> dict:
|
||||
}
|
||||
|
||||
|
||||
class TestManifestBottleRuntime(unittest.TestCase):
|
||||
def test_default_runc_when_absent(self):
|
||||
m = Manifest.from_json_obj(_bottle(_ABSENT))
|
||||
self.assertEqual("runc", m.bottles["dev"].runtime)
|
||||
class TestManifestRuntimeRemoved(unittest.TestCase):
|
||||
def test_loads_when_runtime_absent(self):
|
||||
m = Manifest.from_json_obj(_manifest(_ABSENT))
|
||||
self.assertIn("dev", m.bottles)
|
||||
|
||||
def test_explicit_runc(self):
|
||||
m = Manifest.from_json_obj(_bottle("runc"))
|
||||
self.assertEqual("runc", m.bottles["dev"].runtime)
|
||||
def test_bottle_dataclass_has_no_runtime_attribute(self):
|
||||
"""Structural check: the field has been removed from the dataclass."""
|
||||
b = Bottle()
|
||||
self.assertFalse(hasattr(b, "runtime"))
|
||||
|
||||
def test_explicit_runsc(self):
|
||||
m = Manifest.from_json_obj(_bottle("runsc"))
|
||||
self.assertEqual("runsc", m.bottles["dev"].runtime)
|
||||
def test_rejects_runsc_value_with_helpful_message(self):
|
||||
captured = io.StringIO()
|
||||
old_stderr = sys.stderr
|
||||
sys.stderr = captured
|
||||
try:
|
||||
with self.assertRaises(Die):
|
||||
Manifest.from_json_obj(_manifest("runsc"))
|
||||
finally:
|
||||
sys.stderr = old_stderr
|
||||
msg = captured.getvalue()
|
||||
self.assertIn("'runtime'", msg, "error names the field")
|
||||
self.assertIn("auto-detect", msg, "error points at the new behavior")
|
||||
|
||||
def test_rejects_unknown_runtime(self):
|
||||
def test_rejects_runc_value(self):
|
||||
with self.assertRaises(Die):
|
||||
Manifest.from_json_obj(_bottle("kata-runtime"))
|
||||
Manifest.from_json_obj(_manifest("runc"))
|
||||
|
||||
def test_rejects_unknown_value(self):
|
||||
with self.assertRaises(Die):
|
||||
Manifest.from_json_obj(_manifest("kata-runtime"))
|
||||
|
||||
def test_rejects_non_string(self):
|
||||
"""Any presence of the field is an error; type is not consulted."""
|
||||
with self.assertRaises(Die):
|
||||
Manifest.from_json_obj(_bottle(42))
|
||||
|
||||
def test_rejects_empty_string(self):
|
||||
with self.assertRaises(Die):
|
||||
Manifest.from_json_obj(_bottle(""))
|
||||
Manifest.from_json_obj(_manifest(42))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user