Option C Tools
Loading...
Searching...
No Matches
test_mcp_policy.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_mcp/test_mcp_policy.py
4
5"""
6Purpose
7-------
8Regression tests for :mod:`oct.mcp.policy` — ``TOOL_MANIFEST``,
9``ToolSpec``, ``McpPolicy``, and ``PolicyDecision``.
10
11Responsibilities
12----------------
13- Verify all profile behaviours (default, strict, airgapped).
14- Verify safe_mode enforcement and unknown tool rejection.
15- Verify manifest completeness for registered tools.
16
17Diagnostics
18-----------
19Domain: MCP-TESTS
20Levels:
21 L2 — test lifecycle
22 L3 — assertion details
23 L4 — deep tracing
24
25Contracts
26---------
27- Tests are in-memory policy checks; no subprocess or filesystem writes.
28"""
29
30from __future__ import annotations
31
32import pytest
33
34from oct.mcp.policy import (
35 TOOL_MANIFEST,
36 McpPolicy,
37 PolicyDecision,
38 ToolSpec,
39)
40
41
42# -------------------------------------------------------------------
43# TOOL_MANIFEST
44# -------------------------------------------------------------------
45
46_PHASE_5A_TOOLS = {
47 "oct_lint", "oct_health", "oct_skeleton", "oct_deps",
48 "oct_test", "oct_typecheck", "oct_diag",
49 "oct_git_status", "oct_git_check",
50}
51
52_PHASE_5B_TOOLS = {
53 "oct_format", "oct_docs", "oct_export_source", "oct_new",
54 "oct_scaffold", "oct_clean", "oct_install_hooks",
55 "oct_git_commit", "oct_git_hooks_install", "oct_git_init",
56 "oct_git_changelog",
57}
58
59
62 assert len(TOOL_MANIFEST) == 20
63
65 assert _PHASE_5A_TOOLS.issubset(set(TOOL_MANIFEST.keys()))
66
68 assert _PHASE_5B_TOOLS.issubset(set(TOOL_MANIFEST.keys()))
69
71 for name in _PHASE_5A_TOOLS:
72 spec = TOOL_MANIFEST[name]
73 assert spec.destructive is False, f"{name} should not be destructive"
74
76 for name in _PHASE_5B_TOOLS:
77 spec = TOOL_MANIFEST[name]
78 assert spec.destructive is True, f"{name} should be destructive"
79
81 for name in _PHASE_5A_TOOLS:
82 spec = TOOL_MANIFEST[name]
83 assert spec.auto_approve is True, f"{name} should be auto_approve"
84
86 for name in _PHASE_5B_TOOLS:
87 spec = TOOL_MANIFEST[name]
88 assert spec.auto_approve is False, f"{name} should not be auto_approve"
89
91 for name in _PHASE_5A_TOOLS:
92 spec = TOOL_MANIFEST[name]
93 assert "read" in spec.permissions, f"{name} must have read permission"
94
96 for name in _PHASE_5B_TOOLS:
97 spec = TOOL_MANIFEST[name]
98 assert "write" in spec.permissions, f"{name} must have write permission"
99
101 for name, spec in TOOL_MANIFEST.items():
102 assert spec.cli, f"{name} must have a cli subcommand"
103
104
105# -------------------------------------------------------------------
106# ToolSpec
107# -------------------------------------------------------------------
108
111 spec = ToolSpec(cli="lint", permissions={"read"})
112 assert isinstance(spec.permissions, frozenset)
113
114 def test_frozen(self):
115 spec = ToolSpec(cli="lint", permissions=frozenset({"read"}))
116 with pytest.raises((AttributeError, TypeError)):
117 spec.cli = "other" # frozen=True should prevent this
118
119
120# -------------------------------------------------------------------
121# McpPolicy — constructor
122# -------------------------------------------------------------------
123
126 policy = McpPolicy()
127 assert policy.profile == "default"
128
130 policy = McpPolicy(profile="strict")
131 assert policy.profile == "strict"
132
134 policy = McpPolicy(profile="airgapped")
135 assert policy.profile == "airgapped"
136
138 with pytest.raises(ValueError, match="Invalid MCP profile"):
139 McpPolicy(profile="superadmin")
140
141
142# -------------------------------------------------------------------
143# McpPolicy — default profile
144# -------------------------------------------------------------------
145
148 policy = McpPolicy(profile="default")
149 for tool in TOOL_MANIFEST:
150 decision = policy.check(tool)
151 assert decision.allowed, f"{tool} should be allowed in default profile"
152
154 policy = McpPolicy(profile="default")
155 decision = policy.check("evil_tool")
156 assert decision.allowed is False
157 assert decision.decision == "denied"
158 assert decision.policy_rule == "unknown_tool"
159
161 policy = McpPolicy(profile="default")
162 decision = policy.check("oct_lint")
163 assert decision.reason != ""
164
165
166# -------------------------------------------------------------------
167# McpPolicy — safe mode
168# -------------------------------------------------------------------
169
172 policy = McpPolicy(safe_mode=True)
173 for tool in TOOL_MANIFEST:
174 decision = policy.check(tool)
175 assert decision.allowed is False
176 assert decision.policy_rule == "safe_mode"
177
179 policy = McpPolicy(safe_mode=True)
180 decision = policy.check("oct_lint")
181 assert "safe mode" in decision.reason.lower()
182
183
184# -------------------------------------------------------------------
185# McpPolicy — airgapped profile
186# -------------------------------------------------------------------
187
190 policy = McpPolicy(profile="airgapped")
191 for tool in TOOL_MANIFEST:
192 decision = policy.check(tool)
193 assert decision.allowed is False
194 assert decision.policy_rule == "airgapped_profile"
195
197 policy = McpPolicy(profile="airgapped")
198 decision = policy.check("unknown_tool")
199 assert decision.allowed is False
200
202 policy = McpPolicy(profile="airgapped")
203 decision = policy.check("oct_lint")
204 assert "airgapped" in decision.reason.lower()
205
206
207# -------------------------------------------------------------------
208# McpPolicy — strict profile
209# -------------------------------------------------------------------
210
213 policy = McpPolicy(profile="strict")
214 for tool in TOOL_MANIFEST:
215 spec = TOOL_MANIFEST[tool]
216 decision = policy.check(tool)
217 if not spec.destructive:
218 assert decision.allowed, f"{tool} should be allowed in strict (non-destructive)"
219
221 """Phase 5B write tools (destructive=True) are all denied in strict profile."""
222 policy = McpPolicy(profile="strict")
223 for tool in _PHASE_5B_TOOLS:
224 decision = policy.check(tool)
225 assert decision.allowed is False, f"{tool} should be denied in strict"
226 assert "destructive" in decision.policy_rule
227
228
229# -------------------------------------------------------------------
230# PolicyDecision
231# -------------------------------------------------------------------
232
235 d = PolicyDecision(allowed=True, decision="allowed", policy_rule="x", reason="ok")
236 assert d.allowed is True
237
239 d = PolicyDecision(allowed=False, decision="denied", policy_rule="safe_mode", reason="blocked")
240 assert d.allowed is False