"""TC-12: compose invariant — orchestrator-watchdog is a separate service. It declares its own build (watchdog/Dockerfile), restart policy, mem_limit, and mounts docker.sock read-only (:ro). Parses the real docker-compose.yml. """ import pathlib import yaml REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] def _compose(): with open(REPO_ROOT / "docker-compose.yml") as f: return yaml.safe_load(f) def test_watchdog_service_declared(): svc = _compose()["services"] assert "orchestrator-watchdog" in svc def test_watchdog_builds_from_watchdog_dockerfile(): wd = _compose()["services"]["orchestrator-watchdog"] build = wd["build"] assert isinstance(build, dict) assert build["dockerfile"] == "watchdog/Dockerfile" assert build["context"] == "." def test_watchdog_has_restart_and_mem_limit(): wd = _compose()["services"]["orchestrator-watchdog"] assert wd["restart"] == "unless-stopped" assert wd["mem_limit"] == "128m" # thin stack, not Grafana/Prometheus def test_docker_sock_mounted_read_only(): wd = _compose()["services"]["orchestrator-watchdog"] sock = [v for v in wd["volumes"] if "docker.sock" in v] assert sock, "docker.sock must be mounted" assert all(v.endswith(":ro") for v in sock), "docker.sock must be :ro" def test_host_paths_mounted_read_only(): wd = _compose()["services"]["orchestrator-watchdog"] # Every bind mount the watchdog uses is read-only (it only reads). for v in wd["volumes"]: assert v.endswith(":ro"), f"watchdog mount must be :ro: {v}" def test_env_file_is_optional(): # A missing .env.watchdog must not break `docker compose up` (self-hosting). wd = _compose()["services"]["orchestrator-watchdog"] env_file = wd["env_file"] assert isinstance(env_file, list) assert env_file[0]["required"] is False def test_watchdog_dockerfile_exists_and_is_stdlib_only(): df = REPO_ROOT / "watchdog" / "Dockerfile" assert df.exists() text = df.read_text() # No pip install of third-party deps (stdlib-only, D1). assert "pip install" not in text assert "COPY requirements" not in text assert "requirements.txt" not in text