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)
139 lines
5.0 KiB
Python
139 lines
5.0 KiB
Python
"""Contract tests for list_activities 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 TestListActivitiesContract:
|
|
"""Contract tests verifying list_activities tool interface matches spec."""
|
|
|
|
async def test_list_activities_returns_paginated_results(
|
|
self, sample_xer_single_project: Path
|
|
) -> None:
|
|
"""list_activities returns activities with pagination metadata."""
|
|
from xer_mcp.tools.list_activities import list_activities
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
result = await list_activities()
|
|
|
|
assert "activities" in result
|
|
assert "pagination" in result
|
|
assert len(result["activities"]) == 5
|
|
assert result["pagination"]["total_count"] == 5
|
|
assert result["pagination"]["offset"] == 0
|
|
assert result["pagination"]["limit"] == 100
|
|
assert result["pagination"]["has_more"] is False
|
|
|
|
async def test_list_activities_with_limit(self, sample_xer_single_project: Path) -> None:
|
|
"""list_activities respects limit parameter."""
|
|
from xer_mcp.tools.list_activities import list_activities
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
result = await list_activities(limit=2)
|
|
|
|
assert len(result["activities"]) == 2
|
|
assert result["pagination"]["limit"] == 2
|
|
assert result["pagination"]["has_more"] is True
|
|
|
|
async def test_list_activities_with_offset(self, sample_xer_single_project: Path) -> None:
|
|
"""list_activities respects offset parameter."""
|
|
from xer_mcp.tools.list_activities import list_activities
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
result = await list_activities(offset=2, limit=2)
|
|
|
|
assert len(result["activities"]) == 2
|
|
assert result["pagination"]["offset"] == 2
|
|
|
|
async def test_list_activities_filter_by_date_range(
|
|
self, sample_xer_single_project: Path
|
|
) -> None:
|
|
"""list_activities filters by date range."""
|
|
from xer_mcp.tools.list_activities import list_activities
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
# Filter to January only
|
|
result = await list_activities(start_date="2026-01-01", end_date="2026-01-31")
|
|
|
|
# Should include activities in January
|
|
for activity in result["activities"]:
|
|
assert "2026-01" in activity["target_start_date"]
|
|
|
|
async def test_list_activities_filter_by_activity_type(
|
|
self, sample_xer_single_project: Path
|
|
) -> None:
|
|
"""list_activities filters by activity type."""
|
|
from xer_mcp.tools.list_activities import list_activities
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
result = await list_activities(activity_type="TT_Mile")
|
|
|
|
assert len(result["activities"]) == 2
|
|
for activity in result["activities"]:
|
|
assert activity["task_type"] == "TT_Mile"
|
|
|
|
async def test_list_activities_filter_by_wbs(self, sample_xer_single_project: Path) -> None:
|
|
"""list_activities filters by WBS ID."""
|
|
from xer_mcp.tools.list_activities import list_activities
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
result = await list_activities(wbs_id="101")
|
|
|
|
# WBS 101 has 3 activities in the fixture
|
|
assert len(result["activities"]) == 3
|
|
|
|
async def test_list_activities_no_file_loaded_returns_error(self) -> None:
|
|
"""list_activities without loaded file returns NO_FILE_LOADED error."""
|
|
from xer_mcp.server import set_file_loaded
|
|
from xer_mcp.tools.list_activities import list_activities
|
|
|
|
set_file_loaded(False)
|
|
result = await list_activities()
|
|
|
|
assert "error" in result
|
|
assert result["error"]["code"] == "NO_FILE_LOADED"
|
|
|
|
async def test_list_activities_returns_expected_fields(
|
|
self, sample_xer_single_project: Path
|
|
) -> None:
|
|
"""list_activities returns activities with all expected fields."""
|
|
from xer_mcp.tools.list_activities import list_activities
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
result = await list_activities()
|
|
|
|
activity = result["activities"][0]
|
|
assert "task_id" in activity
|
|
assert "task_code" in activity
|
|
assert "task_name" in activity
|
|
assert "task_type" in activity
|
|
assert "target_start_date" in activity
|
|
assert "target_end_date" in activity
|
|
assert "status_code" in activity
|
|
assert "driving_path_flag" in activity
|