Implement complete MCP server for parsing Primavera P6 XER files and exposing schedule data through MCP tools. All 4 user stories complete. Tools implemented: - load_xer: Parse XER files into SQLite database - list_activities: Query activities with pagination and filtering - get_activity: Get activity details by ID - list_relationships: Query activity dependencies - get_predecessors/get_successors: Query activity relationships - get_project_summary: Project overview with counts - list_milestones: Query milestone activities - get_critical_path: Query driving path activities Features: - Tab-delimited XER format parsing with pluggable table handlers - In-memory SQLite database for fast queries - Pagination with 100-item default limit - Multi-project file support with project selection - ISO8601 date formatting - NO_FILE_LOADED error handling for all query tools Test coverage: 81 tests (contract, integration, unit)
108 lines
4.0 KiB
Python
108 lines
4.0 KiB
Python
"""Contract tests for load_xer MCP tool."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from xer_mcp.db import db
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup_db():
|
|
"""Initialize and clear database for each test."""
|
|
db.initialize()
|
|
yield
|
|
db.clear()
|
|
|
|
|
|
class TestLoadXerContract:
|
|
"""Contract tests verifying load_xer tool interface matches spec."""
|
|
|
|
async def test_load_single_project_returns_success(
|
|
self, sample_xer_single_project: Path
|
|
) -> None:
|
|
"""load_xer with single-project file returns success and project info."""
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
result = await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
assert result["success"] is True
|
|
assert "project" in result
|
|
assert result["project"]["proj_id"] == "1001"
|
|
assert result["project"]["proj_short_name"] == "Test Project"
|
|
assert "activity_count" in result
|
|
assert result["activity_count"] == 5
|
|
assert "relationship_count" in result
|
|
assert result["relationship_count"] == 5
|
|
|
|
async def test_load_multi_project_without_selection_returns_list(
|
|
self, sample_xer_multi_project: Path
|
|
) -> None:
|
|
"""load_xer with multi-project file without project_id returns available projects."""
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
result = await load_xer(file_path=str(sample_xer_multi_project))
|
|
|
|
assert result["success"] is False
|
|
assert "available_projects" in result
|
|
assert len(result["available_projects"]) == 2
|
|
assert "message" in result
|
|
assert "project_id" in result["message"].lower()
|
|
|
|
async def test_load_multi_project_with_selection_returns_success(
|
|
self, sample_xer_multi_project: Path
|
|
) -> None:
|
|
"""load_xer with multi-project file and project_id returns selected project."""
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
result = await load_xer(file_path=str(sample_xer_multi_project), project_id="1001")
|
|
|
|
assert result["success"] is True
|
|
assert result["project"]["proj_id"] == "1001"
|
|
assert result["project"]["proj_short_name"] == "Project Alpha"
|
|
|
|
async def test_load_nonexistent_file_returns_error(self, nonexistent_xer_path: Path) -> None:
|
|
"""load_xer with missing file returns FILE_NOT_FOUND error."""
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
result = await load_xer(file_path=str(nonexistent_xer_path))
|
|
|
|
assert result["success"] is False
|
|
assert "error" in result
|
|
assert result["error"]["code"] == "FILE_NOT_FOUND"
|
|
|
|
async def test_load_invalid_file_returns_error(self, invalid_xer_file: Path) -> None:
|
|
"""load_xer with invalid file returns PARSE_ERROR error."""
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
result = await load_xer(file_path=str(invalid_xer_file))
|
|
|
|
assert result["success"] is False
|
|
assert "error" in result
|
|
assert result["error"]["code"] == "PARSE_ERROR"
|
|
|
|
async def test_load_replaces_previous_file(
|
|
self, sample_xer_single_project: Path, sample_xer_empty: Path
|
|
) -> None:
|
|
"""Loading a new file replaces the previous file's data."""
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
# Load first file
|
|
result1 = await load_xer(file_path=str(sample_xer_single_project))
|
|
assert result1["activity_count"] == 5
|
|
|
|
# Load second file (empty)
|
|
result2 = await load_xer(file_path=str(sample_xer_empty))
|
|
assert result2["activity_count"] == 0
|
|
|
|
async def test_load_returns_plan_dates(self, sample_xer_single_project: Path) -> None:
|
|
"""load_xer returns project plan start and end dates."""
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
result = await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
assert "plan_start_date" in result["project"]
|
|
assert "plan_end_date" in result["project"]
|
|
# Dates should be ISO8601 format
|
|
assert "T" in result["project"]["plan_start_date"]
|