Files
agentci/tests/test_opencode_deployment.py
T
StanPonomarev 73243c1191
Publish container image / Build and push (push) Successful in 31s
fix ci
2026-07-25 12:40:02 +02:00

206 lines
7.1 KiB
Python

import json
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).parents[1]
def _indent(line: str) -> int:
return len(line) - len(line.lstrip())
def _service(name: str) -> list[str]:
lines = (ROOT / "compose.yaml").read_text().splitlines()
marker = f" {name}:"
start = lines.index(marker) + 1
end = next(
(
index
for index in range(start, len(lines))
if lines[index].strip() and _indent(lines[index]) <= 2
),
len(lines),
)
return lines[start:end]
def _section(lines: list[str], name: str, *, indent: int = 4) -> list[str]:
marker = f"{' ' * indent}{name}:"
start = lines.index(marker) + 1
end = next(
(
index
for index in range(start, len(lines))
if lines[index].strip() and _indent(lines[index]) <= indent
),
len(lines),
)
return lines[start:end]
def _value(value: str) -> str:
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] == '"':
return str(json.loads(value))
if len(value) >= 2 and value[0] == value[-1] == "'":
return value[1:-1]
return value
def _mapping(lines: list[str], name: str, *, indent: int = 4) -> dict[str, str]:
entries = _section(lines, name, indent=indent)
result: dict[str, str] = {}
for line in entries:
if _indent(line) != indent + 2 or line.lstrip().startswith("-"):
continue
key, separator, value = line.strip().partition(":")
if separator:
result[key] = _value(value)
return result
def _sequence(lines: list[str], name: str, *, indent: int = 4) -> list[str]:
entries = _section(lines, name, indent=indent)
prefix = f"{' ' * (indent + 2)}- "
return [_value(line.removeprefix(prefix)) for line in entries if line.startswith(prefix)]
def _scalar(lines: list[str], name: str, *, indent: int = 4) -> str:
prefix = f"{' ' * indent}{name}:"
line = next(line for line in lines if line.startswith(prefix))
return _value(line.removeprefix(prefix))
def test_config_preserves_builtin_permissions_and_restricts_research() -> None:
config = json.loads((ROOT / "opencode" / "opencode.json").read_text())
assert "permission" not in config
assert all(name not in config["agent"] for name in ("build", "plan", "general"))
assert "permission" not in config["agent"]["explore"]
assert config["agent"]["research"]["permission"] == {
"*": "deny",
"websearch": "allow",
"context7_*": "allow",
"gh_grep_*": "allow",
}
assert config["mcp"]["codegraph"]["command"] == ["codegraph", "serve", "--mcp"]
assert config["mcp"]["context7"]["url"] == "https://mcp.context7.com/mcp"
assert config["agent"]["explore"]["model"] == "{env:AGENTCI_EXPLORE_MODEL}"
assert config["agent"]["explore"]["variant"] == "{env:AGENTCI_EXPLORE_VARIANT}"
assert config["agent"]["research"]["variant"] == "{env:AGENTCI_RESEARCH_VARIANT}"
def test_compose_services_have_expected_runtime_contract() -> None:
agentci = _service("agentci")
opencode = _service("opencode")
agentci_environment = _mapping(agentci, "environment")
opencode_environment = _mapping(opencode, "environment")
assert agentci_environment["AGENTCI_OPENCODE_URL"] == "http://opencode:4096"
assert agentci_environment["AGENTCI_EXPLORE_VARIANT"] == ("${AGENTCI_EXPLORE_VARIANT:-low}")
assert agentci_environment["AGENTCI_RESEARCH_VARIANT"] == ("${AGENTCI_RESEARCH_VARIANT:-high}")
assert opencode_environment["HOME"] == "/etc/opencode/home"
assert opencode_environment["OPENCODE_DISABLE_EXTERNAL_SKILLS"] == "1"
assert opencode_environment["OPENCODE_ENABLE_EXA"] == "1"
assert "OPENCODE_DISABLE_DEFAULT_PLUGINS" not in opencode_environment
assert _sequence(agentci, "volumes") == [
"agentci_data:/var/lib/agentci",
"./install-scripts:/etc/agentci/install-scripts:ro",
]
assert _sequence(opencode, "volumes") == [
"agentci_data:/var/lib/agentci",
"opencode_home:/var/lib/opencode",
]
assert _sequence(agentci, "tmpfs") == ["/run/agentci:mode=1777"]
assert _sequence(opencode, "tmpfs") == ["/run/agentci:mode=1777"]
assert json.loads(_scalar(opencode, "command")) == [
"opencode",
"serve",
"--hostname",
"0.0.0.0",
"--port",
"4096",
]
image = "${AGENTCI_IMAGE:-git.krtss.de/stanponomarev/agentci:latest}"
assert _scalar(agentci, "image") == image
assert _scalar(opencode, "image") == image
assert all(line.strip() != "build:" for line in [*agentci, *opencode])
dependency = _section(_section(agentci, "depends_on"), "opencode", indent=6)
assert _mapping([" dependency:", *dependency], "dependency", indent=6) == {
"condition": "service_healthy"
}
healthcheck = _mapping(opencode, "healthcheck")
health_command = json.loads(healthcheck["test"])
assert health_command[0] == "CMD-SHELL"
assert "/global/health" in health_command[1]
assert "OPENCODE_SERVER_PASSWORD_FILE" in health_command[1]
def test_compose_has_no_sandbox_security_exceptions() -> None:
compose = (ROOT / "compose.yaml").read_text()
for forbidden in (
"cap_add:",
"security_opt:",
"privileged:",
"seccomp=unconfined",
"apparmor=unconfined",
"bubblewrap",
):
assert forbidden not in compose
def test_gitea_workflow_publishes_main_images() -> None:
lines = (ROOT / ".gitea" / "workflows" / "publish-image.yaml").read_text().splitlines()
push = _section(_section(lines, "on", indent=0), "push", indent=2)
assert _sequence(push, "branches", indent=4) == ["main"]
assert _mapping(lines, "env", indent=0)["IMAGE_NAME"] == (
"git.krtss.de/stanponomarev/agentci"
)
assert " ${{ env.IMAGE_NAME }}:latest" in lines
assert " ${{ env.IMAGE_NAME }}:${{ gitea.sha }}" in lines
assert " username: ${{ secrets.REGISTRY_USERNAME }}" in lines
assert " password: ${{ secrets.REGISTRY_TOKEN }}" in lines
def test_container_pins_opencode_major_version_contract() -> None:
lines = [line.strip() for line in (ROOT / "Dockerfile").read_text().splitlines()]
build_arguments = {line.removeprefix("ARG ") for line in lines if line.startswith("ARG ")}
assert "AGENTCI_OPENCODE_VERSION=^1" in build_arguments
assert any(
line.rstrip("\\").strip() == '"opencode-ai@${AGENTCI_OPENCODE_VERSION}"' for line in lines
)
def test_compose_document_validates_when_compose_cli_is_available() -> None:
docker = shutil.which("docker")
if docker is None:
pytest.skip("Docker Compose CLI is not installed")
probe = subprocess.run(
[docker, "compose", "version"],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
if probe.returncode:
pytest.skip("Docker Compose provider is not available")
result = subprocess.run(
[docker, "compose", "config", "--quiet"],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr