from __future__ import annotations import fnmatch import json import tempfile import unittest from pathlib import Path 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_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()