hatch-xclam 0.7.1.dev1__py3-none-any.whl → 0.7.1.dev3__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.
@@ -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, False, False, False
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, False, False, False
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, True, True, True
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)