174 lines
6.4 KiB
Python
174 lines
6.4 KiB
Python
import json
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
SCRIPT = Path(__file__).parents[2] / "scripts/check-runtime-config.py"
|
|
|
|
|
|
def field(name: str, process: str = "shared", mode: str = "effective") -> dict:
|
|
return {
|
|
"env_name": name,
|
|
"process": process,
|
|
"mode": mode,
|
|
"required": False,
|
|
"default": "value",
|
|
"sensitivity": "public",
|
|
}
|
|
|
|
|
|
class RuntimeConfigContractTests(unittest.TestCase):
|
|
def fixture(self) -> Path:
|
|
root = Path(tempfile.mkdtemp())
|
|
(root / "docs/schemas").mkdir(parents=True)
|
|
(root / "deploy/community").mkdir(parents=True)
|
|
contract = {
|
|
"schema_version": 1,
|
|
"fields": [
|
|
field("CRANK_SHARED"),
|
|
field("CRANK_ADMIN", "admin_api"),
|
|
field("CRANK_MCP", "mcp_server"),
|
|
field("CRANK_OLD", mode="deprecated_no_effect"),
|
|
],
|
|
"deployment_only_fields": ["COMPOSE_PROJECT_NAME"],
|
|
}
|
|
(root / "docs/schemas/runtime-config.schema.json").write_text(
|
|
json.dumps(contract), encoding="utf-8"
|
|
)
|
|
section = (
|
|
"# BEGIN GENERATED CRANK RUNTIME CONFIG\n"
|
|
"CRANK_ADMIN=value\nCRANK_MCP=value\nCRANK_SHARED=value\n"
|
|
"# END GENERATED CRANK RUNTIME CONFIG\n"
|
|
)
|
|
for relative in (
|
|
".env.example",
|
|
"deploy/community/.env.example",
|
|
"deploy/community/.env.images.example",
|
|
):
|
|
(root / relative).write_text(section, encoding="utf-8")
|
|
compose = (
|
|
"services:\n"
|
|
" admin-api:\n environment:\n"
|
|
" CRANK_SHARED: ${CRANK_SHARED:-value}\n"
|
|
" CRANK_ADMIN: ${CRANK_ADMIN:-value}\n"
|
|
" mcp-server:\n environment:\n"
|
|
" CRANK_SHARED: ${CRANK_SHARED:-value}\n"
|
|
" CRANK_MCP: ${CRANK_MCP:-value}\n"
|
|
)
|
|
for relative in (
|
|
"docker-compose.yml",
|
|
"deploy/community/docker-compose.yml",
|
|
"deploy/community/docker-compose.images.yml",
|
|
):
|
|
(root / relative).write_text(compose, encoding="utf-8")
|
|
return root
|
|
|
|
def run_check(self, root: Path) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
["python3", str(SCRIPT), "--root", str(root)],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
|
|
def test_valid_contract_passes(self) -> None:
|
|
result = self.run_check(self.fixture())
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
|
|
def test_missing_generated_field_fails_closed(self) -> None:
|
|
root = self.fixture()
|
|
path = root / ".env.example"
|
|
path.write_text(
|
|
path.read_text().replace("CRANK_MCP=value\n", ""), encoding="utf-8"
|
|
)
|
|
result = self.run_check(root)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
|
|
def test_compose_extra_or_wrong_process_field_fails(self) -> None:
|
|
root = self.fixture()
|
|
path = root / "docker-compose.yml"
|
|
path.write_text(
|
|
path.read_text().replace(
|
|
"CRANK_ADMIN: ${CRANK_ADMIN:-value}",
|
|
"CRANK_ADMIN: ${CRANK_ADMIN:-value}\n"
|
|
" CRANK_MCP: ${CRANK_MCP:-value}",
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
result = self.run_check(root)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
|
|
def test_divergent_inline_default_fails(self) -> None:
|
|
root = self.fixture()
|
|
path = root / "docker-compose.yml"
|
|
path.write_text(
|
|
path.read_text().replace("${CRANK_SHARED:-value}", "${CRANK_SHARED:-drift}", 1),
|
|
encoding="utf-8",
|
|
)
|
|
result = self.run_check(root)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("divergent Compose inline default", result.stderr)
|
|
|
|
def test_wrong_interpolation_reference_fails(self) -> None:
|
|
root = self.fixture()
|
|
path = root / "docker-compose.yml"
|
|
path.write_text(
|
|
path.read_text().replace(
|
|
"CRANK_ADMIN: ${CRANK_ADMIN:-value}",
|
|
"CRANK_ADMIN: ${CRANK_SHARED:-value}",
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
result = self.run_check(root)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("wrong Compose interpolation reference", result.stderr)
|
|
|
|
def test_required_and_empty_operator_forms_are_enforced(self) -> None:
|
|
for required, expression in (
|
|
(True, "${CRANK_SHARED:-value}"),
|
|
(False, "${CRANK_SHARED-value}"),
|
|
(False, "${CRANK_SHARED:?required}"),
|
|
):
|
|
with self.subTest(required=required, expression=expression):
|
|
root = self.fixture()
|
|
schema_path = root / "docs/schemas/runtime-config.schema.json"
|
|
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
schema["fields"][0]["required"] = required
|
|
schema_path.write_text(json.dumps(schema), encoding="utf-8")
|
|
compose_path = root / "docker-compose.yml"
|
|
compose_path.write_text(
|
|
compose_path.read_text().replace(
|
|
"${CRANK_SHARED:-value}", expression, 1
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
result = self.run_check(root)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
|
|
def test_required_empty_aware_error_form_passes(self) -> None:
|
|
root = self.fixture()
|
|
schema_path = root / "docs/schemas/runtime-config.schema.json"
|
|
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
schema["fields"][0]["required"] = True
|
|
schema_path.write_text(json.dumps(schema), encoding="utf-8")
|
|
for relative in (
|
|
"docker-compose.yml",
|
|
"deploy/community/docker-compose.yml",
|
|
"deploy/community/docker-compose.images.yml",
|
|
):
|
|
path = root / relative
|
|
path.write_text(
|
|
path.read_text().replace(
|
|
"${CRANK_SHARED:-value}", "${CRANK_SHARED:?must be configured}"
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
result = self.run_check(root)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|