Add computed driving flag to all relationship queries (list_relationships, get_predecessors, get_successors). A relationship is marked as driving when the predecessor's early end date plus lag determines the successor's early start date. Changes: - Add early_start_date and early_end_date columns to activities schema - Parse early dates from TASK table in XER files - Implement is_driving_relationship() helper with 24hr tolerance for calendar gaps - Update all relationship queries to compute and return driving flag - Add contract and unit tests for driving flag functionality - Update spec, contracts, and documentation
96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
"""Contract tests for get_successors 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 TestGetSuccessorsContract:
|
|
"""Contract tests verifying get_successors tool interface."""
|
|
|
|
async def test_get_successors_returns_list(self, sample_xer_single_project: Path) -> None:
|
|
"""get_successors returns successor activities."""
|
|
from xer_mcp.tools.get_successors import get_successors
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
# A1010 (2002) has two successors: A1020 (2003) and A1030 (2004)
|
|
result = await get_successors(activity_id="2002")
|
|
|
|
assert "activity_id" in result
|
|
assert result["activity_id"] == "2002"
|
|
assert "successors" in result
|
|
assert len(result["successors"]) == 2
|
|
|
|
async def test_get_successors_includes_relationship_details(
|
|
self, sample_xer_single_project: Path
|
|
) -> None:
|
|
"""get_successors includes relationship type and lag."""
|
|
from xer_mcp.tools.get_successors import get_successors
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
result = await get_successors(activity_id="2001")
|
|
|
|
succ = result["successors"][0]
|
|
assert "relationship_type" in succ
|
|
assert "lag_hr_cnt" in succ
|
|
assert succ["relationship_type"] in ["FS", "SS", "FF", "SF"]
|
|
|
|
async def test_get_successors_empty_list_for_no_successors(
|
|
self, sample_xer_single_project: Path
|
|
) -> None:
|
|
"""get_successors returns empty list when no successors exist."""
|
|
from xer_mcp.tools.get_successors import get_successors
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
# A1040 (2005) has no successors
|
|
result = await get_successors(activity_id="2005")
|
|
|
|
assert "successors" in result
|
|
assert len(result["successors"]) == 0
|
|
|
|
async def test_get_successors_no_file_loaded_returns_error(self) -> None:
|
|
"""get_successors without loaded file returns NO_FILE_LOADED error."""
|
|
from xer_mcp.server import set_file_loaded
|
|
from xer_mcp.tools.get_successors import get_successors
|
|
|
|
set_file_loaded(False)
|
|
result = await get_successors(activity_id="2002")
|
|
|
|
assert "error" in result
|
|
assert result["error"]["code"] == "NO_FILE_LOADED"
|
|
|
|
async def test_get_successors_includes_driving_flag(
|
|
self, sample_xer_single_project: Path
|
|
) -> None:
|
|
"""get_successors includes driving flag for each successor."""
|
|
from xer_mcp.tools.get_successors import get_successors
|
|
from xer_mcp.tools.load_xer import load_xer
|
|
|
|
await load_xer(file_path=str(sample_xer_single_project))
|
|
|
|
# A1000 (2001) has one successor: A1010 (2002)
|
|
result = await get_successors(activity_id="2001")
|
|
|
|
assert "successors" in result
|
|
assert len(result["successors"]) >= 1
|
|
# All successors should have a driving flag
|
|
for succ in result["successors"]:
|
|
assert "driving" in succ
|
|
assert isinstance(succ["driving"], bool)
|