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