from __future__ import annotations
import os
import subprocess
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[1]
def test_runtime_dependency_versions_are_explicitly_locked() -> None:
project = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
lock = (ROOT / "uv.lock").read_text(encoding="utf-8")
assert '"fastapi==0.116.1"' in project
assert '"pydantic==2.11.7"' in project
assert '"uvicorn[standard]==0.35.0"' in project
assert 'name = "pydantic"\nversion = "2.11.7"' in lock
def test_container_artifacts_are_pinned_and_preserve_the_java_upload_path() -> None:
dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8")
migration_dockerfile = (ROOT / "Dockerfile.migrations").read_text(encoding="utf-8")
migration_pom = (ROOT / "migration-pom.xml").read_text(encoding="utf-8")
assert dockerfile.startswith("FROM python:3.10.20-bookworm AS build\n")
assert dockerfile.count("FROM python:3.10.20-slim-bookworm") == 1
assert "COPY --from=ghcr.io/astral-sh/uv:0.11.28 /uv /uvx /bin/" in dockerfile
assert "UV_HTTP_TIMEOUT=120" in dockerfile
assert "UV_HTTP_RETRIES=10" in dockerfile
assert "apt-get" not in dockerfile
assert "COPY --from=build --chown=10001:10001 /app/.venv ./.venv" in dockerfile
assert "USER 10001:10001" in dockerfile
assert "ln -s /data/uploads /app/uploadfile" in dockerfile
assert 'STOPSIGNAL SIGTERM' in dockerfile
assert "FROM maven:3.9.9-eclipse-temurin-21 AS build" in migration_dockerfile
assert "FROM eclipse-temurin:21-jre" in migration_dockerfile
assert "--mount=type=cache,target=/root/.m2/repository" in migration_dockerfile
assert "-Daether.connector.basic.threads=1" in migration_dockerfile
assert "-Daether.connector.connectTimeout=15000" in migration_dockerfile
assert "-Daether.connector.requestTimeout=60000" in migration_dockerfile
assert "-Daether.connector.http.retryHandler.count=5" in migration_dockerfile
assert "USER 10001:10001" in migration_dockerfile
assert "STOPSIGNAL SIGTERM" in migration_dockerfile
assert "manager-api-liquibase-runner-1.0.0-all.jar" in migration_dockerfile
assert "true" in migration_pom
assert "all" in migration_pom
assert ":latest" not in dockerfile + migration_dockerfile
def test_compose_separates_migrations_api_jobs_and_nginx() -> None:
compose = yaml.safe_load((ROOT / "docker-compose.yml").read_text(encoding="utf-8"))
services = compose["services"]
assert set(services) == {
"manager-api-migrate",
"manager-api-fastapi",
"manager-api-jobs",
"manager-api-nginx",
}
api = services["manager-api-fastapi"]
jobs = services["manager-api-jobs"]
assert api["depends_on"]["manager-api-migrate"]["condition"] == "service_completed_successfully"
assert jobs["command"] == ["python", "-m", "app.jobs.worker"]
assert api["read_only"] is True and jobs["read_only"] is True
assert api["stop_grace_period"] == "40s"
expected_upload = ["${MANAGER_API_UPLOAD_SOURCE:-manager-api-uploads}:/data/uploads"]
assert api["volumes"] == jobs["volumes"] == expected_upload
assert api["environment"]["APP_GRACEFUL_SHUTDOWN_SECONDS"] == "${APP_GRACEFUL_SHUTDOWN_SECONDS:-30}"
assert jobs["environment"]["APP_GRACEFUL_SHUTDOWN_SECONDS"] == "${APP_GRACEFUL_SHUTDOWN_SECONDS:-30}"
def test_nginx_health_and_startup_scripts_are_wired() -> None:
nginx = (ROOT / "deploy" / "nginx.conf").read_text(encoding="utf-8")
nginx_dockerfile = (ROOT / "Dockerfile.nginx").read_text(encoding="utf-8")
nginx_entrypoint = ROOT / "deploy" / "nginx-entrypoint.sh"
compose_text = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
compose = yaml.safe_load(compose_text)
nginx_service = compose["services"]["manager-api-nginx"]
assert "location /xiaozhi/" in nginx
assert "location = /xiaozhi" in nginx
assert "server ${MANAGER_API_UPSTREAM};" in nginx
assert "proxy_request_buffering off;" in nginx
assert "proxy_buffering off;" in nginx
assert "client_max_body_size 100m;" in nginx
assert nginx_dockerfile.startswith("FROM nginx:1.28.0-alpine\n")
assert ":latest" not in nginx_dockerfile
assert "envsubst '${MANAGER_API_UPSTREAM}'" in nginx_dockerfile
assert nginx_service["build"]["dockerfile"] == "main/manager-api-fastapi/Dockerfile.nginx"
assert nginx_service["environment"] == {
"MANAGER_API_UPSTREAM": "${MANAGER_API_UPSTREAM:-manager-api-fastapi:8002}"
}
assert nginx_service["read_only"] is True
assert set(nginx_service["tmpfs"]) == {
"/var/cache/nginx:size=32m",
"/var/run:size=1m",
"/tmp:size=16m", # noqa: S108 - literal deployment mount, not a test scratch path
}
assert nginx_service["depends_on"]["manager-api-fastapi"]["condition"] == "service_healthy"
assert "/xiaozhi/health/ready" in compose_text
assert os.access(nginx_entrypoint, os.X_OK)
syntax = subprocess.run( # noqa: S603
["/bin/sh", "-n", str(nginx_entrypoint)],
check=False,
capture_output=True,
text=True,
)
assert syntax.returncode == 0, syntax.stderr
for script_name in (
"container-entrypoint.sh",
"run-migrations.sh",
"isolated-env.sh",
"start-api.sh",
"start-jobs.sh",
):
script = ROOT / "scripts" / script_name
assert script.read_text(encoding="utf-8").startswith("#!/bin/sh\nset -eu\n")
assert os.access(script, os.X_OK), f"{script_name} must be executable"
def test_docker_context_excludes_local_runtimes_secrets_and_build_products() -> None:
dockerignore = (ROOT.parents[1] / ".dockerignore").read_text(encoding="utf-8")
for ignored in (
".env",
".runtime/",
".venv-*/",
"**/.venv/",
"**/.test-runtime/",
"**/node_modules/",
"**/dist/",
"**/target/",
"**/uploadfile/",
"main/xiaozhi-server/models/",
"main/xiaozhi-server/data/",
):
assert ignored in dockerignore
def test_start_scripts_are_executable_without_shell_reinterpretation() -> None:
environment = {**os.environ, "PYTHON_BIN": "/bin/echo"}
api = subprocess.run( # noqa: S603
[str(ROOT / "scripts" / "start-api.sh")],
check=True,
capture_output=True,
text=True,
env=environment,
)
assert "-m uvicorn app.main:app" in api.stdout
assert "--timeout-graceful-shutdown 30" in api.stdout
jobs = subprocess.run( # noqa: S603
[str(ROOT / "scripts" / "start-jobs.sh")],
check=True,
capture_output=True,
text=True,
env=environment,
)
assert jobs.stdout.strip() == "-m app.jobs.worker"
override = subprocess.run( # noqa: S603
[str(ROOT / "scripts" / "container-entrypoint.sh"), "/bin/echo", "override-ok"],
check=True,
capture_output=True,
text=True,
)
assert override.stdout.strip() == "override-ok"
def test_repository_local_restart_entrypoint_keeps_fastapi_default_and_java_rollback() -> None:
restart = ROOT.parents[1] / "scripts" / "restart-local-services.sh"
assert os.access(restart, os.X_OK)
syntax = subprocess.run( # noqa: S603
["/bin/sh", "-n", str(restart)],
check=False,
capture_output=True,
text=True,
)
assert syntax.returncode == 0, syntax.stderr
help_result = subprocess.run( # noqa: S603
[str(restart), "--help"],
check=True,
capture_output=True,
text=True,
)
assert "--manager-api fastapi|java" in help_result.stdout
assert "The FastAPI manager is the default" in help_result.stdout
def test_local_migration_script_prefers_bundled_jdk_maven_and_repository() -> None:
script = (ROOT / "scripts" / "run-migrations.sh").read_text(encoding="utf-8")
assert 'RUNTIME_ROOT="${PROJECT_DIR}/../../.runtime"' in script
assert 'MAVEN="${RUNTIME_ROOT}/maven/bin/mvn"' in script
assert 'MAVEN_REPOSITORY_ARGS="-Dmaven.repo.local=${RUNTIME_ROOT}/m2"' in script
assert 'JAVA="${RUNTIME_ROOT}/jdk/bin/java"' in script
assert " clean package" not in script
assert "manager-api-liquibase-runner-1.0.0-all.jar" in script