Files
awoooi/apps/api/tests/test_mcp_version_lifecycle_service.py
2026-07-15 03:20:59 +08:00

264 lines
9.0 KiB
Python

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"""
<html><head><title>Context7 MCP</title>
<meta name="description" content="Version-specific documentation MCP">
<link rel="canonical" href="https://mcp.so/servers/context7-mcp">
</head><body>
"""
first, first_scope = _stable_catalog_fingerprint(
stable_head + b'<a href="/servers/random-a">Random A</a></body></html>',
source_url="https://mcp.so/servers/context7-mcp",
observed_version=None,
)
second, second_scope = _stable_catalog_fingerprint(
stable_head + b'<a href="/servers/random-b">Random B</a></body></html>',
source_url="https://mcp.so/servers/context7-mcp",
observed_version=None,
)
assert first == second
assert first_scope == second_scope == "title_description_canonical_version"
def test_migration_is_additive_rls_and_does_not_store_raw_content() -> None:
root = Path(__file__).resolve().parents[3]
migration = (
root / "apps/api/migrations/mcp_version_lifecycle_controller_2026-07-15.sql"
).read_text(encoding="utf-8")
lowered = migration.lower()
assert "create table if not exists awooop_mcp_version_lifecycle_run" in lowered
assert "create table if not exists awooop_mcp_version_observation" in lowered
assert "force row level security" in lowered
assert (
"with check (project_id = current_setting('app.project_id', true))" in lowered
)
assert "drop table" not in lowered
assert "references awooop_projects" not in lowered
assert lowered.count("check (project_id = 'awoooi')") == 2
assert "request_body" not in lowered
assert "response_body" not in lowered
assert "raw_content jsonb" not in lowered
workflow = (root / ".gitea/workflows/cd.yaml").read_text(encoding="utf-8")
assert "Apply MCP Version Lifecycle Receipt Schema" in workflow
assert "mcp_version_lifecycle_schema_verified" in workflow
assert "mcp_version_lifecycle_runtime_role_read_verified" in workflow