Files
ITWorx_MCP_Hub-Public/tests/test_catalog.py
T

426 lines
18 KiB
Python

from __future__ import annotations
import fnmatch
import json
import tempfile
import unittest
from pathlib import Path
from typing import Any
from uuid import uuid4
from fastapi import FastAPI
from fastapi.testclient import TestClient
from services.control_api.catalog.loader import CatalogLoader
from services.control_api.catalog.reconciler import CatalogReconciler
from services.control_api.catalog.router import build_catalog_router
from services.control_api.catalog.store import MemoryReconcileRunStore
from services.control_api.contextforge.client import ContextForgeClient
from services.control_api.contextforge.fake import FakeContextForge
from services.control_api.contextforge.models import ContextForgeSettings
ROOT = Path(__file__).resolve().parents[1]
def adapter_for(fake: FakeContextForge) -> ContextForgeClient:
return ContextForgeClient(
ContextForgeSettings(base_url="https://gateway.invalid", auth_token_env="TOKEN"), # noqa: S106
transport=fake.transport(),
environ={"TOKEN": "fake-token"},
)
class CatalogLoaderTests(unittest.TestCase):
def test_schema_store_loads_once_per_catalog_load_without_changing_validation(self) -> None:
class CountingCatalogLoader(CatalogLoader):
store_loads = 0
def _schema_store(self) -> dict[str, Any]:
self.store_loads += 1
return super()._schema_store()
loader = CountingCatalogLoader(ROOT / "catalog")
report = loader.load()
self.assertTrue(report.valid, report.issues)
self.assertEqual(len(report.documents), 281)
self.assertEqual(report.issues, ())
self.assertEqual(loader.store_loads, 1)
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "catalog"
project_dir = root / "projects"
project_dir.mkdir(parents=True)
payload = json.loads(
(ROOT / "catalog/projects/general-infrastructure.yaml").read_text(encoding="utf-8")
)
payload["spec"]["virtualServers"][0]["endpointAlias"] = "not-an-alias"
payload["spec"]["capabilityPacks"] = []
payload["spec"]["connectorBindings"] = []
payload["spec"]["credentialToken"] = "plain-secret-value"
(project_dir / "bad.yaml").write_text(json.dumps(payload), encoding="utf-8")
invalid_loader = CountingCatalogLoader(root, schema_root=ROOT / "contracts/schemas")
invalid_report = invalid_loader.load()
self.assertFalse(invalid_report.valid)
self.assertEqual(
{
(issue.code, issue.source.file, issue.source.pointer)
for issue in invalid_report.issues
},
{
(
"schema_error",
"projects/bad.yaml",
"/spec/virtualServers/0/endpointAlias",
),
("schema_error", "projects/bad.yaml", "/spec/capabilityPacks"),
("schema_error", "projects/bad.yaml", "/spec"),
("secret_value", "projects/bad.yaml", "/spec/credentialToken"),
},
)
self.assertEqual(invalid_loader.store_loads, 1)
self.assertNotIn(
"plain-secret-value",
json.dumps([document.payload for document in invalid_report.documents]),
)
def test_committed_catalog_validates_without_network_or_secret_values(self) -> None:
report = CatalogLoader(ROOT / "catalog").load()
self.assertTrue(report.valid, report.issues)
self.assertEqual(len(report.documents), 281)
self.assertEqual(len(report.issues), 0)
self.assertIsNotNone(report.desired_hash)
def test_openchamber_catalog_stops_at_external_codex_review(self) -> None:
project = json.loads(
(ROOT / "catalog/projects/openchamber-agent.yaml").read_text(encoding="utf-8")
)
allowed = project["spec"]["virtualServers"][0]["toolAllowPatterns"]
manifests = {
json.loads(path.read_text(encoding="utf-8"))["spec"]["canonicalName"]
for path in (ROOT / "catalog/tools").glob("*.yaml")
}
self.assertNotIn("opencode.delivery.control.merge", allowed)
self.assertNotIn("opencode.delivery.control.merge", manifests)
self.assertIn("opencode.delivery.control.result", allowed)
def test_stitch_controlled_writes_are_exact_and_policy_bound(self) -> None:
policy = json.loads(
(ROOT / "catalog/policies/042-stitch-governed-design-writes.yaml").read_text(
encoding="utf-8"
)
)
match = policy["spec"]["match"]
self.assertEqual(match["project"], "stitch-design")
self.assertEqual(match["connectors"], ["stitch"])
self.assertEqual(match["riskClasses"], ["R2"])
self.assertEqual(
set(match["toolPatterns"]),
{
"stitch.project.create",
"stitch.screen.edit",
"stitch.screen.generate",
"stitch.screen.variants",
},
)
self.assertTrue(policy["spec"]["conditions"]["requireEnabledClient"])
self.assertTrue(policy["spec"]["conditions"]["requireBinding"])
def test_gitea_analysis_profile_stays_small_and_job_only(self) -> None:
profile = json.loads(
(ROOT / "catalog/projects/gitea-analysis.yaml").read_text(encoding="utf-8")
)
server = profile["spec"]["virtualServers"][0]
self.assertEqual(server["endpointAlias"], "/mcp/gitea-analysis/prod")
self.assertEqual(
server["toolAllowPatterns"],
[
"projectbrain.analysis.start",
"projectbrain.analysis.list",
"projectbrain.analysis.status",
"projectbrain.analysis.results",
"projectbrain.analysis.cancel",
"projectbrain.analysis.render",
],
)
def test_gitea_loop_profile_is_focused_and_closed_loop_complete(self) -> None:
profile = json.loads(
(ROOT / "catalog/projects/gitea-loop.yaml").read_text(encoding="utf-8")
)
server = profile["spec"]["virtualServers"][0]
self.assertEqual(server["endpointAlias"], "/mcp/gitea-loop/prod")
self.assertLessEqual(len(server["toolAllowPatterns"]), 16)
self.assertTrue(
{
"gitea.repository.create",
"gitea.change.review",
"gitea.change.reconcile",
"gitea.validation.failure.get",
"gitea.pullrequest.sync",
"gitea.pullrequest.merge",
"gitea.pullrequest.validation.ensure",
"gitea.workspace.status",
"gitea.workspace.run",
"projectbrain.*",
}.issubset(server["toolAllowPatterns"])
)
self.assertEqual(len(server["toolAllowPatterns"]), 16)
self.assertIn("gitea-controlled-writer", profile["spec"]["capabilityPacks"])
self.assertEqual(
{binding["ref"] for binding in profile["spec"]["connectorBindings"]},
{"gitea-default", "projectbrain-default"},
)
def test_gitea_loop_tools_have_explicit_browser_safe_manifests(self) -> None:
profile = json.loads(
(ROOT / "catalog/projects/gitea-loop.yaml").read_text(encoding="utf-8")
)
patterns = set(profile["spec"]["virtualServers"][0]["toolAllowPatterns"])
manifests = {}
for path in (ROOT / "catalog/tools").glob("*.yaml"):
document = json.loads(path.read_text(encoding="utf-8"))
manifests[document["spec"]["canonicalName"]] = document["spec"]
allowed = {
name
for name in manifests
if any(fnmatch.fnmatchcase(name, pattern) for pattern in patterns)
}
explicit = {pattern for pattern in patterns if not any(char in pattern for char in "*?[")}
self.assertEqual(explicit - manifests.keys(), set())
self.assertTrue({name for name in manifests if name.startswith("projectbrain.")} <= allowed)
for name in allowed:
limits = manifests[name]["limits"]
self.assertLessEqual(limits["maxOutputBytes"], 524_288, name)
self.assertLessEqual(limits["maxItems"], 100, name)
for name in ("gitea.workspace.status", "projectbrain.analysis.status"):
self.assertIn("pollAfterMs", manifests[name]["description"])
def test_server_ops_profile_is_small_and_appops_only(self) -> None:
profile = json.loads(
(ROOT / "catalog/projects/server-ops.yaml").read_text(encoding="utf-8")
)
server = profile["spec"]["virtualServers"][0]
self.assertEqual(server["endpointAlias"], "/mcp/server-ops/prod")
self.assertEqual(server["toolAllowPatterns"], ["server.*"])
self.assertEqual(profile["spec"]["capabilityPacks"], ["appops-operator"])
self.assertEqual(
{binding["ref"] for binding in profile["spec"]["connectorBindings"]},
{"appops-default"},
)
def test_invalid_field_and_secret_report_file_and_pointer(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "catalog"
project_dir = root / "projects"
project_dir.mkdir(parents=True)
payload = json.loads(
(ROOT / "catalog/projects/general-infrastructure.yaml").read_text()
)
payload["spec"]["virtualServers"][0]["endpointAlias"] = "not-an-alias"
payload["spec"]["credentialToken"] = "plain-secret-value"
(project_dir / "bad.yaml").write_text(json.dumps(payload), encoding="utf-8")
report = CatalogLoader(root, schema_root=ROOT / "contracts/schemas").load()
self.assertFalse(report.valid)
self.assertTrue(
any(
issue.code == "schema_error" and issue.source.file == "projects/bad.yaml"
for issue in report.issues
)
)
self.assertTrue(
any(
issue.code == "secret_value" and issue.source.pointer == "/spec/credentialToken"
for issue in report.issues
)
)
self.assertNotIn(
"plain-secret-value", json.dumps([document.payload for document in report.documents])
)
def test_duplicate_identity_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "catalog/projects"
root.mkdir(parents=True)
source = json.loads((ROOT / "catalog/projects/general-infrastructure.yaml").read_text())
(root / "one.yaml").write_text(json.dumps(source), encoding="utf-8")
(root / "two.yaml").write_text(json.dumps(source), encoding="utf-8")
report = CatalogLoader(root.parent, schema_root=ROOT / "contracts/schemas").load()
self.assertFalse(report.valid)
self.assertTrue(any(issue.code == "duplicate_identity" for issue in report.issues))
def test_semantically_identical_key_order_has_same_hash(self) -> None:
with (
tempfile.TemporaryDirectory() as first_directory,
tempfile.TemporaryDirectory() as second_directory,
):
first_root = Path(first_directory) / "catalog"
second_root = Path(second_directory) / "catalog"
import shutil
shutil.copytree(ROOT / "catalog", first_root)
shutil.copytree(ROOT / "catalog", second_root)
source = json.loads((ROOT / "catalog/projects/general-infrastructure.yaml").read_text())
(first_root / "projects/general-infrastructure.yaml").write_text(
json.dumps(source), encoding="utf-8"
)
(second_root / "projects/general-infrastructure.yaml").write_text(
json.dumps(source, sort_keys=True), encoding="utf-8"
)
first = CatalogLoader(first_root, schema_root=ROOT / "contracts/schemas").load()
second = CatalogLoader(second_root, schema_root=ROOT / "contracts/schemas").load()
self.assertTrue(first.valid, first.issues)
self.assertEqual(first.desired_hash, second.desired_hash)
class CatalogReconcilerTests(unittest.TestCase):
def setUp(self) -> None:
self.fake = FakeContextForge()
self.adapter = adapter_for(self.fake)
self.loader = CatalogLoader(ROOT / "catalog", public_base_url="https://mcp.example.com")
self.reconciler = CatalogReconciler(
self.loader, self.adapter, store=MemoryReconcileRunStore()
)
self.workspace_id = uuid4()
self.project_id = uuid4()
def tearDown(self) -> None:
self.adapter.close()
def test_apply_then_apply_again_is_noop(self) -> None:
first = self.reconciler.apply(self.workspace_id, self.project_id)
self.assertEqual(first.status, "succeeded")
self.assertTrue(any(item["action"] == "create" for item in first.applied_changes))
requests_after_first = len(self.fake.requests)
second = self.reconciler.apply(self.workspace_id, self.project_id)
self.assertEqual(second.status, "succeeded")
self.assertEqual(second.applied_changes, ())
self.assertEqual(len(self.fake.requests), requests_after_first + 2)
def test_partial_gateway_failure_is_truthful_and_retryable(self) -> None:
class FailingAdapter:
def __init__(self, client: ContextForgeClient) -> None:
self.client = client
self.failed = False
def list_gateways(self, **kwargs: object):
return self.client.list_gateways(**kwargs)
def ensure_gateway(self, desired):
if not self.failed:
self.failed = True
raise RuntimeError("simulated partial failure")
return self.client.ensure_gateway(desired)
failing = FailingAdapter(self.adapter)
failing_reconciler = CatalogReconciler(
self.loader, failing, store=MemoryReconcileRunStore()
)
failed = failing_reconciler.apply(self.workspace_id, self.project_id)
self.assertEqual(failed.status, "failed")
self.assertTrue(failed.verification["resumable"])
self.assertEqual(failed.applied_changes[0]["status"], "failed")
recovered = failing_reconciler.apply(self.workspace_id, self.project_id)
self.assertEqual(recovered.status, "succeeded")
def test_unknown_gateway_is_drift_and_not_deleted(self) -> None:
from services.control_api.contextforge.models import GatewayDesiredState
self.adapter.ensure_gateway(
GatewayDesiredState(
slug="operator-owned", url="https://example.com/mcp", description="unknown"
)
)
plan = self.reconciler.preview()
self.assertTrue(any(item.slug == "operator-owned" for item in plan.drift))
self.reconciler.apply(self.workspace_id, self.project_id)
self.assertIn("operator-owned", {item["name"] for item in self.fake.gateways.values()})
def test_lock_recovers_after_exception_and_authoritative_run_store_survives_restart(
self,
) -> None:
from datetime import UTC, datetime
from packages.hub_core.domain import Project, ReconcileRun, Scope, Workspace
from services.control_api.catalog.reconciler import ReconciliationLock
from services.control_api.catalog.store import SqlAlchemyReconcileRunStore
from services.control_api.persistence.db import create_engine_from_url, session_factory
from services.control_api.persistence.models import Base
from services.control_api.persistence.repositories import (
ProjectRepository,
WorkspaceRepository,
)
engine = create_engine_from_url("sqlite:///:memory:")
Base.metadata.create_all(engine)
sessions = session_factory(engine)
now = datetime.now(UTC)
with self.assertRaises(RuntimeError):
with ReconciliationLock("wp060-recovery"):
raise RuntimeError("simulated redis/cache loss")
with ReconciliationLock("wp060-recovery"):
pass
with sessions.begin() as session:
WorkspaceRepository(session, Scope.workspace(self.workspace_id)).create(
Workspace(
id=self.workspace_id,
slug="itworx",
name="ITWorx",
created_at=now,
updated_at=now,
)
)
ProjectRepository(session, Scope.workspace(self.workspace_id)).create(
Project(
id=self.project_id,
workspace_id=self.workspace_id,
slug="catalog",
name="Catalog",
created_at=now,
updated_at=now,
)
)
store = SqlAlchemyReconcileRunStore(
session, Scope.project(self.workspace_id, self.project_id)
)
run = ReconcileRun(
workspace_id=self.workspace_id,
project_id=self.project_id,
desired_hash="a" * 64,
status="created",
created_at=now,
updated_at=now,
)
stored = store.create(run)
updated = stored.model_copy(
update={"status": "planning", "revision": 2, "updated_at": now}
)
store.save(updated)
engine.dispose()
def test_router_requires_authorization_and_exposes_scoped_operations(self) -> None:
app = FastAPI()
app.include_router(build_catalog_router(self.reconciler, authorize=lambda _: None))
client = TestClient(app)
denied_app = FastAPI()
denied_app.include_router(build_catalog_router(self.reconciler))
denied = TestClient(denied_app).post("/catalog/validate")
self.assertEqual(denied.status_code, 403)
valid = client.post("/catalog/validate")
self.assertEqual(valid.status_code, 200)
body = {"workspace_id": str(self.workspace_id), "project_id": str(self.project_id)}
applied = client.post("/catalog/apply", json=body)
self.assertEqual(applied.status_code, 200)
run = client.get(f"/catalog/status/{applied.json()['id']}")
self.assertEqual(run.status_code, 200)
if __name__ == "__main__":
unittest.main()