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)
90 lines
3.1 KiB
Python
90 lines
3.1 KiB
Python
"""Contract tests for get_activity 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 TestGetActivityContract:
|
|
"""Contract tests verifying get_activity tool interface matches spec."""
|
|
|
|
async def test_get_activity_returns_details(self, sample_xer_single_project: Path) -> None:
|
|
"""get_activity returns complete activity details."""
|
|
from xer_mcp.tools.get_activity import get_activity
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
result = await get_activity(activity_id="2002")
|
|
|
|
assert result["task_id"] == "2002"
|
|
assert result["task_code"] == "A1010"
|
|
assert result["task_name"] == "Site Preparation"
|
|
assert result["task_type"] == "TT_Task"
|
|
assert "target_start_date" in result
|
|
assert "target_end_date" in result
|
|
assert "wbs_id" in result
|
|
assert "predecessor_count" in result
|
|
assert "successor_count" in result
|
|
|
|
async def test_get_activity_includes_wbs_name(self, sample_xer_single_project: Path) -> None:
|
|
"""get_activity includes WBS name from lookup."""
|
|
from xer_mcp.tools.get_activity import get_activity
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
result = await get_activity(activity_id="2002")
|
|
|
|
assert "wbs_name" in result
|
|
|
|
async def test_get_activity_includes_relationship_counts(
|
|
self, sample_xer_single_project: Path
|
|
) -> None:
|
|
"""get_activity includes predecessor and successor counts."""
|
|
from xer_mcp.tools.get_activity import get_activity
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
# A1010 (2002) has 1 predecessor (A1000) and 2 successors (A1020, A1030)
|
|
result = await get_activity(activity_id="2002")
|
|
|
|
assert result["predecessor_count"] == 1
|
|
assert result["successor_count"] == 2
|
|
|
|
async def test_get_activity_not_found_returns_error(
|
|
self, sample_xer_single_project: Path
|
|
) -> None:
|
|
"""get_activity with invalid ID returns ACTIVITY_NOT_FOUND error."""
|
|
from xer_mcp.tools.get_activity import get_activity
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
result = await get_activity(activity_id="nonexistent")
|
|
|
|
assert "error" in result
|
|
assert result["error"]["code"] == "ACTIVITY_NOT_FOUND"
|
|
|
|
async def test_get_activity_no_file_loaded_returns_error(self) -> None:
|
|
"""get_activity without loaded file returns NO_FILE_LOADED error."""
|
|
from xer_mcp.server import set_file_loaded
|
|
from xer_mcp.tools.get_activity import get_activity
|
|
|
|
set_file_loaded(False)
|
|
result = await get_activity(activity_id="2002")
|
|
|
|
assert "error" in result
|
|
assert result["error"]["code"] == "NO_FILE_LOADED"
|