Files
bot-bottle/tests/unit/test_smolfile.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

113 lines
3.7 KiB
Python

"""Unit: Smolfile renderer for the smolmachines backend (PRD 0023).
Pure-function tests on `smolfile_build` + `smolfile_render`. The
schema we emit is narrow (env list + `[network] allow_cidrs`), so
the tests exhaustively cover what lands on disk."""
from __future__ import annotations
import unittest
from claude_bottle.backend.smolmachines.smolfile import (
smolfile_build,
smolfile_render,
)
class TestSmolfileBuild(unittest.TestCase):
def _build(self, **kwargs):
defaults = dict(
env={"HTTPS_PROXY": "http://192.168.50.2:8888"},
bundle_ip="192.168.50.2",
)
defaults.update(kwargs)
return smolfile_build(**defaults)
def test_env_renders_as_sorted_KEY_VALUE_list(self):
# Sorted by key so renderer output is deterministic.
cfg = self._build(env={
"ZED": "one",
"ALPHA": "two",
"HTTPS_PROXY": "http://192.168.50.2:8888",
})
self.assertEqual(
[
"ALPHA=two",
"HTTPS_PROXY=http://192.168.50.2:8888",
"ZED=one",
],
cfg["env"],
)
def test_allow_cidrs_is_single_slash_32(self):
# TSI's single-IP allowlist. Anything else would
# re-introduce the loopback / LAN reachability the PRD
# design carefully avoids.
cfg = self._build(bundle_ip="10.20.30.40")
self.assertEqual(
{"allow_cidrs": ["10.20.30.40/32"]},
cfg["network"],
)
def test_no_image_or_command_emitted(self):
# The chunk-1 renderer (under the abandoned gvproxy design)
# emitted `name = ...` + `[[net]] attachment="unixgram"`.
# The new renderer carries only the per-bottle overrides;
# image / entrypoint / cmd come from the .smolmachine
# artifact, not the Smolfile.
cfg = self._build()
self.assertNotIn("image", cfg)
self.assertNotIn("entrypoint", cfg)
self.assertNotIn("cmd", cfg)
self.assertNotIn("command", cfg)
self.assertNotIn("name", cfg)
class TestSmolfileRender(unittest.TestCase):
def _render(self, **kwargs):
defaults = dict(
env={"HTTPS_PROXY": "http://192.168.50.2:8888"},
bundle_ip="192.168.50.2",
)
defaults.update(kwargs)
return smolfile_render(smolfile_build(**defaults))
def test_round_trip_through_tomllib(self):
import tomllib # stdlib in 3.11+
rendered = self._render()
parsed = tomllib.loads(rendered)
self.assertIn(
"HTTPS_PROXY=http://192.168.50.2:8888",
parsed["env"],
)
self.assertEqual(
["192.168.50.2/32"],
parsed["network"]["allow_cidrs"],
)
def test_no_tsi_outbound_localhost_only(self):
# Whole point of the design pivot: never emit
# `--outbound-localhost-only` or similar that would
# re-open host loopback.
text = self._render()
self.assertNotIn("outbound_localhost_only", text)
self.assertNotIn("outbound-localhost-only", text)
# And no gvproxy / virtio-net carve-out leaked from the
# abandoned first draft.
self.assertNotIn("unixgram", text)
self.assertNotIn("gvproxy", text.lower())
def test_special_chars_in_env_value_escape(self):
import tomllib
cfg = smolfile_build(
env={"WITH_QUOTES": 'has "double" quotes'},
bundle_ip="10.0.0.1",
)
rendered = smolfile_render(cfg)
parsed = tomllib.loads(rendered)
self.assertIn('WITH_QUOTES=has "double" quotes', parsed["env"])
if __name__ == "__main__":
unittest.main()