Skip to content

fix(card): handle Card footer deserialization errors#479

Merged
jamiepine merged 4 commits intomainfrom
fix/card-footer-deserialization-478
Mar 24, 2026
Merged

fix(card): handle Card footer deserialization errors#479
jamiepine merged 4 commits intomainfrom
fix/card-footer-deserialization-478

Conversation

@vsumner
Copy link
Copy Markdown
Collaborator

@vsumner vsumner commented Mar 23, 2026

Summary

  • Add CardFooter struct to support structured footer data with text and optional icon_url fields
  • Implement custom deserializer using serde::de::Visitor to accept both string ("footer": "text") and object ("footer": {"text": "..."}) forms
  • Update text_from_cards() to access footer content via .text property
  • Update Discord embed builder to use footer.text.clone() for CreateEmbedFooter
  • Add WARN-level logging in SpacebotHook::on_tool_result() when tool errors occur
  • Add 6 unit tests covering all footer deserialization scenarios

Fixes #478

Testing

# Run footer-specific tests
cargo test card_footer --lib

# Full test suite
cargo test --lib
just gate-pr

All footer deserialization tests pass:

  • card_footer_deserializes_from_string - plain string footer
  • card_footer_deserializes_from_object - object with text and icon_url
  • card_footer_deserializes_from_object_text_only - the problematic case from reply tool silently swallows JSON deserialization errors #478
  • card_footer_deserializes_when_missing - optional footer field
  • card_footer_deserializes_when_null - null footer value
  • card_footer_display_trait_works and card_footer_as_str_works - utility methods

Notes (Optional)

The root cause was the LLM sending footer as {"text": "Week of March 23, 2026"} when the schema expected a plain string. The custom deserializer now handles both forms so the tool call doesn't fail. Errors are now logged at WARN level in hooks so operators can see when tools fail (including deserialization errors that happen before the tool's call() method).

The `reply` tool was failing silently when the LLM sent malformed JSON
arguments for card footers. The footer field could be either a plain
string or an object with `text` and optional `icon_url` fields.

Changes:
- Add `CardFooter` struct with `text` and `icon_url` fields
- Add custom deserializer to accept both string and object forms
- Update `text_from_cards()` and Discord embed builder
- Add WARN-level logging when tool errors occur in hooks

This ensures deserialization errors are visible in logs and can be
returned to the LLM for self-correction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78643250-61be-4aab-9a88-297d35cf510e

📥 Commits

Reviewing files that changed from the base of the PR and between f428779 and 09512fb.

📒 Files selected for processing (2)
  • src/lib.rs
  • src/messaging/discord.rs

Walkthrough

Adds structured card footer support (string or object) with custom deserialization and Display; updates Discord embed footer construction to use footer text and optional icon; logs tool-call failures whose result starts with "Toolset error:"; and adds .worktrees/ to .gitignore.

Changes

Cohort / File(s) Summary
Git ignore
/.gitignore
Added .worktrees/ entry to ignore worktree directories.
Tool failure logging
src/hooks/spacebot.rs
Emit a tracing::warn! when a tool result string begins with "Toolset error:", logging process_id, tool_name, and the full result prior to existing handling.
Card footer: API, serde, and tests
src/lib.rs
Replaced Card.footer: Option<String> with Option<CardFooter>; added pub struct CardFooter { text: String, icon_url: Option<String> }, Display, as_str(), new(), and deserialize_card_footer that accepts either a string or an object; updated footer-to-text rendering and added unit tests for string/object/null/missing cases.
Discord embed footer usage
src/messaging/discord.rs
Build Discord embed footer with CreateEmbedFooter::new(footer.text.clone()) and conditionally set icon_url before attaching the footer to the embed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: implementing a CardFooter struct with custom deserialization to handle both string and object footer formats, directly fixing the deserialization error issue.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, explaining the CardFooter struct addition, custom deserializer implementation, logging improvements, and test coverage.
Linked Issues check ✅ Passed The PR partially addresses issue #478's objectives: deserialization robustness for footer and WARN-level logging are implemented, but tool success semantics and channel/error propagation remain incomplete as noted in PR objectives.
Out of Scope Changes check ✅ Passed Changes are focused on the footer deserialization problem and logging visibility. The .gitignore and SpacebotHook logging changes are minor supporting changes directly related to the core issue.
Docstring Coverage ✅ Passed Docstring coverage is 95.24% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/card-footer-deserialization-478

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/messaging/discord.rs (1)

1040-1042: Wire up the icon_url field from CardFooter to the Discord embed footer.

The CardFooter struct includes an optional icon_url field that is currently not used when creating the embed footer. Serenity's CreateEmbedFooter supports footer icons via the .icon_url() method.

♻️ Proposed implementation
     if let Some(footer) = &card.footer {
-        embed = embed.footer(CreateEmbedFooter::new(footer.text.clone()));
+        let mut embed_footer = CreateEmbedFooter::new(footer.text.clone());
+        if let Some(icon_url) = &footer.icon_url {
+            embed_footer = embed_footer.icon_url(icon_url);
+        }
+        embed = embed.footer(embed_footer);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/messaging/discord.rs` around lines 1040 - 1042, The footer's icon_url is
not being forwarded to the Discord embed; update the footer construction (where
card.footer is handled) to set the icon when present: create a CreateEmbedFooter
with footer.text then, if CardFooter.icon_url is Some, call .icon_url(...) with
that value before passing it to embed.footer. Use CardFooter, card.footer,
CreateEmbedFooter, and the .icon_url() builder method so the embed includes the
footer icon when provided.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/messaging/discord.rs`:
- Around line 1040-1042: The footer's icon_url is not being forwarded to the
Discord embed; update the footer construction (where card.footer is handled) to
set the icon when present: create a CreateEmbedFooter with footer.text then, if
CardFooter.icon_url is Some, call .icon_url(...) with that value before passing
it to embed.footer. Use CardFooter, card.footer, CreateEmbedFooter, and the
.icon_url() builder method so the embed includes the footer icon when provided.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 27ceccf0-9379-4edb-9717-1d7a25718d3a

📥 Commits

Reviewing files that changed from the base of the PR and between 6f81f39 and f0cb36b.

📒 Files selected for processing (4)
  • .gitignore
  • src/hooks/spacebot.rs
  • src/lib.rs
  • src/messaging/discord.rs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib.rs`:
- Around line 773-777: The CardFooter struct adds an icon_url field that is
deserialized but never used by the Discord renderer; update the renderer that
builds the Discord footer (the code in src/messaging/discord.rs that currently
constructs the footer from footer.text only) to include footer.icon_url when
present (e.g., set the Discord footer's icon/url property from
CardFooter.icon_url.clone() or equivalent), or if you prefer not to support
icons yet remove icon_url from CardFooter to avoid dead fields—ensure you
reference CardFooter and the footer construction site in the Discord renderer
when making the change.
- Around line 838-851: The deserializer reads "icon_url" as a bare String which
fails on explicit nulls; change the map.next_value call for the "icon_url" key
to deserialize into Option<String> (e.g., use
map.next_value::<Option<String>>()?) so icon_url can be Some(String) or None
when JSON has null, then construct CardFooter { text: t, icon_url } as before;
also add a regression test case that deserializes {"footer":
{"text":"x","icon_url": null}} (alongside the existing footer tests) to ensure
null is accepted.
- Around line 766-768: The schema and deserializer for Card.footer are wrong:
remove or change the #[schemars(with = "Option<String>")] override so the
generated schema reflects Option<CardFooter> (i.e., the object with text and
icon_url) instead of a plain string, and fix the custom deserializer function
deserialize_card_footer to accept a nullable icon by reading icon_url as
Option<String> (use map.next_value::<Option<String>>()? when handling the
"icon_url" key) so {"footer": {"text":"x","icon_url":null}} deserializes
correctly; ensure CardFooter and deserialize_card_footer are the referenced
symbols you edit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1011653e-e73b-4843-920c-639e60a4f960

📥 Commits

Reviewing files that changed from the base of the PR and between f0cb36b and f428779.

📒 Files selected for processing (1)
  • src/lib.rs

@vsumner vsumner requested a review from jamiepine March 23, 2026 23:05
- Remove incorrect schemars override so schema reflects Option<CardFooter>
- Fix icon_url deserialization to handle null via Option<String>
- Add icon_url support in Discord embed builder
- Add regression test for icon_url: null
@vsumner vsumner enabled auto-merge March 24, 2026 01:40
@jamiepine jamiepine disabled auto-merge March 24, 2026 19:21
@jamiepine jamiepine merged commit 44b48b6 into main Mar 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reply tool silently swallows JSON deserialization errors

2 participants