Skip to content

Commit 2858ba5

Browse files
LittleCoinCoinLittleCoinCoin
authored andcommitted
tests(codex): add comprehensive Codex host strategy test suite
Add 15 tests across 3 test files covering all Codex functionality: Strategy Tests (8 tests) - test_mcp_codex_host_strategy.py: - Config path resolution (verifies .codex/config.toml) - Config key validation (verifies 'mcp_servers' underscore format) - STDIO server configuration validation - HTTP server configuration validation - Host availability detection - Read configuration success (with nested env parsing) - Read configuration when file doesn't exist - Write configuration preserves [features] section Backup Integration Tests (4 tests) - test_mcp_codex_backup_integration.py: - Write creates backup by default when file exists - Write skips backup when no_backup=True - No backup created for new files - 'codex' hostname supported in backup system Model Validation Tests (3 tests) - test_mcp_codex_model_validation.py: - Codex-specific fields accepted in MCPServerConfigCodex - from_omni() conversion works correctly - HOST_MODEL_REGISTRY contains Codex mapping Test Data Files: - valid_config.toml: Complete config with features and env sections - stdio_server.toml: STDIO server example - http_server.toml: HTTP server with authentication All tests follow established patterns from Kiro integration tests and use wobble.decorators.regression_test decorator.
1 parent 4e55b34 commit 2858ba5

File tree

6 files changed

+469
-0
lines changed

6 files changed

+469
-0
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)