148 lines
5.1 KiB
Python
148 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from services.runtime.connector_projectbrain import (
|
|
PUBLIC_TO_UPSTREAM,
|
|
UPSTREAM_TO_PUBLIC,
|
|
projectbrain_readiness_status,
|
|
rewrite_request,
|
|
rewrite_response,
|
|
upstream_headers,
|
|
)
|
|
|
|
|
|
def test_projectbrain_tool_catalog_is_bijective_and_namespaced() -> None:
|
|
assert len(UPSTREAM_TO_PUBLIC) == len(PUBLIC_TO_UPSTREAM) == 57
|
|
assert all(name.startswith("projectbrain.") for name in PUBLIC_TO_UPSTREAM)
|
|
assert PUBLIC_TO_UPSTREAM["projectbrain.workspace.render"] == "render_project_workspace"
|
|
assert PUBLIC_TO_UPSTREAM["projectbrain.analysis.start"] == "start_repository_analysis"
|
|
assert PUBLIC_TO_UPSTREAM["projectbrain.finding.issue.sync"] == "create_finding_issue"
|
|
assert PUBLIC_TO_UPSTREAM["projectbrain.portfolio.get"] == "get_portfolio_control_plane"
|
|
assert PUBLIC_TO_UPSTREAM["projectbrain.delivery.changeset.apply"] == "apply_feature_changeset"
|
|
assert PUBLIC_TO_UPSTREAM["projectbrain.delivery.merge"] == "merge_feature_delivery"
|
|
assert (
|
|
PUBLIC_TO_UPSTREAM["projectbrain.autonomy.executor.heartbeat"] == "heartbeat_auto_executor"
|
|
)
|
|
assert PUBLIC_TO_UPSTREAM["projectbrain.autonomy.work.claim"] == "claim_auto_work"
|
|
assert PUBLIC_TO_UPSTREAM["projectbrain.autonomy.work.report"] == "report_auto_work"
|
|
|
|
|
|
def test_auto_work_claim_catalog_requires_executor_run_id() -> None:
|
|
manifest_path = (
|
|
Path(__file__).parents[1] / "catalog" / "tools" / "projectbrain-autonomy-work-claim.yaml"
|
|
)
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
schema = manifest["spec"]["inputSchema"]
|
|
|
|
assert set(schema["required"]) == {"project_id", "run_id"}
|
|
assert schema["properties"]["run_id"] == {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 160,
|
|
}
|
|
|
|
|
|
def test_auto_work_report_catalog_supports_blocker_classification() -> None:
|
|
manifest = json.loads(
|
|
(
|
|
Path(__file__).parents[1]
|
|
/ "catalog"
|
|
/ "tools"
|
|
/ "projectbrain-autonomy-work-report.yaml"
|
|
).read_text(encoding="utf-8")
|
|
)
|
|
schema = manifest["spec"]["inputSchema"]
|
|
|
|
assert schema["properties"]["blocker_type"] == {
|
|
"type": "string",
|
|
"enum": ["human", "retryable"],
|
|
}
|
|
assert schema["properties"]["retry_after_minutes"] == {
|
|
"type": "integer",
|
|
"minimum": 5,
|
|
"maximum": 1440,
|
|
}
|
|
assert schema["additionalProperties"] is False
|
|
|
|
|
|
def test_proxy_rewrites_only_registered_tool_calls() -> None:
|
|
request = {
|
|
"jsonrpc": "2.0",
|
|
"id": 7,
|
|
"method": "tools/call",
|
|
"params": {"name": "projectbrain.context.get", "arguments": {"project_id": "demo"}},
|
|
}
|
|
rewritten = rewrite_request(request)
|
|
assert rewritten["params"]["name"] == "get_project_context"
|
|
assert rewritten["params"]["arguments"] == {"project_id": "demo"}
|
|
with pytest.raises(ValueError, match="not registered"):
|
|
rewrite_request({"method": "tools/call", "params": {"name": "filesystem.delete"}})
|
|
|
|
|
|
def test_proxy_rewrites_tool_discovery_and_drops_unknown_upstream_tools() -> None:
|
|
response = {
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": {
|
|
"tools": [
|
|
{"name": "list_projects", "description": "projects"},
|
|
{"name": "future_unsafe_tool", "description": "not catalogued"},
|
|
]
|
|
},
|
|
}
|
|
rewritten = rewrite_response(response, "tools/list")
|
|
assert rewritten["result"]["tools"] == [
|
|
{"name": "projectbrain.project.list", "description": "projects"}
|
|
]
|
|
assert rewrite_response(response, "initialize") is response
|
|
|
|
|
|
def test_proxy_forwards_the_bounded_streamable_http_context() -> None:
|
|
headers = upstream_headers(
|
|
{
|
|
"accept": "*/*",
|
|
"content-type": "application/json",
|
|
"mcp-session-id": "session-7",
|
|
"mcp-protocol-version": "2025-06-18",
|
|
"last-event-id": "event-9",
|
|
"x-itworx-client-id": "9efcd87b-bf6f-4bdc-8592-c62fa0e6da8d",
|
|
"cookie": "must-not-leak",
|
|
},
|
|
"service-token",
|
|
has_body=True,
|
|
)
|
|
|
|
assert headers == {
|
|
"Authorization": "Bearer service-token",
|
|
"Accept": "application/json, text/event-stream",
|
|
"Content-Type": "application/json",
|
|
"mcp-session-id": "session-7",
|
|
"mcp-protocol-version": "2025-06-18",
|
|
"last-event-id": "event-9",
|
|
"x-itworx-client-id": "9efcd87b-bf6f-4bdc-8592-c62fa0e6da8d",
|
|
}
|
|
|
|
|
|
def test_proxy_omits_content_type_without_a_body() -> None:
|
|
assert upstream_headers({}, "service-token", has_body=False) == {
|
|
"Authorization": "Bearer service-token",
|
|
"Accept": "application/json, text/event-stream",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("payload", "expected"),
|
|
[
|
|
({"status": "ready"}, "ready"),
|
|
({"data": {"status": "degraded"}}, "degraded"),
|
|
({"data": {"status": "blocked"}}, "blocked"),
|
|
({"data": None}, None),
|
|
],
|
|
)
|
|
def test_projectbrain_readiness_status_accepts_api_envelopes(payload, expected) -> None:
|
|
assert projectbrain_readiness_status(payload) == expected
|