test(protocol): add core and REST/A2A/MCP parity tests

master
oabrivard 6 days ago
parent 8bfe6c518f
commit bfd752ac39

@ -7,6 +7,7 @@ dependencies = [
"apscheduler", "apscheduler",
"fastapi", "fastapi",
"mcp", "mcp",
"pytest",
"google-api-python-client", "google-api-python-client",
"google-auth", "google-auth",
"google-auth-oauthlib", "google-auth-oauthlib",

@ -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",
}
]

@ -551,6 +551,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
] ]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]] [[package]]
name = "jiter" name = "jiter"
version = "0.13.0" version = "0.13.0"
@ -814,6 +823,7 @@ dependencies = [
{ name = "google-auth" }, { name = "google-auth" },
{ name = "google-auth-oauthlib" }, { name = "google-auth-oauthlib" },
{ name = "mcp" }, { name = "mcp" },
{ name = "pytest" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "strands-agents", extra = ["openai"] }, { name = "strands-agents", extra = ["openai"] },
{ name = "uvicorn", extra = ["standard"] }, { name = "uvicorn", extra = ["standard"] },
@ -827,11 +837,21 @@ requires-dist = [
{ name = "google-auth" }, { name = "google-auth" },
{ name = "google-auth-oauthlib" }, { name = "google-auth-oauthlib" },
{ name = "mcp" }, { name = "mcp" },
{ name = "pytest" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "strands-agents", extras = ["openai"] }, { name = "strands-agents", extras = ["openai"] },
{ name = "uvicorn", extras = ["standard"] }, { name = "uvicorn", extras = ["standard"] },
] ]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]] [[package]]
name = "proto-plus" name = "proto-plus"
version = "1.27.1" version = "1.27.1"
@ -1015,6 +1035,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" },
] ]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.11.0" version = "2.11.0"
@ -1038,6 +1067,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
] ]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"

Loading…
Cancel
Save