import unittest import ast from pathlib import Path import sys # Ensure the project root is in the path for imports REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT / "opax-mcp")) from capability_bridge import build_capability_system_context from capability_registry import ( list_capabilities, Capability, Availability, ) class TestCapabilityBridge(unittest.TestCase): def setUp(self): self.context_output = build_capability_system_context() def test_output_is_deterministic(self): self.assertEqual(self.context_output, build_capability_system_context()) def test_output_has_all_three_headings(self): self.assertIn("\nAvailable now:", self.context_output) self.assertIn("\nPlanned, but not active in this chat:", self.context_output) self.assertIn("\nUnavailable:", self.context_output) def test_available_section_contains_correct_capabilities(self): active_capabilities = [ c for c in list_capabilities() if c.availability == Availability.ACTIVE ] self.assertIn("- Emma chat: Conversational reasoning through Emma.", self.context_output) self.assertIn("- Analyze user-provided text: Analyze, summarize, and structure text provided in the current request.", self.context_output) # Isolate the "Available now" section to count items accurately start_index = self.context_output.find("Available now:") end_index = self.context_output.find("\n\nPlanned, but not active in this chat:") available_section = self.context_output[start_index:end_index] self.assertEqual(available_section.count("- "), len(active_capabilities)) def test_planned_section_contains_all_planned_capabilities(self): planned_capabilities = [ c for c in list_capabilities() if c.availability == Availability.PLANNED ] self.assertEqual(len(planned_capabilities), 17) planned_capability_ids = {c.id for c in planned_capabilities} self.assertIn("gitea.list_repo_files", planned_capability_ids) for capability in planned_capabilities: self.assertIn(f"- {capability.display_name}: {capability.description}", self.context_output) def test_unavailable_section_is_correct(self): self.assertIn("- Arbitrary terminal access is not available.", self.context_output) # Ensure no other forbidden capabilities are listed by name self.assertNotIn("Arbitrary terminal shell:", self.context_output) def test_bridge_text_does_not_contain_forbidden_internal_details(self): forbidden_substrings = [ # IDs "emma.chat", "terminal.arbitrary_shell", # Backend tool names "run_emma", "list_commits", "get_file", "create_issue", "push_file", "build_and_deploy_service", # Internal metadata "required_actor_scope", "approval_binding", "audit_required", "rollback_required", "self_approval_forbidden", # Target names / infrastructure "git.vauco.no", "opax-mcp", "propane-will-491900-m5", "us-central1", ] for substring in forbidden_substrings: self.assertNotIn(substring, self.context_output) def test_bridge_module_has_no_prohibited_imports(self): bridge_path = REPO_ROOT / "opax-mcp" / "capability_bridge.py" with open(bridge_path, "r", encoding="utf-8") as f: tree = ast.parse(f.read()) forbidden_imports = { "os", "subprocess", "requests", "httpx", "google", "gitea", "firebase", "firestore", } for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: self.assertNotIn(alias.name.split('.')[0], forbidden_imports) elif isinstance(node, ast.ImportFrom): if node.module: self.assertNotIn(node.module.split('.')[0], forbidden_imports) def test_bridge_does_not_mutate_registry(self): # This is implicitly tested by the deterministic output and other tests, # but a direct check confirms the source objects are not modified. initial_capabilities = list_capabilities() build_capability_system_context() self.assertEqual(initial_capabilities, list_capabilities()) def test_bridge_output_includes_all_rules(self): rules_block = ( "Rules:\n" "- Planned capabilities are not available in this chat and must not be claimed as active.\n" "- No tool, repository, deployment, memory, document retrieval, external service or state-changing action is activated by this capability summary.\n" "- Future write or high-impact capabilities require a concrete, explicit, bound user approval before execution." ) self.assertIn(rules_block, self.context_output) def test_bridge_does_not_expose_backend_tool_names(self): all_capabilities = list_capabilities() tool_names = {c.backend_tool for c in all_capabilities if c.backend_tool} for tool_name in tool_names: self.assertNotIn(tool_name, self.context_output) class TestServerIntegrationContract(unittest.TestCase): def test_run_emma_integration_in_server_py(self): server_path = REPO_ROOT / "opax-mcp" / "server.py" with open(server_path, "r", encoding="utf-8") as f: tree = ast.parse(f.read()) run_emma_func = None for node in ast.walk(tree): if isinstance(node, ast.AsyncFunctionDef) and node.name == "run_emma": run_emma_func = node break self.assertIsNotNone(run_emma_func, "async def run_emma not found in server.py") # 1. References build_capability_system_context call_to_build_context = any( isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == 'build_capability_system_context' for n in ast.walk(run_emma_func) ) self.assertTrue(call_to_build_context, "run_emma does not call build_capability_system_context") # 2. Passes system_context to canonical_emma.run call_to_run = None for node in ast.walk(run_emma_func): if isinstance(node, ast.Call) and hasattr(node.func, 'value') and hasattr(node.func.value, 'id') and node.func.value.id == 'canonical_emma' and node.func.attr == 'run': call_to_run = node break self.assertIsNotNone(call_to_run, "run_emma does not call canonical_emma.run") has_system_context_kw = any( kw.arg == 'system_context' for kw in call_to_run.keywords ) self.assertTrue(has_system_context_kw, "system_context is not passed to canonical_emma.run") # 3. Preserves history=[] has_history_kw = any( kw.arg == 'history' and isinstance(kw.value, ast.List) and not kw.value.elts for kw in call_to_run.keywords ) self.assertTrue(has_history_kw, "history=[] is not preserved in call to canonical_emma.run") # 4. Preserves prompt argument structure prompt_kw = next((kw for kw in call_to_run.keywords if kw.arg == 'prompt'), None) self.assertIsNotNone(prompt_kw, "prompt argument missing in call to canonical_emma.run") # A basic check to ensure it's calling p.get, not a simple variable self.assertIsInstance(prompt_kw.value, ast.Call, "prompt argument is not a function call") if __name__ == "__main__": unittest.main()