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