hatch-xclam 0.7.1.dev3__py3-none-any.whl → 0.8.0.dev1__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 (81) hide show
  1. hatch/__init__.py +1 -1
  2. hatch/cli/__init__.py +71 -0
  3. hatch/cli/__main__.py +1035 -0
  4. hatch/cli/cli_env.py +865 -0
  5. hatch/cli/cli_mcp.py +1965 -0
  6. hatch/cli/cli_package.py +566 -0
  7. hatch/cli/cli_system.py +136 -0
  8. hatch/cli/cli_utils.py +1289 -0
  9. hatch/cli_hatch.py +160 -2838
  10. hatch/mcp_host_config/__init__.py +10 -10
  11. hatch/mcp_host_config/adapters/__init__.py +34 -0
  12. hatch/mcp_host_config/adapters/base.py +170 -0
  13. hatch/mcp_host_config/adapters/claude.py +105 -0
  14. hatch/mcp_host_config/adapters/codex.py +104 -0
  15. hatch/mcp_host_config/adapters/cursor.py +83 -0
  16. hatch/mcp_host_config/adapters/gemini.py +75 -0
  17. hatch/mcp_host_config/adapters/kiro.py +78 -0
  18. hatch/mcp_host_config/adapters/lmstudio.py +79 -0
  19. hatch/mcp_host_config/adapters/registry.py +149 -0
  20. hatch/mcp_host_config/adapters/vscode.py +83 -0
  21. hatch/mcp_host_config/backup.py +5 -3
  22. hatch/mcp_host_config/fields.py +126 -0
  23. hatch/mcp_host_config/models.py +161 -456
  24. hatch/mcp_host_config/reporting.py +57 -16
  25. hatch/mcp_host_config/strategies.py +155 -87
  26. hatch/template_generator.py +1 -1
  27. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/METADATA +3 -2
  28. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/RECORD +52 -43
  29. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/WHEEL +1 -1
  30. hatch_xclam-0.8.0.dev1.dist-info/entry_points.txt +2 -0
  31. tests/cli_test_utils.py +280 -0
  32. tests/integration/cli/__init__.py +14 -0
  33. tests/integration/cli/test_cli_reporter_integration.py +2439 -0
  34. tests/integration/mcp/__init__.py +0 -0
  35. tests/integration/mcp/test_adapter_serialization.py +173 -0
  36. tests/regression/cli/__init__.py +16 -0
  37. tests/regression/cli/test_color_logic.py +268 -0
  38. tests/regression/cli/test_consequence_type.py +298 -0
  39. tests/regression/cli/test_error_formatting.py +328 -0
  40. tests/regression/cli/test_result_reporter.py +586 -0
  41. tests/regression/cli/test_table_formatter.py +211 -0
  42. tests/regression/mcp/__init__.py +0 -0
  43. tests/regression/mcp/test_field_filtering.py +162 -0
  44. tests/test_cli_version.py +7 -5
  45. tests/test_data/fixtures/cli_reporter_fixtures.py +184 -0
  46. tests/unit/__init__.py +0 -0
  47. tests/unit/mcp/__init__.py +0 -0
  48. tests/unit/mcp/test_adapter_protocol.py +138 -0
  49. tests/unit/mcp/test_adapter_registry.py +158 -0
  50. tests/unit/mcp/test_config_model.py +146 -0
  51. hatch_xclam-0.7.1.dev3.dist-info/entry_points.txt +0 -2
  52. tests/integration/test_mcp_kiro_integration.py +0 -153
  53. tests/regression/test_mcp_codex_backup_integration.py +0 -162
  54. tests/regression/test_mcp_codex_host_strategy.py +0 -163
  55. tests/regression/test_mcp_codex_model_validation.py +0 -117
  56. tests/regression/test_mcp_kiro_backup_integration.py +0 -241
  57. tests/regression/test_mcp_kiro_cli_integration.py +0 -141
  58. tests/regression/test_mcp_kiro_decorator_registration.py +0 -71
  59. tests/regression/test_mcp_kiro_host_strategy.py +0 -214
  60. tests/regression/test_mcp_kiro_model_validation.py +0 -116
  61. tests/regression/test_mcp_kiro_omni_conversion.py +0 -104
  62. tests/test_mcp_atomic_operations.py +0 -276
  63. tests/test_mcp_backup_integration.py +0 -308
  64. tests/test_mcp_cli_all_host_specific_args.py +0 -496
  65. tests/test_mcp_cli_backup_management.py +0 -295
  66. tests/test_mcp_cli_direct_management.py +0 -456
  67. tests/test_mcp_cli_discovery_listing.py +0 -582
  68. tests/test_mcp_cli_host_config_integration.py +0 -823
  69. tests/test_mcp_cli_package_management.py +0 -360
  70. tests/test_mcp_cli_partial_updates.py +0 -859
  71. tests/test_mcp_environment_integration.py +0 -520
  72. tests/test_mcp_host_config_backup.py +0 -257
  73. tests/test_mcp_host_configuration_manager.py +0 -331
  74. tests/test_mcp_host_registry_decorator.py +0 -348
  75. tests/test_mcp_pydantic_architecture_v4.py +0 -603
  76. tests/test_mcp_server_config_models.py +0 -242
  77. tests/test_mcp_server_config_type_field.py +0 -221
  78. tests/test_mcp_sync_functionality.py +0 -316
  79. tests/test_mcp_user_feedback_reporting.py +0 -359
  80. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/licenses/LICENSE +0 -0
  81. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/top_level.txt +0 -0
@@ -1,162 +0,0 @@
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
-
@@ -1,163 +0,0 @@
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
-
@@ -1,117 +0,0 @@
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
-
@@ -1,241 +0,0 @@
1
- """Tests for Kiro MCP backup integration.
2
-
3
- This module tests the integration between KiroHostStrategy and the backup system,
4
- ensuring that Kiro configurations are properly backed up during write operations.
5
- """
6
-
7
- import json
8
- import tempfile
9
- import unittest
10
- from pathlib import Path
11
- from unittest.mock import patch, MagicMock
12
-
13
- from wobble.decorators import regression_test
14
-
15
- from hatch.mcp_host_config.strategies import KiroHostStrategy
16
- from hatch.mcp_host_config.models import HostConfiguration, MCPServerConfig
17
- from hatch.mcp_host_config.backup import MCPHostConfigBackupManager, BackupResult
18
-
19
-
20
- class TestKiroBackupIntegration(unittest.TestCase):
21
- """Test Kiro backup integration with host strategy."""
22
-
23
- def setUp(self):
24
- """Set up test environment."""
25
- self.temp_dir = Path(tempfile.mkdtemp(prefix="test_kiro_backup_"))
26
- self.config_dir = self.temp_dir / ".kiro" / "settings"
27
- self.config_dir.mkdir(parents=True)
28
- self.config_file = self.config_dir / "mcp.json"
29
-
30
- self.backup_dir = self.temp_dir / "backups"
31
- self.backup_manager = MCPHostConfigBackupManager(backup_root=self.backup_dir)
32
-
33
- self.strategy = KiroHostStrategy()
34
-
35
- def tearDown(self):
36
- """Clean up test environment."""
37
- import shutil
38
- shutil.rmtree(self.temp_dir, ignore_errors=True)
39
-
40
- @regression_test
41
- def test_write_configuration_creates_backup_by_default(self):
42
- """Test that write_configuration creates backup by default when file exists."""
43
- # Create initial configuration
44
- initial_config = {
45
- "mcpServers": {
46
- "existing-server": {
47
- "command": "uvx",
48
- "args": ["existing-package"]
49
- }
50
- },
51
- "otherSettings": {
52
- "theme": "dark"
53
- }
54
- }
55
-
56
- with open(self.config_file, 'w') as f:
57
- json.dump(initial_config, f, indent=2)
58
-
59
- # Create new configuration to write
60
- server_config = MCPServerConfig(
61
- command="uvx",
62
- args=["new-package"]
63
- )
64
- host_config = HostConfiguration(servers={"new-server": server_config})
65
-
66
- # Mock the strategy's get_config_path to return our test file
67
- # Mock the backup manager creation to use our test backup manager
68
- with patch.object(self.strategy, 'get_config_path', return_value=self.config_file), \
69
- patch('hatch.mcp_host_config.strategies.MCPHostConfigBackupManager', return_value=self.backup_manager):
70
- # Write configuration (should create backup)
71
- result = self.strategy.write_configuration(host_config, no_backup=False)
72
-
73
- # Verify write succeeded
74
- self.assertTrue(result)
75
-
76
- # Verify backup was created
77
- backups = self.backup_manager.list_backups("kiro")
78
- self.assertEqual(len(backups), 1)
79
-
80
- # Verify backup contains original content
81
- backup_content = json.loads(backups[0].file_path.read_text())
82
- self.assertEqual(backup_content, initial_config)
83
-
84
- # Verify new configuration was written
85
- new_content = json.loads(self.config_file.read_text())
86
- self.assertIn("new-server", new_content["mcpServers"])
87
- self.assertEqual(new_content["otherSettings"], {"theme": "dark"}) # Preserved
88
-
89
- @regression_test
90
- def test_write_configuration_skips_backup_when_requested(self):
91
- """Test that write_configuration skips backup when no_backup=True."""
92
- # Create initial configuration
93
- initial_config = {
94
- "mcpServers": {
95
- "existing-server": {
96
- "command": "uvx",
97
- "args": ["existing-package"]
98
- }
99
- }
100
- }
101
-
102
- with open(self.config_file, 'w') as f:
103
- json.dump(initial_config, f, indent=2)
104
-
105
- # Create new configuration to write
106
- server_config = MCPServerConfig(
107
- command="uvx",
108
- args=["new-package"]
109
- )
110
- host_config = HostConfiguration(servers={"new-server": server_config})
111
-
112
- # Mock the strategy's get_config_path to return our test file
113
- # Mock the backup manager creation to use our test backup manager
114
- with patch.object(self.strategy, 'get_config_path', return_value=self.config_file), \
115
- patch('hatch.mcp_host_config.strategies.MCPHostConfigBackupManager', return_value=self.backup_manager):
116
- # Write configuration with no_backup=True
117
- result = self.strategy.write_configuration(host_config, no_backup=True)
118
-
119
- # Verify write succeeded
120
- self.assertTrue(result)
121
-
122
- # Verify no backup was created
123
- backups = self.backup_manager.list_backups("kiro")
124
- self.assertEqual(len(backups), 0)
125
-
126
- # Verify new configuration was written
127
- new_content = json.loads(self.config_file.read_text())
128
- self.assertIn("new-server", new_content["mcpServers"])
129
-
130
- @regression_test
131
- def test_write_configuration_no_backup_for_new_file(self):
132
- """Test that no backup is created when writing to a new file."""
133
- # Ensure config file doesn't exist
134
- self.assertFalse(self.config_file.exists())
135
-
136
- # Create configuration to write
137
- server_config = MCPServerConfig(
138
- command="uvx",
139
- args=["new-package"]
140
- )
141
- host_config = HostConfiguration(servers={"new-server": server_config})
142
-
143
- # Mock the strategy's get_config_path to return our test file
144
- # Mock the backup manager creation to use our test backup manager
145
- with patch.object(self.strategy, 'get_config_path', return_value=self.config_file), \
146
- patch('hatch.mcp_host_config.strategies.MCPHostConfigBackupManager', return_value=self.backup_manager):
147
- # Write configuration
148
- result = self.strategy.write_configuration(host_config, no_backup=False)
149
-
150
- # Verify write succeeded
151
- self.assertTrue(result)
152
-
153
- # Verify no backup was created (file didn't exist)
154
- backups = self.backup_manager.list_backups("kiro")
155
- self.assertEqual(len(backups), 0)
156
-
157
- # Verify configuration was written
158
- self.assertTrue(self.config_file.exists())
159
- new_content = json.loads(self.config_file.read_text())
160
- self.assertIn("new-server", new_content["mcpServers"])
161
-
162
- @regression_test
163
- def test_backup_failure_prevents_write(self):
164
- """Test that backup failure prevents configuration write."""
165
- # Create initial configuration
166
- initial_config = {
167
- "mcpServers": {
168
- "existing-server": {
169
- "command": "uvx",
170
- "args": ["existing-package"]
171
- }
172
- }
173
- }
174
-
175
- with open(self.config_file, 'w') as f:
176
- json.dump(initial_config, f, indent=2)
177
-
178
- # Create new configuration to write
179
- server_config = MCPServerConfig(
180
- command="uvx",
181
- args=["new-package"]
182
- )
183
- host_config = HostConfiguration(servers={"new-server": server_config})
184
-
185
- # Mock backup manager to fail
186
- with patch('hatch.mcp_host_config.strategies.MCPHostConfigBackupManager') as mock_backup_class:
187
- mock_backup_manager = MagicMock()
188
- mock_backup_manager.create_backup.return_value = BackupResult(
189
- success=False,
190
- error_message="Backup failed"
191
- )
192
- mock_backup_class.return_value = mock_backup_manager
193
-
194
- # Mock the strategy's get_config_path to return our test file
195
- with patch.object(self.strategy, 'get_config_path', return_value=self.config_file):
196
- # Write configuration (should fail due to backup failure)
197
- result = self.strategy.write_configuration(host_config, no_backup=False)
198
-
199
- # Verify write failed
200
- self.assertFalse(result)
201
-
202
- # Verify original configuration is unchanged
203
- current_content = json.loads(self.config_file.read_text())
204
- self.assertEqual(current_content, initial_config)
205
-
206
- @regression_test
207
- def test_kiro_hostname_supported_in_backup_system(self):
208
- """Test that 'kiro' hostname is supported by the backup system."""
209
- # Create test configuration file
210
- test_config = {
211
- "mcpServers": {
212
- "test-server": {
213
- "command": "uvx",
214
- "args": ["test-package"]
215
- }
216
- }
217
- }
218
-
219
- with open(self.config_file, 'w') as f:
220
- json.dump(test_config, f, indent=2)
221
-
222
- # Test backup creation with 'kiro' hostname
223
- result = self.backup_manager.create_backup(self.config_file, "kiro")
224
-
225
- # Verify backup succeeded
226
- self.assertTrue(result.success)
227
- self.assertIsNotNone(result.backup_path)
228
- self.assertTrue(result.backup_path.exists())
229
-
230
- # Verify backup filename format
231
- expected_pattern = r"mcp\.json\.kiro\.\d{8}_\d{6}_\d{6}"
232
- import re
233
- self.assertRegex(result.backup_path.name, expected_pattern)
234
-
235
- # Verify backup content
236
- backup_content = json.loads(result.backup_path.read_text())
237
- self.assertEqual(backup_content, test_config)
238
-
239
-
240
- if __name__ == '__main__':
241
- unittest.main()