This commit implements the first phase of the MCP server as outlined in PROJECT_SUMMARY.md Option A: Model Discovery. New Features: - Complete .sim file parser (XML-based) - Expression extraction from .sim and .prt files - Solution, FEM, materials, loads, constraints extraction - Structured JSON output for LLM consumption - Markdown formatting for human-readable output Implementation Details: - mcp_server/tools/model_discovery.py: Core parser and discovery logic - SimFileParser class: Handles XML parsing of .sim files - discover_fea_model(): Main MCP tool function - format_discovery_result_for_llm(): Markdown formatter - mcp_server/tools/__init__.py: Updated to export new functions - mcp_server/tools/README.md: Complete documentation for MCP tools Testing & Examples: - examples/test_bracket.sim: Sample .sim file for testing - tests/mcp_server/tools/test_model_discovery.py: Comprehensive unit tests - Manual testing verified: Successfully extracts 4 expressions, solution info, mesh data, materials, loads, and constraints Validation: - Command-line tool works: python mcp_server/tools/model_discovery.py examples/test_bracket.sim - Output includes both Markdown and JSON formats - Error handling for missing files and invalid formats Next Steps (Phase 2): - Port optimization engine from P04 Atomizer - Implement build_optimization_config tool - Create pluggable result extractor system References: - PROJECT_SUMMARY.md: Option A (lines 339-350) - mcp_server/prompts/system_prompt.md: Model Discovery workflow
212 lines
6.9 KiB
Python
212 lines
6.9 KiB
Python
"""
|
|
Unit tests for MCP Model Discovery Tool
|
|
|
|
Tests the .sim file parser and FEA model discovery functionality.
|
|
"""
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
# Add project root to path
|
|
project_root = Path(__file__).parent.parent.parent.parent
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
from mcp_server.tools.model_discovery import (
|
|
discover_fea_model,
|
|
format_discovery_result_for_llm,
|
|
SimFileParser
|
|
)
|
|
|
|
|
|
class TestSimFileParser:
|
|
"""Test the SimFileParser class"""
|
|
|
|
@pytest.fixture
|
|
def example_sim_path(self):
|
|
"""Path to example .sim file"""
|
|
return project_root / "examples" / "test_bracket.sim"
|
|
|
|
def test_parser_initialization(self, example_sim_path):
|
|
"""Test that parser initializes correctly"""
|
|
parser = SimFileParser(example_sim_path)
|
|
assert parser.sim_path.exists()
|
|
assert parser.tree is not None
|
|
assert parser.root is not None
|
|
|
|
def test_parser_file_not_found(self):
|
|
"""Test error handling for missing file"""
|
|
with pytest.raises(FileNotFoundError):
|
|
SimFileParser("/nonexistent/path/file.sim")
|
|
|
|
def test_parser_invalid_extension(self):
|
|
"""Test error handling for non-.sim file"""
|
|
with pytest.raises(ValueError):
|
|
SimFileParser(project_root / "README.md")
|
|
|
|
def test_extract_solutions(self, example_sim_path):
|
|
"""Test solution extraction"""
|
|
parser = SimFileParser(example_sim_path)
|
|
solutions = parser.extract_solutions()
|
|
|
|
assert len(solutions) > 0
|
|
assert solutions[0]['name'] == 'Structural Analysis 1'
|
|
assert solutions[0]['type'] == 'Static Structural'
|
|
assert solutions[0]['solver'] == 'NX Nastran'
|
|
|
|
def test_extract_expressions(self, example_sim_path):
|
|
"""Test expression extraction"""
|
|
parser = SimFileParser(example_sim_path)
|
|
expressions = parser.extract_expressions()
|
|
|
|
assert len(expressions) > 0
|
|
|
|
# Check for expected expressions
|
|
expr_names = [e['name'] for e in expressions]
|
|
assert 'wall_thickness' in expr_names
|
|
assert 'hole_diameter' in expr_names
|
|
assert 'rib_spacing' in expr_names
|
|
|
|
# Check expression values
|
|
wall_thickness = next(e for e in expressions if e['name'] == 'wall_thickness')
|
|
assert wall_thickness['value'] == '5.0'
|
|
assert wall_thickness['units'] == 'mm'
|
|
|
|
def test_extract_fem_info(self, example_sim_path):
|
|
"""Test FEM information extraction"""
|
|
parser = SimFileParser(example_sim_path)
|
|
fem_info = parser.extract_fem_info()
|
|
|
|
# Check mesh info
|
|
assert 'mesh' in fem_info
|
|
assert fem_info['mesh']['name'] == 'Bracket Mesh'
|
|
assert fem_info['mesh']['node_count'] == '8234'
|
|
assert fem_info['mesh']['element_count'] == '4521'
|
|
|
|
# Check materials
|
|
assert len(fem_info['materials']) > 0
|
|
assert fem_info['materials'][0]['name'] == 'Aluminum 6061-T6'
|
|
|
|
# Check loads
|
|
assert len(fem_info['loads']) > 0
|
|
assert fem_info['loads'][0]['name'] == 'Applied Force'
|
|
|
|
# Check constraints
|
|
assert len(fem_info['constraints']) > 0
|
|
assert fem_info['constraints'][0]['name'] == 'Fixed Support'
|
|
|
|
|
|
class TestDiscoverFEAModel:
|
|
"""Test the main discover_fea_model function"""
|
|
|
|
@pytest.fixture
|
|
def example_sim_path(self):
|
|
"""Path to example .sim file"""
|
|
return str(project_root / "examples" / "test_bracket.sim")
|
|
|
|
def test_successful_discovery(self, example_sim_path):
|
|
"""Test successful model discovery"""
|
|
result = discover_fea_model(example_sim_path)
|
|
|
|
assert result['status'] == 'success'
|
|
assert result['file_exists'] is True
|
|
assert 'solutions' in result
|
|
assert 'expressions' in result
|
|
assert 'fem_info' in result
|
|
assert 'summary' in result
|
|
|
|
# Check summary statistics
|
|
assert result['summary']['solution_count'] >= 1
|
|
assert result['summary']['expression_count'] >= 3
|
|
|
|
def test_file_not_found_error(self):
|
|
"""Test error handling for missing file"""
|
|
result = discover_fea_model("/nonexistent/file.sim")
|
|
|
|
assert result['status'] == 'error'
|
|
assert result['error_type'] == 'file_not_found'
|
|
assert 'message' in result
|
|
assert 'suggestion' in result
|
|
|
|
def test_result_structure(self, example_sim_path):
|
|
"""Test that result has expected structure"""
|
|
result = discover_fea_model(example_sim_path)
|
|
|
|
# Check top-level keys
|
|
expected_keys = ['status', 'sim_file', 'file_exists', 'solutions',
|
|
'expressions', 'fem_info', 'linked_files', 'metadata', 'summary']
|
|
|
|
for key in expected_keys:
|
|
assert key in result, f"Missing key: {key}"
|
|
|
|
# Check summary keys
|
|
expected_summary_keys = ['solution_count', 'expression_count',
|
|
'material_count', 'load_count', 'constraint_count']
|
|
|
|
for key in expected_summary_keys:
|
|
assert key in result['summary'], f"Missing summary key: {key}"
|
|
|
|
|
|
class TestFormatDiscoveryResult:
|
|
"""Test the Markdown formatting function"""
|
|
|
|
@pytest.fixture
|
|
def example_sim_path(self):
|
|
"""Path to example .sim file"""
|
|
return str(project_root / "examples" / "test_bracket.sim")
|
|
|
|
def test_format_success_result(self, example_sim_path):
|
|
"""Test formatting of successful discovery"""
|
|
result = discover_fea_model(example_sim_path)
|
|
formatted = format_discovery_result_for_llm(result)
|
|
|
|
assert isinstance(formatted, str)
|
|
assert '# FEA Model Analysis' in formatted
|
|
assert 'Solutions' in formatted
|
|
assert 'Expressions' in formatted
|
|
assert 'wall_thickness' in formatted
|
|
|
|
def test_format_error_result(self):
|
|
"""Test formatting of error result"""
|
|
result = discover_fea_model("/nonexistent/file.sim")
|
|
formatted = format_discovery_result_for_llm(result)
|
|
|
|
assert isinstance(formatted, str)
|
|
assert '❌' in formatted or 'Error' in formatted
|
|
assert result['message'] in formatted
|
|
|
|
|
|
# Integration test
|
|
def test_end_to_end_workflow():
|
|
"""
|
|
Test the complete workflow:
|
|
1. Discover model
|
|
2. Format for LLM
|
|
3. Verify output is useful
|
|
"""
|
|
example_sim = str(project_root / "examples" / "test_bracket.sim")
|
|
|
|
# Step 1: Discover
|
|
result = discover_fea_model(example_sim)
|
|
assert result['status'] == 'success'
|
|
|
|
# Step 2: Format
|
|
formatted = format_discovery_result_for_llm(result)
|
|
assert len(formatted) > 100 # Should be substantial output
|
|
|
|
# Step 3: Verify key information is present
|
|
assert 'wall_thickness' in formatted
|
|
assert 'Aluminum' in formatted
|
|
assert 'Static Structural' in formatted
|
|
|
|
print("\n" + "="*60)
|
|
print("INTEGRATION TEST OUTPUT:")
|
|
print("="*60)
|
|
print(formatted)
|
|
print("="*60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run tests with pytest
|
|
pytest.main([__file__, "-v", "-s"])
|