From 71ac555f2522c7898e058ee67ccb99a208b9b06c Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 2 Jun 2026 15:50:43 +0000 Subject: [PATCH 1/4] =?UTF-8?q?docs(prd):=20add=20PRD=200044=20=E2=80=94?= =?UTF-8?q?=20print=20parity=20across=20backends?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prds/0044-print-parity-across-backends.md | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 docs/prds/0044-print-parity-across-backends.md diff --git a/docs/prds/0044-print-parity-across-backends.md b/docs/prds/0044-print-parity-across-backends.md new file mode 100644 index 0000000..20fa1f3 --- /dev/null +++ b/docs/prds/0044-print-parity-across-backends.md @@ -0,0 +1,119 @@ +# PRD 0044: Print Parity Across Backends + +- **Status:** Draft +- **Author:** didericis-claude +- **Created:** 2026-06-02 +- **Issue:** #96 + +## Summary + +Hoist `git_gate_plan`, `egress_plan`, `agent_provision`, and `supervise_plan` +from the concrete `BottlePlan` subclasses up to `BottlePlan`, and implement +`print` concretely there. This eliminates the two per-backend output divergences +and ensures any future backend gets correct preflight rendering for free. + +## Problem + +`BottlePlan.print` is `@abstractmethod`, so each backend provides its own +implementation. The two current implementations have drifted: + +| Field | Docker | smolmachines | +|---|---|---| +| git gate lines | `upstream_host:upstream_port` from resolved `git_gate_plan.upstreams` | `Name → Upstream` from manifest `bottle.git` | +| egress lines | `host [auth:scheme]` | `host` only (auth dropped) | + +The smolmachines docstring says "same shape as the Docker backend's so operators +see one format across backends" — that intent is real but nothing enforces it. + +The env_names divergence previously noted in this issue was resolved by PRD 0038 +(smolmachines env contract): `resolved.forwarded` is now merged into +`agent_provision.guest_env` at prepare time on both backends, so displayed env +names are equivalent. + +## Goals / Success Criteria + +- `BottlePlan` carries `git_gate_plan`, `egress_plan`, `agent_provision`, and + `supervise_plan` as concrete fields; subclasses no longer declare them + independently. +- `BottlePlan.print` is a concrete method; subclasses have no `print` + implementation of their own. +- Both backends render git gate lines as `name → upstream_host:upstream_port` + (using `git_gate_plan.upstreams`), not the manifest-level URL. +- Both backends render egress lines as `host [auth:scheme]` (dropping the + annotation only when `auth_scheme` is empty). +- Unit tests assert the unified output for both backends from a single shared + test helper. + +## Non-goals + +- No changes to the Docker or smolmachines launch, prepare, or cleanup paths. +- No changes to how env values are resolved or injected (that is PRD 0038). +- No changes to the manifest schema or `GitEntry`. +- No new CLI flags or dashboard changes. + +## Scope + +In scope: + +- `bot_bottle/backend/__init__.py` — add `git_gate_plan`, `egress_plan`, + `agent_provision`, and `supervise_plan` fields to `BottlePlan`; replace + `@abstractmethod print` with a concrete implementation. +- `bot_bottle/backend/docker/bottle_plan.py` — remove the four hoisted fields + and the `print` method. +- `bot_bottle/backend/smolmachines/bottle_plan.py` — remove the four hoisted + fields and the `print` method. +- `tests/unit/` — add or update tests asserting unified preflight output; a + shared helper can build a minimal plan fixture for each backend and assert + the same lines appear. + +Out of scope: + +- Changes to `bot_bottle/backend/print_util.py` beyond what the new `print` + implementation requires. +- Changes to `BottleCleanupPlan.print` or any other print method. +- Integration tests that launch a real bottle. + +## Design + +Move the four fields that both concrete subclasses already declare — +`git_gate_plan: GitGatePlan`, `egress_plan: EgressPlan`, +`agent_provision: AgentProvisionPlan`, `supervise_plan: SupervisePlan | None` +— up to `BottlePlan`. Both backends' `prepare` paths already produce these with +the same types, so no prepare-time changes are needed. + +Replace the `@abstractmethod` `print` with a concrete implementation on +`BottlePlan` that: + +1. Builds `env_names` from `bottle.env.keys() | agent_provision.guest_env.keys()` + filtered through `agent_provision.hidden_env_names`. +2. Builds git gate lines from `git_gate_plan.upstreams` as + `f"{u.name} → {u.upstream_host}:{u.upstream_port}"`. +3. Builds egress lines from `egress_plan.routes` as + `f"{r.host} [auth:{r.auth_scheme}]"` when `r.auth_scheme` is non-empty, + else `r.host`. +4. Renders the standard two-column preflight block (leading blank line, agent, + provider, env, skills, bottle, git identity, git gate, egress, trailing blank + line). + +Docker's `forwarded_env` keys are already merged into `agent_provision.guest_env` +via the `agent_provision_plan` builder, so no special handling is needed for +env_names. + +## Testing Strategy + +- Add a shared fixture builder (e.g. `make_plan(backend)`) in a new or existing + unit test module that constructs a minimal `DockerBottlePlan` and + `SmolmachinesBottlePlan` from the same spec and plan fields. +- Assert that `plan.print(remote_control=False)` produces identical git gate and + egress lines for both backends given the same `git_gate_plan` and + `egress_plan`. +- Test the `auth_scheme` annotation: present when non-empty, absent otherwise. +- Test git gate rendering: `name → host:port` format. + +Run: + +- `python3 -m unittest discover -s tests/unit` + +## Open Questions + +None. -- 2.52.0 From 53219a55e16923c7d00951c09ab1a77de5a2a761 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 2 Jun 2026 16:08:20 +0000 Subject: [PATCH 2/4] refactor: hoist plan fields and print to BottlePlan base class (PRD 0044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move git_gate_plan, egress_plan, supervise_plan, and agent_provision from DockerBottlePlan and SmolmachinesBottlePlan into BottlePlan. Replace the abstract print method with a single concrete implementation that renders git gate entries as "name → upstream_host:upstream_port" and egress routes with conditional "[auth:scheme]" annotations. --- bot_bottle/backend/__init__.py | 55 +++++++++++++- bot_bottle/backend/docker/bottle_plan.py | 73 ++----------------- .../backend/smolmachines/bottle_plan.py | 56 +------------- 3 files changed, 59 insertions(+), 125 deletions(-) diff --git a/bot_bottle/backend/__init__.py b/bot_bottle/backend/__init__.py index c55faac..6fc9aa9 100644 --- a/bot_bottle/backend/__init__.py +++ b/bot_bottle/backend/__init__.py @@ -32,15 +32,21 @@ manifest does not carry a backend field; the host picks. from __future__ import annotations import os +import sys from abc import ABC, abstractmethod from contextlib import AbstractContextManager from dataclasses import dataclass from pathlib import Path from typing import Any, Generic, Sequence, TypeVar -from ..log import die +from ..agent_provider import AgentProvisionPlan +from ..egress import EgressPlan +from ..git_gate import GitGatePlan +from ..log import die, info from ..manifest import GitEntry, Manifest +from ..supervise import SupervisePlan from ..util import expand_tilde +from .print_util import print_multi, visible_agent_env_names from .util import host_skill_dir @@ -65,15 +71,56 @@ class BottleSpec: @dataclass(frozen=True) class BottlePlan(ABC): """Base output of a backend's prepare step. Concrete subclasses - (e.g. DockerBottlePlan) add backend-specific resolved fields and - implement `print`.""" + (e.g. DockerBottlePlan) add backend-specific resolved fields.""" spec: BottleSpec stage_dir: Path + git_gate_plan: GitGatePlan + egress_plan: EgressPlan + supervise_plan: SupervisePlan | None + agent_provision: AgentProvisionPlan - @abstractmethod def print(self, *, remote_control: bool) -> None: """Render the y/N preflight summary to stderr.""" + del remote_control + spec = self.spec + manifest = spec.manifest + agent = manifest.agents[spec.agent_name] + bottle = manifest.bottle_for(spec.agent_name) + + env_names = visible_agent_env_names( + sorted( + set(bottle.env.keys()) + | set(self.agent_provision.guest_env.keys()) + ), + hidden_env_names=self.agent_provision.hidden_env_names, + ) + + print(file=sys.stderr) + info(f"agent : {spec.agent_name}") + info(f"provider : {self.agent_provision.template}") + print_multi("env ", env_names) + print_multi("skills ", list(agent.skills)) + info(f"bottle : {agent.bottle}") + + identity = manifest.git_identity_summary(spec.agent_name) + if identity: + info(f" git identity : {identity}") + + git_lines = [ + f"{u.name} → {u.upstream_host}:{u.upstream_port}" + for u in self.git_gate_plan.upstreams + ] + if git_lines: + print_multi(" git gate ", git_lines) + + if self.egress_plan.routes: + egress_lines = [] + for r in self.egress_plan.routes: + auth = f" [auth:{r.auth_scheme}]" if r.auth_scheme else "" + egress_lines.append(f"{r.host}{auth}") + print_multi(" egress ", egress_lines) + print(file=sys.stderr) @dataclass(frozen=True) diff --git a/bot_bottle/backend/docker/bottle_plan.py b/bot_bottle/backend/docker/bottle_plan.py index e6240f1..6fd4a56 100644 --- a/bot_bottle/backend/docker/bottle_plan.py +++ b/bot_bottle/backend/docker/bottle_plan.py @@ -2,30 +2,25 @@ Carries the Docker-specific resolved fields produced by DockerBottleBackend.prepare. The launch step consumes it without -further resolution; show_plan-style rendering is the `print` method. +further resolution; preflight rendering is inherited from BottlePlan. """ from __future__ import annotations -import sys from dataclasses import dataclass, field from pathlib import Path -from ...agent_provider import AgentProvisionPlan, PromptMode -from ...egress import EgressPlan -from ...git_gate import GitGatePlan -from ...log import info +from ...agent_provider import PromptMode from ...pipelock import PipelockProxyPlan -from ...supervise import SupervisePlan from .. import BottlePlan -from ..print_util import print_multi, visible_agent_env_names @dataclass(frozen=True) class DockerBottlePlan(BottlePlan): """Docker-specific resolved fields produced by - DockerBottleBackend.prepare. Inherits `spec` and `stage_dir` from - BottlePlan.""" + DockerBottleBackend.prepare. Inherits `spec`, `stage_dir`, + `git_gate_plan`, `egress_plan`, `supervise_plan`, and + `agent_provision` from BottlePlan.""" slug: str container_name: str @@ -46,13 +41,7 @@ class DockerBottlePlan(BottlePlan): forwarded_env: dict[str, str] = field(repr=False) prompt_file: Path proxy_plan: PipelockProxyPlan - git_gate_plan: GitGatePlan - egress_plan: EgressPlan - # None when bottle.supervise is False. PRD 0013 supervise sidecar - # is opt-in via the manifest's bottle.supervise field. - supervise_plan: SupervisePlan | None use_runsc: bool - agent_provision: AgentProvisionPlan @property def agent_command(self) -> str: @@ -65,55 +54,3 @@ class DockerBottlePlan(BottlePlan): @property def agent_provider_template(self) -> str: return self.agent_provision.template - - def print(self, *, remote_control: bool) -> None: - """Render the y/N preflight summary to stderr — compact form - intended to fit on screen without scrolling. The full - structured shape (image, container, runtime, etc.) lives on - this dataclass for tooling that wants to introspect it.""" - del remote_control # not surfaced in the compact summary - spec = self.spec - manifest = spec.manifest - agent = manifest.agents[spec.agent_name] - bottle = manifest.bottle_for(spec.agent_name) - # The agent sees the union of literal env names (rendered into - # --env-file) and forwarded env names (`-e NAME` with the - # value arriving via subprocess env). The forwarded set holds - # the OAuth token (CLAUDE_CODE_OAUTH_TOKEN) and any host-env - # interpolations from the manifest; egress holds - # upstream tokens in its own environ, so no token forwarding - # from the agent to the proxy is needed. - env_names = visible_agent_env_names( - sorted( - set(bottle.env.keys()) - | set(self.forwarded_env.keys()) - | set(self.agent_provision.guest_env.keys()) - ), - hidden_env_names=self.agent_provision.hidden_env_names, - ) - - print(file=sys.stderr) - info(f"agent : {spec.agent_name}") - info(f"provider : {self.agent_provider_template}") - print_multi("env ", env_names) - print_multi("skills ", list(agent.skills)) - info(f"bottle : {agent.bottle}") - - identity = manifest.git_identity_summary(spec.agent_name) - if identity: - info(f" git identity : {identity}") - - git_lines = [ - f"{u.upstream_host}:{u.upstream_port}" - for u in self.git_gate_plan.upstreams - ] - if git_lines: - print_multi(" git gate ", git_lines) - - if self.egress_plan.routes: - egress_lines = [] - for r in self.egress_plan.routes: - auth = f" [auth:{r.auth_scheme}]" if r.auth_scheme else "" - egress_lines.append(f"{r.host}{auth}") - print_multi(" egress ", egress_lines) - print(file=sys.stderr) diff --git a/bot_bottle/backend/smolmachines/bottle_plan.py b/bot_bottle/backend/smolmachines/bottle_plan.py index 84ef5d4..e3d3cb2 100644 --- a/bot_bottle/backend/smolmachines/bottle_plan.py +++ b/bot_bottle/backend/smolmachines/bottle_plan.py @@ -8,25 +8,20 @@ in chunk 4.""" from __future__ import annotations -import sys from dataclasses import dataclass from pathlib import Path -from ...agent_provider import AgentProvisionPlan, PromptMode -from ...egress import EgressPlan -from ...git_gate import GitGatePlan -from ...log import info +from ...agent_provider import PromptMode from ...pipelock import PipelockProxyPlan -from ...supervise import SupervisePlan from .. import BottlePlan -from ..print_util import print_multi, visible_agent_env_names @dataclass(frozen=True) class SmolmachinesBottlePlan(BottlePlan): """Resolved fields the launch step needs to bring up the bottle. - Inherits `spec` and `stage_dir` from BottlePlan.""" + Inherits `spec`, `stage_dir`, `git_gate_plan`, `egress_plan`, + `supervise_plan`, and `agent_provision` from BottlePlan.""" slug: str # Per-bottle docker subnet for the sidecar bundle container. @@ -77,12 +72,6 @@ class SmolmachinesBottlePlan(BottlePlan): # per-bottle bridge with a pinned IP. The unused fields stay # at their dataclass defaults. proxy_plan: PipelockProxyPlan - git_gate_plan: GitGatePlan - egress_plan: EgressPlan - # None when bottle.supervise is False, matching the docker - # backend's convention. - supervise_plan: SupervisePlan | None - agent_provision: AgentProvisionPlan # Agent-side endpoints. On Docker Desktop the docker bridge # IPs aren't reachable from the smolvm guest (TSI uses macOS # networking; docker container IPs live in the daemon's VM), @@ -110,42 +99,3 @@ class SmolmachinesBottlePlan(BottlePlan): @property def agent_dockerfile_path(self) -> str: return self.agent_provision.dockerfile - - def print(self, *, remote_control: bool) -> None: - """Compact y/N preflight. Same shape as the Docker - backend's so operators see one format across backends.""" - del remote_control # not surfaced in the compact summary - spec = self.spec - manifest = spec.manifest - agent = manifest.agents[spec.agent_name] - bottle = manifest.bottle_for(spec.agent_name) - - env_names = visible_agent_env_names( - sorted( - set(bottle.env.keys()) - | set(self.agent_provision.guest_env.keys()) - ), - hidden_env_names=self.agent_provision.hidden_env_names, - ) - upstreams = [ - f"{g.Name} → {g.Upstream}" for g in bottle.git - ] - # Use the resolved egress_plan (lowercase `host` on the - # plan-level EgressRoute) rather than `bottle.egress.routes`, - # which is the manifest's capitalized-attr form. - routes = [r.host for r in self.egress_plan.routes] - - print(file=sys.stderr) - info(f"agent : {spec.agent_name}") - info(f"provider : {self.agent_provider_template}") - print_multi("env ", env_names) - print_multi("skills ", list(agent.skills)) - info(f"bottle : {agent.bottle}") - identity = manifest.git_identity_summary(spec.agent_name) - if identity: - info(f" git identity : {identity}") - if upstreams: - print_multi(" git gate ", upstreams) - if routes: - print_multi(" egress ", routes) - print(file=sys.stderr) -- 2.52.0 From a0762ac2d33e227e3c2015a551cc0996375b532b Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 2 Jun 2026 16:08:25 +0000 Subject: [PATCH 3/4] test: add cross-backend print parity tests (PRD 0044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shared fixtures build DockerBottlePlan and SmolmachinesBottlePlan from identical git_gate_plan and egress_plan inputs and assert that both backends render the same git gate lines (name → host:port) and egress lines (host [auth:scheme] when authenticated, host alone otherwise). --- tests/unit/test_plan_print_parity.py | 246 +++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 tests/unit/test_plan_print_parity.py diff --git a/tests/unit/test_plan_print_parity.py b/tests/unit/test_plan_print_parity.py new file mode 100644 index 0000000..2e76361 --- /dev/null +++ b/tests/unit/test_plan_print_parity.py @@ -0,0 +1,246 @@ +"""Unit: BottlePlan.print parity across Docker and smolmachines (PRD 0044). + +Both backends inherit a single concrete print() from BottlePlan. These +tests verify that identical git_gate_plan and egress_plan inputs produce +identical preflight output regardless of backend-specific fields. +""" + +from __future__ import annotations + +import io +import sys +import tempfile +import unittest +from pathlib import Path + +from bot_bottle.agent_provider import AgentProvisionPlan +from bot_bottle.backend import BottleSpec +from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan +from bot_bottle.backend.smolmachines.bottle_plan import SmolmachinesBottlePlan +from bot_bottle.egress import EgressPlan, EgressRoute +from bot_bottle.git_gate import GitGatePlan, GitGateUpstream +from bot_bottle.manifest import Manifest +from bot_bottle.pipelock import PipelockProxyPlan + + +def _manifest() -> Manifest: + return Manifest.from_json_obj({ + "bottles": {"dev": {}}, + "agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}}, + }) + + +def _spec(manifest: Manifest, tmp: str) -> BottleSpec: + return BottleSpec( + manifest=manifest, + agent_name="demo", + copy_cwd=False, + user_cwd=tmp, + identity="test-00001", + ) + + +def _git_gate_plan(tmp: str) -> GitGatePlan: + stage = Path(tmp) + return GitGatePlan( + slug="test-00001", + entrypoint_script=stage / "entrypoint.sh", + hook_script=stage / "hook.sh", + access_hook_script=stage / "access-hook.sh", + upstreams=( + GitGateUpstream( + name="myrepo", + upstream_url="ssh://git@gitea.example.com:30009/org/myrepo.git", + upstream_host="gitea.example.com", + upstream_port="30009", + identity_file="/dev/null", + known_host_key="ssh-ed25519 AAAA...", + extra_hosts={}, + ), + ), + ) + + +def _egress_plan(tmp: str) -> EgressPlan: + return EgressPlan( + slug="test-00001", + routes_path=Path(tmp) / "egress.yaml", + routes=( + EgressRoute( + host="api.example.com", + path_allowlist=("/v1/",), + auth_scheme="bearer", + token_env="EGRESS_TOKEN_0", + token_ref="TOKEN", + ), + EgressRoute( + host="static.example.com", + path_allowlist=("/",), + ), + ), + token_env_map={"EGRESS_TOKEN_0": "TOKEN"}, + ) + + +def _agent_provision() -> AgentProvisionPlan: + return AgentProvisionPlan( + template="claude", + command="claude", + prompt_mode="append_file", + image="", + dockerfile="", + guest_env={"HTTPS_PROXY": "http://127.0.0.1:9999"}, + ) + + +def _proxy_plan(tmp: str) -> PipelockProxyPlan: + return PipelockProxyPlan( + yaml_path=Path(tmp) / "pipelock.yaml", + slug="test-00001", + ) + + +def _docker_plan(spec: BottleSpec, tmp: str) -> DockerBottlePlan: + stage = Path(tmp) + return DockerBottlePlan( + spec=spec, + stage_dir=stage, + git_gate_plan=_git_gate_plan(tmp), + egress_plan=_egress_plan(tmp), + supervise_plan=None, + agent_provision=_agent_provision(), + slug="test-00001", + container_name="bot-bottle-test-00001", + container_name_pinned=False, + image="bot-bottle-claude:latest", + derived_image="", + runtime_image="bot-bottle-claude:latest", + dockerfile_path="", + env_file=stage / "env", + forwarded_env={}, + prompt_file=stage / "prompt.txt", + proxy_plan=_proxy_plan(tmp), + use_runsc=False, + ) + + +def _smolmachines_plan(spec: BottleSpec, tmp: str) -> SmolmachinesBottlePlan: + stage = Path(tmp) + return SmolmachinesBottlePlan( + spec=spec, + stage_dir=stage, + git_gate_plan=_git_gate_plan(tmp), + egress_plan=_egress_plan(tmp), + supervise_plan=None, + agent_provision=_agent_provision(), + slug="test-00001", + bundle_subnet="10.99.0.0/24", + bundle_gateway="10.99.0.1", + bundle_ip="10.99.0.2", + machine_name="bot-bottle-test-00001", + agent_image_ref="bot-bottle-claude:latest", + guest_env={"HTTPS_PROXY": "http://127.0.0.1:9999"}, + prompt_file=stage / "prompt.txt", + proxy_plan=_proxy_plan(tmp), + ) + + +def _capture_print(plan: DockerBottlePlan | SmolmachinesBottlePlan) -> list[str]: + buf = io.StringIO() + orig = sys.stderr + sys.stderr = buf + try: + plan.print(remote_control=False) + finally: + sys.stderr = orig + return buf.getvalue().splitlines() + + +class TestGitGatePrintParity(unittest.TestCase): + """Both backends render git gate entries as 'name → host:port'.""" + + def setUp(self) -> None: + self._tmp = tempfile.mkdtemp(prefix="plan-print-parity-") + manifest = _manifest() + spec = _spec(manifest, self._tmp) + self._docker_lines = _capture_print(_docker_plan(spec, self._tmp)) + self._smol_lines = _capture_print(_smolmachines_plan(spec, self._tmp)) + + def _git_gate_lines(self, lines: list[str]) -> list[str]: + return [ln for ln in lines if "git gate" in ln] + + def test_docker_renders_name_arrow_host_port(self) -> None: + git_lines = self._git_gate_lines(self._docker_lines) + self.assertEqual(1, len(git_lines)) + self.assertIn("myrepo → gitea.example.com:30009", git_lines[0]) + + def test_smolmachines_renders_name_arrow_host_port(self) -> None: + git_lines = self._git_gate_lines(self._smol_lines) + self.assertEqual(1, len(git_lines)) + self.assertIn("myrepo → gitea.example.com:30009", git_lines[0]) + + def test_git_gate_lines_match_across_backends(self) -> None: + self.assertEqual( + self._git_gate_lines(self._docker_lines), + self._git_gate_lines(self._smol_lines), + ) + + +class TestEgressPrintParity(unittest.TestCase): + """Both backends render egress with auth annotation where present.""" + + def setUp(self) -> None: + self._tmp = tempfile.mkdtemp(prefix="plan-print-parity-") + manifest = _manifest() + spec = _spec(manifest, self._tmp) + self._docker_lines = _capture_print(_docker_plan(spec, self._tmp)) + self._smol_lines = _capture_print(_smolmachines_plan(spec, self._tmp)) + + def _egress_section(self, lines: list[str]) -> list[str]: + """Return lines from the egress label through the last route entry. + + print_multi renders the first route on the label line and + aligns additional routes as indented continuation lines + (no repeated label). Collect the label line plus every + non-blank, non-labelled line that follows before the next + top-level section begins.""" + result: list[str] = [] + collecting = False + indent_prefix = None + for ln in lines: + stripped = ln.lstrip() + if "egress" in stripped and ":" in stripped: + collecting = True + # Determine the continuation indent from this line's prefix. + idx = ln.index("egress") + indent_prefix = ln[:idx] + result.append(ln) + elif collecting: + if ln.startswith(indent_prefix) and "egress" not in ln and ":" not in ln.lstrip()[:20]: + result.append(ln) + else: + break + return result + + def test_docker_includes_auth_annotation(self) -> None: + combined = "\n".join(self._egress_section(self._docker_lines)) + self.assertIn("api.example.com [auth:bearer]", combined) + + def test_smolmachines_includes_auth_annotation(self) -> None: + combined = "\n".join(self._egress_section(self._smol_lines)) + self.assertIn("api.example.com [auth:bearer]", combined) + + def test_unauthenticated_route_has_no_annotation(self) -> None: + full = "\n".join(self._docker_lines) + self.assertIn("static.example.com", full) + self.assertNotIn("static.example.com [auth:", full) + + def test_egress_lines_match_across_backends(self) -> None: + self.assertEqual( + self._egress_section(self._docker_lines), + self._egress_section(self._smol_lines), + ) + + +if __name__ == "__main__": + unittest.main() -- 2.52.0 From fcd1b34e49a7746806f55bdbd1cfae08b2d85274 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 2 Jun 2026 16:08:34 +0000 Subject: [PATCH 4/4] docs: mark PRD 0044 Active --- docs/prds/0044-print-parity-across-backends.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/prds/0044-print-parity-across-backends.md b/docs/prds/0044-print-parity-across-backends.md index 20fa1f3..35c75c6 100644 --- a/docs/prds/0044-print-parity-across-backends.md +++ b/docs/prds/0044-print-parity-across-backends.md @@ -1,6 +1,6 @@ # PRD 0044: Print Parity Across Backends -- **Status:** Draft +- **Status:** Active - **Author:** didericis-claude - **Created:** 2026-06-02 - **Issue:** #96 -- 2.52.0