refactor(manifest): convert TypedDict to frozen dataclasses
test / run tests/run_tests.py (pull_request) Successful in 14s

Replace the TypedDict + 14 manifest_* free functions with frozen
dataclasses (SshEntry, BottleEgress, Bottle, Agent, Manifest) carrying
their own validators and constructors. Call sites import Manifest and
chain attribute access; the manifest_* helpers and manifest_validate
are gone.

Behavior changes worth flagging:
- Agent.bottle is now required (was optional with a "(none)" fallback).
  Manifest.from_json_obj dies if any agent lacks a 'bottle' field or
  references an undefined bottle, where previously start.py raised the
  error lazily for the specific agent being launched.
- ssh.py now takes SshEntry instances; Host/IdentityFile shape checks
  moved upstream into Manifest construction, leaving only the IdentityFile
  filesystem-existence check in ssh_validate_entries.
- pipelock_bottle_allowlist's per-element string check is dropped — the
  Manifest validator enforces it at load.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 21:20:15 -04:00
parent 36cb0c53bf
commit 1f36d53f7b
11 changed files with 387 additions and 408 deletions
+18 -16
View File
@@ -1,16 +1,18 @@
"""Unit: bottle runtime — manifest_bottle_runtime returns the configured
runtime (defaulting to runc); manifest_validate rejects unknown values,
non-strings, and empty strings."""
"""Unit: bottle runtime — Manifest.from_json_obj defaults runtime to runc,
accepts runsc, and rejects unknown values, non-strings, and empty strings."""
import unittest
from claude_bottle.log import Die
from claude_bottle.manifest import manifest_bottle_runtime, manifest_validate
from claude_bottle.manifest import Manifest
def _bottle(runtime_value: object | None) -> dict:
"""Build a minimal manifest with one bottle whose runtime field is
set (or absent if `runtime_value is _ABSENT`)."""
_ABSENT = object()
def _bottle(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 = {}
if runtime_value is not _ABSENT:
bottle["runtime"] = runtime_value
@@ -20,30 +22,30 @@ def _bottle(runtime_value: object | None) -> dict:
}
_ABSENT = object()
class TestManifestBottleRuntime(unittest.TestCase):
def test_default_runc_when_absent(self):
self.assertEqual("runc", manifest_bottle_runtime(_bottle(_ABSENT), "dev"))
m = Manifest.from_json_obj(_bottle(_ABSENT))
self.assertEqual("runc", m.bottles["dev"].runtime)
def test_explicit_runc(self):
self.assertEqual("runc", manifest_bottle_runtime(_bottle("runc"), "dev"))
m = Manifest.from_json_obj(_bottle("runc"))
self.assertEqual("runc", m.bottles["dev"].runtime)
def test_explicit_runsc(self):
self.assertEqual("runsc", manifest_bottle_runtime(_bottle("runsc"), "dev"))
m = Manifest.from_json_obj(_bottle("runsc"))
self.assertEqual("runsc", m.bottles["dev"].runtime)
def test_rejects_unknown_runtime(self):
with self.assertRaises(Die):
manifest_validate(_bottle("kata-runtime"))
Manifest.from_json_obj(_bottle("kata-runtime"))
def test_rejects_non_string(self):
with self.assertRaises(Die):
manifest_validate(_bottle(42))
Manifest.from_json_obj(_bottle(42))
def test_rejects_empty_string(self):
with self.assertRaises(Die):
manifest_validate(_bottle(""))
Manifest.from_json_obj(_bottle(""))
if __name__ == "__main__":