Publish curated Unraid-AppOps-Gateway source from 84f2a9958b2bdf1c1c13553925026d75f32703c3
This commit is contained in:
1 parent
8dd4b40252
commit
5d598db0df
14 files changed
+2765
-46
No files matched your search
@@ -0,0 +1,397 @@
|
||||
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"],
|
||||
}
|
||||
Reference in new issue
Block a user