Files
xer-mcp/tests/integration/test_xer_parsing.py
Bill Ballou ccc8296418 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)
2026-01-06 21:27:35 -05:00

155 lines
5.6 KiB
Python

"""Integration tests for XER parsing and database loading."""
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 TestXerParsing:
"""Integration tests for parsing XER files and loading into database."""
def test_load_single_project_xer(self, sample_xer_single_project: Path) -> None:
"""Should parse XER and load data into SQLite database."""
from xer_mcp.db.loader import load_parsed_data
from xer_mcp.parser.xer_parser import XerParser
parser = XerParser()
parsed = parser.parse(sample_xer_single_project)
# Load all data for the single project
load_parsed_data(parsed, project_id="1001")
# Verify data in database
with db.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM projects")
assert cur.fetchone()[0] == 1
cur.execute("SELECT COUNT(*) FROM activities")
assert cur.fetchone()[0] == 5
cur.execute("SELECT COUNT(*) FROM relationships")
assert cur.fetchone()[0] == 5
cur.execute("SELECT COUNT(*) FROM wbs")
assert cur.fetchone()[0] == 3
def test_load_preserves_date_precision(self, sample_xer_single_project: Path) -> None:
"""Should preserve date/time precision from XER file."""
from xer_mcp.db.loader import load_parsed_data
from xer_mcp.parser.xer_parser import XerParser
parser = XerParser()
parsed = parser.parse(sample_xer_single_project)
load_parsed_data(parsed, project_id="1001")
with db.cursor() as cur:
cur.execute("SELECT target_start_date FROM activities WHERE task_code = ?", ("A1010",))
date_str = cur.fetchone()[0]
# Should be ISO8601 with time component
assert "T" in date_str
assert "07:00" in date_str
def test_load_activities_indexed_by_type(self, sample_xer_single_project: Path) -> None:
"""Should be able to efficiently query activities by type."""
from xer_mcp.db.loader import load_parsed_data
from xer_mcp.parser.xer_parser import XerParser
parser = XerParser()
parsed = parser.parse(sample_xer_single_project)
load_parsed_data(parsed, project_id="1001")
with db.cursor() as cur:
# Query milestones
cur.execute("SELECT COUNT(*) FROM activities WHERE task_type = ?", ("TT_Mile",))
milestone_count = cur.fetchone()[0]
assert milestone_count == 2
# Query tasks
cur.execute("SELECT COUNT(*) FROM activities WHERE task_type = ?", ("TT_Task",))
task_count = cur.fetchone()[0]
assert task_count == 3
def test_load_critical_path_activities(self, sample_xer_single_project: Path) -> None:
"""Should be able to query critical path activities via index."""
from xer_mcp.db.loader import load_parsed_data
from xer_mcp.parser.xer_parser import XerParser
parser = XerParser()
parsed = parser.parse(sample_xer_single_project)
load_parsed_data(parsed, project_id="1001")
with db.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM activities WHERE driving_path_flag = 1")
critical_count = cur.fetchone()[0]
# Activities A1000, A1010, A1030, A1040 are on critical path
assert critical_count == 4
def test_load_replaces_previous_data(self, sample_xer_single_project: Path) -> None:
"""Loading a new file should replace previous data."""
from xer_mcp.db.loader import load_parsed_data
from xer_mcp.parser.xer_parser import XerParser
parser = XerParser()
parsed = parser.parse(sample_xer_single_project)
# Load first time
load_parsed_data(parsed, project_id="1001")
with db.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM activities")
first_count = cur.fetchone()[0]
# Clear and load again
db.clear()
load_parsed_data(parsed, project_id="1001")
with db.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM activities")
second_count = cur.fetchone()[0]
assert first_count == second_count
class TestMultiProjectHandling:
"""Integration tests for multi-project XER file handling."""
def test_load_selected_project_from_multi(self, sample_xer_multi_project: Path) -> None:
"""Should load only the selected project from multi-project file."""
from xer_mcp.db.loader import load_parsed_data
from xer_mcp.parser.xer_parser import XerParser
parser = XerParser()
parsed = parser.parse(sample_xer_multi_project)
# Load only Project Alpha
load_parsed_data(parsed, project_id="1001")
with db.cursor() as cur:
cur.execute("SELECT proj_short_name FROM projects")
names = [row[0] for row in cur.fetchall()]
assert names == ["Project Alpha"]
cur.execute("SELECT COUNT(*) FROM activities WHERE proj_id = ?", ("1001",))
assert cur.fetchone()[0] == 1
def test_multi_project_list_available(self, sample_xer_multi_project: Path) -> None:
"""Parser should report all available projects."""
from xer_mcp.parser.xer_parser import XerParser
parser = XerParser()
parsed = parser.parse(sample_xer_multi_project)
assert len(parsed.projects) == 2
proj_ids = {p["proj_id"] for p in parsed.projects}
assert proj_ids == {"1001", "1002"}