"""
Tests for MCP tool integration and global state management.
"""
import os
from unittest.mock import Mock, patch
import pytest
import mcp_odoo_shell.server
from mcp_odoo_shell import (
execute_odoo_code,
get_shell_manager,
list_odoo_models,
odoo_model_info,
reset_odoo_shell,
)
class TestGetShellManager:
"""Test the global shell manager singleton."""
def test_creates_manager_from_env_vars(self, reset_global_state):
"""Test that get_shell_manager creates manager from environment variables."""
env_vars = {
'ODOO_BIN_PATH': '/test/odoo-bin',
'ODOO_ADDONS_PATH': '/test/addons',
'ODOO_DATABASE': 'test_db',
'ODOO_CONFIG_FILE': '/test/config.conf'
}
with patch.dict(os.environ, env_vars):
manager = get_shell_manager()
assert manager.odoo_bin_path == '/test/odoo-bin'
assert manager.addons_path == '/test/addons'
assert manager.db_name == 'test_db'
assert manager.config_file == '/test/config.conf'
def test_uses_defaults_when_env_vars_missing(self, reset_global_state):
"""Test that get_shell_manager uses defaults when env vars are missing."""
# Remove env vars by patching os.getenv directly
with patch('os.getenv', side_effect=lambda key, default=None: {
'ODOO_BIN_PATH': '/usr/bin/odoo-bin',
'ODOO_ADDONS_PATH': '/odoo/addons',
'ODOO_DATABASE': 'odoo',
'ODOO_CONFIG_FILE': None
}.get(key, default)):
manager = get_shell_manager()
assert manager.odoo_bin_path == '/usr/bin/odoo-bin'
assert manager.addons_path == '/odoo/addons'
assert manager.db_name == 'odoo'
assert manager.config_file is None
def test_returns_same_instance_on_subsequent_calls(self, reset_global_state):
"""Test that get_shell_manager returns the same instance (singleton)."""
manager1 = get_shell_manager()
manager2 = get_shell_manager()
assert manager1 is manager2
def test_respects_existing_global_manager(self, reset_global_state):
"""Test that existing global manager is reused."""
# Set a custom manager
custom_manager = Mock()
mcp_odoo_shell.server.shell_manager = custom_manager
result = get_shell_manager()
assert result is custom_manager
class TestExecuteOdooCode:
"""Test the execute_odoo_code MCP tool."""
def test_executes_code_successfully(self, reset_global_state):
"""Test successful code execution."""
mock_manager = Mock()
mock_manager.execute_code.return_value = "Result: 42"
with patch('mcp_odoo_shell.tools.get_shell_manager', return_value=mock_manager):
result = execute_odoo_code("2 + 2")
mock_manager.execute_code.assert_called_once_with("2 + 2")
assert result == "Result: 42"
def test_handles_execution_exception(self, reset_global_state):
"""Test handling of exceptions during code execution."""
mock_manager = Mock()
mock_manager.execute_code.side_effect = RuntimeError("Shell crashed")
with patch('mcp_odoo_shell.tools.get_shell_manager', return_value=mock_manager):
result = execute_odoo_code("bad code")
assert result == "Error executing Odoo code: Shell crashed"
def test_handles_timeout_exception(self, reset_global_state):
"""Test handling of timeout exceptions."""
mock_manager = Mock()
mock_manager.execute_code.side_effect = TimeoutError("Command execution timed out after 30 seconds")
with patch('mcp_odoo_shell.tools.get_shell_manager', return_value=mock_manager):
result = execute_odoo_code("slow_operation()")
assert result == "Error executing Odoo code: Command execution timed out after 30 seconds"
class TestResetOdooShell:
"""Test the reset_odoo_shell MCP tool."""
def test_resets_shell_successfully(self, reset_global_state):
"""Test successful shell reset."""
mock_manager = Mock()
mcp_odoo_shell.server.shell_manager = mock_manager
result = reset_odoo_shell()
mock_manager.stop.assert_called_once()
assert mcp_odoo_shell.server.shell_manager is None
assert result == "Odoo shell session reset successfully"
def test_handles_no_existing_shell(self, reset_global_state):
"""Test reset when no shell exists."""
# No shell manager set
assert mcp_odoo_shell.server.shell_manager is None
result = reset_odoo_shell()
assert result == "Odoo shell session reset successfully"
assert mcp_odoo_shell.server.shell_manager is None
def test_handles_stop_exception(self, reset_global_state):
"""Test handling of exceptions during shell stop."""
mock_manager = Mock()
mock_manager.stop.side_effect = RuntimeError("Failed to stop")
mcp_odoo_shell.server.shell_manager = mock_manager
result = reset_odoo_shell()
assert result == "Error resetting shell: Failed to stop"
# Looking at the actual code: shell_manager = None is in try block,
# exception occurs during stop(), so shell_manager is NOT cleared
assert mcp_odoo_shell.server.shell_manager is mock_manager
class TestListOdooModels:
"""Test the list_odoo_models MCP tool."""
def test_lists_models_without_pattern(self, reset_global_state):
"""Test listing all models without pattern."""
expected_code = """
models = env.registry.keys()
if '':
models = [m for m in models if '' in m]
for model in sorted(models)[:50]: # Limit to first 50
print(model)
"""
with patch('mcp_odoo_shell.tools.execute_odoo_code') as mock_execute:
mock_execute.return_value = "res.partner\nres.users\nsale.order"
result = list_odoo_models()
mock_execute.assert_called_once_with(expected_code)
assert result == "res.partner\nres.users\nsale.order"
def test_lists_models_with_pattern(self, reset_global_state):
"""Test listing models with pattern filter."""
expected_code = """
models = env.registry.keys()
if 'sale':
models = [m for m in models if 'sale' in m]
for model in sorted(models)[:50]: # Limit to first 50
print(model)
"""
with patch('mcp_odoo_shell.tools.execute_odoo_code') as mock_execute:
mock_execute.return_value = "sale.order\nsale.order.line"
result = list_odoo_models("sale")
mock_execute.assert_called_once_with(expected_code)
assert result == "sale.order\nsale.order.line"
class TestOdooModelInfo:
"""Test the odoo_model_info MCP tool."""
def test_gets_model_info_successfully(self, reset_global_state):
"""Test successful model info retrieval."""
expected_code = """
try:
model = env['res.partner']
print(f"Model: res.partner")
print(f"Description: {model._description}")
print(f"Table: {model._table}")
print("\\nFields:")
for field_name, field in model._fields.items():
print(f" {field_name}: {type(field).__name__}")
except KeyError:
print(f"Model 'res.partner' not found")
except Exception as e:
print(f"Error: {e}")
"""
expected_output = """Model: res.partner
Description: Partner
Table: res_partner
Fields:
name: Char
email: Char"""
with patch('mcp_odoo_shell.tools.execute_odoo_code') as mock_execute:
mock_execute.return_value = expected_output
result = odoo_model_info("res.partner")
mock_execute.assert_called_once_with(expected_code)
assert result == expected_output
def test_handles_model_not_found(self, reset_global_state):
"""Test handling when model is not found."""
expected_code = """
try:
model = env['nonexistent.model']
print(f"Model: nonexistent.model")
print(f"Description: {model._description}")
print(f"Table: {model._table}")
print("\\nFields:")
for field_name, field in model._fields.items():
print(f" {field_name}: {type(field).__name__}")
except KeyError:
print(f"Model 'nonexistent.model' not found")
except Exception as e:
print(f"Error: {e}")
"""
with patch('mcp_odoo_shell.tools.execute_odoo_code') as mock_execute:
mock_execute.return_value = "Model 'nonexistent.model' not found"
result = odoo_model_info("nonexistent.model")
mock_execute.assert_called_once_with(expected_code)
assert result == "Model 'nonexistent.model' not found"
class TestGlobalStateManagement:
"""Test global state management across tools."""
def test_shell_manager_persists_across_tool_calls(self, reset_global_state):
"""Test that shell manager persists across multiple tool calls."""
# First tool call should create manager
with patch('mcp_odoo_shell.server.get_shell_manager') as mock_get:
mock_manager = Mock()
mock_get.return_value = mock_manager
execute_odoo_code("print('first')")
first_call_count = mock_get.call_count
execute_odoo_code("print('second')")
second_call_count = mock_get.call_count
# Should reuse the same manager
assert second_call_count == first_call_count * 2
def test_reset_clears_state_for_subsequent_calls(self, reset_global_state):
"""Test that reset clears state for subsequent tool calls."""
# Create initial manager
mock_manager = Mock()
mcp_odoo_shell.server.shell_manager = mock_manager
# Reset should clear it
reset_odoo_shell()
assert mcp_odoo_shell.server.shell_manager is None
# Next call should create new manager
with patch('mcp_odoo_shell.tools.get_shell_manager') as mock_get:
new_manager = Mock()
mock_get.return_value = new_manager
execute_odoo_code("test")
mock_get.assert_called_once()
# The shell_manager is still None because execute_odoo_code calls get_shell_manager
# but doesn't store the result back to the global variable
# get_shell_manager() manages the global state internally