9ac5b19322
Addresses review on #519 (@didericis 6143 + the fail-hard direction over the codex skip). Base owns the flow (6143 / ADR 0006). `attach_bottled_agents_to_gateway()` is now a concrete method on `BottleBackend` that delegates to `gateway_attach.reconcile_running_bottles`; backends override only three primitives — `_gateway_attach_resources()`, `_running_bottles()`, `_attach_bottle_to_gateway()`. The shared control flow + error policy live in one place so a backend can't drift onto a bespoke loop or a silent skip. Keeps the reconcile flow + `GatewayAttachResources` out of `backend/base.py` (the size guardrail) in a dedicated `backend/gateway_attach.py`. New ADR 0006 records the "shared behaviour in the base backend, subclasses override primitives" theme. Fail hard, never skip (the maintainer's direction over the codex review's skip). Any attach failure now aborts bring-up instead of being logged and skipped: a bottle that silently can't reach the fresh gateway (its egress just starts failing TLS) is worse than a loud failure. Every bottle is attempted and the failures are raised together (aggregate `InfraLaunchError`) so one bring-up surfaces the full blast radius. Resource gathering (the CA fetch) also fails hard. PRD 0081 goal + design updated to match (reversed from the earlier "tolerate per-bottle failures" draft). Bumps the base.py guardrail cap 580->600 for the new contract surface (the delegator + 3 abstract primitives); the flow itself lives in gateway_attach.py. Refs #516, #519. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
4.3 KiB
Python
102 lines
4.3 KiB
Python
"""Architecture rules that should fail before coupling becomes entrenched."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
class TestCliBackendBoundaries(unittest.TestCase):
|
|
def test_cli_does_not_import_a_concrete_backend(self) -> None:
|
|
forbidden = (
|
|
"backend.docker", "backend.firecracker", "backend.macos_container",
|
|
"bot_bottle.backend.docker", "bot_bottle.backend.firecracker",
|
|
"bot_bottle.backend.macos_container",
|
|
)
|
|
violations: list[str] = []
|
|
for path in (ROOT / "bot_bottle" / "cli").rglob("*.py"):
|
|
tree = ast.parse(path.read_text(), filename=str(path))
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.ImportFrom):
|
|
module = node.module
|
|
if module and module.startswith(forbidden):
|
|
violations.append(
|
|
f"{path.relative_to(ROOT)}:{node.lineno}: {module}"
|
|
)
|
|
if isinstance(node, ast.Import):
|
|
violations.extend(
|
|
f"{path.relative_to(ROOT)}:{node.lineno}: {alias.name}"
|
|
for alias in node.names if alias.name.startswith(forbidden)
|
|
)
|
|
self.assertEqual([], violations, "generic CLI imports concrete backend internals:\n" +
|
|
"\n".join(violations))
|
|
|
|
|
|
class TestRuntimeModuleSizes(unittest.TestCase):
|
|
def test_no_runtime_module_grows_beyond_global_ceiling(self) -> None:
|
|
"""A coarse ceiling catches new monoliths; focused caps stay tighter."""
|
|
ceiling = 850
|
|
oversized = [
|
|
f"{path.relative_to(ROOT)} ({len(path.read_text().splitlines())})"
|
|
for path in (ROOT / "bot_bottle").rglob("*.py")
|
|
if len(path.read_text().splitlines()) > ceiling
|
|
]
|
|
self.assertEqual(
|
|
[], oversized,
|
|
f"runtime modules must stay at or below {ceiling} lines: "
|
|
+ ", ".join(oversized),
|
|
)
|
|
|
|
def test_egress_modules_stay_focused(self) -> None:
|
|
caps = {
|
|
"addon_core.py": 100,
|
|
"schema.py": 400,
|
|
"types.py": 180,
|
|
"matching.py": 180,
|
|
"dlp.py": 180,
|
|
"context.py": 140,
|
|
}
|
|
directory = ROOT / "bot_bottle" / "gateway" / "egress"
|
|
oversized = [f"{name} ({len((directory / name).read_text().splitlines())}>{cap})"
|
|
for name, cap in caps.items()
|
|
if len((directory / name).read_text().splitlines()) > cap]
|
|
self.assertEqual([], oversized, "split a module rather than raising its cap: " +
|
|
", ".join(oversized))
|
|
|
|
def test_runtime_code_uses_focused_egress_modules(self) -> None:
|
|
"""addon_core is compatibility-only, never an internal dependency."""
|
|
violations: list[str] = []
|
|
package = ROOT / "bot_bottle"
|
|
facade = package / "gateway" / "egress" / "addon_core.py"
|
|
package_init = package / "gateway" / "egress" / "__init__.py"
|
|
for path in package.rglob("*.py"):
|
|
if path in (facade, package_init):
|
|
continue
|
|
text = path.read_text()
|
|
if "gateway.egress.addon_core import" in text or \
|
|
".addon_core import" in text:
|
|
violations.append(str(path.relative_to(ROOT)))
|
|
self.assertEqual([], violations)
|
|
|
|
def test_backend_contract_does_not_absorb_preparation_logic(self) -> None:
|
|
# base.py holds the backend *contract*; implementation lives elsewhere.
|
|
# The cap is a tripwire against absorbing preparation/impl logic, not a
|
|
# ban on new contract surface — bumped 580->600 for the PRD 0081
|
|
# gateway-attach contract (delegator + 3 abstract primitives; the flow
|
|
# itself lives in backend/gateway_attach.py, not here).
|
|
caps = {
|
|
ROOT / "bot_bottle" / "backend" / "base.py": 600,
|
|
ROOT / "bot_bottle" / "backend" / "preparation.py": 160,
|
|
}
|
|
oversized = [
|
|
f"{path.relative_to(ROOT)} "
|
|
f"({len(path.read_text().splitlines())}>{cap})"
|
|
for path, cap in caps.items()
|
|
if len(path.read_text().splitlines()) > cap
|
|
]
|
|
self.assertEqual([], oversized)
|