from __future__ import annotations import asyncio import json import sys from types import SimpleNamespace import pytest from mcp import Client from test_hub_component_transport import call from test_hub_component_transport import controller as _controller_fixture from backend.app import mcp_server from backend.app.services.deployment_broker import DeploymentBrokerClient REPOSITORY = "jens/registered-app" REQUEST_ID = "11111111-1111-4111-8111-111111111111" PREFIX = "/v1/appops/registered-legacy/" CAS = { "expectedSourceFingerprint": "a" * 64, "expectedRuntimeFingerprint": "b" * 64, "expectedStateSha256": "c" * 64, "expectedContainerIds": {"registered-app": "d" * 64}, } @pytest.fixture def controller(monkeypatch): return _controller_fixture.__wrapped__(monkeypatch) @pytest.fixture def broker_calls(monkeypatch): calls = [] monkeypatch.setattr( DeploymentBrokerClient, "_request", lambda _self, method, path, **kwargs: calls.append((method, path, kwargs)) or {"status": "observed"}, ) return calls def test_observe_client_accepts_only_repository_and_exact_route(broker_calls): result = DeploymentBrokerClient().registered_legacy_operation( "observe", {"repository": REPOSITORY} ) assert result == {"status": "observed"} assert broker_calls[0][0:2] == ("POST", PREFIX + "observe") assert broker_calls[0][2]["payload"] == {"repository": REPOSITORY} with pytest.raises(ValueError): DeploymentBrokerClient().registered_legacy_operation( "observe", {"repository": REPOSITORY, "environment": {"secret": "value"}} ) assert len(broker_calls) == 1 def test_baseline_default_mcp_preserves_original_payload(broker_calls): mcp_server.appops_registered_legacy_baseline(REPOSITORY, REQUEST_ID) assert broker_calls[0][2]["payload"] == { "repository": REPOSITORY, "requestId": REQUEST_ID, } def test_baseline_capture_mcp_forwards_exact_optin_bindings(broker_calls): mcp_server.appops_registered_legacy_baseline( REPOSITORY, REQUEST_ID, capture_runtime_environment=True, expected_source_fingerprint=CAS["expectedSourceFingerprint"], expected_runtime_fingerprint=CAS["expectedRuntimeFingerprint"], expected_state_sha256=CAS["expectedStateSha256"], expected_container_ids=CAS["expectedContainerIds"], ) assert broker_calls[0][2]["payload"] == { "repository": REPOSITORY, "requestId": REQUEST_ID, "captureRuntimeEnvironment": True, **CAS, } @pytest.mark.parametrize( "capture,bindings", [(False, CAS), (True, {}), (1, CAS), ("true", CAS)] ) def test_capture_contract_rejects_before_broker(capture, bindings, broker_calls): with pytest.raises(ValueError): mcp_server.appops_registered_legacy_baseline( REPOSITORY, REQUEST_ID, capture_runtime_environment=capture, expected_source_fingerprint=bindings.get("expectedSourceFingerprint"), expected_runtime_fingerprint=bindings.get("expectedRuntimeFingerprint"), expected_state_sha256=bindings.get("expectedStateSha256"), expected_container_ids=bindings.get("expectedContainerIds"), ) assert not broker_calls def test_observe_http_has_no_request_id_or_mutating_helpers(controller, monkeypatch): module, _projects = controller calls = [] def observe(payload, **kwargs): calls.append((payload, kwargs)) return { "status": "observed", "repository": REPOSITORY, "sourceFingerprint": CAS["expectedSourceFingerprint"], "runtimeFingerprint": CAS["expectedRuntimeFingerprint"], "expectedStateSha256": CAS["expectedStateSha256"], "containerIDs": CAS["expectedContainerIds"], "imageIDs": {"registered-app": "sha256:" + "e" * 64}, "environmentMismatchKeys": ["registered-app/SERVER_URL"], } fake = SimpleNamespace( observe=observe, read_registered_state=lambda *_args: ({}, None), RegisteredLegacyAdoptionError=ValueError, ) monkeypatch.setitem(sys.modules, "registered_legacy_adoption", fake) monkeypatch.setattr( module, "_registered_legacy_api_reader", lambda *_args: pytest.fail("observe must not read Git API"), ) code, result = call( module, PREFIX + "observe", {"repository": REPOSITORY}, "_handle_registered_legacy", ) assert code == 200 and result["status"] == "observed" assert calls[0][0] == {"repository": REPOSITORY} assert "environment" not in result assert result["environmentMismatchKeys"] == ["registered-app/SERVER_URL"] assert ( call( module, PREFIX + "observe", {"repository": REPOSITORY, "requestId": REQUEST_ID}, "_handle_registered_legacy", )[0] == 400 ) def test_observe_mcp_schema_is_readonly_and_calls_no_other_operation(broker_calls): async def exercise(): async with Client(mcp_server.appops_mcp) as client: listing = await client.list_tools() tool = next( item for item in listing.tools if item.name == "appops.registered_legacy.observe" ) assert tool.annotations.read_only_hint is True assert tool.annotations.destructive_hint is False assert set(tool.input_schema["properties"]) == {"repository"} result = await client.call_tool(tool.name, {"repository": REPOSITORY}) assert not result.is_error asyncio.run(exercise()) assert len(broker_calls) == 1 assert broker_calls[0][2]["payload"] == {"repository": REPOSITORY} @pytest.mark.parametrize( "changes", [ {"captureRuntimeEnvironment": "true", **CAS}, {"captureRuntimeEnvironment": 1, **CAS}, {"captureRuntimeEnvironment": False, **CAS}, {"captureRuntimeEnvironment": True}, {"captureRuntimeEnvironment": True, **CAS, "expectedSourceFingerprint": "bad"}, {"captureRuntimeEnvironment": True, **CAS, "expectedContainerIds": {}}, {"environment": {"PRIVATE_KEY": "caller-value-must-not-escape"}}, ], ) def test_broker_baseline_rejects_incomplete_or_untyped_capture_before_transport( changes, broker_calls ): with pytest.raises(ValueError) as error: DeploymentBrokerClient().registered_legacy_operation( "baseline", {"repository": REPOSITORY, "requestId": REQUEST_ID, **changes} ) assert "caller-value-must-not-escape" not in str(error.value) assert not broker_calls @pytest.mark.parametrize( "changes", [ {"captureRuntimeEnvironment": "true", **CAS}, {"captureRuntimeEnvironment": 1, **CAS}, {"captureRuntimeEnvironment": False, **CAS}, {"captureRuntimeEnvironment": True}, { "captureRuntimeEnvironment": True, **CAS, "environment": {"PRIVATE_KEY": "caller-value-must-not-escape"}, }, ], ) def test_http_capture_shape_rejects_before_registry_or_core( changes, controller, monkeypatch ): module, _ = controller monkeypatch.setattr( module, "_load_projects", lambda: pytest.fail("invalid capture must not reach core"), ) code, result = call( module, PREFIX + "baseline", {"repository": REPOSITORY, "requestId": REQUEST_ID, **changes}, "_handle_registered_legacy", ) assert code == 400 and result == {"status": "invalid_contract"} assert "caller-value-must-not-escape" not in json.dumps(result) def test_http_observation_extra_environment_is_rejected_without_value_echo( controller, monkeypatch ): module, _ = controller fake = SimpleNamespace( observe=lambda *_args, **_kwargs: { "status": "observed", "environment": {"PRIVATE_KEY": "private-observer-value"}, }, read_registered_state=lambda *_args: ({}, None), RegisteredLegacyAdoptionError=type("AdoptionError", (RuntimeError,), {}), ) monkeypatch.setitem(sys.modules, "registered_legacy_adoption", fake) code, result = call( module, PREFIX + "observe", {"repository": REPOSITORY}, "_handle_registered_legacy", ) assert code == 503 and result == {"status": "registered_controller_unavailable"} assert "private-observer-value" not in json.dumps(result) def test_observe_requires_existing_signed_controller_authentication( controller, monkeypatch ): module, _ = controller monkeypatch.setattr( module, "_self_upgrade_signature_is_valid", lambda *_args, **_kwargs: False ) monkeypatch.setattr( module, "_load_projects", lambda: pytest.fail("unauthenticated observe must not read registry"), ) assert ( call( module, PREFIX + "observe", {"repository": REPOSITORY}, "_handle_registered_legacy", )[0] == 403 ) def test_mcp_wire_capture_boolean_does_not_coerce_strings_or_numbers(broker_calls): async def exercise(): async with Client(mcp_server.appops_mcp) as client: listing = await client.list_tools() tool = next( item for item in listing.tools if item.name == "appops.registered_legacy.baseline" ) assert tool.annotations.read_only_hint is True assert ( tool.input_schema["properties"]["capture_runtime_environment"][ "default" ] is False ) for value in ("true", 1): result = await client.call_tool( tool.name, { "repository": REPOSITORY, "request_id": REQUEST_ID, "capture_runtime_environment": value, }, ) assert result.is_error asyncio.run(exercise()) assert not broker_calls def test_http_observe_real_registered_runtime_returns_only_safe_keynames( controller, monkeypatch, tmp_path ): from contextlib import nullcontext from functools import partial from test_registered_legacy_runtime import ( _load_module, production_runtime as runtime_fixture, ) core = _load_module() runtime, boundary, project, model = runtime_fixture.__wrapped__( core, monkeypatch, tmp_path ) repository = "jens/quiz-monitor" model["services"]["quiz-server"]["environment"]["KNOWLEDGE_OPTIMIZER_MODEL"] = ( "intended-test-model" ) boundary.live["Config"]["Env"].append( "KNOWLEDGE_OPTIMIZER_MODEL=observed-test-model" ) monkeypatch.setattr(core, "_global_deploy_lock", lambda: nullcontext()) # Reuse the adapter fixture's validated registration against its temporary source tree. monkeypatch.setattr( core, "validate_registered_legacy_target", lambda repo, target: target["slug"] if repo == repository else pytest.fail("wrong registered target"), ) monkeypatch.setattr( core, "read_registered_state", lambda *_args, **_kwargs: ({}, None) ) monkeypatch.setattr( core, "observe", partial(core.observe, runtime=runtime, state_root=tmp_path / "state"), ) monkeypatch.setitem(sys.modules, "registered_legacy_adoption", core) module, _projects = controller monkeypatch.setattr(module, "_load_projects", lambda: {repository: project}) code, result = call( module, PREFIX + "observe", {"repository": repository}, "_handle_registered_legacy", ) assert code == 200 and result["status"] == "observed" assert result["environmentMismatchKeys"] == [ "quiz-server/KNOWLEDGE_OPTIMIZER_MODEL" ] assert result["expectedStateSha256"] == core._state_digest(None) assert set(result) == { "status", "repository", "expectedStateSha256", "sourceFingerprint", "runtimeFingerprint", "containerIDs", "imageIDs", "environmentMismatchKeys", } serialized = json.dumps(result) assert "intended-test-model" not in serialized assert "observed-test-model" not in serialized assert "198.19.1.2" not in serialized def test_http_complete_capture_forwards_no_environment_values(controller, monkeypatch): module, _projects = controller called = [] fake = SimpleNamespace( baseline=lambda payload, **_kwargs: called.append(payload) or { "status": "baseline_captured", "capturedEnvironmentKeys": ["service/SETTING"], }, read_registered_state=lambda *_args: ({}, None), RegisteredLegacyAdoptionError=ValueError, ) monkeypatch.setitem(sys.modules, "registered_legacy_adoption", fake) payload = { "repository": REPOSITORY, "requestId": REQUEST_ID, "captureRuntimeEnvironment": True, **CAS, } code, result = call( module, PREFIX + "baseline", payload, "_handle_registered_legacy" ) assert code == 200 and called == [payload] assert result == { "status": "baseline_captured", "capturedEnvironmentKeys": ["service/SETTING"], }