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()