635 lines
28 KiB
Python
635 lines
28 KiB
Python
import ast
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from backend.app.services.mcp_hub_deployment_reconciliation import (
|
|
McpHubDeploymentReconciliation,
|
|
)
|
|
|
|
|
|
class FakeResponse:
|
|
def __init__(self, payload):
|
|
self.payload = payload
|
|
|
|
def json(self):
|
|
return self.payload
|
|
|
|
|
|
class FakePortainer:
|
|
def __init__(self, exit_code=0):
|
|
self.exit_code = exit_code
|
|
self.calls = []
|
|
|
|
def get_container_status(self, name):
|
|
assert name == "gitea-deploy-control"
|
|
return {
|
|
"status": "ok",
|
|
"container": {
|
|
"id": "controller-id",
|
|
"name": "gitea-deploy-control",
|
|
"state": "running",
|
|
},
|
|
}
|
|
|
|
def _config(self):
|
|
return SimpleNamespace(endpoint_id="3")
|
|
|
|
def _request(self, method, path, **kwargs):
|
|
self.calls.append((method, path, kwargs))
|
|
if path.endswith("/containers/controller-id/exec"):
|
|
return FakeResponse({"Id": "exec-1"})
|
|
if path.endswith("/exec/exec-1/start"):
|
|
return FakeResponse({})
|
|
if path.endswith("/exec/exec-1/json"):
|
|
return FakeResponse({"Running": False, "ExitCode": self.exit_code})
|
|
raise AssertionError(path)
|
|
|
|
|
|
def test_reconciliation_is_fixed_and_read_only_for_mcp_hub():
|
|
fake = FakePortainer()
|
|
result = McpHubDeploymentReconciliation(fake).run()
|
|
assert result == {
|
|
"status": "reconciled",
|
|
"repository": "jens/itworx_mcp_hub",
|
|
"composeProject": "itworx-mcp-hub",
|
|
}
|
|
|
|
command = fake.calls[0][2]["json"]["Cmd"]
|
|
assert command[:2] == ["python3", "-c"]
|
|
assert command[3:] == ["jens/itworx_mcp_hub", "itworx-mcp-hub"]
|
|
script = command[2]
|
|
assert "/controller-config/projects.json" in script
|
|
assert "/controller-hub-releases/controller-config/projects.json" in script
|
|
assert "/mnt/cache/appdata/gitea-deploy-runners/state" in script
|
|
assert "os.path.samefile(registry_path, host_registry_path)" in script
|
|
assert 'os.open(project_lock_path, os.O_RDONLY' in script
|
|
assert 'os.open(global_lock_path, os.O_RDONLY' in script
|
|
assert 'state_root.mkdir' not in script
|
|
assert 'project_lock_path.parent.mkdir' not in script
|
|
assert 'os.replace(' not in script
|
|
assert 'shutil.copy2(' not in script
|
|
assert 'state_path.write_text(' not in script
|
|
assert '"status": "success"' not in script
|
|
assert '"live_legacy_provenance": True' not in script
|
|
|
|
|
|
def test_unknown_or_legacy_state_requires_the_guarded_deployment_path():
|
|
result = McpHubDeploymentReconciliation(FakePortainer(exit_code=56)).run()
|
|
assert result == {
|
|
"status": "topology_migration_required",
|
|
"repository": "jens/itworx_mcp_hub",
|
|
"composeProject": "itworx-mcp-hub",
|
|
"reason": "MCP Hub legacy, unknown, mixed, or unresolved migration state requires guarded deployment or data-aware recovery",
|
|
}
|
|
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
assert 'set(registered_live_containers) == expected_fixed' in script
|
|
assert 'raise SystemExit(56 if recorded_live_status in {"unknown", "healthy"} else 52)' in script
|
|
assert 'recorded_status == "success"' in script
|
|
assert 'recorded_live_status == "healthy"' in script
|
|
assert 'state.get("live_legacy_provenance") is False' in script
|
|
assert 'recorded_artifacts.get("topology_contract") == topology_contract' in script
|
|
assert 'receipt.get("schema") == "gitea-deploy-receipt.v1"' in script
|
|
assert 'receipt.get("live") is None' in script
|
|
|
|
|
|
def test_modern_success_requires_a_complete_exact_receipt_and_protected_ancestry():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert 'set(receipt) == {"schema", "repository", "revision", "tree", "treeDigest", "build", "containers", "live"}' in script
|
|
assert 'receipt_identity_valid' in script
|
|
assert 'receipt.get("revision") == recorded_live' in script
|
|
assert 'git_run("merge-base", "--is-ancestor", recorded_live, main_revision)' in script
|
|
assert 'set(dict(receipt.get("containers") or {})) == expected_runtime' in script
|
|
assert 'print("already-reconciled", flush=True)' in script
|
|
assert 'print("reconciled", flush=True)' not in script
|
|
assert 'migration_path = migration_root / "active-intent.json"' in script
|
|
assert 'intent.get("status") != "complete"' in script
|
|
assert 'elif migration_attempt_id:' in script
|
|
assert 'intent.get("completedStateSha256") != hashlib.sha256(state_bytes).hexdigest()' in script
|
|
assert 'observed.get("historicalStateSha256") != intent.get("baselineStateSha256")' in script
|
|
assert 'rf"[0-9]+-{recorded_live[:12]}-[0-9a-f]{{16}}"' in script
|
|
|
|
|
|
def test_reconciliation_intent_guard_rejects_unknown_and_accepts_exact_complete_intent():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
parsed = ast.parse(script)
|
|
function = next(
|
|
node for node in parsed.body
|
|
if isinstance(node, ast.FunctionDef) and node.name == "validate_migration_intent_state"
|
|
)
|
|
namespace = {
|
|
"Path": Path,
|
|
"hashlib": hashlib,
|
|
"json": json,
|
|
"os": os,
|
|
"re": re,
|
|
"stat": SimpleNamespace(S_IMODE=lambda _mode: 0o600),
|
|
"trusted_private_path": lambda *_args, **_kwargs: True,
|
|
}
|
|
exec(compile(ast.Module(body=[function], type_ignores=[]), "intent-test", "exec"), namespace)
|
|
validate = namespace["validate_migration_intent_state"]
|
|
repository = "jens/itworx_mcp_hub"
|
|
candidate = "b" * 40
|
|
migration_attempt = "a" * 32
|
|
state_bytes = b"exact-success-state-bytes"
|
|
|
|
for raw in (b'{"status":"started"}', b"not-json"):
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
state_root = Path(temporary)
|
|
migration_root = state_root / "mcp-hub-topology-migrations"
|
|
migration_root.mkdir()
|
|
(migration_root / "active-intent.json").write_bytes(raw)
|
|
last_attempt = {"metadata": {"mcpHubMigrationAttemptId": migration_attempt}}
|
|
with pytest.raises(SystemExit) as exc:
|
|
validate(
|
|
state_root, state_bytes, repository, candidate, "success", "healthy",
|
|
last_attempt, "success", candidate,
|
|
)
|
|
assert exc.value.code == 56
|
|
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
last_attempt = {"metadata": {"mcpHubMigrationAttemptId": migration_attempt}}
|
|
with pytest.raises(SystemExit) as exc:
|
|
validate(
|
|
Path(temporary), state_bytes, repository, candidate, "success", "healthy",
|
|
last_attempt, "success", candidate,
|
|
)
|
|
assert exc.value.code == 56
|
|
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
state_root = Path(temporary)
|
|
migration_root = state_root / "mcp-hub-topology-migrations"
|
|
migration_root.mkdir()
|
|
intent = {
|
|
"schema": "itworx.mcp-hub-migration-intent/v1",
|
|
"status": "complete",
|
|
"attemptId": migration_attempt,
|
|
"repository": repository,
|
|
"candidateRevision": candidate,
|
|
"priorLiveRevision": "c" * 40,
|
|
"backupManifestSha256": "d" * 64,
|
|
"backupComposeSha256": "e" * 64,
|
|
"baselineStateSha256": "f" * 64,
|
|
"startedAtEpoch": 10,
|
|
"completedAtEpoch": 11,
|
|
"completedStateSha256": "0" * 64,
|
|
}
|
|
active_intent = migration_root / "active-intent.json"
|
|
active_intent.write_text(json.dumps(intent), encoding="utf-8")
|
|
last_attempt = {
|
|
"metadata": {
|
|
"mcpHubMigrationAttemptId": migration_attempt,
|
|
"mcpHubBackupReceipt": {
|
|
"repository": repository,
|
|
"revision": candidate,
|
|
"restoreVerified": True,
|
|
"manifestSha256": "d" * 64,
|
|
"composeSha256": "e" * 64,
|
|
},
|
|
}
|
|
}
|
|
with pytest.raises(SystemExit) as exc:
|
|
validate(
|
|
state_root, state_bytes, repository, candidate, "success", "healthy",
|
|
last_attempt, "success", candidate,
|
|
)
|
|
assert exc.value.code == 56
|
|
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
state_root = Path(temporary)
|
|
migration_root = state_root / "mcp-hub-topology-migrations"
|
|
receipt_root = state_root / "mcp-hub-backup-receipts"
|
|
backup_root = state_root / "mcp-hub-backups"
|
|
migration_root.mkdir()
|
|
receipt_root.mkdir()
|
|
backup_root.mkdir()
|
|
backup_directory = backup_root / f"1780400000-{candidate[:12]}-0123456789abcdef"
|
|
backup_directory.mkdir()
|
|
compose_digest = "e" * 64
|
|
manifest = {
|
|
"schema": "itworx.mcp-hub-backup/v1",
|
|
"repository": repository,
|
|
"revision": candidate,
|
|
"backupDirectory": str(backup_directory),
|
|
"composeSha256": compose_digest,
|
|
"configRestoreVerified": True,
|
|
"restore": {"status": "verified"},
|
|
"databases": {"itworx_mcp_hub": {}, "contextforge": {}},
|
|
}
|
|
manifest_path = backup_directory / "manifest.json"
|
|
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
manifest_digest = hashlib.sha256(manifest_path.read_bytes()).hexdigest()
|
|
receipt_path = receipt_root / f"1780400000-{candidate[:12]}-0123456789abcdef.json"
|
|
backup_receipt = {
|
|
"schema": "itworx.mcp-hub-backup-receipt/v1",
|
|
"repository": repository,
|
|
"revision": candidate,
|
|
"backupDirectory": str(backup_directory),
|
|
"manifestSha256": manifest_digest,
|
|
"composeSha256": compose_digest,
|
|
"restoreVerified": True,
|
|
"createdAtEpoch": 1780400000,
|
|
"receiptPath": str(receipt_path),
|
|
}
|
|
receipt_path.write_text(
|
|
json.dumps({key: value for key, value in backup_receipt.items() if key != "receiptPath"}),
|
|
encoding="utf-8",
|
|
)
|
|
observed_path = migration_root / f"observed-baseline-{candidate[:12]}-0123456789abcdef0123.json"
|
|
observed = {
|
|
"schema": "itworx.mcp-hub-observed-baseline/v1",
|
|
"claim": "observed_mixed_runtime_facts_only",
|
|
"candidateRevision": candidate,
|
|
"historicalStateSha256": "f" * 64,
|
|
"backupManifestSha256": manifest_digest,
|
|
"backupComposeSha256": compose_digest,
|
|
"backupRestoreVerified": True,
|
|
"postgresVolumeBaseline": [{}, {}],
|
|
}
|
|
observed_path.write_text(json.dumps(observed), encoding="utf-8")
|
|
observed_digest = hashlib.sha256(observed_path.read_bytes()).hexdigest()
|
|
successful_state_sha = hashlib.sha256(state_bytes).hexdigest()
|
|
intent = {
|
|
"schema": "itworx.mcp-hub-migration-intent/v1",
|
|
"status": "complete",
|
|
"attemptId": migration_attempt,
|
|
"repository": repository,
|
|
"candidateRevision": candidate,
|
|
"priorLiveRevision": "c" * 40,
|
|
"backupManifestSha256": manifest_digest,
|
|
"backupComposeSha256": compose_digest,
|
|
"baselineStateSha256": "f" * 64,
|
|
"startedAtEpoch": 10,
|
|
"completedAtEpoch": 11,
|
|
"completedStateSha256": successful_state_sha,
|
|
"observedBaselinePath": str(observed_path),
|
|
"observedBaselineSha256": observed_digest,
|
|
}
|
|
(migration_root / "active-intent.json").write_text(json.dumps(intent), encoding="utf-8")
|
|
last_attempt = {
|
|
"metadata": {
|
|
"mcpHubMigrationAttemptId": migration_attempt,
|
|
"mcpHubBackupReceipt": backup_receipt,
|
|
}
|
|
}
|
|
|
|
validate(
|
|
state_root, state_bytes, repository, candidate, "success", "healthy",
|
|
last_attempt, "success", candidate,
|
|
)
|
|
|
|
|
|
def test_container_reverification_requires_exact_health_service_label_ids_and_mounts():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert 'runtime_state.get("Running") is not True or health != "healthy"' in script
|
|
assert 'labels.get("com.docker.compose.project") != expected_compose_project' in script
|
|
assert 'labels.get("com.docker.compose.service") != name_service' in script
|
|
assert 'if recorded.get(key) != value or receipt_item.get(key) != value:' in script
|
|
assert 'live_attested_mounts(container) != recorded_mounts' in script
|
|
assert 'receipt_item.get("mounts_sha256")' in script
|
|
assert 'def live_attested_mounts(container):' in script
|
|
assert 'docker", "volume", "inspect", name' in script
|
|
assert 'mount["created_at"] = created_at' in script
|
|
assert 'created.astimezone(timezone.utc) > datetime.now(timezone.utc)' in script
|
|
|
|
|
|
def test_reconciliation_distinguishes_recreated_postgres_volume_by_created_at():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
parsed = ast.parse(script)
|
|
function = next(
|
|
node for node in parsed.body
|
|
if isinstance(node, ast.FunctionDef) and node.name == "live_attested_mounts"
|
|
)
|
|
volume = {
|
|
"Name": "hub-postgres",
|
|
"Driver": "local",
|
|
"Mountpoint": "/var/lib/docker/volumes/hub-postgres/_data",
|
|
"Options": {},
|
|
"CreatedAt": "2020-01-01T10:00:00Z",
|
|
}
|
|
namespace = {
|
|
"json": json,
|
|
"os": os,
|
|
"subprocess": SimpleNamespace(
|
|
PIPE=subprocess.PIPE,
|
|
DEVNULL=subprocess.DEVNULL,
|
|
run=lambda *_args, **_kwargs: SimpleNamespace(
|
|
returncode=0, stdout=json.dumps([volume])
|
|
)
|
|
),
|
|
"datetime": datetime,
|
|
"timezone": timezone,
|
|
}
|
|
exec(compile(ast.Module(body=[function], type_ignores=[]), "mount-test", "exec"), namespace)
|
|
attest = namespace["live_attested_mounts"]
|
|
container = {
|
|
"Mounts": [{
|
|
"Type": "volume", "Name": "hub-postgres",
|
|
"Source": "/var/lib/docker/volumes/hub-postgres/_data",
|
|
"Destination": "/var/lib/postgresql/data", "Driver": "local", "RW": True,
|
|
}]
|
|
}
|
|
|
|
original = attest(container)
|
|
assert original[0]["created_at"] == "2020-01-01T10:00:00Z"
|
|
volume["CreatedAt"] = "2021-01-01T10:00:00Z"
|
|
recreated = attest(container)
|
|
assert recreated[0]["name"] == original[0]["name"]
|
|
assert recreated[0]["source"] == original[0]["source"]
|
|
assert recreated[0]["created_at"] != original[0]["created_at"]
|
|
|
|
|
|
def test_each_service_uses_its_fixed_image_repository_or_reviewed_external_contract():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert 'def fixed_image_reference(service_name, image_ref):' in script
|
|
assert '"dataplane-router": "gitea.example.com/jens/itworx-mcp-hub/dataplane"' in script
|
|
assert '"docker-read-broker": "gitea.example.com/jens/itworx-mcp-hub/docker-read-broker"' in script
|
|
assert 'artifact["image_source"] != owned_image_source' in script
|
|
assert 'artifact["image_revision"] != recorded_live' in script
|
|
assert 'main_declares_external_image(image_ref)' in script
|
|
assert 'main_declares_contextforge_compat_image(' in script
|
|
assert 'def image_digest_matches(image, image_ref):' in script
|
|
assert 'aliases.add("docker.io/library/" + repository)' in script
|
|
|
|
|
|
def test_external_candidate_pins_are_exact_and_declared_by_protected_main():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
parsed = ast.parse(script)
|
|
function = next(
|
|
node for node in parsed.body
|
|
if isinstance(node, ast.FunctionDef) and node.name == "fixed_image_reference"
|
|
)
|
|
declared = {
|
|
"postgres:18.6@sha256:0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04",
|
|
"redis:8.10.2@sha256:7ef5b5cec96495a04ca7feff88a9492efeab8053fb284d24bdd73344c9245a48",
|
|
}
|
|
namespace = {
|
|
"re": re,
|
|
"main_declares_external_image": lambda image_ref: image_ref in declared,
|
|
"main_declares_sessionless_dataplane_image": lambda *_args: False,
|
|
"main_declares_contextforge_compat_image": lambda *_args: False,
|
|
}
|
|
exec(compile(ast.Module(body=[function], type_ignores=[]), "fixed-image-test", "exec"), namespace)
|
|
fixed = namespace["fixed_image_reference"]
|
|
|
|
assert fixed("postgres", "postgres:18.4@sha256:3a82e1f56c8f0f5616a11103ac3d47e632c3938698946a7ad26da0df1334744a")
|
|
assert fixed(
|
|
"postgres",
|
|
"postgres:18.6@sha256:0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04",
|
|
)
|
|
assert fixed("redis", "redis:8.10.0@sha256:c29e49ab2f85760a3827b53882e6dd9f5c6c3f0bb7d724e07bb31cbf275a5236")
|
|
assert fixed("redis", "redis:8.10.2@sha256:7ef5b5cec96495a04ca7feff88a9492efeab8053fb284d24bdd73344c9245a48")
|
|
assert not fixed("postgres", "postgres:18.6@sha256:" + "a" * 64)
|
|
assert not fixed("redis", "redis:8.10.3@sha256:" + "b" * 64)
|
|
for service in ("web", "control-api", "dataplane-router", "connector-ingestion"):
|
|
assert fixed(service, "sha256:" + "a" * 64)
|
|
for service in ("postgres", "redis", "contextforge", "ingestion-clamd", "foreign"):
|
|
assert not fixed(service, "sha256:" + "a" * 64)
|
|
|
|
namespace["main_declares_external_image"] = lambda _image_ref: False
|
|
assert not fixed("postgres", "postgres:18.6@sha256:0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04")
|
|
assert not fixed("redis", "redis:8.10.2@sha256:7ef5b5cec96495a04ca7feff88a9492efeab8053fb284d24bdd73344c9245a48")
|
|
|
|
|
|
@pytest.fixture
|
|
def contextforge_reconciliation_proof():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
parsed = ast.parse(fake.calls[0][2]["json"]["Cmd"][2])
|
|
functions = [
|
|
node for node in parsed.body
|
|
if isinstance(node, ast.FunctionDef)
|
|
and node.name in {
|
|
"main_declares_contextforge_compat_image", "fixed_image_reference",
|
|
"receipt_bound_image_reference",
|
|
}
|
|
]
|
|
revision = "a" * 40
|
|
main_revision = "b" * 40
|
|
manifest = (
|
|
f"CONTEXTFORGE_UPSTREAM_COMMIT={'c' * 40}\n"
|
|
"CONTEXTFORGE_ALEMBIC_HEAD=123456789abc\n"
|
|
"CONTEXTFORGE_MCP_SDK=1.2.3\n"
|
|
"CONTEXTFORGE_LOCAL_IMAGE=itworx-mcp-hub/contextforge-schema:123456789abc\n"
|
|
)
|
|
builder = (
|
|
'git -C "$SRC" fetch --depth 1 origin "$CONTEXTFORGE_UPSTREAM_COMMIT"\n'
|
|
'git -C "$SRC" checkout --detach FETCH_HEAD\n'
|
|
'MCP_REQUIREMENT="mcp==${CONTEXTFORGE_MCP_SDK}"\n'
|
|
'alembic -c mcpgateway/alembic.ini heads\n'
|
|
'docker tag "$STAGED_IMAGE" "$CONTEXTFORGE_LOCAL_IMAGE"\n'
|
|
'--release-candidate\n'
|
|
'CANDIDATE_IMAGE="itworx-mcp-hub/contextforge-schema:${APPOPS_SOURCE_REVISION}"\n'
|
|
'docker tag "$STAGED_IMAGE" "$CANDIDATE_IMAGE"\n'
|
|
)
|
|
files = {
|
|
f"{commit}:infrastructure/images/contextforge/compat-image.env": manifest
|
|
for commit in (revision, main_revision)
|
|
}
|
|
files.update({
|
|
f"{commit}:scripts/build_contextforge_compat_image.sh": builder
|
|
for commit in (revision, main_revision)
|
|
})
|
|
|
|
def git_run(*args):
|
|
assert args[0] == "show"
|
|
return SimpleNamespace(returncode=0 if args[1] in files else 1,
|
|
stdout=files.get(args[1], ""))
|
|
|
|
namespace = {"re": re, "git_run": git_run, "main_revision": main_revision,
|
|
"recorded_live": revision}
|
|
exec( # noqa: S102 -- Execute only extracted repository-owned verifier functions.
|
|
compile(ast.Module(body=functions, type_ignores=[]), "contextforge-proof", "exec"), namespace
|
|
)
|
|
return SimpleNamespace(namespace=namespace, files=files, revision=revision)
|
|
|
|
|
|
def test_contextforge_reconciliation_accepts_exact_receipt_revision(contextforge_reconciliation_proof):
|
|
proof = contextforge_reconciliation_proof
|
|
namespace = proof.namespace
|
|
fixed = namespace["fixed_image_reference"]
|
|
reference = f"itworx-mcp-hub/contextforge-schema:{proof.revision}"
|
|
image_id = "sha256:" + "d" * 64
|
|
record = {"image_id": image_id, "image_ref": reference}
|
|
bound = namespace["receipt_bound_image_reference"](image_id, image_id, record, dict(record))
|
|
assert fixed("contextforge", bound)
|
|
assert fixed("contextforge", "itworx-mcp-hub/contextforge-schema:123456789abc")
|
|
# A later main may remove the candidate builder; the recorded receipt's
|
|
# ancestor revision must supply the source proof for this immutable image.
|
|
proof.files[f"{'b' * 40}:scripts/build_contextforge_compat_image.sh"] = ""
|
|
assert fixed("contextforge", bound)
|
|
|
|
|
|
@pytest.mark.parametrize("mutation", [
|
|
"wrong_revision", "short_revision", "mutable_tag", "wrong_service",
|
|
"missing_candidate_flag", "missing_candidate_assignment", "missing_candidate_tag",
|
|
"missing_fetch", "bad_manifest", "missing_recorded_source",
|
|
])
|
|
def test_contextforge_reconciliation_rejects_unbound_candidates(contextforge_reconciliation_proof, mutation):
|
|
proof = contextforge_reconciliation_proof
|
|
reference = f"itworx-mcp-hub/contextforge-schema:{proof.revision}"
|
|
service = "contextforge"
|
|
builder_path = f"{proof.revision}:scripts/build_contextforge_compat_image.sh"
|
|
fragments = {
|
|
"missing_candidate_flag": '--release-candidate',
|
|
"missing_candidate_assignment": 'CANDIDATE_IMAGE="itworx-mcp-hub/contextforge-schema:${APPOPS_SOURCE_REVISION}"',
|
|
"missing_candidate_tag": 'docker tag "$STAGED_IMAGE" "$CANDIDATE_IMAGE"',
|
|
"missing_fetch": 'git -C "$SRC" fetch --depth 1 origin "$CONTEXTFORGE_UPSTREAM_COMMIT"',
|
|
}
|
|
if mutation == "wrong_revision":
|
|
reference = f"itworx-mcp-hub/contextforge-schema:{'b' * 40}"
|
|
elif mutation == "short_revision":
|
|
reference = f"itworx-mcp-hub/contextforge-schema:{proof.revision[:12]}"
|
|
elif mutation == "mutable_tag":
|
|
reference = "itworx-mcp-hub/contextforge-schema:latest"
|
|
elif mutation == "wrong_service":
|
|
service = "foreign"
|
|
elif mutation in fragments:
|
|
proof.files[builder_path] = proof.files[builder_path].replace(fragments[mutation], "")
|
|
elif mutation == "bad_manifest":
|
|
path = f"{proof.revision}:infrastructure/images/contextforge/compat-image.env"
|
|
proof.files[path] = proof.files[path].replace("CONTEXTFORGE_ALEMBIC_HEAD=123456789abc", "CONTEXTFORGE_ALEMBIC_HEAD=invalid")
|
|
else:
|
|
proof.files.pop(builder_path)
|
|
assert not proof.namespace["fixed_image_reference"](service, reference)
|
|
|
|
|
|
def test_digest_attestation_normalizes_tagged_docker_hub_repo_digests():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
parsed = ast.parse(script)
|
|
function = next(
|
|
node for node in parsed.body
|
|
if isinstance(node, ast.FunctionDef) and node.name == "image_digest_matches"
|
|
)
|
|
namespace = {"re": re}
|
|
exec(compile(ast.Module(body=[function], type_ignores=[]), "reconcile-test", "exec"), namespace)
|
|
matches = namespace["image_digest_matches"]
|
|
digest = "sha256:" + "a" * 64
|
|
|
|
assert matches({"RepoDigests": [f"postgres@{digest}"]}, f"postgres:18.6@{digest}")
|
|
assert matches({"RepoDigests": [f"docker.io/library/redis@{digest}"]}, f"redis:8.10.2@{digest}")
|
|
assert matches({"RepoDigests": [f"clamav/clamav@{digest}"]}, f"clamav/clamav:1.4.6@{digest}")
|
|
assert matches(
|
|
{"RepoDigests": [f"registry.example:5000/team/web@{digest}"]},
|
|
f"registry.example:5000/team/web:release@{digest}",
|
|
)
|
|
assert not matches(
|
|
{"RepoDigests": [f"redis@sha256:{'b' * 64}"]}, f"redis:8.10.2@{digest}"
|
|
)
|
|
assert not matches({"RepoDigests": [f"foreign/redis@{digest}"]}, f"redis:8.10.2@{digest}")
|
|
|
|
|
|
def test_id_launched_image_uses_exact_state_and_receipt_reference():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
parsed = ast.parse(script)
|
|
function = next(
|
|
node for node in parsed.body
|
|
if isinstance(node, ast.FunctionDef) and node.name == "receipt_bound_image_reference"
|
|
)
|
|
namespace = {"re": re}
|
|
exec( # noqa: S102
|
|
compile(ast.Module(body=[function], type_ignores=[]), "receipt-image-test", "exec"),
|
|
namespace,
|
|
)
|
|
bound_reference = namespace["receipt_bound_image_reference"]
|
|
image_id = "sha256:" + "a" * 64
|
|
image_ref = (
|
|
"postgres:18.6@sha256:"
|
|
"0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04"
|
|
)
|
|
recorded = {"image_id": image_id, "image_ref": image_ref}
|
|
receipt_item = dict(recorded)
|
|
|
|
assert bound_reference(image_id, image_id, recorded, receipt_item) == image_ref
|
|
assert bound_reference(image_ref, image_id, recorded, receipt_item) == image_ref
|
|
assert 'image_ref = receipt_bound_image_reference(' in script
|
|
assert script.index('image_ref = receipt_bound_image_reference(') < script.index(
|
|
'if not fixed_image_reference(name_service, image_ref):'
|
|
)
|
|
assert 'image_digest_matches(image, image_ref)' in script
|
|
|
|
for launch_image, live_id, state, receipt in (
|
|
("sha256:" + "c" * 64, image_id, recorded, receipt_item),
|
|
(
|
|
image_id, image_id,
|
|
{**recorded, "image_id": "sha256:" + "c" * 64}, receipt_item,
|
|
),
|
|
(
|
|
image_id, image_id,
|
|
recorded, {**receipt_item, "image_id": "sha256:" + "c" * 64},
|
|
),
|
|
(
|
|
image_id, image_id,
|
|
{**recorded, "image_ref": "redis:8.10.2@sha256:" + "b" * 64},
|
|
receipt_item,
|
|
),
|
|
(image_id, image_id, recorded, {**receipt_item, "image_ref": ""}),
|
|
(image_id, "arbitrary-image-id", recorded, receipt_item),
|
|
(image_id, image_id,
|
|
{**recorded, "image_ref": "sha256:" + "b" * 64},
|
|
{**receipt_item, "image_ref": "sha256:" + "b" * 64}),
|
|
):
|
|
with pytest.raises(SystemExit) as exc:
|
|
bound_reference(launch_image, live_id, state, receipt)
|
|
assert exc.value.code == 52
|
|
|
|
|
|
def test_reconciliation_reports_invalid_state_and_live_evidence_fail_closed():
|
|
expected = {
|
|
42: ("validation_failed", "MCP Hub deployment state file is unavailable or invalid"),
|
|
45: ("validation_failed", "a live MCP Hub container is not running and healthy"),
|
|
47: ("validation_failed", "live MCP Hub Compose project differs from the controller target"),
|
|
51: ("validation_failed", "live MCP Hub image revision or external immutable image is not proven by the recorded-live to main chain"),
|
|
52: ("validation_failed", "controller state or live facts do not match the exact modern MCP Hub receipt"),
|
|
56: ("topology_migration_required", "MCP Hub legacy, unknown, mixed, or unresolved migration state requires guarded deployment or data-aware recovery"),
|
|
}
|
|
for exit_code, (status, reason) in expected.items():
|
|
result = McpHubDeploymentReconciliation(FakePortainer(exit_code=exit_code)).run()
|
|
assert result["status"] == status
|
|
assert result["reason"] == reason
|
|
|
|
|
|
def test_reconciliation_only_allows_fixed_17_topology_and_rejects_registry_drift():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert 'project.get("branch") != "main"' in script
|
|
assert 'project.get("slug") != expected_compose_project' in script
|
|
assert 'project.get("compose_project") != expected_compose_project' in script
|
|
assert 'frozenset(expected_containers) not in {frozenset(expected_fixed), frozenset(expected_runtime)}' in script
|
|
assert 'set(expected_containers) != expected_runtime' in script
|
|
assert 'set(project_names) != set(expected_containers)' in script
|
|
assert '"itworx-mcp-hub-connector-ingestion-1"' in script
|
|
assert '"itworx-mcp-hub-ingestion-clamd-1"' in script
|