302 lines
14 KiB
Python
302 lines
14 KiB
Python
from types import SimpleNamespace
|
|
|
|
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_to_mcp_hub_and_reconciles_only_verified_failed_forward_state():
|
|
fake = FakePortainer()
|
|
result = McpHubDeploymentReconciliation(fake).run()
|
|
|
|
assert result == {
|
|
"status": "reconciled",
|
|
"repository": "jens/itworx_mcp_hub",
|
|
"composeProject": "itworx-mcp-hub",
|
|
}
|
|
|
|
create = fake.calls[0]
|
|
command = create[2]["json"]["Cmd"]
|
|
assert command[:2] == ["python3", "-c"]
|
|
assert command[3:] == ["jens/itworx_mcp_hub", "itworx-mcp-hub"]
|
|
assert create[2]["json"]["AttachStdout"] is False
|
|
assert create[2]["json"]["AttachStderr"] is False
|
|
|
|
script = command[2]
|
|
assert "/controller-config/projects.json" in script
|
|
assert "/controller-hub-releases/controller-config/projects.json" in script
|
|
assert "os.path.samefile(registry_path, host_registry_path)" in script
|
|
assert "/mnt/cache/appdata/gitea-deploy-runners/state" in script
|
|
assert "/srv/example/appdata/gitea-deploy-runners/deploy.lock" not in script
|
|
assert "/mnt/cache/appdata/gitea-deploy-runners/deploy.lock" in script
|
|
assert 'project.get("branch") != "main"' in script
|
|
assert 'project.get("compose_project") != expected_compose_project' in script
|
|
assert 'recorded_status != "failed"' in script
|
|
assert 'recorded_live_status not in {"healthy", "unknown"}' in script
|
|
assert 'attempt_status != "failed"' in script
|
|
assert 'attempt_revision == recorded_live' in script
|
|
assert 'git_run("merge-base", "--is-ancestor", recorded_live, attempt_revision)' in script
|
|
assert 'git_run("merge-base", "--is-ancestor", attempt_revision, main_revision)' in script
|
|
|
|
|
|
def test_unknown_live_status_is_accepted_only_for_a_proven_snapshot_stage_failure_without_rollback_error():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert 'last_attempt_message = str(last_attempt.get("message") or "")' in script
|
|
assert "snapshot_failure_prefixes = (" in script
|
|
assert 'recorded_live_status == "unknown"' in script
|
|
assert 'last_attempt_message.startswith(snapshot_failure_prefixes)' in script
|
|
assert '"; rollback error:" in last_attempt_message' in script
|
|
assert '"; source rollback error:" in last_attempt_message' in script
|
|
assert "raise SystemExit(55)" in script
|
|
|
|
|
|
def test_reconciliation_proves_complete_live_compose_topology_and_preserves_actual_image_ids():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert 'expected_containers = project.get("containers")' in script
|
|
assert "set(project_names) != set(expected_containers)" in script
|
|
assert 'labels.get("com.docker.compose.project") != expected_compose_project' in script
|
|
assert 'not runtime_state.get("Running")' in script
|
|
assert 'health in {"starting", "unhealthy"}' in script
|
|
assert 'image_id = str(container.get("Image") or "")' in script
|
|
assert '"containers": container_artifacts' in script
|
|
assert '"compose_sha256": ""' in script
|
|
assert '"topology_contract": topology_contract' in script
|
|
assert '"live_legacy_provenance": True' in script
|
|
assert '"live_artifacts": artifacts' in script
|
|
assert '"live_revision": recorded_live' in script
|
|
|
|
|
|
def test_reconciliation_recognizes_only_the_fixed_mcp_hub_oci_source_as_owned():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert 'owned_image_source = "https://gitea.example.com/jens/itworx-mcp-hub"' in script
|
|
assert "if image_source == owned_image_source:" in script
|
|
assert 'git_run("merge-base", "--is-ancestor", recorded_live, image_revision)' in script
|
|
assert 'git_run("merge-base", "--is-ancestor", image_revision, main_revision)' in script
|
|
assert 'image_source.startswith("https://gitea.example.com/")' not in script
|
|
assert 'image_source == repository' not in script
|
|
assert '"live_revision": main_revision' not in script
|
|
assert '"live_revision": attempt_revision' not in script
|
|
|
|
|
|
def test_reconciliation_requires_the_controller_17_service_topology_migration():
|
|
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 15-to-17 topology migration requires its guarded protected-main deployment",
|
|
}
|
|
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
assert 'topology_contract = "itworx.mcp-hub-topology/17-v1"' in script
|
|
assert 'recorded_artifacts.get("topology_contract") == topology_contract' in script
|
|
assert '"topology_contract": topology_contract' in script
|
|
|
|
|
|
def test_owned_image_ancestry_can_fail_closed_to_the_canonical_gitea_store():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert 'canonical_repository_root = Path("/mnt/cache/appdata/gitea/git/repositories")' in script
|
|
assert 'canonical_bare_repo = canonical_repository_root / owner / f"{name}.git"' in script
|
|
assert "def canonical_git_run(*args):" in script
|
|
assert "def canonical_image_ancestry_is_proven(" in script
|
|
assert 'resolved_recorded = canonical_git_run("rev-parse", f"{recorded_revision}^{{commit}}")' in script
|
|
assert 'resolved_image = canonical_git_run("rev-parse", f"{image_revision}^{{commit}}")' in script
|
|
assert 'resolved_main = canonical_git_run("rev-parse", "refs/heads/main")' in script
|
|
assert 'resolved_main.stdout.strip() == expected_main' in script
|
|
assert '"merge-base", "--is-ancestor", recorded_revision, image_revision' in script
|
|
assert '"merge-base", "--is-ancestor", image_revision, expected_main' in script
|
|
assert "if not mounted_ancestry_proven and not canonical_image_ancestry_is_proven(" in script
|
|
assert "canonical_bare_repo.is_symlink()" in script
|
|
|
|
|
|
def test_unchanged_recorded_image_ids_do_not_reinterpret_historical_oci_revision_labels():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
change_probe = "image_changed = image_id != recorded_image_id"
|
|
provenance_gate = "if image_changed:"
|
|
compat_probe = "contextforge_compat_image = main_declares_contextforge_compat_image(name, image_ref)"
|
|
dataplane_probe = "main_declares_sessionless_dataplane_image(name, image_ref)"
|
|
owned_probe = "elif image_source == owned_image_source:"
|
|
external_probe = "elif not main_declares_external_image(image_ref):"
|
|
|
|
assert change_probe in script
|
|
assert provenance_gate in script
|
|
assert compat_probe in script
|
|
assert dataplane_probe in script
|
|
assert "not already_reconciled" in script
|
|
assert owned_probe in script
|
|
assert external_probe in script
|
|
assert script.index(change_probe) < script.index(provenance_gate)
|
|
assert script.index(provenance_gate) < script.index(compat_probe)
|
|
assert script.index(compat_probe) < script.index(dataplane_probe)
|
|
assert script.index(dataplane_probe) < script.index(owned_probe)
|
|
assert script.index(owned_probe) < script.index(external_probe)
|
|
assert "if image_id != recorded_image_id or container_id != recorded_container_id:" in script
|
|
|
|
|
|
def test_external_image_drift_requires_an_immutable_digest_declared_in_reviewed_main_compose():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert "def main_declares_external_image(image_ref):" in script
|
|
assert "@sha256:" in script
|
|
assert 'git_run("show", f"{main_revision}:{compose_path}")' in script
|
|
assert '"infrastructure/compose/docker-compose.yml"' in script
|
|
assert '"infrastructure/compose/docker-compose.prod.yml"' in script
|
|
assert 'line.strip() in {f"image: {image_ref}", f\'image: "{image_ref}"\', f"image: \'{image_ref}\'"}' in script
|
|
assert "main_declares_external_image(image_ref)" in script
|
|
assert "elif not main_declares_external_image(image_ref):" in script
|
|
|
|
|
|
def test_contextforge_compat_image_drift_requires_exact_reviewed_main_contract():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert 'container_name != "itworx-mcp-hub-contextforge-1"' in script
|
|
assert 'f"{main_revision}:infrastructure/images/contextforge/compat-image.env"' in script
|
|
assert 'f"{main_revision}:scripts/build_contextforge_compat_image.sh"' in script
|
|
assert '"CONTEXTFORGE_UPSTREAM_COMMIT"' in script
|
|
assert '"CONTEXTFORGE_ALEMBIC_HEAD"' in script
|
|
assert '"CONTEXTFORGE_MCP_SDK"' in script
|
|
assert '"CONTEXTFORGE_LOCAL_IMAGE"' in script
|
|
assert 're.fullmatch(r"[0-9a-f]{40}", upstream_commit)' in script
|
|
assert 're.fullmatch(r"[0-9a-f]{12}", alembic_head)' in script
|
|
assert 'local_image != f"itworx-mcp-hub/contextforge-schema:{alembic_head}"' in script
|
|
assert 'git -C "$SRC" fetch --depth 1 origin "$CONTEXTFORGE_UPSTREAM_COMMIT"' in script
|
|
assert 'git -C "$SRC" checkout --detach FETCH_HEAD' in script
|
|
assert 'MCP_REQUIREMENT="mcp==${CONTEXTFORGE_MCP_SDK}"' in script
|
|
assert 'alembic -c mcpgateway/alembic.ini heads' in script
|
|
assert 'docker tag "$STAGED_IMAGE" "$CONTEXTFORGE_LOCAL_IMAGE"' in script
|
|
assert 'image_ref.startswith("itworx-mcp-hub/contextforge-schema:")' not in script
|
|
|
|
|
|
compat_probe = "contextforge_compat_image = main_declares_contextforge_compat_image(name, image_ref)"
|
|
owned_probe = "elif image_source == owned_image_source:"
|
|
assert compat_probe in script
|
|
assert "if contextforge_compat_image or sessionless_dataplane_image:" in script
|
|
assert owned_probe in script
|
|
assert script.index(compat_probe) < script.index(owned_probe)
|
|
|
|
|
|
def test_sessionless_dataplane_baseline_is_one_time_and_commit_bound():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert 'container_name != "itworx-mcp-hub-dataplane-router-1"' in script
|
|
assert 'image_ref != "itworx-mcp-hub/dataplane:sessionless-final"' in script
|
|
assert 'sessionless_commit = "b519a8c4cf17cd9df00a575acfe096838aab7789"' in script
|
|
assert 'f"{sessionless_commit}:services/dataplane_router/router.py"' in script
|
|
assert 'f"{sessionless_commit}:tests/test_dataplane_session_headers.py"' in script
|
|
assert 'f"{main_revision}:evidence/mcp-session-boundary-20260916/SUMMARY.md"' in script
|
|
assert 'MCP_TRANSPORT_STATE_HEADERS = frozenset({b"mcp-session-id"})' in script
|
|
assert 'del response.headers["mcp-session-id"]' in script
|
|
assert '"2025-11-25"' in script
|
|
assert '"2026-07-28"' in script
|
|
assert '"permanently enforce stateless public transport"' in script
|
|
assert (
|
|
'git_run("merge-base", "--is-ancestor", sessionless_commit, main_revision)'
|
|
in script
|
|
)
|
|
assert "not already_reconciled" in script
|
|
|
|
|
|
def test_already_reconciled_state_is_reverified_before_idempotent_success():
|
|
fake = FakePortainer()
|
|
McpHubDeploymentReconciliation(fake).run()
|
|
script = fake.calls[0][2]["json"]["Cmd"][2]
|
|
|
|
assert "already_reconciled = (" in script
|
|
assert script.index("already_reconciled = (") < script.index("project_names =")
|
|
assert script.index("container_artifacts = {}") < script.index("if already_reconciled:")
|
|
assert 'print("already-reconciled", flush=True)' in script
|
|
assert "if drift_found:" in script
|
|
|
|
|
|
def test_reconciliation_refuses_unknown_runtime_topology():
|
|
result = McpHubDeploymentReconciliation(FakePortainer(exit_code=47)).run()
|
|
assert result == {
|
|
"status": "validation_failed",
|
|
"reason": "live MCP Hub Compose project differs from the controller target",
|
|
}
|
|
|
|
|
|
def test_reconciliation_refuses_unproven_runtime_revision_drift():
|
|
result = McpHubDeploymentReconciliation(FakePortainer(exit_code=51)).run()
|
|
assert result == {
|
|
"status": "validation_failed",
|
|
"reason": "live MCP Hub image revision or external immutable image is not proven by the recorded-live to main chain",
|
|
}
|
|
|
|
|
|
def test_reconciliation_refuses_state_that_is_not_the_known_failed_forward_shape():
|
|
result = McpHubDeploymentReconciliation(FakePortainer(exit_code=52)).run()
|
|
assert result == {
|
|
"status": "validation_failed",
|
|
"reason": "controller state drift is not the exact known recoverable MCP Hub state",
|
|
}
|
|
|
|
|
|
def test_reconciliation_refuses_unknown_state_without_snapshot_stage_proof():
|
|
result = McpHubDeploymentReconciliation(FakePortainer(exit_code=55)).run()
|
|
assert result == {
|
|
"status": "validation_failed",
|
|
"reason": "unknown MCP Hub live status was not produced by a proven recoverable snapshot-stage failure",
|
|
}
|