Merge remote-tracking branch 'origin/main' into codex/p0-obs-002-20260715
This commit is contained in:
@@ -34,17 +34,24 @@ def test_catalog_contains_exact_12_products_and_explicit_missing_rows() -> None:
|
||||
assert catalog["source_policy"]["github_allowed"] is False
|
||||
assert catalog["security_policy"]["external_rag_auto_write_allowed"] is False
|
||||
|
||||
capabilities = {
|
||||
row["capability_id"]: row for row in catalog["capabilities"]
|
||||
}
|
||||
capabilities = {row["capability_id"]: row for row in catalog["capabilities"]}
|
||||
assert capabilities["rejected.github"]["decision"] == "rejected"
|
||||
assert capabilities["quarantine.agent-bounty-mcp"]["decision"] == "quarantine"
|
||||
assert capabilities["external.smithery-site-audit-shadow"]["decision"] == "evaluation_shadow_candidate"
|
||||
assert capabilities["external.smithery-site-audit-shadow"]["deployment_allowed"] is False
|
||||
assert capabilities["rejected.smithery-technical-analysis"]["decision"] == "rejected"
|
||||
assert "tool_output_contains_cross_tool_prompt_injection" in capabilities[
|
||||
"quarantine.agent-bounty-mcp"
|
||||
]["blockers"]
|
||||
assert (
|
||||
capabilities["external.smithery-site-audit-shadow"]["decision"]
|
||||
== "evaluation_shadow_candidate"
|
||||
)
|
||||
assert (
|
||||
capabilities["external.smithery-site-audit-shadow"]["deployment_allowed"]
|
||||
is False
|
||||
)
|
||||
assert (
|
||||
capabilities["rejected.smithery-technical-analysis"]["decision"] == "rejected"
|
||||
)
|
||||
assert (
|
||||
"tool_output_contains_cross_tool_prompt_injection"
|
||||
in capabilities["quarantine.agent-bounty-mcp"]["blockers"]
|
||||
)
|
||||
assert not any(
|
||||
row["deployment_allowed"]
|
||||
for row in catalog["capabilities"]
|
||||
@@ -73,7 +80,10 @@ def test_overview_joins_committed_product_matrix_without_runtime_claims() -> Non
|
||||
products = {row["product_id"]: row for row in payload["products"]}
|
||||
assert products["ewoooc"]["canonical_repo"] == "wooo/ewoooc"
|
||||
assert products["momo-pro-system"]["canonical_repo"] == "wooo/momo-pro-system"
|
||||
assert products["agent-bounty-protocol"]["inventory_state"] == "source_detected_quarantined"
|
||||
assert (
|
||||
products["agent-bounty-protocol"]["inventory_state"]
|
||||
== "source_detected_quarantined"
|
||||
)
|
||||
|
||||
|
||||
def test_catalog_rejects_external_deployment_without_immutable_receipts(
|
||||
@@ -102,15 +112,53 @@ def test_overview_endpoint_returns_real_committed_catalog_without_runtime() -> N
|
||||
app.include_router(router, prefix="/api/v1")
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/mcp-control-plane/overview?include_runtime=false"
|
||||
)
|
||||
response = client.get("/api/v1/mcp-control-plane/overview?include_runtime=false")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["summary"]["product_count"] == 12
|
||||
assert data["runtime"]["status"] == "not_requested"
|
||||
assert data["operation_boundaries"]["github_allowed"] is False
|
||||
assert data["operation_boundaries"]["public_response_redacted_aggregate_only"] is True
|
||||
assert (
|
||||
data["operation_boundaries"]["public_response_redacted_aggregate_only"] is True
|
||||
)
|
||||
assert data["operation_boundaries"]["mutation_endpoint_exposed"] is False
|
||||
assert data["operation_boundaries"]["operator_auth_required_for_future_mutation"] is True
|
||||
assert (
|
||||
data["operation_boundaries"]["operator_auth_required_for_future_mutation"]
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
def test_overview_promotes_durable_lifecycle_receipt_not_static_policy() -> None:
|
||||
catalog = load_mcp_control_plane_catalog(_OPERATIONS_DIR)
|
||||
matrix = load_latest_product_governance_matrix_readback(_OPERATIONS_DIR)
|
||||
runtime = {
|
||||
"status": "ready",
|
||||
"provider_registry": {"provider_count": 9, "tool_count": 39},
|
||||
"gateway_audit_24h": {"call_count": 1},
|
||||
"rag": {"total_chunks": 1},
|
||||
"version_lifecycle": {
|
||||
"status": "ready",
|
||||
"latest_run": {
|
||||
"run_id": "11111111-1111-1111-1111-111111111111",
|
||||
"status": "completed_no_write_policy_blocked",
|
||||
"fresh": True,
|
||||
},
|
||||
"blockers": ["external_candidates_not_promotion_ready"],
|
||||
},
|
||||
}
|
||||
|
||||
payload = build_mcp_control_plane_overview(
|
||||
catalog=catalog,
|
||||
product_matrix=matrix,
|
||||
runtime=runtime,
|
||||
)
|
||||
|
||||
assert payload["version_lifecycle"]["runtime_state"] == "running_fail_closed"
|
||||
assert payload["version_lifecycle"]["runtime"]["status"] == "ready"
|
||||
assert "external_candidates_not_promotion_ready" in payload["active_blockers"]
|
||||
assert (
|
||||
payload["operation_boundaries"]["normalized_version_receipt_write_allowed"]
|
||||
is True
|
||||
)
|
||||
assert payload["operation_boundaries"]["production_upgrade_allowed"] is False
|
||||
|
||||
254
apps/api/tests/test_mcp_version_lifecycle_service.py
Normal file
254
apps/api/tests/test_mcp_version_lifecycle_service.py
Normal file
@@ -0,0 +1,254 @@
|
||||
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'{"version":"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'{"version":"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'{"version":"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'{"version":"1.2.3"}') == "1.2.3"
|
||||
assert (
|
||||
_extract_unambiguous_version(b'{"version":"1.2.3","latestVersion":"2.0.0"}')
|
||||
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 "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
|
||||
Reference in New Issue
Block a user