"""Contract tests for load_xer MCP tool.""" import sqlite3 from pathlib import Path import pytest from xer_mcp.db import db @pytest.fixture(autouse=True) def setup_db(): """Reset database state for each test.""" # Close any existing connection if db.is_initialized: db.close() yield # Cleanup after test if db.is_initialized: db.close() class TestLoadXerContract: """Contract tests verifying load_xer tool interface matches spec.""" async def test_load_single_project_returns_success( self, sample_xer_single_project: Path ) -> None: """load_xer with single-project file returns success and project info.""" from xer_mcp.tools.load_xer import load_xer result = await load_xer(file_path=str(sample_xer_single_project)) assert result["success"] is True assert "project" in result assert result["project"]["proj_id"] == "1001" assert result["project"]["proj_short_name"] == "Test Project" assert "activity_count" in result assert result["activity_count"] == 5 assert "relationship_count" in result assert result["relationship_count"] == 5 async def test_load_multi_project_without_selection_returns_list( self, sample_xer_multi_project: Path ) -> None: """load_xer with multi-project file without project_id returns available projects.""" from xer_mcp.tools.load_xer import load_xer result = await load_xer(file_path=str(sample_xer_multi_project)) assert result["success"] is False assert "available_projects" in result assert len(result["available_projects"]) == 2 assert "message" in result assert "project_id" in result["message"].lower() async def test_load_multi_project_with_selection_returns_success( self, sample_xer_multi_project: Path ) -> None: """load_xer with multi-project file and project_id returns selected project.""" from xer_mcp.tools.load_xer import load_xer result = await load_xer(file_path=str(sample_xer_multi_project), project_id="1001") assert result["success"] is True assert result["project"]["proj_id"] == "1001" assert result["project"]["proj_short_name"] == "Project Alpha" async def test_load_nonexistent_file_returns_error(self, nonexistent_xer_path: Path) -> None: """load_xer with missing file returns FILE_NOT_FOUND error.""" from xer_mcp.tools.load_xer import load_xer result = await load_xer(file_path=str(nonexistent_xer_path)) assert result["success"] is False assert "error" in result assert result["error"]["code"] == "FILE_NOT_FOUND" async def test_load_invalid_file_returns_error(self, invalid_xer_file: Path) -> None: """load_xer with invalid file returns PARSE_ERROR error.""" from xer_mcp.tools.load_xer import load_xer result = await load_xer(file_path=str(invalid_xer_file)) assert result["success"] is False assert "error" in result assert result["error"]["code"] == "PARSE_ERROR" async def test_load_replaces_previous_file( self, sample_xer_single_project: Path, sample_xer_empty: Path ) -> None: """Loading a new file replaces the previous file's data.""" from xer_mcp.tools.load_xer import load_xer # Load first file result1 = await load_xer(file_path=str(sample_xer_single_project)) assert result1["activity_count"] == 5 # Load second file (empty) result2 = await load_xer(file_path=str(sample_xer_empty)) assert result2["activity_count"] == 0 async def test_load_returns_plan_dates(self, sample_xer_single_project: Path) -> None: """load_xer returns project plan start and end dates.""" from xer_mcp.tools.load_xer import load_xer result = await load_xer(file_path=str(sample_xer_single_project)) assert "plan_start_date" in result["project"] assert "plan_end_date" in result["project"] # Dates should be ISO8601 format assert "T" in result["project"]["plan_start_date"] class TestLoadXerPersistentDatabase: """Contract tests for persistent database functionality.""" async def test_load_xer_with_db_path_creates_file( self, tmp_path: Path, sample_xer_single_project: Path ) -> None: """load_xer with db_path creates persistent database file.""" from xer_mcp.tools.load_xer import load_xer db_file = tmp_path / "schedule.db" result = await load_xer( file_path=str(sample_xer_single_project), db_path=str(db_file), ) assert result["success"] is True assert db_file.exists() assert result["database"]["db_path"] == str(db_file) assert result["database"]["is_persistent"] is True async def test_load_xer_with_empty_db_path_auto_generates(self, tmp_path: Path) -> None: """load_xer with empty db_path generates path from XER filename.""" from xer_mcp.tools.load_xer import load_xer # Create XER file in tmp_path xer_file = tmp_path / "my_schedule.xer" from tests.conftest import SAMPLE_XER_SINGLE_PROJECT xer_file.write_text(SAMPLE_XER_SINGLE_PROJECT) result = await load_xer(file_path=str(xer_file), db_path="") assert result["success"] is True expected_db = str(tmp_path / "my_schedule.sqlite") assert result["database"]["db_path"] == expected_db assert result["database"]["is_persistent"] is True assert Path(expected_db).exists() async def test_load_xer_without_db_path_uses_memory( self, sample_xer_single_project: Path ) -> None: """load_xer without db_path uses in-memory database (backward compatible).""" from xer_mcp.tools.load_xer import load_xer result = await load_xer(file_path=str(sample_xer_single_project)) assert result["success"] is True assert result["database"]["db_path"] == ":memory:" assert result["database"]["is_persistent"] is False async def test_load_xer_database_contains_all_data( self, tmp_path: Path, sample_xer_single_project: Path ) -> None: """Persistent database contains all parsed data.""" from xer_mcp.tools.load_xer import load_xer db_file = tmp_path / "schedule.db" result = await load_xer( file_path=str(sample_xer_single_project), db_path=str(db_file), ) # Verify data via direct SQL conn = sqlite3.connect(str(db_file)) cursor = conn.execute("SELECT COUNT(*) FROM activities") count = cursor.fetchone()[0] conn.close() assert count == result["activity_count"] async def test_load_xer_response_includes_database_info( self, tmp_path: Path, sample_xer_single_project: Path ) -> None: """load_xer response includes complete database info.""" from xer_mcp.tools.load_xer import load_xer db_file = tmp_path / "schedule.db" result = await load_xer( file_path=str(sample_xer_single_project), db_path=str(db_file), ) assert "database" in result db_info = result["database"] assert "db_path" in db_info assert "is_persistent" in db_info assert "source_file" in db_info assert "loaded_at" in db_info assert "schema" in db_info async def test_load_xer_response_schema_includes_tables( self, tmp_path: Path, sample_xer_single_project: Path ) -> None: """load_xer response schema includes table information.""" from xer_mcp.tools.load_xer import load_xer db_file = tmp_path / "schedule.db" result = await load_xer( file_path=str(sample_xer_single_project), db_path=str(db_file), ) schema = result["database"]["schema"] assert "version" in schema assert "tables" in schema table_names = [t["name"] for t in schema["tables"]] assert "activities" in table_names assert "relationships" in table_names async def test_load_xer_error_on_invalid_path(self, sample_xer_single_project: Path) -> None: """load_xer returns error for invalid path.""" from xer_mcp.tools.load_xer import load_xer result = await load_xer( file_path=str(sample_xer_single_project), db_path="/nonexistent/dir/file.db", ) assert result["success"] is False # Either FILE_NOT_WRITABLE or DATABASE_ERROR is acceptable # depending on how SQLite reports the error assert result["error"]["code"] in ("FILE_NOT_WRITABLE", "DATABASE_ERROR") class TestLoadXerToolSchema: """Tests for MCP tool schema.""" async def test_load_xer_tool_schema_includes_db_path(self) -> None: """MCP tool schema includes db_path parameter.""" from xer_mcp.server import list_tools tools = await list_tools() load_xer_tool = next(t for t in tools if t.name == "load_xer") props = load_xer_tool.inputSchema["properties"] assert "db_path" in props assert props["db_path"]["type"] == "string"