hatch-xclam 0.7.0.dev12__py3-none-any.whl → 0.7.1__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.
Files changed (87) hide show
  1. hatch/cli_hatch.py +120 -18
  2. hatch/mcp_host_config/__init__.py +4 -2
  3. hatch/mcp_host_config/backup.py +62 -31
  4. hatch/mcp_host_config/models.py +125 -1
  5. hatch/mcp_host_config/strategies.py +268 -1
  6. {hatch_xclam-0.7.0.dev12.dist-info → hatch_xclam-0.7.1.dist-info}/METADATA +41 -32
  7. hatch_xclam-0.7.1.dist-info/RECORD +105 -0
  8. hatch_xclam-0.7.1.dist-info/top_level.txt +2 -0
  9. tests/integration/__init__.py +5 -0
  10. tests/integration/test_mcp_kiro_integration.py +153 -0
  11. tests/regression/__init__.py +5 -0
  12. tests/regression/test_mcp_codex_backup_integration.py +162 -0
  13. tests/regression/test_mcp_codex_host_strategy.py +163 -0
  14. tests/regression/test_mcp_codex_model_validation.py +117 -0
  15. tests/regression/test_mcp_kiro_backup_integration.py +241 -0
  16. tests/regression/test_mcp_kiro_cli_integration.py +141 -0
  17. tests/regression/test_mcp_kiro_decorator_registration.py +71 -0
  18. tests/regression/test_mcp_kiro_host_strategy.py +214 -0
  19. tests/regression/test_mcp_kiro_model_validation.py +116 -0
  20. tests/regression/test_mcp_kiro_omni_conversion.py +104 -0
  21. tests/test_data_utils.py +108 -0
  22. tests/test_mcp_cli_all_host_specific_args.py +194 -1
  23. tests/test_mcp_cli_direct_management.py +8 -5
  24. hatch_xclam-0.7.0.dev12.dist-info/RECORD +0 -152
  25. hatch_xclam-0.7.0.dev12.dist-info/top_level.txt +0 -3
  26. node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py +0 -45
  27. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSNew.py +0 -365
  28. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSProject.py +0 -206
  29. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSSettings.py +0 -1272
  30. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSSettings_test.py +0 -1547
  31. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSToolFile.py +0 -59
  32. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSUserFile.py +0 -152
  33. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSUtil.py +0 -270
  34. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSVersion.py +0 -574
  35. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/__init__.py +0 -704
  36. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/common.py +0 -709
  37. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/common_test.py +0 -173
  38. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/easy_xml.py +0 -169
  39. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/easy_xml_test.py +0 -113
  40. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/flock_tool.py +0 -55
  41. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/__init__.py +0 -0
  42. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/analyzer.py +0 -805
  43. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/android.py +0 -1172
  44. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/cmake.py +0 -1319
  45. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/compile_commands_json.py +0 -128
  46. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/dump_dependency_json.py +0 -104
  47. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/eclipse.py +0 -462
  48. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/gypd.py +0 -89
  49. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/gypsh.py +0 -56
  50. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py +0 -2745
  51. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/msvs.py +0 -3976
  52. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/msvs_test.py +0 -44
  53. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/ninja.py +0 -2965
  54. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/ninja_test.py +0 -67
  55. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/xcode.py +0 -1391
  56. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/xcode_test.py +0 -26
  57. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input.py +0 -3112
  58. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input_test.py +0 -99
  59. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/mac_tool.py +0 -767
  60. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/msvs_emulation.py +0 -1260
  61. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/ninja_syntax.py +0 -174
  62. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/simple_copy.py +0 -61
  63. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/win_tool.py +0 -373
  64. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcode_emulation.py +0 -1939
  65. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcode_emulation_test.py +0 -54
  66. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcode_ninja.py +0 -303
  67. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcodeproj_file.py +0 -3196
  68. node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xml_fix.py +0 -65
  69. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/__init__.py +0 -15
  70. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_elffile.py +0 -108
  71. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_manylinux.py +0 -252
  72. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_musllinux.py +0 -83
  73. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_parser.py +0 -359
  74. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_structures.py +0 -61
  75. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_tokenizer.py +0 -192
  76. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/markers.py +0 -252
  77. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/metadata.py +0 -825
  78. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/py.typed +0 -0
  79. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/requirements.py +0 -90
  80. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/specifiers.py +0 -1030
  81. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/tags.py +0 -553
  82. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/utils.py +0 -172
  83. node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/version.py +0 -563
  84. node_modules/npm/node_modules/node-gyp/gyp/test_gyp.py +0 -261
  85. {hatch_xclam-0.7.0.dev12.dist-info → hatch_xclam-0.7.1.dist-info}/WHEEL +0 -0
  86. {hatch_xclam-0.7.0.dev12.dist-info → hatch_xclam-0.7.1.dist-info}/entry_points.txt +0 -0
  87. {hatch_xclam-0.7.0.dev12.dist-info → hatch_xclam-0.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,153 @@
1
+ """
2
+ Kiro MCP Integration Tests
3
+
4
+ End-to-end integration tests combining CLI, model conversion, and strategy operations.
5
+ """
6
+
7
+ import unittest
8
+ from unittest.mock import patch, MagicMock
9
+
10
+ from wobble.decorators import integration_test
11
+
12
+ from hatch.cli_hatch import handle_mcp_configure
13
+ from hatch.mcp_host_config.models import (
14
+ HOST_MODEL_REGISTRY,
15
+ MCPHostType,
16
+ MCPServerConfigKiro
17
+ )
18
+
19
+
20
+ class TestKiroIntegration(unittest.TestCase):
21
+ """Test suite for end-to-end Kiro integration."""
22
+
23
+ @integration_test(scope="component")
24
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
25
+ def test_kiro_end_to_end_configuration(self, mock_manager_class):
26
+ """Test complete Kiro configuration workflow."""
27
+ # Setup mocks
28
+ mock_manager = MagicMock()
29
+ mock_manager_class.return_value = mock_manager
30
+
31
+ mock_result = MagicMock()
32
+ mock_result.success = True
33
+ mock_manager.configure_server.return_value = mock_result
34
+
35
+ # Execute CLI command with Kiro-specific arguments
36
+ result = handle_mcp_configure(
37
+ host='kiro',
38
+ server_name='augment-server',
39
+ command='auggie',
40
+ args=['--mcp', '-m', 'default'],
41
+ disabled=False,
42
+ auto_approve_tools=['codebase-retrieval', 'fetch'],
43
+ disable_tools=['dangerous-tool'],
44
+ auto_approve=True
45
+ )
46
+
47
+ # Verify success
48
+ self.assertEqual(result, 0)
49
+
50
+ # Verify configuration manager was called
51
+ mock_manager.configure_server.assert_called_once()
52
+
53
+ # Verify server configuration
54
+ call_args = mock_manager.configure_server.call_args
55
+ server_config = call_args.kwargs['server_config']
56
+
57
+ # Verify all Kiro-specific fields
58
+ self.assertFalse(server_config.disabled)
59
+ self.assertEqual(len(server_config.autoApprove), 2)
60
+ self.assertEqual(len(server_config.disabledTools), 1)
61
+ self.assertIn('codebase-retrieval', server_config.autoApprove)
62
+ self.assertIn('dangerous-tool', server_config.disabledTools)
63
+
64
+ @integration_test(scope="system")
65
+ def test_kiro_host_model_registry_integration(self):
66
+ """Test Kiro integration with HOST_MODEL_REGISTRY."""
67
+ # Verify Kiro is in registry
68
+ self.assertIn(MCPHostType.KIRO, HOST_MODEL_REGISTRY)
69
+
70
+ # Verify correct model class
71
+ model_class = HOST_MODEL_REGISTRY[MCPHostType.KIRO]
72
+ self.assertEqual(model_class.__name__, "MCPServerConfigKiro")
73
+
74
+ # Test model instantiation
75
+ model_instance = model_class(
76
+ name="test-server",
77
+ command="auggie",
78
+ disabled=True
79
+ )
80
+ self.assertTrue(model_instance.disabled)
81
+
82
+ @integration_test(scope="component")
83
+ def test_kiro_model_to_strategy_workflow(self):
84
+ """Test workflow from model creation to strategy operations."""
85
+ # Import to trigger registration
86
+ import hatch.mcp_host_config.strategies
87
+ from hatch.mcp_host_config.host_management import MCPHostRegistry
88
+
89
+ # Create Kiro model
90
+ kiro_model = MCPServerConfigKiro(
91
+ name="workflow-test",
92
+ command="auggie",
93
+ args=["--mcp"],
94
+ disabled=False,
95
+ autoApprove=["codebase-retrieval"]
96
+ )
97
+
98
+ # Get Kiro strategy
99
+ strategy = MCPHostRegistry.get_strategy(MCPHostType.KIRO)
100
+
101
+ # Verify strategy can validate the model
102
+ self.assertTrue(strategy.validate_server_config(kiro_model))
103
+
104
+ # Verify model fields are accessible
105
+ self.assertEqual(kiro_model.command, "auggie")
106
+ self.assertFalse(kiro_model.disabled)
107
+ self.assertIn("codebase-retrieval", kiro_model.autoApprove)
108
+
109
+ @integration_test(scope="end_to_end")
110
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
111
+ def test_kiro_complete_lifecycle(self, mock_manager_class):
112
+ """Test complete Kiro server lifecycle: create, configure, validate."""
113
+ # Setup mocks
114
+ mock_manager = MagicMock()
115
+ mock_manager_class.return_value = mock_manager
116
+
117
+ mock_result = MagicMock()
118
+ mock_result.success = True
119
+ mock_manager.configure_server.return_value = mock_result
120
+
121
+ # Step 1: Configure server via CLI
122
+ result = handle_mcp_configure(
123
+ host='kiro',
124
+ server_name='lifecycle-test',
125
+ command='auggie',
126
+ args=['--mcp', '-w', '.'],
127
+ disabled=False,
128
+ auto_approve_tools=['codebase-retrieval'],
129
+ auto_approve=True
130
+ )
131
+
132
+ # Verify CLI success
133
+ self.assertEqual(result, 0)
134
+
135
+ # Step 2: Verify configuration manager interaction
136
+ mock_manager.configure_server.assert_called_once()
137
+ call_args = mock_manager.configure_server.call_args
138
+
139
+ # Step 3: Verify server configuration structure
140
+ server_config = call_args.kwargs['server_config']
141
+ self.assertEqual(server_config.name, 'lifecycle-test')
142
+ self.assertEqual(server_config.command, 'auggie')
143
+ self.assertIn('--mcp', server_config.args)
144
+ self.assertIn('-w', server_config.args)
145
+ self.assertFalse(server_config.disabled)
146
+ self.assertIn('codebase-retrieval', server_config.autoApprove)
147
+
148
+ # Step 4: Verify model type
149
+ self.assertIsInstance(server_config, MCPServerConfigKiro)
150
+
151
+
152
+ if __name__ == '__main__':
153
+ unittest.main()
@@ -0,0 +1,5 @@
1
+ """
2
+ Regression tests for Hatch MCP functionality.
3
+
4
+ These tests validate existing functionality to prevent breaking changes.
5
+ """
@@ -0,0 +1,162 @@
1
+ """
2
+ Codex MCP Backup Integration Tests
3
+
4
+ Tests for Codex TOML backup integration including backup creation,
5
+ restoration, and the no_backup parameter.
6
+ """
7
+
8
+ import unittest
9
+ import tempfile
10
+ import tomllib
11
+ from pathlib import Path
12
+
13
+ from wobble.decorators import regression_test
14
+
15
+ from hatch.mcp_host_config.strategies import CodexHostStrategy
16
+ from hatch.mcp_host_config.models import MCPServerConfig, HostConfiguration
17
+ from hatch.mcp_host_config.backup import MCPHostConfigBackupManager, BackupInfo
18
+
19
+
20
+ class TestCodexBackupIntegration(unittest.TestCase):
21
+ """Test suite for Codex backup integration."""
22
+
23
+ def setUp(self):
24
+ """Set up test environment."""
25
+ self.strategy = CodexHostStrategy()
26
+
27
+ @regression_test
28
+ def test_write_configuration_creates_backup_by_default(self):
29
+ """Test that write_configuration creates backup by default when file exists."""
30
+ with tempfile.TemporaryDirectory() as tmpdir:
31
+ config_path = Path(tmpdir) / "config.toml"
32
+ backup_dir = Path(tmpdir) / "backups"
33
+
34
+ # Create initial config
35
+ initial_toml = """[mcp_servers.old-server]
36
+ command = "old-command"
37
+ """
38
+ config_path.write_text(initial_toml)
39
+
40
+ # Create new configuration
41
+ new_config = HostConfiguration(servers={
42
+ 'new-server': MCPServerConfig(
43
+ command='new-command',
44
+ args=['--test']
45
+ )
46
+ })
47
+
48
+ # Patch paths
49
+ from unittest.mock import patch
50
+ with patch.object(self.strategy, 'get_config_path', return_value=config_path):
51
+ with patch('hatch.mcp_host_config.strategies.MCPHostConfigBackupManager') as MockBackupManager:
52
+ # Create a real backup manager with custom backup dir
53
+ real_backup_manager = MCPHostConfigBackupManager(backup_root=backup_dir)
54
+ MockBackupManager.return_value = real_backup_manager
55
+
56
+ # Write configuration (should create backup)
57
+ success = self.strategy.write_configuration(new_config, no_backup=False)
58
+ self.assertTrue(success)
59
+
60
+ # Verify backup was created
61
+ backup_files = list(backup_dir.glob('codex/*.toml.*'))
62
+ self.assertGreater(len(backup_files), 0, "Backup file should be created")
63
+
64
+ @regression_test
65
+ def test_write_configuration_skips_backup_when_requested(self):
66
+ """Test that write_configuration skips backup when no_backup=True."""
67
+ with tempfile.TemporaryDirectory() as tmpdir:
68
+ config_path = Path(tmpdir) / "config.toml"
69
+ backup_dir = Path(tmpdir) / "backups"
70
+
71
+ # Create initial config
72
+ initial_toml = """[mcp_servers.old-server]
73
+ command = "old-command"
74
+ """
75
+ config_path.write_text(initial_toml)
76
+
77
+ # Create new configuration
78
+ new_config = HostConfiguration(servers={
79
+ 'new-server': MCPServerConfig(
80
+ command='new-command'
81
+ )
82
+ })
83
+
84
+ # Patch paths
85
+ from unittest.mock import patch
86
+ with patch.object(self.strategy, 'get_config_path', return_value=config_path):
87
+ with patch('hatch.mcp_host_config.strategies.MCPHostConfigBackupManager') as MockBackupManager:
88
+ real_backup_manager = MCPHostConfigBackupManager(backup_root=backup_dir)
89
+ MockBackupManager.return_value = real_backup_manager
90
+
91
+ # Write configuration with no_backup=True
92
+ success = self.strategy.write_configuration(new_config, no_backup=True)
93
+ self.assertTrue(success)
94
+
95
+ # Verify no backup was created
96
+ if backup_dir.exists():
97
+ backup_files = list(backup_dir.glob('codex/*.toml.*'))
98
+ self.assertEqual(len(backup_files), 0, "No backup should be created when no_backup=True")
99
+
100
+ @regression_test
101
+ def test_write_configuration_no_backup_for_new_file(self):
102
+ """Test that no backup is created when writing to a new file."""
103
+ with tempfile.TemporaryDirectory() as tmpdir:
104
+ config_path = Path(tmpdir) / "config.toml"
105
+ backup_dir = Path(tmpdir) / "backups"
106
+
107
+ # Don't create initial file - this is a new file
108
+
109
+ # Create new configuration
110
+ new_config = HostConfiguration(servers={
111
+ 'new-server': MCPServerConfig(
112
+ command='new-command'
113
+ )
114
+ })
115
+
116
+ # Patch paths
117
+ from unittest.mock import patch
118
+ with patch.object(self.strategy, 'get_config_path', return_value=config_path):
119
+ with patch('hatch.mcp_host_config.strategies.MCPHostConfigBackupManager') as MockBackupManager:
120
+ real_backup_manager = MCPHostConfigBackupManager(backup_root=backup_dir)
121
+ MockBackupManager.return_value = real_backup_manager
122
+
123
+ # Write configuration to new file
124
+ success = self.strategy.write_configuration(new_config, no_backup=False)
125
+ self.assertTrue(success)
126
+
127
+ # Verify file was created
128
+ self.assertTrue(config_path.exists())
129
+
130
+ # Verify no backup was created (nothing to backup)
131
+ if backup_dir.exists():
132
+ backup_files = list(backup_dir.glob('codex/*.toml.*'))
133
+ self.assertEqual(len(backup_files), 0, "No backup for new file")
134
+
135
+ @regression_test
136
+ def test_codex_hostname_supported_in_backup_system(self):
137
+ """Test that 'codex' hostname is supported by the backup system."""
138
+ with tempfile.TemporaryDirectory() as tmpdir:
139
+ config_path = Path(tmpdir) / "config.toml"
140
+ backup_dir = Path(tmpdir) / "backups"
141
+
142
+ # Create a config file
143
+ config_path.write_text("[mcp_servers.test]\ncommand = 'test'\n")
144
+
145
+ # Create backup manager
146
+ backup_manager = MCPHostConfigBackupManager(backup_root=backup_dir)
147
+
148
+ # Create backup with 'codex' hostname - should not raise validation error
149
+ result = backup_manager.create_backup(config_path, 'codex')
150
+
151
+ # Verify backup succeeded
152
+ self.assertTrue(result.success, "Backup with 'codex' hostname should succeed")
153
+ self.assertIsNotNone(result.backup_path)
154
+
155
+ # Verify backup filename follows pattern
156
+ backup_filename = result.backup_path.name
157
+ self.assertTrue(backup_filename.startswith('config.toml.codex.'))
158
+
159
+
160
+ if __name__ == '__main__':
161
+ unittest.main()
162
+
@@ -0,0 +1,163 @@
1
+ """
2
+ Codex MCP Host Strategy Tests
3
+
4
+ Tests for CodexHostStrategy implementation including path resolution,
5
+ configuration read/write, TOML handling, and host detection.
6
+ """
7
+
8
+ import unittest
9
+ import tempfile
10
+ import tomllib
11
+ from unittest.mock import patch, mock_open, MagicMock
12
+ from pathlib import Path
13
+
14
+ from wobble.decorators import regression_test
15
+
16
+ from hatch.mcp_host_config.strategies import CodexHostStrategy
17
+ from hatch.mcp_host_config.models import MCPServerConfig, HostConfiguration
18
+
19
+ # Import test data loader from local tests module
20
+ import sys
21
+ from pathlib import Path
22
+ sys.path.insert(0, str(Path(__file__).parent.parent))
23
+ from test_data_utils import MCPHostConfigTestDataLoader
24
+
25
+
26
+ class TestCodexHostStrategy(unittest.TestCase):
27
+ """Test suite for CodexHostStrategy implementation."""
28
+
29
+ def setUp(self):
30
+ """Set up test environment."""
31
+ self.strategy = CodexHostStrategy()
32
+ self.test_data_loader = MCPHostConfigTestDataLoader()
33
+
34
+ @regression_test
35
+ def test_codex_config_path_resolution(self):
36
+ """Test Codex configuration path resolution."""
37
+ config_path = self.strategy.get_config_path()
38
+
39
+ # Verify path structure (use normalized path for cross-platform compatibility)
40
+ self.assertIsNotNone(config_path)
41
+ normalized_path = str(config_path).replace('\\', '/')
42
+ self.assertTrue(normalized_path.endswith('.codex/config.toml'))
43
+ self.assertEqual(config_path.name, 'config.toml')
44
+ self.assertEqual(config_path.suffix, '.toml') # Verify TOML extension
45
+
46
+ @regression_test
47
+ def test_codex_config_key(self):
48
+ """Test Codex configuration key."""
49
+ config_key = self.strategy.get_config_key()
50
+ # Codex uses underscore, not camelCase
51
+ self.assertEqual(config_key, "mcp_servers")
52
+ self.assertNotEqual(config_key, "mcpServers") # Verify different from other hosts
53
+
54
+ @regression_test
55
+ def test_codex_server_config_validation_stdio(self):
56
+ """Test Codex STDIO server configuration validation."""
57
+ # Test local server validation
58
+ local_config = MCPServerConfig(
59
+ command="npx",
60
+ args=["-y", "package"]
61
+ )
62
+ self.assertTrue(self.strategy.validate_server_config(local_config))
63
+
64
+ @regression_test
65
+ def test_codex_server_config_validation_http(self):
66
+ """Test Codex HTTP server configuration validation."""
67
+ # Test remote server validation
68
+ remote_config = MCPServerConfig(
69
+ url="https://api.example.com/mcp"
70
+ )
71
+ self.assertTrue(self.strategy.validate_server_config(remote_config))
72
+
73
+ @patch('pathlib.Path.exists')
74
+ @regression_test
75
+ def test_codex_host_availability_detection(self, mock_exists):
76
+ """Test Codex host availability detection."""
77
+ # Test when Codex directory exists
78
+ mock_exists.return_value = True
79
+ self.assertTrue(self.strategy.is_host_available())
80
+
81
+ # Test when Codex directory doesn't exist
82
+ mock_exists.return_value = False
83
+ self.assertFalse(self.strategy.is_host_available())
84
+
85
+ @regression_test
86
+ def test_codex_read_configuration_success(self):
87
+ """Test successful Codex TOML configuration reading."""
88
+ # Load test data
89
+ test_toml_path = Path(__file__).parent.parent / "test_data" / "codex" / "valid_config.toml"
90
+
91
+ with patch.object(self.strategy, 'get_config_path', return_value=test_toml_path):
92
+ config = self.strategy.read_configuration()
93
+
94
+ # Verify configuration was read
95
+ self.assertIsInstance(config, HostConfiguration)
96
+ self.assertIn('context7', config.servers)
97
+
98
+ # Verify server details
99
+ server = config.servers['context7']
100
+ self.assertEqual(server.command, 'npx')
101
+ self.assertEqual(server.args, ['-y', '@upstash/context7-mcp'])
102
+
103
+ # Verify nested env section was parsed correctly
104
+ self.assertIsNotNone(server.env)
105
+ self.assertEqual(server.env.get('MY_VAR'), 'value')
106
+
107
+ @regression_test
108
+ def test_codex_read_configuration_file_not_exists(self):
109
+ """Test Codex configuration reading when file doesn't exist."""
110
+ non_existent_path = Path("/non/existent/path/config.toml")
111
+
112
+ with patch.object(self.strategy, 'get_config_path', return_value=non_existent_path):
113
+ config = self.strategy.read_configuration()
114
+
115
+ # Should return empty configuration without error
116
+ self.assertIsInstance(config, HostConfiguration)
117
+ self.assertEqual(len(config.servers), 0)
118
+
119
+ @regression_test
120
+ def test_codex_write_configuration_preserves_features(self):
121
+ """Test that write_configuration preserves [features] section."""
122
+ with tempfile.TemporaryDirectory() as tmpdir:
123
+ config_path = Path(tmpdir) / "config.toml"
124
+
125
+ # Create initial config with features section
126
+ initial_toml = """[features]
127
+ rmcp_client = true
128
+
129
+ [mcp_servers.existing]
130
+ command = "old-command"
131
+ """
132
+ config_path.write_text(initial_toml)
133
+
134
+ # Create new configuration to write
135
+ new_config = HostConfiguration(servers={
136
+ 'new-server': MCPServerConfig(
137
+ command='new-command',
138
+ args=['--test']
139
+ )
140
+ })
141
+
142
+ # Write configuration
143
+ with patch.object(self.strategy, 'get_config_path', return_value=config_path):
144
+ success = self.strategy.write_configuration(new_config, no_backup=True)
145
+ self.assertTrue(success)
146
+
147
+ # Read back and verify features section preserved
148
+ with open(config_path, 'rb') as f:
149
+ result_data = tomllib.load(f)
150
+
151
+ # Verify features section preserved
152
+ self.assertIn('features', result_data)
153
+ self.assertTrue(result_data['features'].get('rmcp_client'))
154
+
155
+ # Verify new server added
156
+ self.assertIn('mcp_servers', result_data)
157
+ self.assertIn('new-server', result_data['mcp_servers'])
158
+ self.assertEqual(result_data['mcp_servers']['new-server']['command'], 'new-command')
159
+
160
+
161
+ if __name__ == '__main__':
162
+ unittest.main()
163
+
@@ -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
+