From 23f86d7a1c815b279b86f5fd650e89ee053ca734 Mon Sep 17 00:00:00 2001 From: shiva Date: Tue, 18 Aug 2026 14:18:42 +0530 Subject: [PATCH 1/2] Auto-committed by item done: uncommitted changes at completion Agentflare-Branch: task/151-handoff-mcp-tool-intermittently-drops-re Agentflare-Item: 151 --- src/mcp_server/handoff.rs | 31 +++++++++++++++++++++++++++++++ src/mcp_server/types.rs | 10 ++++++---- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/mcp_server/handoff.rs b/src/mcp_server/handoff.rs index 5df7e9be..f500b1a1 100644 --- a/src/mcp_server/handoff.rs +++ b/src/mcp_server/handoff.rs @@ -898,4 +898,35 @@ mod tests { "a stale, never-`done`/`released` claim must not permanently block re-labeling" ); } + + #[test] + fn a_json_payload_that_omits_completed_still_deserializes_and_fails_with_the_friendly_message() { + // Regression: `completed`/`remaining` had no `#[serde(default)]`, so + // a tool-call JSON that omits the key entirely (as opposed to + // sending an empty string) failed at the rmcp `Parameters` + // extractor's own deserialization step with a raw serde "missing + // field `completed`" error -- before `handoff_impl`'s friendlier + // validation (checked below) ever ran. Any caller whose JSON + // generation intermittently drops the key hit this raw error + // instead of the actionable one. `#[serde(default)]` makes a + // missing key deserialize to "", which then flows into the existing + // empty-string check uniformly. + let json = serde_json::json!({ + "recipient": "claude-code", + "name": "do the thing", + "content": "content", + "remaining": "everything", + }); + let req: HandoffRequest = serde_json::from_value(json).expect( + "a JSON payload missing `completed` must still deserialize into HandoffRequest", + ); + assert_eq!(req.completed, ""); + + let (_tmp, mcp) = test_mcp(); + let err = mcp.handoff_impl(req).unwrap_err(); + assert!( + err.to_string().contains("completed and remaining are required"), + "{err}" + ); + } } diff --git a/src/mcp_server/types.rs b/src/mcp_server/types.rs index 5685385f..f7d48706 100644 --- a/src/mcp_server/types.rs +++ b/src/mcp_server/types.rs @@ -181,6 +181,12 @@ pub(crate) struct HandoffRequest { description = "The work product being handed off (diff, review, document, ...). Prepend the brief so the recipient knows the ask. Attached to the item as an asset." )] pub(crate) content: String, + #[schemars(description = "What's done so far — required, part of the structured payload.")] + #[serde(default)] + pub(crate) completed: String, + #[schemars(description = "What's left to do — required, part of the structured payload.")] + #[serde(default)] + pub(crate) remaining: String, #[schemars( description = "html | markdown | mermaid | diagram | text (default: markdown) — picks the attached asset's extension/mime type" )] @@ -235,10 +241,6 @@ pub(crate) struct HandoffRequest { )] #[serde(default)] pub(crate) last_commit: Option, - #[schemars(description = "What's done so far — required, part of the structured payload.")] - pub(crate) completed: String, - #[schemars(description = "What's left to do — required, part of the structured payload.")] - pub(crate) remaining: String, #[schemars(description = "Known blockers, if any.")] #[serde(default)] pub(crate) blockers: Option>, From cef1568c9e26ae0ed8635e1b86149c3e007fb5de Mon Sep 17 00:00:00 2001 From: shiva Date: Tue, 18 Aug 2026 15:02:43 +0530 Subject: [PATCH 2/2] fix(handoff): apply rustfmt to regression test Co-authored-by: Cursor Agentflare-Agent: cursor Agentflare-Branch: task/151-handoff-mcp-tool-intermittently-drops-re Agentflare-Item: 151 --- src/mcp_server/handoff.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/mcp_server/handoff.rs b/src/mcp_server/handoff.rs index f500b1a1..a64cc55c 100644 --- a/src/mcp_server/handoff.rs +++ b/src/mcp_server/handoff.rs @@ -900,7 +900,8 @@ mod tests { } #[test] - fn a_json_payload_that_omits_completed_still_deserializes_and_fails_with_the_friendly_message() { + fn a_json_payload_that_omits_completed_still_deserializes_and_fails_with_the_friendly_message() + { // Regression: `completed`/`remaining` had no `#[serde(default)]`, so // a tool-call JSON that omits the key entirely (as opposed to // sending an empty string) failed at the rmcp `Parameters` @@ -925,7 +926,8 @@ mod tests { let (_tmp, mcp) = test_mcp(); let err = mcp.handoff_impl(req).unwrap_err(); assert!( - err.to_string().contains("completed and remaining are required"), + err.to_string() + .contains("completed and remaining are required"), "{err}" ); }