feat(compose): optional body_html on send_email #30
No reviewers
Labels
No labels
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jlxq0/jmap-mcp!30
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "html-body"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #29.
send_emailbuilt a text-only email and the schema had no way to say otherwise, so an agent composing a styled briefing had no route at all.Nothing outside this repo moves
This is the one part of the change that can break something elsewhere, so it is measured rather than asserted:
Read back from
src/mcp.rsas it stands, immediately aboveasync fn send_email:The tool is still
send_email, the description is unchanged, andread_only_hintis stillfalse.tool-scope.shgenerates the same deny lines and no mount's scope moves. One file changed.Behaviour
body_htmlabsent: theEmailobject is byte-identical to what this tool sent before the parameter existed. Asserted as a whole object, not spot checks, because an extra key passes every assertion about the keys you named.body_htmlpresent: both bodies go out with distinct part ids,textBodynaming the plain one andhtmlBodythe HTML one. Sharing an id would make them the same part and the client would render whichever it found first.body_text:invalid_params. A briefing that arrives blank on a client that will not render HTML is worse than an unstyled one.No sanitiser on the outbound HTML. It is composed by the calling agent rather than received from anyone, which is the distinction
read_email's wrapping already draws. A test pins the pass-through, including astyleattribute and an&in a query string, so nobody adds one later by reflex.Four mutations, each red on exactly one test
build_email_object, theif let Some(html)line: emithtmlBodywhenNonelefthad"htmlBody": Array [],rightdid notwithout_html_the_object_is_unchangedhtmlBodypart id"h"→"b", shared with the text partassertion left == right failed: one bodyValue per partwith_html_both_bodies_go_out_with_distinct_part_idsbodyValues["h"]line: strip astyleattribute firstoutbound_html_is_passed_through_unalteredvalidate_send_bodies, the guard condition made unreachablemust be refused: ()html_without_text_is_invalid_paramsEach left the other three green.
And the mutation that tells you nothing, run deliberately for contrast:
build_email_objectreturningValue::Nullreds three at once. That is the precondition mutation, and it is why the four above are targeted at one line each.Gate
Toolchain read from
ci.ymlrather than assumed:toolchain: 1.93.0. Run throughbuild-slot.sh.clippy, tail:
test:
The first gate run was rc=101:
needless_pass_by_valueonfrom_header: Valueatsrc/mcp.rs:935,-D clippy::needless-pass-by-value implied by -D warnings. Taken by reference as clippy suggested, and all four mutations re-verified afterwards because the signature changed under them.Alan found the hole in review: validate_send_bodies was tested by testing the function, and nothing held that it was reached. Deleting its single call site in send_email left all 187 tests green while HTML-with-empty-text sent in production. Reproduced here before changing anything. All four of my mutations targeted function bodies and none targeted reachability, so the set looked thorough with one whole class outside it. The check now lives inside build_email_object, which returns Result. There is no call to omit, and html_without_text_is_invalid_params reaches it through the one path the handler uses, so removing the guard reds that test. An object builder that cannot construct an invalid object beats a guard somebody has to remember to call. Mutations, each red on exactly one test and green on the other three: emit htmlBody when body_html is None -> without_html_the_object_is_unchanged share one partId between the bodies -> with_html_both_bodies_..._part_ids rewrite the outbound html -> outbound_html_is_passed_through_unaltered remove the guard from the builder -> html_without_text_is_invalid_params And one that reds NOTHING, reported rather than dropped: replacing the handler's `?` with unwrap_or_else(|_| json!({})) discards the error and no test notices. Closing that needs a test driving the handler itself; no unit test on either function reaches it. AGENTS.md records the class: a mutation set that only edits function bodies cannot see whether the function is called, and mutate the call site as well as the callee. Gate on 1.93.0 from ci.yml, through build-slot.sh: fmt rc=0, clippy rc=0, test rc=0, 187 passed. Closes #29 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Merging. Gate run here on 1.93.0 through
build-slot.sh, and I reproduced both the fix and the hole you reported rather than taking either.The residual is real and I confirmed it, same mutation, same result:
Merging with it open, and the reason is that the two failures are not the same size. Deleting a validation call is something a person does while tidying and leaves no trace in review. Writing
unwrap_or_else(|_| json!({}))over a?is a deliberate act that a reader sees. The guard can no longer be omitted by accident, which was the whole of what I held for.Reporting a mutation that reds nothing, rather than dropping it, is what made this decision available. A set of four all-red mutations and a sentence saying the wiring is covered would have been accepted and would have been wrong, and I would not have found it twice.
Filed as
jlxq0/jmap-mcp#31: a handler-level harness that drivessend_emailfar enough to hold its error path. Its own PR, not this one, and not tonight.What goes in
AGENTS.mdfrom thisA mutation set that only edits function bodies cannot see whether the function is called. All four of the first set were thorough, targeted, each red on exactly one test, and every one pointed a layer below the fault. Coverage of a guard reads as coverage of its use, which is the same shape as
caldav-mcp's 120 tests over a reverted default.Delete the call, not just the function.