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.
- hatch/cli_hatch.py +120 -18
- hatch/mcp_host_config/__init__.py +4 -2
- hatch/mcp_host_config/backup.py +62 -31
- hatch/mcp_host_config/models.py +125 -1
- hatch/mcp_host_config/strategies.py +268 -1
- {hatch_xclam-0.7.0.dev12.dist-info → hatch_xclam-0.7.1.dist-info}/METADATA +41 -32
- hatch_xclam-0.7.1.dist-info/RECORD +105 -0
- hatch_xclam-0.7.1.dist-info/top_level.txt +2 -0
- tests/integration/__init__.py +5 -0
- tests/integration/test_mcp_kiro_integration.py +153 -0
- tests/regression/__init__.py +5 -0
- 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/regression/test_mcp_kiro_backup_integration.py +241 -0
- tests/regression/test_mcp_kiro_cli_integration.py +141 -0
- tests/regression/test_mcp_kiro_decorator_registration.py +71 -0
- tests/regression/test_mcp_kiro_host_strategy.py +214 -0
- tests/regression/test_mcp_kiro_model_validation.py +116 -0
- tests/regression/test_mcp_kiro_omni_conversion.py +104 -0
- tests/test_data_utils.py +108 -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.0.dev12.dist-info/RECORD +0 -152
- hatch_xclam-0.7.0.dev12.dist-info/top_level.txt +0 -3
- node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py +0 -45
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSNew.py +0 -365
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSProject.py +0 -206
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSSettings.py +0 -1272
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSSettings_test.py +0 -1547
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSToolFile.py +0 -59
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSUserFile.py +0 -152
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSUtil.py +0 -270
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/MSVSVersion.py +0 -574
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/__init__.py +0 -704
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/common.py +0 -709
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/common_test.py +0 -173
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/easy_xml.py +0 -169
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/easy_xml_test.py +0 -113
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/flock_tool.py +0 -55
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/__init__.py +0 -0
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/analyzer.py +0 -805
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/android.py +0 -1172
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/cmake.py +0 -1319
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/compile_commands_json.py +0 -128
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/dump_dependency_json.py +0 -104
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/eclipse.py +0 -462
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/gypd.py +0 -89
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/gypsh.py +0 -56
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py +0 -2745
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/msvs.py +0 -3976
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/msvs_test.py +0 -44
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/ninja.py +0 -2965
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/ninja_test.py +0 -67
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/xcode.py +0 -1391
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/generator/xcode_test.py +0 -26
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input.py +0 -3112
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/input_test.py +0 -99
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/mac_tool.py +0 -767
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/msvs_emulation.py +0 -1260
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/ninja_syntax.py +0 -174
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/simple_copy.py +0 -61
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/win_tool.py +0 -373
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcode_emulation.py +0 -1939
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcode_emulation_test.py +0 -54
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcode_ninja.py +0 -303
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xcodeproj_file.py +0 -3196
- node_modules/npm/node_modules/node-gyp/gyp/pylib/gyp/xml_fix.py +0 -65
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/__init__.py +0 -15
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_elffile.py +0 -108
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_manylinux.py +0 -252
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_musllinux.py +0 -83
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_parser.py +0 -359
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_structures.py +0 -61
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/_tokenizer.py +0 -192
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/markers.py +0 -252
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/metadata.py +0 -825
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/py.typed +0 -0
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/requirements.py +0 -90
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/specifiers.py +0 -1030
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/tags.py +0 -553
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/utils.py +0 -172
- node_modules/npm/node_modules/node-gyp/gyp/pylib/packaging/version.py +0 -563
- node_modules/npm/node_modules/node-gyp/gyp/test_gyp.py +0 -261
- {hatch_xclam-0.7.0.dev12.dist-info → hatch_xclam-0.7.1.dist-info}/WHEEL +0 -0
- {hatch_xclam-0.7.0.dev12.dist-info → hatch_xclam-0.7.1.dist-info}/entry_points.txt +0 -0
- {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,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
|
+
|