rewrite phase 1
This commit is contained in:
@@ -1,10 +1,81 @@
|
||||
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:
|
||||
root = Path(__file__).parents[1]
|
||||
config = json.loads((root / "opencode" / "opencode.json").read_text())
|
||||
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"))
|
||||
@@ -22,22 +93,100 @@ def test_config_preserves_builtin_permissions_and_restricts_research() -> None:
|
||||
assert config["agent"]["research"]["variant"] == "{env:AGENTCI_RESEARCH_VARIANT}"
|
||||
|
||||
|
||||
def test_compose_removes_codex_sandbox_exceptions() -> None:
|
||||
root = Path(__file__).parents[1]
|
||||
compose = (root / "compose.yaml").read_text()
|
||||
dockerfile = (root / "Dockerfile").read_text()
|
||||
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")
|
||||
|
||||
for forbidden in ("cap_add", "seccomp=unconfined", "apparmor=unconfined", "bubblewrap"):
|
||||
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",
|
||||
]
|
||||
|
||||
build = _section(agentci, "build")
|
||||
assert _mapping(build, "args", indent=6)["AGENTCI_OPENCODE_VERSION"] == (
|
||||
"${AGENTCI_OPENCODE_VERSION:-^1}"
|
||||
)
|
||||
assert all("no_cache" not in line for line in build)
|
||||
|
||||
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
|
||||
assert "no_cache" not in compose
|
||||
assert "opencode_home:/var/lib/opencode" in compose
|
||||
assert "HOME: /etc/opencode/home" in compose
|
||||
assert "OPENCODE_DISABLE_EXTERNAL_SKILLS" in compose
|
||||
assert "OPENCODE_DISABLE_DEFAULT_PLUGINS" not in compose
|
||||
assert 'OPENCODE_ENABLE_EXA: "1"' in compose
|
||||
assert "AGENTCI_EXPLORE_VARIANT: ${AGENTCI_EXPLORE_VARIANT:-low}" in compose
|
||||
assert "AGENTCI_RESEARCH_VARIANT: ${AGENTCI_RESEARCH_VARIANT:-high}" in compose
|
||||
assert compose.count("/run/agentci:mode=1777") == 2
|
||||
assert "AGENTCI_OPENCODE_VERSION: ${AGENTCI_OPENCODE_VERSION:-^1}" in compose
|
||||
assert "ARG AGENTCI_OPENCODE_VERSION=^1" in dockerfile
|
||||
assert '"opencode-ai@${AGENTCI_OPENCODE_VERSION}"' in dockerfile
|
||||
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user