Skip to content

Commit 752fa0f

Browse files
Ooscaarclaude
andauthored
fix: make failed-post retry work — enum status mismatch (zernio-claude-plugin#1) (#33)
The MCP retry path never recognized a failed post. Two bugs conspired: 1. posts_retry compared the post's status — a generated plain Enum with an unstable positional name (Status10 in the released build, renumbered on every spec regen) — against the handwritten PostStatus str-enum. Two different enum classes never compare equal, so every failed post was rejected with 'is not in failed status (current: Status10.FAILED)'. 2. posts_retry_all_failed / posts_list_failed passed PostStatus.FAILED into the query string, where httpx serialized it via str() as 'status=PostStatus.FAILED' instead of 'status=failed', so the API matched nothing and the tools reported no failed posts. Fixes: - codegen: add --use-subclass-enum so generated enums are (str, Enum) and compare equal to their plain value and to the PostStatus twin (models regenerated). - resources: unwrap Enum members to .value in _build_params/_build_payload — in the generator template (each generated resource carries its own copy that shadows BaseResource) and in the handwritten BaseResource (resources regenerated). - mcp: posts_retry guard compares plain status values and reports the clean value ('current: scheduled') in the warning. - tests: unit tests pin enum unwrapping and value-compatibility; test_mcp_retry_regression.py drives the real MCP tools against a strict in-memory fake API (httpx.MockTransport, no network) that only matches the literal status=failed — both bugs reproduce exactly against the unfixed code with the same error messages as the issue. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 5f26567 commit 752fa0f

64 files changed

Lines changed: 1580 additions & 273 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

scripts/generate_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def main() -> int:
5656
"--field-constraints",
5757
"--use-field-description",
5858
"--capitalise-enum-members",
59+
"--use-subclass-enum",
5960
"--use-default-kwarg",
6061
"--collapse-root-models",
6162
"--use-union-operator",

scripts/generate_resources.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,20 +394,39 @@ def generate_resource_class(
394394
" default for optional string args, and the API rejects empty query",
395395
" values (e.g. ``platform=``) with a 400. Filtering here keeps both direct",
396396
" SDK callers and MCP tool callers safe.",
397+
"",
398+
" Enum members are unwrapped to their value: httpx serializes params",
399+
' via str(), which yields "ClassName.MEMBER" for Enum members, so',
400+
" e.g. ``status=PostStatus.FAILED`` would otherwise reach the API as",
401+
" ``status=PostStatus.FAILED`` instead of ``status=failed``.",
397402
' """',
403+
" from enum import Enum",
398404
" def to_camel(s: str) -> str:",
399405
' parts = s.split("_")',
400406
' return parts[0] + "".join(p.title() for p in parts[1:])',
401-
' return {to_camel(k): v for k, v in kwargs.items() if v is not None and v != ""}',
407+
" result: dict[str, Any] = {}",
408+
" for k, v in kwargs.items():",
409+
" if isinstance(v, Enum):",
410+
" v = v.value",
411+
' if v is None or v == "":',
412+
" continue",
413+
" result[to_camel(k)] = v",
414+
" return result",
402415
"",
403416
" def _build_payload(self, **kwargs: Any) -> dict[str, Any]:",
404-
' """Build request payload, filtering None values."""',
417+
' """Build request payload, filtering None values. Enum members are',
418+
' unwrapped to their value so JSON bodies carry e.g. "failed" rather',
419+
" than a raw Enum member (plain Enum members are not JSON-serializable).",
420+
' """',
405421
" from datetime import datetime",
422+
" from enum import Enum",
406423
" def to_camel(s: str) -> str:",
407424
' parts = s.split("_")',
408425
' return parts[0] + "".join(p.title() for p in parts[1:])',
409426
" result: dict[str, Any] = {}",
410427
" for k, v in kwargs.items():",
428+
" if isinstance(v, Enum):",
429+
" v = v.value",
411430
" if v is None:",
412431
" continue",
413432
" if isinstance(v, datetime):",

src/late/client/late_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@
1717
AdCampaignsResource,
1818
AdCreativesResource,
1919
AdInsightsResource,
20-
AdTargetingResource,
2120
AdsResource,
21+
AdTargetingResource,
2222
AnalyticsResource,
2323
ApiKeysResource,
2424
BroadcastsResource,
2525
CallsResource,
2626
CommentAutomationsResource,
2727
CommentsResource,
28-
ConnectResource,
2928
ConnectedAppsResource,
29+
ConnectResource,
3030
ContactsResource,
3131
ConversionsResource,
3232
CustomFieldsResource,

src/late/mcp/server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -756,8 +756,9 @@ def posts_retry(post_id: str) -> str:
756756
post = post_response.post
757757
if not post:
758758
return f"\u274c Post {post_id} not found"
759-
if post.status != PostStatus.FAILED:
760-
return f"\u26a0\ufe0f Post {post_id} is not in failed status (current: {post.status})"
759+
status_value = post.status.value if post.status else "unknown"
760+
if status_value != PostStatus.FAILED.value:
761+
return f"\u26a0\ufe0f Post {post_id} is not in failed status (current: {status_value})"
761762
except Exception as e:
762763
return f"\u274c Could not find post {post_id}: {e}"
763764

0 commit comments

Comments
 (0)