152 lines
5.9 KiB
Python
152 lines
5.9 KiB
Python
import ast
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(REPO_ROOT / "opax-mcp"))
|
|
|
|
from capability_registry import (
|
|
CAPABILITIES,
|
|
Availability,
|
|
RiskLevel,
|
|
get_capability,
|
|
list_active_capabilities,
|
|
list_capabilities,
|
|
)
|
|
|
|
|
|
class TestCapabilityRegistry(unittest.TestCase):
|
|
def setUp(self):
|
|
self.capabilities = list_capabilities()
|
|
|
|
def test_registry_contains_exactly_twenty_one_capabilities(self):
|
|
self.assertEqual(len(self.capabilities), 21)
|
|
self.assertEqual(len(CAPABILITIES), 21)
|
|
|
|
def test_identifiers_are_unique(self):
|
|
identifiers = [capability.id for capability in self.capabilities]
|
|
self.assertEqual(len(identifiers), len(set(identifiers)))
|
|
|
|
def test_order_is_deterministic_and_starts_with_active_capabilities(self):
|
|
self.assertEqual(
|
|
[capability.id for capability in self.capabilities[:2]],
|
|
["emma.chat", "emma.analyze_user_text"],
|
|
)
|
|
self.assertEqual(
|
|
[capability.id for capability in self.capabilities],
|
|
list(CAPABILITIES.keys()),
|
|
)
|
|
|
|
def test_active_capabilities_are_exactly_the_two_permitted_capabilities(self):
|
|
active = list_active_capabilities()
|
|
self.assertEqual(
|
|
[capability.id for capability in active],
|
|
["emma.chat", "emma.analyze_user_text"],
|
|
)
|
|
for capability in active:
|
|
self.assertEqual(capability.risk_level, RiskLevel.READ)
|
|
self.assertFalse(capability.approval_required)
|
|
self.assertFalse(capability.self_approval_forbidden)
|
|
|
|
def test_write_and_high_impact_capabilities_are_planned_and_gated(self):
|
|
gated = [
|
|
capability
|
|
for capability in self.capabilities
|
|
if capability.risk_level in {RiskLevel.WRITE, RiskLevel.HIGH_IMPACT}
|
|
]
|
|
self.assertTrue(gated)
|
|
for capability in gated:
|
|
self.assertEqual(capability.availability, Availability.PLANNED)
|
|
self.assertTrue(capability.approval_required)
|
|
self.assertTrue(capability.audit_required)
|
|
self.assertTrue(capability.self_approval_forbidden)
|
|
|
|
def test_forbidden_shell_capability_has_no_execution_target(self):
|
|
capability = get_capability("terminal.arbitrary_shell")
|
|
self.assertIsNotNone(capability)
|
|
self.assertEqual(capability.risk_level, RiskLevel.FORBIDDEN)
|
|
self.assertEqual(capability.availability, Availability.FORBIDDEN)
|
|
self.assertIsNone(capability.backend_tool)
|
|
self.assertFalse(capability.approval_required)
|
|
self.assertTrue(capability.self_approval_forbidden)
|
|
self.assertEqual(capability.allowed_targets, ())
|
|
|
|
def test_unknown_capability_returns_none(self):
|
|
self.assertIsNone(get_capability("unknown"))
|
|
|
|
def test_public_registry_mapping_is_immutable(self):
|
|
with self.assertRaises(TypeError):
|
|
CAPABILITIES["new.capability"] = get_capability("emma.chat")
|
|
|
|
def test_schema_mappings_are_immutable(self):
|
|
for capability in self.capabilities:
|
|
with self.assertRaises(TypeError):
|
|
capability.input_schema["unexpected"] = "value"
|
|
with self.assertRaises(TypeError):
|
|
capability.output_schema["unexpected"] = "value"
|
|
|
|
def test_registry_module_has_no_external_execution_imports(self):
|
|
module_path = REPO_ROOT / "opax-mcp" / "capability_registry.py"
|
|
tree = ast.parse(module_path.read_text(encoding="utf-8"))
|
|
forbidden_roots = {
|
|
"os",
|
|
"subprocess",
|
|
"requests",
|
|
"httpx",
|
|
"google",
|
|
"gitea",
|
|
"firebase",
|
|
"firestore",
|
|
}
|
|
imported_roots = set()
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
imported_roots.update(alias.name.split(".")[0] for alias in node.names)
|
|
if isinstance(node, ast.ImportFrom) and node.module:
|
|
imported_roots.add(node.module.split(".")[0])
|
|
self.assertFalse(imported_roots & forbidden_roots)
|
|
|
|
def test_active_and_planned_capabilities_have_required_metadata(self):
|
|
for capability in self.capabilities:
|
|
if capability.availability == Availability.FORBIDDEN:
|
|
continue
|
|
self.assertTrue(capability.id)
|
|
self.assertTrue(capability.display_name)
|
|
self.assertTrue(capability.description)
|
|
self.assertTrue(capability.category)
|
|
self.assertTrue(capability.execution_owner)
|
|
self.assertTrue(capability.allowed_targets)
|
|
|
|
def test_planned_write_and_high_impact_bindings_and_rollbacks(self):
|
|
rollback_required_ids = {
|
|
"gitea.push_change",
|
|
"cloudrun.deploy_digest",
|
|
"emma.update_ui",
|
|
"emma.update_backend",
|
|
"emma.update_model_config",
|
|
}
|
|
for capability in self.capabilities:
|
|
if capability.risk_level not in {RiskLevel.WRITE, RiskLevel.HIGH_IMPACT}:
|
|
continue
|
|
self.assertTrue(capability.approval_binding)
|
|
self.assertEqual(
|
|
capability.rollback_required,
|
|
capability.id in rollback_required_ids,
|
|
)
|
|
|
|
def test_gitea_list_repo_files_capability(self):
|
|
capability = get_capability("gitea.list_repo_files")
|
|
self.assertIsNotNone(capability)
|
|
self.assertEqual(capability.risk_level, RiskLevel.READ)
|
|
self.assertEqual(capability.availability, Availability.PLANNED)
|
|
self.assertEqual(capability.backend_tool, "list_repo_files")
|
|
self.assertIn("authenticated_user", capability.required_actor_scope)
|
|
self.assertIn("gitea_read", capability.required_actor_scope)
|
|
self.assertIn("git.vauco.no", capability.allowed_targets)
|
|
self.assertEqual(capability.external_source_policy, "internal_authoritative_only")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|