44e2b5a897
test / integration-docker (pull_request) Successful in 13s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / unit (pull_request) Successful in 42s
lint / lint (push) Failing after 54s
test / integration-firecracker (pull_request) Successful in 3m17s
test / coverage (pull_request) Successful in 17s
test / publish-infra (pull_request) Has been skipped
Give each service its own store package + manager, and cut the supervise module
along the control/data-plane boundary so nothing in the shared layer reaches up
into the orchestrator.
Stores, by owner:
- bot_bottle/store/ keeps only the shared base (DbStore, migrations) and the
concrete stores that aren't service-owned (audit_store, config_store).
- bot_bottle/orchestrator/store/ now houses the orchestrator-owned stores —
queue_store (supervise queue), secret_store, config_store — plus a new
orchestrator store_manager that migrates them (composing audit/config
downward from the base). The old shared store_manager is gone.
Supervise plane, by tier:
- bot_bottle/supervisor/ (NEUTRAL, importable by every tier including the
gateway): types.py (the Proposal/Response/AuditEntry wire types + the tool/
status/poll constants + the shared daemon constants moved out of
supervise.py) and plan.py (SupervisePlan, a pure DTO).
- bot_bottle/orchestrator/supervisor/ (orchestrator-only): queue.py (the
queue/audit I/O wrappers + render_diff + sha256_hex) and supervise.py (the
Supervise lifecycle that stages the DB via the store manager). Its __init__
re-exports the neutral vocabulary so orchestrator-side callers import from
one place.
The gateway now imports only bot_bottle.supervisor.types (never
bot_bottle.supervise), so the data plane holds no code dependency on the
orchestrator — it reaches the queue over the control-plane RPC. This removes
the circular import that moving queue_store under orchestrator introduced
(supervise -> orchestrator -> service -> supervise).
supervise_types.py -> supervisor/types.py; supervise.py deleted (split). Full
unit suite green (2251).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
"""Integration: PRD 0024 chunk 1 — the gateway image builds
|
|
and the daemon binaries are present + executable inside it.
|
|
|
|
This test does NOT exercise the daemons running against real
|
|
config (routes.yaml, etc) — that lands in chunk 2 when the
|
|
renderer wires the bundle into compose. What we verify here is
|
|
the chunk-1 contract:
|
|
|
|
- Dockerfile.gateway builds (multi-stage works, base layers
|
|
pull, COPYs resolve).
|
|
- gitleaks, mitmdump are at the documented paths and answer
|
|
`--version`.
|
|
- The Python init at /app/gateway_init.py runs and prints the
|
|
expected "no daemons selected" line when the supervisor is
|
|
pointed at an empty daemon set.
|
|
|
|
Skips cleanly when docker is unavailable, or under act_runner
|
|
where the host bind-mount topology breaks multi-stage builds
|
|
that pull large bases.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import unittest
|
|
|
|
from tests._docker import skip_unless_docker
|
|
|
|
|
|
_IMAGE = "bot-bottle-gateway-test:chunk1"
|
|
_DOCKERFILE = "Dockerfile.gateway"
|
|
|
|
|
|
@skip_unless_docker()
|
|
@unittest.skipIf(
|
|
os.environ.get("GITEA_ACTIONS") == "true",
|
|
"skipped under act_runner: multi-stage build pulls a 200+MB "
|
|
"mitmproxy base + two upstream gateway images; runner storage "
|
|
"+ time budget make this an interactive-only test",
|
|
)
|
|
class TestGatewayImage(unittest.TestCase):
|
|
"""Builds the image once for the class, then runs a few
|
|
`docker run` probes against it."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
proc = subprocess.run(
|
|
["docker", "build", "-t", _IMAGE,
|
|
"-f", _DOCKERFILE, "."],
|
|
cwd=repo_root,
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
)
|
|
if proc.returncode != 0:
|
|
raise unittest.SkipTest(
|
|
f"docker build failed; skipping image probes.\n"
|
|
f"{proc.stdout.decode('utf-8', errors='replace')[-2000:]}"
|
|
)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls) -> None:
|
|
subprocess.run(
|
|
["docker", "image", "rm", "-f", _IMAGE],
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
)
|
|
|
|
def _run_in_image(self, *cmd: str, timeout: float = 30.0) -> tuple[int, str]:
|
|
proc = subprocess.run(
|
|
["docker", "run", "--rm", "--entrypoint", cmd[0], _IMAGE,
|
|
*cmd[1:]],
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
timeout=timeout,
|
|
)
|
|
return proc.returncode, proc.stdout.decode("utf-8", errors="replace")
|
|
|
|
def test_gitleaks_binary_present_and_versioned(self):
|
|
rc, out = self._run_in_image("/usr/bin/gitleaks", "version")
|
|
self.assertEqual(0, rc, msg=out)
|
|
# gitleaks prints a bare version string like "v8.x.y".
|
|
self.assertRegex(out, r"v?\d+\.\d+")
|
|
|
|
def test_mitmdump_binary_present_and_versioned(self):
|
|
rc, out = self._run_in_image("mitmdump", "--version")
|
|
self.assertEqual(0, rc, msg=out)
|
|
self.assertIn("Mitmproxy", out)
|
|
|
|
def test_python_imports_supervise_module(self):
|
|
# The supervise daemon is installed as part of the bot_bottle
|
|
# package (5ad3449), not as flat sibling modules under /app.
|
|
# Probe that the package imports resolve inside the image.
|
|
rc, out = self._run_in_image(
|
|
"python3", "-c",
|
|
"from bot_bottle.supervisor import types; from bot_bottle.gateway import supervise_server; print('ok')",
|
|
)
|
|
self.assertEqual(0, rc, msg=out)
|
|
self.assertIn("ok", out)
|
|
|
|
def test_init_supervisor_runs_with_no_daemons(self):
|
|
# `nothing` matches no canonical daemon → supervisor exits 0
|
|
# immediately with the documented message. Confirms the
|
|
# ENTRYPOINT wiring works.
|
|
proc = subprocess.run(
|
|
["docker", "run", "--rm",
|
|
"-e", "BOT_BOTTLE_GATEWAY_DAEMONS=nothing",
|
|
_IMAGE],
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
timeout=10.0,
|
|
)
|
|
out = proc.stdout.decode("utf-8", errors="replace")
|
|
self.assertEqual(0, proc.returncode, msg=out)
|
|
self.assertIn("no daemons selected", out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|