feat: implement XER MCP Server with 9 schedule query tools
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)
This commit is contained in:
91
tests/contract/test_get_critical_path.py
Normal file
91
tests/contract/test_get_critical_path.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Contract tests for get_critical_path 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 TestGetCriticalPathContract:
|
||||
"""Contract tests verifying get_critical_path tool interface."""
|
||||
|
||||
async def test_get_critical_path_returns_critical_activities(
|
||||
self, sample_xer_single_project: Path
|
||||
) -> None:
|
||||
"""get_critical_path returns activities with driving_path_flag set."""
|
||||
from xer_mcp.tools.get_critical_path import get_critical_path
|
||||
from xer_mcp.tools.load_xer import load_xer
|
||||
|
||||
await load_xer(file_path=str(sample_xer_single_project))
|
||||
|
||||
result = await get_critical_path()
|
||||
|
||||
assert "critical_activities" in result
|
||||
assert len(result["critical_activities"]) == 4
|
||||
|
||||
async def test_get_critical_path_includes_expected_fields(
|
||||
self, sample_xer_single_project: Path
|
||||
) -> None:
|
||||
"""get_critical_path returns activities with required fields."""
|
||||
from xer_mcp.tools.get_critical_path import get_critical_path
|
||||
from xer_mcp.tools.load_xer import load_xer
|
||||
|
||||
await load_xer(file_path=str(sample_xer_single_project))
|
||||
|
||||
result = await get_critical_path()
|
||||
|
||||
activity = result["critical_activities"][0]
|
||||
assert "task_id" in activity
|
||||
assert "task_code" in activity
|
||||
assert "task_name" in activity
|
||||
assert "target_start_date" in activity
|
||||
assert "target_end_date" in activity
|
||||
assert "total_float_hr_cnt" in activity
|
||||
|
||||
async def test_get_critical_path_ordered_by_date(self, sample_xer_single_project: Path) -> None:
|
||||
"""get_critical_path returns activities ordered by start date."""
|
||||
from xer_mcp.tools.get_critical_path import get_critical_path
|
||||
from xer_mcp.tools.load_xer import load_xer
|
||||
|
||||
await load_xer(file_path=str(sample_xer_single_project))
|
||||
|
||||
result = await get_critical_path()
|
||||
|
||||
activities = result["critical_activities"]
|
||||
for i in range(len(activities) - 1):
|
||||
assert activities[i]["target_start_date"] <= activities[i + 1]["target_start_date"]
|
||||
|
||||
async def test_get_critical_path_excludes_non_critical(
|
||||
self, sample_xer_single_project: Path
|
||||
) -> None:
|
||||
"""get_critical_path excludes activities not on critical path."""
|
||||
from xer_mcp.tools.get_critical_path import get_critical_path
|
||||
from xer_mcp.tools.load_xer import load_xer
|
||||
|
||||
await load_xer(file_path=str(sample_xer_single_project))
|
||||
|
||||
result = await get_critical_path()
|
||||
|
||||
# A1020 "Foundation Work" has driving_path_flag = N
|
||||
activity_names = [a["task_name"] for a in result["critical_activities"]]
|
||||
assert "Foundation Work" not in activity_names
|
||||
|
||||
async def test_get_critical_path_no_file_loaded_returns_error(self) -> None:
|
||||
"""get_critical_path without loaded file returns NO_FILE_LOADED error."""
|
||||
from xer_mcp.server import set_file_loaded
|
||||
from xer_mcp.tools.get_critical_path import get_critical_path
|
||||
|
||||
set_file_loaded(False)
|
||||
result = await get_critical_path()
|
||||
|
||||
assert "error" in result
|
||||
assert result["error"]["code"] == "NO_FILE_LOADED"
|
||||
Reference in New Issue
Block a user