195 lines
6.5 KiB
Python
195 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MODULE_PATH = ROOT / "scripts" / "contextforge_runtime_contract.py"
|
|
CONTRACT_PATH = ROOT / "infrastructure" / "images" / "contextforge" / "runtime-contract.json"
|
|
|
|
|
|
def _load_module():
|
|
spec = importlib.util.spec_from_file_location("contextforge_runtime_contract", MODULE_PATH)
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def _contract() -> dict[str, object]:
|
|
return json.loads(CONTRACT_PATH.read_text(encoding="utf-8"))
|
|
|
|
|
|
def _environment() -> dict[str, str]:
|
|
contract = _contract()
|
|
return dict(contract["productionEnvironment"]) # type: ignore[arg-type]
|
|
|
|
|
|
def test_environment_contract_accepts_exact_production_tuple() -> None:
|
|
module = _load_module()
|
|
module.validate_environment(_contract(), _environment())
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("name", "value"),
|
|
[
|
|
("GUNICORN_WORKERS", "2"),
|
|
("USE_STATEFUL_SESSIONS", "true"),
|
|
("MCPGATEWAY_SESSION_AFFINITY_ENABLED", "true"),
|
|
("JSON_RESPONSE_ENABLED", "false"),
|
|
("EMAIL_AUTH_ENABLED", "true"),
|
|
("MCP_SESSION_POOL_ENABLED", "true"),
|
|
("MCPGATEWAY_SKIP_MIGRATIONS", "false"),
|
|
],
|
|
)
|
|
def test_environment_contract_rejects_transport_or_migration_regression(
|
|
name: str,
|
|
value: str,
|
|
) -> None:
|
|
module = _load_module()
|
|
environment = _environment()
|
|
environment[name] = value
|
|
with pytest.raises(module.RuntimeContractError, match=f"environment:{name}"):
|
|
module.validate_environment(_contract(), environment)
|
|
|
|
|
|
def test_contract_digest_requires_exact_repository_contract() -> None:
|
|
module = _load_module()
|
|
expected = module.contract_sha256(CONTRACT_PATH)
|
|
module.validate_contract_digest(
|
|
CONTRACT_PATH,
|
|
{"ITWORX_CONTEXTFORGE_EXPECTED_CONTRACT_SHA256": expected},
|
|
)
|
|
with pytest.raises(module.RuntimeContractError, match="contract-digest"):
|
|
module.validate_contract_digest(
|
|
CONTRACT_PATH,
|
|
{"ITWORX_CONTEXTFORGE_EXPECTED_CONTRACT_SHA256": "0" * 64},
|
|
)
|
|
|
|
|
|
def test_contract_digest_rejects_missing_or_malformed_configuration() -> None:
|
|
module = _load_module()
|
|
for value in ("", "deadbeef", "G" * 64):
|
|
with pytest.raises(module.RuntimeContractError, match="contract-digest-config"):
|
|
module.validate_contract_digest(
|
|
CONTRACT_PATH,
|
|
{"ITWORX_CONTEXTFORGE_EXPECTED_CONTRACT_SHA256": value},
|
|
)
|
|
|
|
|
|
def test_mcp_runtime_rejects_wrong_sdk_version() -> None:
|
|
module = _load_module()
|
|
with pytest.raises(module.RuntimeContractError, match="mcp-version"):
|
|
module.validate_mcp_runtime(
|
|
_contract(),
|
|
version_getter=lambda _: "2.0.0",
|
|
symbol_loader=lambda _module, _symbol: object(),
|
|
)
|
|
|
|
|
|
def test_mcp_runtime_rejects_missing_legacy_symbol() -> None:
|
|
module = _load_module()
|
|
|
|
def missing(_module: str, _symbol: str):
|
|
raise AttributeError("missing")
|
|
|
|
with pytest.raises(module.RuntimeContractError, match="mcp-symbol"):
|
|
module.validate_mcp_runtime(
|
|
_contract(),
|
|
version_getter=lambda _: "1.30.0",
|
|
symbol_loader=missing,
|
|
)
|
|
|
|
|
|
def test_alembic_head_rejects_schema_mismatch() -> None:
|
|
module = _load_module()
|
|
with pytest.raises(module.RuntimeContractError, match="alembic-head"):
|
|
module.validate_alembic_head(_contract(), ["deadbeef"])
|
|
|
|
|
|
def test_database_revision_rejects_schema_drift() -> None:
|
|
module = _load_module()
|
|
with pytest.raises(module.RuntimeContractError, match="database-revision"):
|
|
module.validate_database_revision(_contract(), ["deadbeef"])
|
|
|
|
|
|
def test_database_revision_accepts_exact_single_head() -> None:
|
|
module = _load_module()
|
|
module.validate_database_revision(_contract(), ["12d4a0c7789c"])
|
|
|
|
|
|
def test_contract_shape_rejects_startup_migration_policy_weakening() -> None:
|
|
module = _load_module()
|
|
contract = _contract()
|
|
contract["migrationPolicy"]["startupMigrations"] = "allowed" # type: ignore[index]
|
|
with pytest.raises(module.RuntimeContractError, match="migration-policy"):
|
|
module.validate_contract_shape(contract)
|
|
|
|
|
|
def test_health_verifier_can_derive_database_url_from_mounted_secret(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
module = _load_module()
|
|
secret = tmp_path / "contextforge_database_password"
|
|
secret.write_text("p@ss:/word", encoding="utf-8")
|
|
monkeypatch.setattr(module, "CONTEXTFORGE_DB_PASSWORD_FILE", secret)
|
|
|
|
resolved = module.resolve_database_url(_environment())
|
|
|
|
assert resolved.startswith("postgresql+psycopg://contextforge:")
|
|
assert "@postgres:5432/contextforge" in resolved
|
|
assert "p@ss:/word" not in resolved
|
|
|
|
|
|
def test_query_database_revisions_is_read_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
module = _load_module()
|
|
calls: list[str] = []
|
|
|
|
class Result:
|
|
def scalars(self):
|
|
return self
|
|
|
|
def all(self):
|
|
return ["12d4a0c7789c"]
|
|
|
|
class Connection:
|
|
def execute(self, statement):
|
|
calls.append(str(statement))
|
|
return Result()
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
return False
|
|
|
|
class Engine:
|
|
def connect(self):
|
|
return Connection()
|
|
|
|
def dispose(self):
|
|
calls.append("dispose")
|
|
|
|
monkeypatch.setattr(module, "create_engine", lambda *_args, **_kwargs: Engine())
|
|
assert module.query_database_revisions("postgresql://redacted") == ["12d4a0c7789c"]
|
|
assert calls[0].strip().upper() == "SELECT VERSION_NUM FROM ALEMBIC_VERSION"
|
|
blocked_tokens = ("UPDATE", "INSERT", "DELETE", "ALTER", "DROP")
|
|
assert all(token not in calls[0].upper() for token in blocked_tokens)
|
|
|
|
|
|
def test_verify_runtime_combines_all_proofs(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
module = _load_module()
|
|
monkeypatch.setattr(module, "installed_mcp_version", lambda: "1.30.0")
|
|
monkeypatch.setattr(module, "load_mcp_symbol", lambda _module, _symbol: SimpleNamespace())
|
|
monkeypatch.setattr(module, "query_image_alembic_heads", lambda: ["12d4a0c7789c"])
|
|
monkeypatch.setattr(module, "query_database_revisions", lambda _url: ["12d4a0c7789c"])
|
|
module.verify_runtime(
|
|
_contract(),
|
|
_environment() | {"DATABASE_URL": "postgresql://redacted"},
|
|
)
|