Files
bot-bottle/tests/unit/test_smolmachines_util.py
T
didericis-claude c73d717f71
test / unit (pull_request) Successful in 21s
test / integration (pull_request) Successful in 39s
feat(smolmachines): rewrite Smolfile to smolvm 0.8.0 schema + drop gvproxy (PRD 0023 chunk 2a)
First sub-PR of chunk 2: rewrite the renderer chunk 1 shipped to
match smolvm 0.8.0's actual Smolfile shape, delete the dead
gvproxy renderer + its tests, simplify the prepare flow now that
there's no gvproxy socket + no loopback-port allocation.

Smolfile renderer:
- Old shape (under the abandoned gvproxy design): name = ...,
  command = [...], [[net]] attachment = "unixgram",
  socket = "...".
- New shape (smolvm 0.8.0): env = [...] (sorted K=V pairs),
  [network] allow_cidrs = ["<bundle-ip>/32"]. Nothing else.
  image / entrypoint / cmd come from the .smolmachine artifact
  built in chunk 2b; cpus / memory left at smolvm defaults.
- Tests assert no leakage of TSI's --outbound-localhost-only or
  the old gvproxy/unixgram keys.

util.py:
- smolmachines_gvproxy_subnet → smolmachines_bundle_subnet,
  returning (subnet, gateway, bundle_ip). bundle_ip is always
  at .2 (gateway .1); subnet is /24, third octet derived from
  the slug hash, skipping the docker-default 17 to avoid the
  common 192.168.17.x collision.
- allocate_loopback_port: deleted. The bundle gets a pinned
  docker IP now; the agent dials that IP directly through TSI.
- smolmachines_preflight: dropped the gvproxy check; only
  smolvm is required.

prepare.py:
- Drops the gvproxy.yaml render + the loopback port allocation
  + the gvproxy_socket field on the plan.
- Derives subnet / gateway / bundle_ip from the slug and
  populates the new SmolmachinesBottlePlan fields.
- Agent env now uses IP-literal URLs (http://<bundle-ip>:8888
  etc) since the guest will have no DNS resolver inside TSI's
  allowlist.

bottle_plan.py:
- Old fields: gvproxy_config_path, gvproxy_socket,
  gvproxy_subnet, gvproxy_gateway, host_port_map.
- New fields: bundle_subnet, bundle_gateway, bundle_ip,
  smolfile_path. (smolmachine artifact path lands in chunk 2b.)

Net: -410 lines. Full unit suite: 516 passing.

The VM lifecycle + bundle bringup + launch wiring + smoke tests
land in chunk 2b.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 04:01:07 -04:00

92 lines
3.4 KiB
Python

"""Unit: smolmachines backend util helpers (PRD 0023)."""
from __future__ import annotations
import unittest
from unittest.mock import patch
from claude_bottle.backend.smolmachines.util import (
smolmachines_bundle_subnet,
smolmachines_preflight,
)
class TestBundleSubnet(unittest.TestCase):
def test_returns_subnet_gateway_and_bundle_ip(self):
subnet, gateway, bundle_ip = smolmachines_bundle_subnet("demo-abc12")
self.assertTrue(subnet.startswith("192.168."))
self.assertTrue(subnet.endswith(".0/24"))
# Gateway at .1, bundle at .2 — fixed convention.
self.assertTrue(gateway.endswith(".1"))
self.assertTrue(bundle_ip.endswith(".2"))
# All three share the same third octet.
third = subnet.split(".")[2]
self.assertEqual(third, gateway.split(".")[2])
self.assertEqual(third, bundle_ip.split(".")[2])
def test_stable_for_same_slug(self):
# Recoverability: `cli.py resume` reuses the slug and
# expects to find the same per-bottle subnet (a fresh
# docker bridge would mean a different IP, and smolvm's
# allow_cidrs would no longer match).
a = smolmachines_bundle_subnet("demo-abc12")
b = smolmachines_bundle_subnet("demo-abc12")
self.assertEqual(a, b)
def test_different_slugs_likely_differ(self):
# Not a guarantee — it's hash-mod-254, collisions exist —
# but two arbitrary slugs shouldn't share a subnet in the
# typical case.
seen = {
smolmachines_bundle_subnet(s)[0]
for s in ("a", "b", "c", "d", "e", "alpha", "beta", "gamma")
}
self.assertGreater(len(seen), 1)
def test_skips_docker_default_octet(self):
# docker's default bridge sits at 172.17.x.x; operators
# often also see 192.168.17.x from VPN clients on macOS.
# The util skips octet 17 → 18 so the smolmachines subnet
# doesn't collide with that historical pain point.
for slug in (f"slug-{i}" for i in range(500)):
subnet, _, _ = smolmachines_bundle_subnet(slug)
self.assertNotEqual("192.168.17.0/24", subnet,
f"slug {slug!r} landed on the skipped octet")
class TestPreflight(unittest.TestCase):
def test_smolvm_present_returns_none(self):
with patch(
"claude_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",
return_value=None,
):
with self.assertRaises(SystemExit) as cm:
smolmachines_preflight()
self.assertNotEqual(0, cm.exception.code)
def test_install_pointer_in_error(self):
import io
import sys
with patch(
"claude_bottle.backend.smolmachines.util.shutil.which",
return_value=None,
):
captured = io.StringIO()
with patch.object(sys, "stderr", captured):
with self.assertRaises(SystemExit):
smolmachines_preflight()
msg = captured.getvalue()
self.assertIn("smolvm", msg)
self.assertIn("smolmachines.com/install.sh", msg)
if __name__ == "__main__":
unittest.main()