test(protocol): add core and REST/A2A/MCP parity tests
parent
8bfe6c518f
commit
bfd752ac39
@ -0,0 +1,65 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from fastapi import Response
|
||||||
|
|
||||||
|
import app.a2a.router as a2a_module
|
||||||
|
from app.a2a.models import A2ARpcRequest
|
||||||
|
|
||||||
|
|
||||||
|
class _Slot(dict):
|
||||||
|
def __getattr__(self, item: str) -> str:
|
||||||
|
return self[item]
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyCoreService:
|
||||||
|
def check_availability(
|
||||||
|
self,
|
||||||
|
start: str,
|
||||||
|
end: str,
|
||||||
|
calendar_ids: list[str] | None,
|
||||||
|
) -> SimpleNamespace:
|
||||||
|
checked = calendar_ids or ["primary"]
|
||||||
|
busy_slots = [_Slot(calendar_id=checked[0], start=start, end=end)]
|
||||||
|
return SimpleNamespace(
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
available=False,
|
||||||
|
busy_slots=busy_slots,
|
||||||
|
checked_calendars=checked,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _AllowAuthBackend:
|
||||||
|
def authenticate(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
x_api_key: str | None,
|
||||||
|
authorization: str | None,
|
||||||
|
required_scopes: set[str],
|
||||||
|
) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def test_a2a_send_message_returns_availability(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(a2a_module, "core_service", _DummyCoreService())
|
||||||
|
monkeypatch.setattr(a2a_module, "auth_backend", _AllowAuthBackend())
|
||||||
|
|
||||||
|
request = A2ARpcRequest(
|
||||||
|
jsonrpc="2.0",
|
||||||
|
id="req-1",
|
||||||
|
method="SendMessage",
|
||||||
|
params={
|
||||||
|
"start": "2026-03-10T09:00:00+01:00",
|
||||||
|
"end": "2026-03-10T10:00:00+01:00",
|
||||||
|
"calendar_ids": ["primary"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response = a2a_module.a2a_rpc(request, Response())
|
||||||
|
|
||||||
|
assert response.error is None
|
||||||
|
assert response.result is not None
|
||||||
|
assert response.result["type"] == "availability.result"
|
||||||
|
assert response.result["availability"]["available"] is False
|
||||||
|
assert response.result["availability"]["checked_calendars"] == ["primary"]
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import app.core.service as core_module
|
||||||
|
from app.config import get_settings
|
||||||
|
from app.core.service import CoreAgentService
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeFreeBusy:
|
||||||
|
def __init__(self, payload: dict[str, object]) -> None:
|
||||||
|
self.payload = payload
|
||||||
|
self.last_query_body: dict[str, object] | None = None
|
||||||
|
|
||||||
|
def query(self, body: dict[str, object]) -> _FakeFreeBusy:
|
||||||
|
self.last_query_body = body
|
||||||
|
return self
|
||||||
|
|
||||||
|
def execute(self) -> dict[str, object]:
|
||||||
|
return self.payload
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeCalendarService:
|
||||||
|
def __init__(self, payload: dict[str, object]) -> None:
|
||||||
|
self._freebusy = _FakeFreeBusy(payload)
|
||||||
|
|
||||||
|
def freebusy(self) -> _FakeFreeBusy:
|
||||||
|
return self._freebusy
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_availability_maps_busy_slots(monkeypatch) -> None:
|
||||||
|
payload = {
|
||||||
|
"calendars": {
|
||||||
|
"primary": {
|
||||||
|
"busy": [
|
||||||
|
{
|
||||||
|
"start": "2026-03-10T09:00:00+01:00",
|
||||||
|
"end": "2026-03-10T10:00:00+01:00",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fake_service = _FakeCalendarService(payload)
|
||||||
|
monkeypatch.setattr(core_module, "build_calendar_service", lambda _: fake_service)
|
||||||
|
|
||||||
|
service = CoreAgentService(settings=get_settings())
|
||||||
|
result = service.check_availability(
|
||||||
|
start="2026-03-10T09:00:00+01:00",
|
||||||
|
end="2026-03-10T10:00:00+01:00",
|
||||||
|
calendar_ids=["primary"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.available is False
|
||||||
|
assert result.checked_calendars == ["primary"]
|
||||||
|
assert len(result.busy_slots) == 1
|
||||||
|
assert result.busy_slots[0].calendar_id == "primary"
|
||||||
|
assert result.busy_slots[0].start == "2026-03-10T09:00:00+01:00"
|
||||||
|
assert result.busy_slots[0].end == "2026-03-10T10:00:00+01:00"
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from fastapi import Response
|
||||||
|
|
||||||
|
import app.a2a.router as a2a_module
|
||||||
|
from app.a2a.models import A2ARpcRequest
|
||||||
|
import app.main as main_module
|
||||||
|
import app.mcp.server as mcp_server_module
|
||||||
|
import app.mcp.tools as mcp_tools_module
|
||||||
|
|
||||||
|
|
||||||
|
class _Slot(dict):
|
||||||
|
def __getattr__(self, item: str) -> str:
|
||||||
|
return self[item]
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyCoreService:
|
||||||
|
def check_availability(
|
||||||
|
self,
|
||||||
|
start: str,
|
||||||
|
end: str,
|
||||||
|
calendar_ids: list[str] | None,
|
||||||
|
) -> SimpleNamespace:
|
||||||
|
checked = calendar_ids or ["primary"]
|
||||||
|
busy_slots = [_Slot(calendar_id=checked[0], start=start, end=end)]
|
||||||
|
return SimpleNamespace(
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
available=False,
|
||||||
|
busy_slots=busy_slots,
|
||||||
|
checked_calendars=checked,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _AllowAuthBackend:
|
||||||
|
def authenticate(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
x_api_key: str | None,
|
||||||
|
authorization: str | None,
|
||||||
|
required_scopes: set[str],
|
||||||
|
) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def test_availability_parity_rest_a2a_mcp(monkeypatch) -> None:
|
||||||
|
dummy_core = _DummyCoreService()
|
||||||
|
allow_auth = _AllowAuthBackend()
|
||||||
|
|
||||||
|
monkeypatch.setattr(main_module, "core_service", dummy_core)
|
||||||
|
monkeypatch.setattr(a2a_module, "core_service", dummy_core)
|
||||||
|
monkeypatch.setattr(mcp_tools_module, "core_service", dummy_core)
|
||||||
|
monkeypatch.setattr(a2a_module, "auth_backend", allow_auth)
|
||||||
|
monkeypatch.setattr(mcp_tools_module, "auth_backend", allow_auth)
|
||||||
|
|
||||||
|
rest_response = asyncio.run(
|
||||||
|
main_module.availability(
|
||||||
|
main_module.AvailabilityRequest(
|
||||||
|
start="2026-03-10T09:00:00+01:00",
|
||||||
|
end="2026-03-10T10:00:00+01:00",
|
||||||
|
calendar_ids=["primary"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).model_dump()
|
||||||
|
|
||||||
|
a2a_response = a2a_module.a2a_rpc(
|
||||||
|
A2ARpcRequest(
|
||||||
|
jsonrpc="2.0",
|
||||||
|
id="req-1",
|
||||||
|
method="SendMessage",
|
||||||
|
params={
|
||||||
|
"start": "2026-03-10T09:00:00+01:00",
|
||||||
|
"end": "2026-03-10T10:00:00+01:00",
|
||||||
|
"calendar_ids": ["primary"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Response(),
|
||||||
|
)
|
||||||
|
assert a2a_response.error is None
|
||||||
|
assert a2a_response.result is not None
|
||||||
|
a2a_payload = a2a_response.result["availability"]
|
||||||
|
|
||||||
|
mcp_payload = mcp_server_module.check_availability(
|
||||||
|
start="2026-03-10T09:00:00+01:00",
|
||||||
|
end="2026-03-10T10:00:00+01:00",
|
||||||
|
calendar_ids=["primary"],
|
||||||
|
ctx=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert rest_response == a2a_payload == mcp_payload
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import app.main as main_module
|
||||||
|
|
||||||
|
|
||||||
|
class _Slot(dict):
|
||||||
|
def __getattr__(self, item: str) -> str:
|
||||||
|
return self[item]
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyCoreService:
|
||||||
|
def check_availability(
|
||||||
|
self,
|
||||||
|
start: str,
|
||||||
|
end: str,
|
||||||
|
calendar_ids: list[str] | None,
|
||||||
|
) -> SimpleNamespace:
|
||||||
|
checked = calendar_ids or ["primary"]
|
||||||
|
busy_slots = [
|
||||||
|
_Slot(
|
||||||
|
calendar_id=checked[0],
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return SimpleNamespace(
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
available=False,
|
||||||
|
busy_slots=busy_slots,
|
||||||
|
checked_calendars=checked,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rest_availability_adapter_returns_expected_payload(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(main_module, "core_service", _DummyCoreService())
|
||||||
|
|
||||||
|
response = asyncio.run(
|
||||||
|
main_module.availability(
|
||||||
|
main_module.AvailabilityRequest(
|
||||||
|
start="2026-03-10T09:00:00+01:00",
|
||||||
|
end="2026-03-10T10:00:00+01:00",
|
||||||
|
calendar_ids=["primary"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = response.model_dump()
|
||||||
|
assert payload["available"] is False
|
||||||
|
assert payload["checked_calendars"] == ["primary"]
|
||||||
|
assert payload["busy_slots"] == [
|
||||||
|
{
|
||||||
|
"calendar_id": "primary",
|
||||||
|
"start": "2026-03-10T09:00:00+01:00",
|
||||||
|
"end": "2026-03-10T10:00:00+01:00",
|
||||||
|
}
|
||||||
|
]
|
||||||
Loading…
Reference in New Issue