from __future__ import annotations # ruff: noqa: E402 import hashlib import os import uuid from datetime import UTC, datetime from pathlib import Path from typing import Any import pytest os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") from src.services.mcp_version_lifecycle_service import ( CatalogFetchReceipt, _extract_unambiguous_version, _stable_catalog_fingerprint, build_candidate_observation, run_mcp_version_lifecycle_once, ) class FakeLifecycleRepository: def __init__(self, previous: dict[str, dict[str, Any]] | None = None) -> None: self.previous = previous or {} self.started: dict[str, Any] | None = None self.observations: list[dict[str, Any]] = [] self.finished: dict[str, Any] | None = None async def start_run(self, payload: dict[str, Any]) -> None: self.started = payload async def latest_observations(self) -> dict[str, dict[str, Any]]: return self.previous async def save_observation(self, payload: dict[str, Any]) -> None: self.observations.append(payload) async def finish_run(self, payload: dict[str, Any]) -> None: self.finished = payload def _catalog() -> dict[str, Any]: return { "capabilities": [ { "capability_id": "external.context7-readonly", "source_type": "external", "catalog_source": "mcp-so", "catalog_url": "https://mcp.so/server/context7", "decision": "development_shadow_candidate", "risk_tier": "medium", "deployment_allowed": False, }, { "capability_id": "external.smithery-site-audit-shadow", "source_type": "external", "catalog_source": "smithery", "catalog_url": "https://smithery.ai/servers/example/site-audit", "decision": "evaluation_shadow_candidate", "risk_tier": "high", "deployment_allowed": False, }, { "capability_id": "rejected.disallowed-source", "source_type": "external", "catalog_source": "smithery", "catalog_url": "https://example.invalid/rejected", "decision": "rejected", "risk_tier": "high", "deployment_allowed": False, }, ] } @pytest.mark.asyncio async def test_lifecycle_run_persists_baselines_and_fail_closed_terminals() -> None: repository = FakeLifecycleRepository() async def fetcher( url: str, source: str, timeout_seconds: int, ) -> CatalogFetchReceipt: assert source in {"mcp-so", "smithery"} assert timeout_seconds > 0 return CatalogFetchReceipt( status="ok", source_url=url, http_status=200, body=b'{"packageVersion":"1.2.3","content":"not persisted"}', ) result = await run_mcp_version_lifecycle_once( _catalog(), trigger_kind="test", repository=repository, fetcher=fetcher, generated_at=datetime(2026, 7, 15, tzinfo=UTC), ) assert result["status"] == "completed_no_write_policy_blocked" assert result["candidate_count"] == 2 assert result["baseline_count"] == 2 assert result["observed_count"] == 2 assert result["policy_blocked_count"] == 2 assert repository.started is not None assert repository.finished is not None assert len(repository.observations) == 2 assert {row["lifecycle_terminal"] for row in repository.observations} == { "no_write_baseline" } for observation in repository.observations: assert observation["observed_version"] == "1.2.3" assert observation["policy_decision"]["external_install_allowed"] is False assert observation["policy_decision"]["rag_write_allowed"] is False assert ( observation["stage_receipts"]["sensor_source_receipt"]["raw_body_stored"] is False ) assert ( observation["stage_receipts"]["bounded_execution"]["status"] == "not_executed" ) def test_changed_version_stops_before_replay_without_immutable_receipts() -> None: old_body = b'{"packageVersion":"1.2.3"}' candidate = _catalog()["capabilities"][0] observation = build_candidate_observation( candidate, fetched=CatalogFetchReceipt( status="ok", source_url=candidate["catalog_url"], http_status=200, body=b'{"packageVersion":"1.3.0"}', ), previous={ "observed_version": "1.2.3", "content_sha256": hashlib.sha256(old_body).hexdigest(), }, run_id=uuid.uuid4(), trace_id="trace-version-change", work_item_id="MCP-VERSION-TEST", observed_at=datetime(2026, 7, 15, tzinfo=UTC), ) assert observation["change_state"] == "version_changed" assert observation["lifecycle_terminal"] == "no_write_policy_blocked" assert observation["stage_receipts"]["check_mode"]["status"] == ( "blocked_immutable_supply_chain_receipts_missing" ) assert ( observation["stage_receipts"]["bounded_execution"][ "external_tool_call_performed" ] is False ) assert ( observation["stage_receipts"]["post_verifier_and_rollback"]["rollback_required"] is False ) @pytest.mark.asyncio async def test_source_failure_is_durable_partial_degraded_no_write() -> None: repository = FakeLifecycleRepository() async def fetcher( url: str, source: str, timeout_seconds: int, ) -> CatalogFetchReceipt: del source, timeout_seconds return CatalogFetchReceipt( status="error", source_url=url, http_status=503, error_class="http_503", ) result = await run_mcp_version_lifecycle_once( _catalog(), trigger_kind="test", repository=repository, fetcher=fetcher, ) assert result["status"] == "partial_degraded" assert result["error_count"] == 2 assert all( row["lifecycle_terminal"] == "no_write_source_error" for row in repository.observations ) assert repository.finished is not None verifier = repository.finished["receipt"]["independent_post_verifier"] assert verifier["external_install_performed"] is False assert verifier["external_rag_write_performed"] is False def test_version_parser_rejects_ambiguous_page_versions() -> None: assert _extract_unambiguous_version(b'{"packageVersion":"1.2.3"}') == "1.2.3" assert ( _extract_unambiguous_version( b'{"packageVersion":"1.2.3","latestVersion":"2.0.0"}' ) is None ) def test_version_parser_ignores_cloudflare_beacon_version() -> None: body = b"data-cf-beacon='{\"version\":\"2024.11.0\",\"token\":\"public\"}'" assert _extract_unambiguous_version(body) is None def test_stable_fingerprint_ignores_randomized_recommendation_sections() -> None: stable_head = b"""