Option C Tools
Loading...
Searching...
No Matches
test_mcp_config.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_mcp/test_mcp_config.py
4
5"""
6Purpose
7-------
8Regression tests for :mod:`oct.mcp.config` — ``McpConfig`` dataclass
9and ``load_config()``.
10
11Responsibilities
12----------------
13- Verify default values, JSON loading, and field clamping.
14- Verify environment variable overrides.
15- Verify file-size guard and malformed JSON fallback.
16
17Diagnostics
18-----------
19Domain: MCP-TESTS
20Levels:
21 L2 — test lifecycle
22 L3 — assertion details
23 L4 — deep tracing
24
25Contracts
26---------
27- Config files are written to ``tmp_path``; no real config is modified.
28"""
29
30from __future__ import annotations
31
32import json
33import os
34from pathlib import Path
35
36import pytest
37
38from oct.mcp.config import (
39 McpConfig,
40 VALID_MCP_PROFILES,
41 _CONFIG_MAX_BYTES,
42 load_config,
43 _apply_json,
44)
45
46
47# -------------------------------------------------------------------
48# McpConfig defaults
49# -------------------------------------------------------------------
50
53 cfg = McpConfig()
54 assert cfg.profile == "default"
55
57 assert McpConfig().rate_limit_per_minute == 30
58
60 assert McpConfig().timeout_default_seconds == 60
61
63 assert McpConfig().timeout_test_seconds == 300
64
66 assert McpConfig().max_output_bytes == 1_048_576
67
69 assert McpConfig().max_jobs == 4
70
72 assert McpConfig().auto_approve_read_tools is True
73
75 assert McpConfig().dry_run_default_for_writes is True
76
78 assert McpConfig().redaction_always_on is False
79
81 assert McpConfig().trust_repo_config is False
82
84 assert McpConfig().safe_mode is False
85
87 assert McpConfig().audit_log_max_files == 20
88
90 assert McpConfig().audit_log_max_size_mb == 5.0
91
92
93# -------------------------------------------------------------------
94# VALID_MCP_PROFILES
95# -------------------------------------------------------------------
96
99 assert "default" in VALID_MCP_PROFILES
100
102 assert "strict" in VALID_MCP_PROFILES
103
105 assert "airgapped" in VALID_MCP_PROFILES
106
108 assert "admin" not in VALID_MCP_PROFILES
109
110
111# -------------------------------------------------------------------
112# load_config — missing file
113# -------------------------------------------------------------------
114
116 def test_returns_defaults_when_no_file(self, tmp_path: Path):
117 cfg = load_config(tmp_path / "nonexistent.json")
118 assert cfg.profile == "default"
119 assert cfg.safe_mode is False
120
121 def test_returns_mcpconfig_instance(self, tmp_path: Path):
122 cfg = load_config(tmp_path / "nonexistent.json")
123 assert isinstance(cfg, McpConfig)
124
125
126# -------------------------------------------------------------------
127# load_config — valid JSON
128# -------------------------------------------------------------------
129
131 def test_loads_profile(self, tmp_path: Path):
132 p = tmp_path / "config.json"
133 p.write_text(json.dumps({"profile": "strict"}))
134 cfg = load_config(p)
135 assert cfg.profile == "strict"
136
137 def test_loads_rate_limit(self, tmp_path: Path):
138 p = tmp_path / "config.json"
139 p.write_text(json.dumps({"rate_limit_per_minute": 10}))
140 cfg = load_config(p)
141 assert cfg.rate_limit_per_minute == 10
142
143 def test_loads_timeout(self, tmp_path: Path):
144 p = tmp_path / "config.json"
145 p.write_text(json.dumps({"timeout_default_seconds": 90}))
146 cfg = load_config(p)
147 assert cfg.timeout_default_seconds == 90
148
149 def test_loads_trust_repo_config(self, tmp_path: Path):
150 p = tmp_path / "config.json"
151 p.write_text(json.dumps({"trust_repo_config": True}))
152 cfg = load_config(p)
153 assert cfg.trust_repo_config is True
154
155 def test_loads_audit_log_path(self, tmp_path: Path):
156 p = tmp_path / "config.json"
157 custom_path = "/custom/audit.log"
158 p.write_text(json.dumps({"audit_log_path": custom_path}))
159 cfg = load_config(p)
160 assert cfg.audit_log_path == custom_path
161
162 def test_loads_audit_log_max_files(self, tmp_path: Path):
163 p = tmp_path / "config.json"
164 p.write_text(json.dumps({"audit_log_max_files": 5}))
165 cfg = load_config(p)
166 assert cfg.audit_log_max_files == 5
167
168 def test_loads_audit_log_max_size_mb(self, tmp_path: Path):
169 p = tmp_path / "config.json"
170 p.write_text(json.dumps({"audit_log_max_size_mb": 10.0}))
171 cfg = load_config(p)
172 assert cfg.audit_log_max_size_mb == 10.0
173
174
175# -------------------------------------------------------------------
176# load_config — field clamping
177# -------------------------------------------------------------------
178
180 def test_rate_limit_clamped_min(self, tmp_path: Path):
181 p = tmp_path / "config.json"
182 p.write_text(json.dumps({"rate_limit_per_minute": 0}))
183 cfg = load_config(p)
184 assert cfg.rate_limit_per_minute == 1 # clamped to min
185
186 def test_rate_limit_clamped_max(self, tmp_path: Path):
187 p = tmp_path / "config.json"
188 p.write_text(json.dumps({"rate_limit_per_minute": 9999}))
189 cfg = load_config(p)
190 assert cfg.rate_limit_per_minute == 1000
191
192 def test_timeout_clamped_min(self, tmp_path: Path):
193 p = tmp_path / "config.json"
194 p.write_text(json.dumps({"timeout_default_seconds": 1}))
195 cfg = load_config(p)
196 assert cfg.timeout_default_seconds == 5
197
198 def test_timeout_clamped_max(self, tmp_path: Path):
199 p = tmp_path / "config.json"
200 p.write_text(json.dumps({"timeout_default_seconds": 9999}))
201 cfg = load_config(p)
202 assert cfg.timeout_default_seconds == 300
203
204 def test_max_jobs_clamped_min(self, tmp_path: Path):
205 p = tmp_path / "config.json"
206 p.write_text(json.dumps({"max_jobs": 0}))
207 cfg = load_config(p)
208 assert cfg.max_jobs == 1
209
210 def test_max_jobs_clamped_max(self, tmp_path: Path):
211 p = tmp_path / "config.json"
212 p.write_text(json.dumps({"max_jobs": 999}))
213 cfg = load_config(p)
214 assert cfg.max_jobs == 16
215
216 def test_audit_log_max_files_clamped(self, tmp_path: Path):
217 p = tmp_path / "config.json"
218 p.write_text(json.dumps({"audit_log_max_files": 999}))
219 cfg = load_config(p)
220 assert cfg.audit_log_max_files == 100
221
222 def test_audit_log_max_size_mb_clamped(self, tmp_path: Path):
223 p = tmp_path / "config.json"
224 p.write_text(json.dumps({"audit_log_max_size_mb": 500.0}))
225 cfg = load_config(p)
226 assert cfg.audit_log_max_size_mb == 100.0
227
228
229# -------------------------------------------------------------------
230# load_config — invalid/unknown fields are ignored
231# -------------------------------------------------------------------
232
234 def test_unknown_field_ignored(self, tmp_path: Path):
235 p = tmp_path / "config.json"
236 p.write_text(json.dumps({"profile": "strict", "unknown_key": "value"}))
237 cfg = load_config(p)
238 assert cfg.profile == "strict"
239
240 def test_invalid_profile_ignored(self, tmp_path: Path):
241 p = tmp_path / "config.json"
242 p.write_text(json.dumps({"profile": "admin"}))
243 cfg = load_config(p)
244 assert cfg.profile == "default" # invalid profile not applied
245
246 def test_malformed_json_returns_defaults(self, tmp_path: Path):
247 p = tmp_path / "config.json"
248 p.write_text("{not valid json}")
249 cfg = load_config(p)
250 assert cfg.profile == "default"
251
252 def test_json_array_not_applied(self, tmp_path: Path):
253 p = tmp_path / "config.json"
254 p.write_text("[1, 2, 3]")
255 cfg = load_config(p)
256 assert cfg.profile == "default"
257
258 def test_boolean_string_not_applied(self, tmp_path: Path):
259 # Only actual JSON booleans should be accepted.
260 p = tmp_path / "config.json"
261 p.write_text(json.dumps({"auto_approve_read_tools": "true"}))
262 cfg = load_config(p)
263 assert cfg.auto_approve_read_tools is True # default unchanged
264
265 def test_file_too_large_returns_defaults(self, tmp_path: Path):
266 p = tmp_path / "config.json"
267 # Write a file larger than 64 KB.
268 p.write_bytes(b'{"profile": "strict"}' + b" " * (_CONFIG_MAX_BYTES + 100))
269 cfg = load_config(p)
270 assert cfg.profile == "default" # large file not applied
271
272
273# -------------------------------------------------------------------
274# load_config — environment variable overrides
275# -------------------------------------------------------------------
276
278 def test_safe_mode_env(self, tmp_path: Path, monkeypatch):
279 monkeypatch.setenv("OCT_MCP_SAFE_MODE", "1")
280 cfg = load_config(tmp_path / "nonexistent.json")
281 assert cfg.safe_mode is True
282
283 def test_safe_mode_env_zero_not_applied(self, tmp_path: Path, monkeypatch):
284 monkeypatch.setenv("OCT_MCP_SAFE_MODE", "0")
285 cfg = load_config(tmp_path / "nonexistent.json")
286 assert cfg.safe_mode is False
287
288 def test_trust_repo_config_env(self, tmp_path: Path, monkeypatch):
289 monkeypatch.setenv("OCT_TRUST_REPO_CONFIG", "1")
290 cfg = load_config(tmp_path / "nonexistent.json")
291 assert cfg.trust_repo_config is True
292
293 def test_profile_env_strict(self, tmp_path: Path, monkeypatch):
294 monkeypatch.setenv("OCT_MCP_PROFILE", "strict")
295 cfg = load_config(tmp_path / "nonexistent.json")
296 assert cfg.profile == "strict"
297
298 def test_profile_env_airgapped(self, tmp_path: Path, monkeypatch):
299 monkeypatch.setenv("OCT_MCP_PROFILE", "airgapped")
300 cfg = load_config(tmp_path / "nonexistent.json")
301 assert cfg.profile == "airgapped"
302
303 def test_profile_env_invalid_ignored(self, tmp_path: Path, monkeypatch):
304 monkeypatch.setenv("OCT_MCP_PROFILE", "superadmin")
305 cfg = load_config(tmp_path / "nonexistent.json")
306 assert cfg.profile == "default"
307
308 def test_env_overrides_file_value(self, tmp_path: Path, monkeypatch):
309 p = tmp_path / "config.json"
310 p.write_text(json.dumps({"profile": "strict"}))
311 monkeypatch.setenv("OCT_MCP_PROFILE", "airgapped")
312 cfg = load_config(p)
313 assert cfg.profile == "airgapped"
test_profile_env_airgapped(self, Path tmp_path, monkeypatch)
test_env_overrides_file_value(self, Path tmp_path, monkeypatch)
test_profile_env_strict(self, Path tmp_path, monkeypatch)
test_profile_env_invalid_ignored(self, Path tmp_path, monkeypatch)
test_safe_mode_env_zero_not_applied(self, Path tmp_path, monkeypatch)
test_trust_repo_config_env(self, Path tmp_path, monkeypatch)
test_safe_mode_env(self, Path tmp_path, monkeypatch)