hatch-xclam 0.7.1.dev1__py3-none-any.whl → 0.7.1.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hatch/cli_hatch.py +90 -21
- hatch/mcp_host_config/__init__.py +4 -2
- hatch/mcp_host_config/backup.py +62 -31
- hatch/mcp_host_config/models.py +98 -5
- hatch/mcp_host_config/strategies.py +172 -1
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev2.dist-info}/METADATA +5 -3
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev2.dist-info}/RECORD +16 -13
- tests/regression/test_mcp_codex_backup_integration.py +162 -0
- tests/regression/test_mcp_codex_host_strategy.py +163 -0
- tests/regression/test_mcp_codex_model_validation.py +117 -0
- tests/test_mcp_cli_all_host_specific_args.py +194 -1
- tests/test_mcp_cli_direct_management.py +8 -5
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev2.dist-info}/WHEEL +0 -0
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev2.dist-info}/entry_points.txt +0 -0
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev2.dist-info}/licenses/LICENSE +0 -0
- {hatch_xclam-0.7.1.dev1.dist-info → hatch_xclam-0.7.1.dev2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Codex MCP Model Validation Tests
|
|
3
|
+
|
|
4
|
+
Tests for MCPServerConfigCodex model validation including Codex-specific fields,
|
|
5
|
+
Omni conversion, and registry integration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
from wobble.decorators import regression_test
|
|
10
|
+
|
|
11
|
+
from hatch.mcp_host_config.models import (
|
|
12
|
+
MCPServerConfigCodex, MCPServerConfigOmni, MCPHostType, HOST_MODEL_REGISTRY
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestCodexModelValidation(unittest.TestCase):
|
|
17
|
+
"""Test suite for Codex model validation."""
|
|
18
|
+
|
|
19
|
+
@regression_test
|
|
20
|
+
def test_codex_specific_fields_accepted(self):
|
|
21
|
+
"""Test that Codex-specific fields are accepted in MCPServerConfigCodex."""
|
|
22
|
+
# Create model with Codex-specific fields
|
|
23
|
+
config = MCPServerConfigCodex(
|
|
24
|
+
command="npx",
|
|
25
|
+
args=["-y", "package"],
|
|
26
|
+
env={"API_KEY": "test"},
|
|
27
|
+
# Codex-specific fields
|
|
28
|
+
env_vars=["PATH", "HOME"],
|
|
29
|
+
cwd="/workspace",
|
|
30
|
+
startup_timeout_sec=10,
|
|
31
|
+
tool_timeout_sec=60,
|
|
32
|
+
enabled=True,
|
|
33
|
+
enabled_tools=["read", "write"],
|
|
34
|
+
disabled_tools=["delete"],
|
|
35
|
+
bearer_token_env_var="AUTH_TOKEN",
|
|
36
|
+
http_headers={"X-Custom": "value"},
|
|
37
|
+
env_http_headers={"X-Auth": "AUTH_VAR"}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Verify all fields are accessible
|
|
41
|
+
self.assertEqual(config.command, "npx")
|
|
42
|
+
self.assertEqual(config.env_vars, ["PATH", "HOME"])
|
|
43
|
+
self.assertEqual(config.cwd, "/workspace")
|
|
44
|
+
self.assertEqual(config.startup_timeout_sec, 10)
|
|
45
|
+
self.assertEqual(config.tool_timeout_sec, 60)
|
|
46
|
+
self.assertTrue(config.enabled)
|
|
47
|
+
self.assertEqual(config.enabled_tools, ["read", "write"])
|
|
48
|
+
self.assertEqual(config.disabled_tools, ["delete"])
|
|
49
|
+
self.assertEqual(config.bearer_token_env_var, "AUTH_TOKEN")
|
|
50
|
+
self.assertEqual(config.http_headers, {"X-Custom": "value"})
|
|
51
|
+
self.assertEqual(config.env_http_headers, {"X-Auth": "AUTH_VAR"})
|
|
52
|
+
|
|
53
|
+
@regression_test
|
|
54
|
+
def test_codex_from_omni_conversion(self):
|
|
55
|
+
"""Test MCPServerConfigCodex.from_omni() conversion."""
|
|
56
|
+
# Create Omni model with Codex-specific fields
|
|
57
|
+
omni = MCPServerConfigOmni(
|
|
58
|
+
command="npx",
|
|
59
|
+
args=["-y", "package"],
|
|
60
|
+
env={"API_KEY": "test"},
|
|
61
|
+
# Codex-specific fields
|
|
62
|
+
env_vars=["PATH"],
|
|
63
|
+
startup_timeout_sec=15,
|
|
64
|
+
tool_timeout_sec=90,
|
|
65
|
+
enabled=True,
|
|
66
|
+
enabled_tools=["read"],
|
|
67
|
+
disabled_tools=["write"],
|
|
68
|
+
bearer_token_env_var="TOKEN",
|
|
69
|
+
headers={"X-Test": "value"}, # Universal field (maps to http_headers in Codex)
|
|
70
|
+
env_http_headers={"X-Env": "VAR"},
|
|
71
|
+
# Non-Codex fields (should be excluded)
|
|
72
|
+
envFile="/path/to/env", # VS Code specific
|
|
73
|
+
disabled=True # Kiro specific
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Convert to Codex model
|
|
77
|
+
codex = MCPServerConfigCodex.from_omni(omni)
|
|
78
|
+
|
|
79
|
+
# Verify Codex fields transferred correctly
|
|
80
|
+
self.assertEqual(codex.command, "npx")
|
|
81
|
+
self.assertEqual(codex.env_vars, ["PATH"])
|
|
82
|
+
self.assertEqual(codex.startup_timeout_sec, 15)
|
|
83
|
+
self.assertEqual(codex.tool_timeout_sec, 90)
|
|
84
|
+
self.assertTrue(codex.enabled)
|
|
85
|
+
self.assertEqual(codex.enabled_tools, ["read"])
|
|
86
|
+
self.assertEqual(codex.disabled_tools, ["write"])
|
|
87
|
+
self.assertEqual(codex.bearer_token_env_var, "TOKEN")
|
|
88
|
+
self.assertEqual(codex.http_headers, {"X-Test": "value"})
|
|
89
|
+
self.assertEqual(codex.env_http_headers, {"X-Env": "VAR"})
|
|
90
|
+
|
|
91
|
+
# Verify non-Codex fields excluded (should not have these attributes)
|
|
92
|
+
with self.assertRaises(AttributeError):
|
|
93
|
+
_ = codex.envFile
|
|
94
|
+
with self.assertRaises(AttributeError):
|
|
95
|
+
_ = codex.disabled
|
|
96
|
+
|
|
97
|
+
@regression_test
|
|
98
|
+
def test_host_model_registry_contains_codex(self):
|
|
99
|
+
"""Test that HOST_MODEL_REGISTRY contains Codex model."""
|
|
100
|
+
# Verify CODEX is in registry
|
|
101
|
+
self.assertIn(MCPHostType.CODEX, HOST_MODEL_REGISTRY)
|
|
102
|
+
|
|
103
|
+
# Verify it maps to correct model class
|
|
104
|
+
self.assertEqual(
|
|
105
|
+
HOST_MODEL_REGISTRY[MCPHostType.CODEX],
|
|
106
|
+
MCPServerConfigCodex
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Verify we can instantiate from registry
|
|
110
|
+
model_class = HOST_MODEL_REGISTRY[MCPHostType.CODEX]
|
|
111
|
+
instance = model_class(command="test")
|
|
112
|
+
self.assertIsInstance(instance, MCPServerConfigCodex)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == '__main__':
|
|
116
|
+
unittest.main()
|
|
117
|
+
|
|
@@ -15,7 +15,7 @@ from hatch.cli_hatch import handle_mcp_configure, parse_input
|
|
|
15
15
|
from hatch.mcp_host_config import MCPHostType
|
|
16
16
|
from hatch.mcp_host_config.models import (
|
|
17
17
|
MCPServerConfigGemini, MCPServerConfigCursor, MCPServerConfigVSCode,
|
|
18
|
-
MCPServerConfigClaude
|
|
18
|
+
MCPServerConfigClaude, MCPServerConfigCodex
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
|
|
@@ -298,6 +298,199 @@ class TestToolFilteringArguments(unittest.TestCase):
|
|
|
298
298
|
self.assertEqual(server_config.excludeTools, ['dangerous_tool'])
|
|
299
299
|
|
|
300
300
|
|
|
301
|
+
class TestAllCodexArguments(unittest.TestCase):
|
|
302
|
+
"""Test ALL Codex-specific CLI arguments."""
|
|
303
|
+
|
|
304
|
+
@patch('hatch.cli_hatch.MCPHostConfigurationManager')
|
|
305
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
306
|
+
def test_all_codex_arguments_accepted(self, mock_stdout, mock_manager_class):
|
|
307
|
+
"""Test that all Codex arguments are accepted and passed to model."""
|
|
308
|
+
mock_manager = MagicMock()
|
|
309
|
+
mock_manager_class.return_value = mock_manager
|
|
310
|
+
|
|
311
|
+
mock_result = MagicMock()
|
|
312
|
+
mock_result.success = True
|
|
313
|
+
mock_result.backup_path = None
|
|
314
|
+
mock_manager.configure_server.return_value = mock_result
|
|
315
|
+
|
|
316
|
+
# Test STDIO server with Codex-specific STDIO fields
|
|
317
|
+
result = handle_mcp_configure(
|
|
318
|
+
host='codex',
|
|
319
|
+
server_name='test-server',
|
|
320
|
+
command='npx',
|
|
321
|
+
args=['-y', '@upstash/context7-mcp'],
|
|
322
|
+
env_vars=['PATH', 'HOME'],
|
|
323
|
+
cwd='/workspace',
|
|
324
|
+
startup_timeout=15,
|
|
325
|
+
tool_timeout=120,
|
|
326
|
+
enabled=True,
|
|
327
|
+
include_tools=['read', 'write'],
|
|
328
|
+
exclude_tools=['delete'],
|
|
329
|
+
auto_approve=True
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Verify success
|
|
333
|
+
self.assertEqual(result, 0)
|
|
334
|
+
|
|
335
|
+
# Verify configure_server was called
|
|
336
|
+
mock_manager.configure_server.assert_called_once()
|
|
337
|
+
|
|
338
|
+
# Verify server_config is MCPServerConfigCodex
|
|
339
|
+
call_args = mock_manager.configure_server.call_args
|
|
340
|
+
server_config = call_args.kwargs['server_config']
|
|
341
|
+
self.assertIsInstance(server_config, MCPServerConfigCodex)
|
|
342
|
+
|
|
343
|
+
# Verify Codex-specific STDIO fields
|
|
344
|
+
self.assertEqual(server_config.env_vars, ['PATH', 'HOME'])
|
|
345
|
+
self.assertEqual(server_config.cwd, '/workspace')
|
|
346
|
+
self.assertEqual(server_config.startup_timeout_sec, 15)
|
|
347
|
+
self.assertEqual(server_config.tool_timeout_sec, 120)
|
|
348
|
+
self.assertTrue(server_config.enabled)
|
|
349
|
+
self.assertEqual(server_config.enabled_tools, ['read', 'write'])
|
|
350
|
+
self.assertEqual(server_config.disabled_tools, ['delete'])
|
|
351
|
+
|
|
352
|
+
@patch('hatch.cli_hatch.MCPHostConfigurationManager')
|
|
353
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
354
|
+
def test_codex_env_vars_list(self, mock_stdout, mock_manager_class):
|
|
355
|
+
"""Test that env_vars accepts multiple values as a list."""
|
|
356
|
+
mock_manager = MagicMock()
|
|
357
|
+
mock_manager_class.return_value = mock_manager
|
|
358
|
+
|
|
359
|
+
mock_result = MagicMock()
|
|
360
|
+
mock_result.success = True
|
|
361
|
+
mock_result.backup_path = None
|
|
362
|
+
mock_manager.configure_server.return_value = mock_result
|
|
363
|
+
|
|
364
|
+
result = handle_mcp_configure(
|
|
365
|
+
host='codex',
|
|
366
|
+
server_name='test-server',
|
|
367
|
+
command='npx',
|
|
368
|
+
args=['-y', 'package'],
|
|
369
|
+
env_vars=['PATH', 'HOME', 'USER'],
|
|
370
|
+
auto_approve=True
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
self.assertEqual(result, 0)
|
|
374
|
+
call_args = mock_manager.configure_server.call_args
|
|
375
|
+
server_config = call_args.kwargs['server_config']
|
|
376
|
+
self.assertEqual(server_config.env_vars, ['PATH', 'HOME', 'USER'])
|
|
377
|
+
|
|
378
|
+
@patch('hatch.cli_hatch.MCPHostConfigurationManager')
|
|
379
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
380
|
+
def test_codex_env_header_parsing(self, mock_stdout, mock_manager_class):
|
|
381
|
+
"""Test that env_header parses KEY=ENV_VAR format correctly."""
|
|
382
|
+
mock_manager = MagicMock()
|
|
383
|
+
mock_manager_class.return_value = mock_manager
|
|
384
|
+
|
|
385
|
+
mock_result = MagicMock()
|
|
386
|
+
mock_result.success = True
|
|
387
|
+
mock_result.backup_path = None
|
|
388
|
+
mock_manager.configure_server.return_value = mock_result
|
|
389
|
+
|
|
390
|
+
result = handle_mcp_configure(
|
|
391
|
+
host='codex',
|
|
392
|
+
server_name='test-server',
|
|
393
|
+
command='npx',
|
|
394
|
+
args=['-y', 'package'],
|
|
395
|
+
env_header=['X-API-Key=API_KEY', 'Authorization=AUTH_TOKEN'],
|
|
396
|
+
auto_approve=True
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
self.assertEqual(result, 0)
|
|
400
|
+
call_args = mock_manager.configure_server.call_args
|
|
401
|
+
server_config = call_args.kwargs['server_config']
|
|
402
|
+
self.assertEqual(server_config.env_http_headers, {
|
|
403
|
+
'X-API-Key': 'API_KEY',
|
|
404
|
+
'Authorization': 'AUTH_TOKEN'
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
@patch('hatch.cli_hatch.MCPHostConfigurationManager')
|
|
408
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
409
|
+
def test_codex_timeout_fields(self, mock_stdout, mock_manager_class):
|
|
410
|
+
"""Test that timeout fields are passed as integers."""
|
|
411
|
+
mock_manager = MagicMock()
|
|
412
|
+
mock_manager_class.return_value = mock_manager
|
|
413
|
+
|
|
414
|
+
mock_result = MagicMock()
|
|
415
|
+
mock_result.success = True
|
|
416
|
+
mock_result.backup_path = None
|
|
417
|
+
mock_manager.configure_server.return_value = mock_result
|
|
418
|
+
|
|
419
|
+
result = handle_mcp_configure(
|
|
420
|
+
host='codex',
|
|
421
|
+
server_name='test-server',
|
|
422
|
+
command='npx',
|
|
423
|
+
args=['-y', 'package'],
|
|
424
|
+
startup_timeout=30,
|
|
425
|
+
tool_timeout=180,
|
|
426
|
+
auto_approve=True
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
self.assertEqual(result, 0)
|
|
430
|
+
call_args = mock_manager.configure_server.call_args
|
|
431
|
+
server_config = call_args.kwargs['server_config']
|
|
432
|
+
self.assertEqual(server_config.startup_timeout_sec, 30)
|
|
433
|
+
self.assertEqual(server_config.tool_timeout_sec, 180)
|
|
434
|
+
|
|
435
|
+
@patch('hatch.cli_hatch.MCPHostConfigurationManager')
|
|
436
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
437
|
+
def test_codex_enabled_flag(self, mock_stdout, mock_manager_class):
|
|
438
|
+
"""Test that enabled flag works as boolean."""
|
|
439
|
+
mock_manager = MagicMock()
|
|
440
|
+
mock_manager_class.return_value = mock_manager
|
|
441
|
+
|
|
442
|
+
mock_result = MagicMock()
|
|
443
|
+
mock_result.success = True
|
|
444
|
+
mock_result.backup_path = None
|
|
445
|
+
mock_manager.configure_server.return_value = mock_result
|
|
446
|
+
|
|
447
|
+
result = handle_mcp_configure(
|
|
448
|
+
host='codex',
|
|
449
|
+
server_name='test-server',
|
|
450
|
+
command='npx',
|
|
451
|
+
args=['-y', 'package'],
|
|
452
|
+
enabled=True,
|
|
453
|
+
auto_approve=True
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
self.assertEqual(result, 0)
|
|
457
|
+
call_args = mock_manager.configure_server.call_args
|
|
458
|
+
server_config = call_args.kwargs['server_config']
|
|
459
|
+
self.assertTrue(server_config.enabled)
|
|
460
|
+
|
|
461
|
+
@patch('hatch.cli_hatch.MCPHostConfigurationManager')
|
|
462
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
463
|
+
def test_codex_reuses_shared_arguments(self, mock_stdout, mock_manager_class):
|
|
464
|
+
"""Test that Codex reuses shared arguments (cwd, include-tools, exclude-tools)."""
|
|
465
|
+
mock_manager = MagicMock()
|
|
466
|
+
mock_manager_class.return_value = mock_manager
|
|
467
|
+
|
|
468
|
+
mock_result = MagicMock()
|
|
469
|
+
mock_result.success = True
|
|
470
|
+
mock_result.backup_path = None
|
|
471
|
+
mock_manager.configure_server.return_value = mock_result
|
|
472
|
+
|
|
473
|
+
result = handle_mcp_configure(
|
|
474
|
+
host='codex',
|
|
475
|
+
server_name='test-server',
|
|
476
|
+
command='npx',
|
|
477
|
+
args=['-y', 'package'],
|
|
478
|
+
cwd='/workspace',
|
|
479
|
+
include_tools=['tool1', 'tool2'],
|
|
480
|
+
exclude_tools=['tool3'],
|
|
481
|
+
auto_approve=True
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
self.assertEqual(result, 0)
|
|
485
|
+
call_args = mock_manager.configure_server.call_args
|
|
486
|
+
server_config = call_args.kwargs['server_config']
|
|
487
|
+
|
|
488
|
+
# Verify shared arguments work for Codex STDIO servers
|
|
489
|
+
self.assertEqual(server_config.cwd, '/workspace')
|
|
490
|
+
self.assertEqual(server_config.enabled_tools, ['tool1', 'tool2'])
|
|
491
|
+
self.assertEqual(server_config.disabled_tools, ['tool3'])
|
|
492
|
+
|
|
493
|
+
|
|
301
494
|
if __name__ == '__main__':
|
|
302
495
|
unittest.main()
|
|
303
496
|
|
|
@@ -40,17 +40,19 @@ class TestMCPConfigureCommand(unittest.TestCase):
|
|
|
40
40
|
try:
|
|
41
41
|
result = main()
|
|
42
42
|
# If main() returns without SystemExit, check the handler was called
|
|
43
|
-
# Updated to include ALL host-specific parameters
|
|
43
|
+
# Updated to include ALL host-specific parameters (27 total)
|
|
44
44
|
mock_handler.assert_called_once_with(
|
|
45
45
|
'claude-desktop', 'weather-server', 'python', ['weather.py'],
|
|
46
|
-
None, None, None, None, False, None, None, None, None, None, None,
|
|
46
|
+
None, None, None, None, False, None, None, None, None, None, None,
|
|
47
|
+
False, None, None, None, None, None, False, None, None, False, False, False
|
|
47
48
|
)
|
|
48
49
|
except SystemExit as e:
|
|
49
50
|
# If SystemExit is raised, it should be 0 (success) and handler should have been called
|
|
50
51
|
if e.code == 0:
|
|
51
52
|
mock_handler.assert_called_once_with(
|
|
52
53
|
'claude-desktop', 'weather-server', 'python', ['weather.py'],
|
|
53
|
-
None, None, None, None, False, None, None, None, None, None, None,
|
|
54
|
+
None, None, None, None, False, None, None, None, None, None, None,
|
|
55
|
+
False, None, None, None, None, None, False, None, None, False, False, False
|
|
54
56
|
)
|
|
55
57
|
else:
|
|
56
58
|
self.fail(f"main() exited with code {e.code}, expected 0")
|
|
@@ -70,11 +72,12 @@ class TestMCPConfigureCommand(unittest.TestCase):
|
|
|
70
72
|
with patch('hatch.cli_hatch.handle_mcp_configure', return_value=0) as mock_handler:
|
|
71
73
|
try:
|
|
72
74
|
main()
|
|
73
|
-
# Updated to include ALL host-specific parameters
|
|
75
|
+
# Updated to include ALL host-specific parameters (27 total)
|
|
74
76
|
mock_handler.assert_called_once_with(
|
|
75
77
|
'cursor', 'file-server', None, None,
|
|
76
78
|
['API_KEY=secret', 'DEBUG=true'], 'http://localhost:8080',
|
|
77
|
-
['Authorization=Bearer token'], None, False, None, None, None, None, None, None,
|
|
79
|
+
['Authorization=Bearer token'], None, False, None, None, None, None, None, None,
|
|
80
|
+
False, None, None, None, None, None, False, None, None, True, True, True
|
|
78
81
|
)
|
|
79
82
|
except SystemExit as e:
|
|
80
83
|
self.assertEqual(e.code, 0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|