From 4c121b13c612ef3574e06d1514c451358215f8b4 Mon Sep 17 00:00:00 2001 From: Miki Palet <64255955+mikipalet@users.noreply.github.com> Date: Sat, 15 Aug 2026 06:17:22 +0000 Subject: [PATCH] fix(models): accept telephony platforms (phone, sms, rcs) in SocialAccount (zernio-claude-plugin#3) GET /v1/accounts returns every connected SocialAccount, including the telephony platforms backing the Phone Numbers product. The generated platform enum only listed social and ads platforms, so the typed accounts.list() path (and the MCP accounts_list core tool) failed pydantic validation for any user with a phone account, while the raw passthrough tool kept working. - add sms, phone, rcs to the SocialAccount platform enum in openapi.yaml and regenerate models (spec fix upstreamed separately so future regens keep the values) - regression tests: AccountsListResponse validates payloads containing phone/sms/rcs accounts and the MCP accounts_list tool returns the full list with fields preserved - bump package to 1.4.508 and SERVICE_VERSION to 1.2.1 (visible at /health) so the deploy is observable Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019c8DtXpqqSunXt4rWRQRTy --- openapi.yaml | 2 +- pyproject.toml | 2 +- src/late/__init__.py | 2 +- src/late/mcp/constants.py | 2 +- src/late/models/_generated/models.py | 5 +- tests/test_accounts_phone_platform.py | 116 ++++++++++++++++++++++++++ uv.lock | 2 +- 7 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 tests/test_accounts_phone_platform.py diff --git a/openapi.yaml b/openapi.yaml index 465e4ff..02c1526 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -6180,7 +6180,7 @@ components: _id: { type: string } platform: type: string - enum: [tiktok, instagram, facebook, youtube, linkedin, twitter, threads, pinterest, reddit, bluesky, googlebusiness, telegram, snapchat, discord, slack, whatsapp, linkedinads, metaads, pinterestads, tiktokads, xads, googleads, openaiads] + enum: [tiktok, instagram, facebook, youtube, linkedin, twitter, threads, pinterest, reddit, bluesky, googlebusiness, telegram, snapchat, discord, slack, whatsapp, linkedinads, metaads, pinterestads, tiktokads, xads, googleads, openaiads, sms, phone, rcs] profileId: oneOf: - type: string diff --git a/pyproject.toml b/pyproject.toml index ac85cb9..1352856 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "zernio-sdk" -version = "1.4.507" +version = "1.4.508" description = "The official Python library for the Zernio API" readme = "README.md" requires-python = ">=3.10" diff --git a/src/late/__init__.py b/src/late/__init__.py index 1a7f374..0c93bd4 100644 --- a/src/late/__init__.py +++ b/src/late/__init__.py @@ -42,7 +42,7 @@ Visibility, ) -__version__ = "1.4.507" +__version__ = "1.4.508" __all__ = [ # Client diff --git a/src/late/mcp/constants.py b/src/late/mcp/constants.py index a7cf7cd..05740a1 100644 --- a/src/late/mcp/constants.py +++ b/src/late/mcp/constants.py @@ -4,7 +4,7 @@ # Server information SERVICE_NAME = "Zernio MCP Server" -SERVICE_VERSION = "1.2.0" +SERVICE_VERSION = "1.2.1" # Streamable HTTP is the primary transport (single endpoint, request/response # with optional chunked streaming — no long-idle connection for proxies or # bridges like mcp-remote to kill). Legacy SSE is still served for backwards diff --git a/src/late/models/_generated/models.py b/src/late/models/_generated/models.py index 71db156..88b24ff 100644 --- a/src/late/models/_generated/models.py +++ b/src/late/models/_generated/models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.yaml -# timestamp: 2026-08-14T17:14:31+00:00 +# timestamp: 2026-08-15T06:14:00+00:00 from __future__ import annotations @@ -7071,6 +7071,9 @@ class Platform13(str, Enum): XADS = "xads" GOOGLEADS = "googleads" OPENAIADS = "openaiads" + SMS = "sms" + PHONE = "phone" + RCS = "rcs" class SocialAccount(BaseModel): diff --git a/tests/test_accounts_phone_platform.py b/tests/test_accounts_phone_platform.py new file mode 100644 index 0000000..5ced1e4 --- /dev/null +++ b/tests/test_accounts_phone_platform.py @@ -0,0 +1,116 @@ +""" +Regression tests for zernio-claude-plugin#3: accounts_list failed pydantic +validation whenever the connected-account list contained a `phone` account. + +GET /v1/accounts returns every SocialAccount on the profile, including the +telephony platforms (`phone`, `sms`, `rcs`) that back the Phone Numbers +product. The generated SocialAccount platform enum only listed the social and +ads platforms, so `AccountsListResponse.model_validate` raised +`accounts.N.platform ... [type=enum, input_value=phone]` and the MCP core tool +errored while the raw passthrough tool (accounts_list_accounts, untyped dict) +kept working. + +These tests drive the real typed SDK path (httpx.MockTransport, no network) +with a payload containing a phone account, exactly the shape that failed in +production. +""" + +from __future__ import annotations + +import json +from typing import Any + +import httpx +import pytest + +import late.client.base as client_base +from late import Late +from late.mcp import server as mcp_server +from late.models import AccountsListResponse + +PHONE_ACCOUNT = { + "_id": "a-phone", + "platform": "phone", + "profileId": "prof_1", + "username": "+14155551234", + "displayName": "Support line", + "isActive": True, +} + +ACCOUNTS_PAYLOAD = { + "accounts": [ + { + "_id": "a-ig", + "platform": "instagram", + "profileId": "prof_1", + "username": "acme", + "isActive": True, + }, + PHONE_ACCOUNT, + { + "_id": "a-sms", + "platform": "sms", + "profileId": "prof_1", + "username": "+14155550000", + "isActive": True, + }, + ], + "hasAnalyticsAccess": False, +} + + +@pytest.fixture() +def client(monkeypatch: pytest.MonkeyPatch) -> Late: + """Route all SDK HTTP traffic to a fake accounts API and wire up MCP.""" + + def handler(request: httpx.Request) -> httpx.Response: + if request.method == "GET" and request.url.path == "/api/v1/accounts": + return httpx.Response( + 200, + content=json.dumps(ACCOUNTS_PAYLOAD), + headers={"Content-Type": "application/json"}, + ) + return httpx.Response(404, json={"error": f"unexpected: {request.url.path}"}) + + real_client = httpx.Client + + def patched_client(**kwargs: Any) -> httpx.Client: + kwargs["transport"] = httpx.MockTransport(handler) + return real_client(**kwargs) + + monkeypatch.setattr(client_base.httpx, "Client", patched_client) + late = Late(api_key="test-key") + monkeypatch.setattr(mcp_server, "_get_client", lambda: late) + return late + + +@pytest.mark.parametrize("platform", ["phone", "sms", "rcs"]) +def test_response_model_accepts_telephony_platforms(platform: str) -> None: + payload = { + "accounts": [{**PHONE_ACCOUNT, "platform": platform}], + "hasAnalyticsAccess": False, + } + response = AccountsListResponse.model_validate(payload) + assert response.accounts[0].platform.value == platform + + +def test_typed_list_preserves_phone_account_fields(client: Late) -> None: + response = client.accounts.list() + + assert isinstance(response, AccountsListResponse) + assert len(response.accounts) == 3 + phone = next(a for a in response.accounts if a.platform.value == "phone") + assert phone.field_id == "a-phone" + assert phone.username == "+14155551234" + assert phone.displayName == "Support line" + assert phone.isActive is True + + +@pytest.mark.usefixtures("client") +def test_mcp_accounts_list_returns_all_accounts() -> None: + result = mcp_server.accounts_list() + + assert "validation error" not in result + assert "Found 3 connected account(s)" in result + assert "phone: +14155551234 (ID: a-phone)" in result + assert "instagram: acme (ID: a-ig)" in result diff --git a/uv.lock b/uv.lock index 3befffb..81474a6 100644 --- a/uv.lock +++ b/uv.lock @@ -2389,7 +2389,7 @@ wheels = [ [[package]] name = "zernio-sdk" -version = "1.4.506" +version = "1.4.508" source = { editable = "." } dependencies = [ { name = "httpx" },