Files
2026-07-13 13:12:33 +08:00

121 lines
4.4 KiB
Python

from __future__ import annotations
import re
from pathlib import Path
import yaml
_ROOT = Path(__file__).parent.parent
def _load_compose() -> dict:
return yaml.safe_load((_ROOT / "compose.yaml").read_text(encoding="utf-8"))
def test_compose_no_version_field() -> None:
data = _load_compose()
assert "version" not in data, (
"compose.yaml must not have a top-level 'version:' field (use Compose v2 syntax)"
)
def test_compose_gateway_port_is_loopback() -> None:
data = _load_compose()
ports = data["services"]["gateway"]["ports"]
assert any(
str(p) == "127.0.0.1:18791:18791" for p in ports
), f"Expected '127.0.0.1:18791:18791' in ports, got: {ports}"
def test_compose_gateway_healthcheck_exists() -> None:
data = _load_compose()
hc = data["services"]["gateway"].get("healthcheck")
assert hc is not None, "services.gateway.healthcheck must be defined"
def test_compose_gateway_environment_has_openrouter_key() -> None:
data = _load_compose()
env = data["services"]["gateway"].get("environment", {})
# environment can be a dict or a list of "KEY=VAL" strings
if isinstance(env, dict):
assert "OPENROUTER_API_KEY" in env, (
f"OPENROUTER_API_KEY missing from environment dict: {env}"
)
else:
keys = [item.split("=")[0] for item in env]
assert "OPENROUTER_API_KEY" in keys, (
f"OPENROUTER_API_KEY missing from environment list: {env}"
)
def _load_dockerfile() -> str:
return (_ROOT / "Dockerfile").read_text(encoding="utf-8")
def test_dockerfile_gateway_port_matches_compose() -> None:
"""Dockerfile's container gateway port must match compose's 18791.
Drift here — e.g. the Dockerfile keeping EXPOSE 18790 while compose
publishes 18791 — makes the documented `docker compose` path
unreachable.
"""
dockerfile = _load_dockerfile()
compose = _load_compose()
ports = [str(p) for p in compose["services"]["gateway"]["ports"]]
assert any("18791:18791" in p for p in ports), (
f"compose.yaml must publish 18791:18791, got: {ports}"
)
assert "18790" not in dockerfile, (
"Dockerfile still references the stale 18790 gateway port; "
"it must use 18791 to match compose.yaml"
)
assert "OPENSQUILLA_GATEWAY_PORT=18791" in dockerfile
assert "EXPOSE 18791" in dockerfile
assert "http://127.0.0.1:18791/healthz" in dockerfile
def test_compose_persists_state_via_named_volume() -> None:
"""Gateway config and state must persist via a Docker named volume
mounted at the image's OPENSQUILLA_STATE_DIR.
The container runs as a non-root user, so a host bind mount to
/root/.opensquilla never receives anything the gateway writes — config
and state would silently vanish on every container recreate.
"""
compose = _load_compose()
dockerfile = _load_dockerfile()
match = re.search(r"OPENSQUILLA_STATE_DIR=(\S+)", dockerfile)
assert match, "Dockerfile must pin OPENSQUILLA_STATE_DIR for a stable volume target"
state_dir = match.group(1)
assert state_dir.startswith("/"), (
f"OPENSQUILLA_STATE_DIR must be an absolute path, got: {state_dir!r}"
)
# This is a static shape test: it pins the Dockerfile/compose text, not a
# built image. The mkdir/chown assertions are coupled to the current
# command spelling — update them alongside any Dockerfile rewording.
# The image pre-creates the state root owned by the non-root user so a
# freshly initialized named volume inherits writable ownership.
assert f"mkdir -p {state_dir}" in dockerfile, (
f"Dockerfile must create the state root {state_dir}"
)
assert re.search(
rf"chown\b[^\n]*opensquilla:opensquilla[^\n]*{re.escape(state_dir)}", dockerfile
), f"Dockerfile must chown {state_dir} to the non-root opensquilla user"
gateway_volumes = [str(v) for v in compose["services"]["gateway"]["volumes"]]
assert f"opensquilla-state:{state_dir}" in gateway_volumes, (
f"gateway must mount the 'opensquilla-state' named volume at {state_dir}, "
f"got: {gateway_volumes}"
)
assert not any("/root/" in v for v in gateway_volumes), (
"compose must not mount into /root — the container runs as a non-root user"
)
assert "opensquilla-state" in compose.get("volumes", {}), (
"compose must declare the top-level 'opensquilla-state' named volume"
)