Skip to content

feat(gmail): warn when drafts update downgrades a rich-text draft to plain-only - #955

Closed
mcinteerj wants to merge 3 commits into
openclaw:mainfrom
mcinteerj:warn-plain-only-draft-update
Closed

feat(gmail): warn when drafts update downgrades a rich-text draft to plain-only#955
mcinteerj wants to merge 3 commits into
openclaw:mainfrom
mcinteerj:warn-plain-only-draft-update

Conversation

@mcinteerj

@mcinteerj mcinteerj commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Scope

gmail drafts update rebuilds the whole message, so updating a draft that carries a text/html part (e.g. a reply composed in Gmail's web UI) with only --body/--body-file silently produces a plain-text-only draft. Gmail then renders the stored ~72-char hard-wrapped plain text literally — the draft looks mangled, with no hint of why.

Update already carries forward attachments (#680/#681) and reply lineage (#942/#944); the body is the remaining silent-replacement surface. Since replacing the body is exactly what the caller asked for, this PR doesn't change behaviour — it adds a stderr-only warning when the existing draft has an HTML body part and the update supplies none:

Warning: draft has an HTML body; this update replaces it with plain text only. Pass --body-html/--body-html-file to keep rich-text formatting.
  • --quote is exempt: quoting regenerates an HTML part.
  • Attachment parts don't count as an HTML body (an attached .html file doesn't trigger it).
  • The existing-draft fetch predicate is extended with the same condition the warning uses, so every plain-only non-quote update inspects the stored MIME tree — including the all-fields path (--to + --reply-to-message-id + --attach) that previously skipped the fetch entirely (see review follow-up below).
  • stdout/--json/--plain output contracts untouched.

Motivation

Real-world agent workflow: an update passing only --body on a Gmail-composed reply draft downgraded it to plain-only; the resulting "mangled wrapping" took a while to diagnose because nothing signalled the multipart → plain transition (2026-08-03, gogcli v0.17.0 — but the same applies on main).

Real behavior proof (live Gmail, redacted)

Run against a real Gmail account through the real API — a throwaway rich-text draft (multipart/alternative), updated with the unpatched v0.34.2 binary and then this branch's binary. Account address and draft id redacted; MIME trees printed from gmail drafts get --json.

$ # (0) starting point — a genuine rich-text draft, as Gmail composes them
$ gog gmail drafts get r2430190…0882 --json | mimetree
multipart/alternative
  text/plain
  text/html

$ # (1) BEFORE — unpatched v0.34.2, update supplying only --body
$ gog-v0.34.2 gmail drafts update r2430190…0882 \
    --subject "test draft" --body "Downgraded silently." --json >/dev/null
exit=0   stderr bytes=0            <-- nothing said
$ gog gmail drafts get r2430190…0882 --json | mimetree
text/plain                          <-- HTML part gone

$ # (2) AFTER — this branch, same real draft restored to rich text, same flags
$ gog-patched gmail drafts update r2430190…0882 \
    --subject "test draft" --body "Hello there. Updated with plain text only." --json >out.json
Warning: draft has an HTML body; this update replaces it with plain text only. Pass --body-html/--body-html-file to keep rich-text formatting.
exit=0
$ head -3 out.json                  <-- stdout still clean JSON
{
  "draftId": "r2430190…0882",
  "inReplyTo": null,
$ gog gmail drafts get r2430190…0882 --json | mimetree
text/plain                          <-- warning was truthful

$ # (3) AFTER — rich draft again, this time supplying --body-html
$ gog-patched gmail drafts update r2430190…0882 \
    --body "Hello there. Updated, rich text kept." \
    --body-html '<div dir="ltr"><div>Hello there. Updated, rich text kept.</div></div>' --json >out.json
exit=0   stderr bytes=0            <-- silent, as intended
$ gog gmail drafts get r2430190…0882 --json | mimetree
multipart/alternative               <-- rich text preserved
  text/plain
  text/html

mimetree is just a shell helper that walks payload.parts and prints mimeType per level. The test draft was deleted afterwards; no other drafts were touched, and nothing was sent.

Review follow-up — the all-fields path (0682a12)

The first review correctly caught that internal/cmd/gmail_drafts.go skips the existing-draft fetch when --to, --reply-to-message-id and --attach are all supplied, leaving existingPayload nil — that invocation still rebuilt the body, so it downgraded rich text unwarned.

Fixed by extending the fetch predicate with the same condition the warning uses (no HTML body supplied && !--quote), so the extra fetch is confined to updates that can actually drop an HTML body. Live re-verification of exactly that combination:

$ gog gmail drafts get r7187264…6691 --json | mimetree
multipart/alternative
  text/plain
  text/html

$ gog-patched gmail drafts update r7187264…6691 \
    --to you@example.com --reply-to-message-id 19c208a…b345 --attach note.txt \
    --subject "test draft" --body "Downgraded via the all-fields path." --json >out.json
Warning: draft has an HTML body; this update replaces it with plain text only. Pass --body-html/--body-html-file to keep rich-text formatting.
exit=0

$ gog gmail drafts get r7187264…6691 --json | mimetree
multipart/mixed                     <-- body downgraded, warning now fires
  text/plain
  text/plain                        (the attachment part)

The regression test was confirmed to fail without the predicate change (expected downgrade warning on the all-fields update path, got: with empty stderr) and pass with it.

Testing

  • make fmt clean, go vet clean; go test ./internal/cmd/ -count=1 passes (full package, 77s).
  • Tests: TestGmailDraftsUpdateCmd_WarnsWhenPlainBodyReplacesHTMLDraft (warning when a multipart/alternative draft is updated with --body only), TestGmailDraftsUpdateCmd_NoWarnWhenHTMLBodyProvided (silent with --body-html), and TestGmailDraftsUpdateCmd_WarnsWhenAllFieldsUpdateSkipsFetchGuard (the --to + --reply-to-message-id + --attach path).
  • Plus the two live Gmail runs above.

User-facing changes

New stderr warning only; no flags added or changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CEKp3XY4d4ktX7LJBcgGVs

Review follow-up — the advisory read (8dbbdfc)

The second review was right that the previous head over-reached: adding inspectForHTMLDowngrade to the fetch predicate made the existing-draft Drafts.Get happen on invocations that need nothing from it (--to + --reply-to-message-id + --attach), while the original return fetchErr was left in place. A transient read failure would then abort an update that main performs — a warning-only read blocking the write it was meant to annotate.

Fixed by splitting the predicate from the error handling:

requireExistingDraft := (!toWasSet && !c.ReplyAll) || strings.TrimSpace(replyToMessageID) == "" || preserveAttachments
inspectForHTMLDowngrade := strings.TrimSpace(htmlBody) == "" && !c.Quote
if requireExistingDraft || inspectForHTMLDowngrade {
	existing, fetchErr := svc.Users.Drafts.Get("me", draftID).Format("full").Do()
	// Advisory whenever nothing but the warning wants it: a failed inspection
	// costs the warning, never the update the caller asked for.
	if fetchErr != nil && requireExistingDraft {
		return fetchErr
	}
	if fetchErr == nil && existing != nil && existing.Message != nil {
		...
	}
}

A read the rebuilt message depends on — stored recipients (no --to), reply lineage (no --reply-to-message-id), attachment carry-forward — still returns the error, because proceeding there would silently drop data. A read wanted only for the warning does not.

Two regression tests cover both halves, on an httptest server that fails the draft GET and accepts the PUT:

  • TestGmailDraftsUpdateCmd_AdvisoryFetchFailureStillUpdates — all-fields invocation, GET returns 503: command exits 0, exactly one PUT is sent, and no warning is printed (none is possible without the payload). Confirmed to fail on the previous head: advisory draft read failure must not abort the update: googleapi: got HTTP response code 503 with body: transient draft read failure.
  • TestGmailDraftsUpdateCmd_RequiredFetchFailureAborts — no --to, same failing GET: the command errors and zero PUTs are sent, so the advisory relaxation didn't leak into reads that matter.

Live re-verification that the user-facing behaviour is unchanged by the error-handling split — same all-fields path, patched binary, real Gmail account (redacted):

$ gog gmail drafts get r6691291…3717 --json | mimetree
multipart/alternative
  text/plain
  text/html

$ gog-patched gmail drafts update r6691291…3717 \
    --to me@example.com --reply-to-message-id 19fce745e1201210 --attach note.txt \
    --subject "test draft" --body "Downgraded via the all-fields path." --json >out.json
Warning: draft has an HTML body; this update replaces it with plain text only. Pass --body-html/--body-html-file to keep rich-text formatting.
exit=0
$ head -4 out.json                  <-- stdout still clean JSON
{
  "attachments": [
    {
      "filename": "note.txt",
$ gog gmail drafts get r6691291…3717 --json | mimetree
multipart/mixed                     <-- warning was truthful
  text/plain
  text/plain                        (the attachment part)

The test draft was deleted afterwards; nothing was sent.

Checks on this head: goimports/gofumpt clean, go vet ./internal/cmd/ clean, golangci-lint run ./internal/cmd/... 0 issues, go test ./internal/cmd/ -count=1 passes (139s).

…plain-only

gmail drafts update rebuilds the whole message, so updating a draft that
has a text/html part (e.g. one composed in Gmail's web UI) with only
--body/--body-file silently produces a plain-text-only draft. Gmail then
renders the stored hard-wrapped plain text literally, which reads as
mangled formatting with no hint of what happened.

Attachments (openclaw#680) and reply lineage (openclaw#942) are already carried forward
on update; the body is the remaining silent-replacement surface. This
adds a stderr-only warning when the existing draft has an HTML body part
and the update supplies none. --quote is exempt since quoting
regenerates an HTML part; output contracts (stdout/--json) untouched.
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. labels Aug 3, 2026
@clawsweeper

clawsweeper Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed August 5, 2026, 7:21 AM ET / 11:21 UTC.

ClawSweeper review

What this changes

Adds a stderr warning when gmail drafts update would replace an existing HTML draft body with plain text only.

Merge readiness

⚠️ Ready for maintainer review - 1 item remains

Keep open for a maintainer UX decision. Current main still silently rebuilds a rich-text draft as plain-only when no HTML body is supplied; this focused advisory patch is technically sound and has real Gmail proof.

Priority: P2
Reviewed head: 8dbbdfc357cd87829358bb52ac3dd9fcaf86f862
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🦞 diamond lobster (5/6) A focused, regression-tested advisory with direct live Gmail proof and no remaining code-level finding.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The PR body provides redacted live Gmail terminal transcripts showing the warning, clean JSON stdout, and preserved rich text when HTML is supplied.
Patch quality 🦞 diamond lobster (5/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The PR body provides redacted live Gmail terminal transcripts showing the warning, clean JSON stdout, and preserved rich text when HTML is supplied.
Evidence reviewed 5 items Current main has the silent replacement path: Main resolves the supplied plain and HTML bodies, conditionally fetches existing state, then rebuilds and replaces the draft without inspecting its MIME body or issuing a warning.
PR keeps the extra read advisory: The PR fetches for MIME inspection when needed, but only returns a fetch error when the rebuilt message actually depends on stored state; otherwise it proceeds without a warning.
Focused regression coverage: The PR covers warning, supplied-HTML silence, the all-fields path, advisory read failure, and required read failure.
Findings None None.
Security None None.

How this fits together

The Gmail draft-update command reads stored draft state when needed, rebuilds the MIME message from CLI inputs, and writes the replacement through Gmail’s API. The added MIME inspection warns on stderr before a plain-only rebuild removes an existing HTML body.

flowchart LR
  A[Draft update flags] --> B[Draft update command]
  B --> C[Existing Gmail draft]
  C --> D[MIME body inspection]
  D --> E[Warning decision]
  B --> F[Rebuilt draft message]
  E --> G[stderr advisory]
  F --> H[Gmail draft update]
Loading

Decision needed

Question Recommendation
Should gmail drafts update emit an advisory stderr warning before a plain-only update removes an existing HTML draft body? Adopt the advisory: Merge the warning because it preserves machine-readable stdout while making a destructive-looking rich-text downgrade visible.

Why: The patch is correct, but adding a new user-visible warning is a product and CLI-UX choice rather than a repair to an established output contract.

Before merge

None.

Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production-to-test delta production +37 net; tests +271 The small command-path change is backed by targeted warning and failure-mode coverage.

Technical review

Best possible solution:

If maintainers want this extra user guidance, retain the existing replacement semantics and merge the MIME-aware stderr advisory with its regression coverage.

Do we have a high-confidence way to reproduce the issue?

Yes. Current main rebuilds the outgoing MIME message from supplied bodies without preserving or inspecting an existing HTML body; the PR also supplies redacted live Gmail before-and-after terminal evidence.

Is this the best way to solve the issue?

Unclear pending product direction: an stderr-only warning is technically the narrowest approach, but maintainers must choose whether this new CLI guidance belongs in the default experience.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against d4a1a6e94707.

Labels

Label justifications:

  • P2: This is a bounded Gmail draft-update usability improvement with no demonstrated message-loss or availability regression in the patched behavior.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body provides redacted live Gmail terminal transcripts showing the warning, clean JSON stdout, and preserved rich text when HTML is supplied.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides redacted live Gmail terminal transcripts showing the warning, clean JSON stdout, and preserved rich text when HTML is supplied.

Evidence

What I checked:

  • Current main has the silent replacement path: Main resolves the supplied plain and HTML bodies, conditionally fetches existing state, then rebuilds and replaces the draft without inspecting its MIME body or issuing a warning. (internal/cmd/gmail_drafts.go:774, d4a1a6e94707)
  • PR keeps the extra read advisory: The PR fetches for MIME inspection when needed, but only returns a fetch error when the rebuilt message actually depends on stored state; otherwise it proceeds without a warning. (internal/cmd/gmail_drafts.go:875, 8dbbdfc357cd)
  • Focused regression coverage: The PR covers warning, supplied-HTML silence, the all-fields path, advisory read failure, and required read failure. (internal/cmd/gmail_drafts_cmd_test.go:1043, 8dbbdfc357cd)
  • Relevant feature history: Draft-update attachment preservation and reply-lineage repair were introduced by chrischall, making them the strongest recent owner signal for this command path. (internal/cmd/gmail_drafts.go:760, 4985e2681e31)
  • Not on main or a release: The reviewed head is not an ancestor of current main, and no local release tag contains it. (8dbbdfc357cd)

Likely related people:

  • chrischall: Introduced attachment preservation and the recent merged reply-lineage repair in the same draft-update command. (role: recent area contributor; confidence: high; commits: 3d5c9cef65fa, 4985e2681e31; files: internal/cmd/gmail_drafts.go, internal/cmd/gmail_drafts_cmd_test.go)
  • Anton Sotkov: Introduced the Gmail drafts-update command that this advisory extends. (role: feature introducer; confidence: high; commits: 21fbf16390e7; files: internal/cmd/gmail_drafts.go)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (16 earlier review cycles; latest 8 shown)
  • reviewed 2026-08-04T13:26:28.887Z sha 0682a12 :: needs changes before merge. :: [P1] Do not make the advisory inspection block the update
  • reviewed 2026-08-04T14:06:50.391Z sha 0682a12 :: needs changes before merge. :: [P1] Keep the advisory MIME inspection non-blocking
  • reviewed 2026-08-04T17:19:34.288Z sha 0682a12 :: needs changes before merge. :: [P1] Keep the advisory MIME inspection non-blocking
  • reviewed 2026-08-04T19:16:29.195Z sha 0682a12 :: needs changes before merge. :: [P1] Keep the advisory MIME inspection non-blocking
  • reviewed 2026-08-04T21:01:47.163Z sha 8dbbdfc :: needs maintainer review before merge. :: none
  • reviewed 2026-08-04T21:21:57.824Z sha 8dbbdfc :: needs maintainer review before merge. :: none
  • reviewed 2026-08-04T22:08:22.350Z sha 8dbbdfc :: needs maintainer review before merge. :: none
  • reviewed 2026-08-05T01:03:07.039Z sha 8dbbdfc :: needs maintainer review before merge. :: none

@mcinteerj

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

Added the requested real-behavior proof to the PR body: a redacted live Gmail run against a genuine multipart/alternative draft, showing (1) unpatched v0.34.2 downgrading it to text/plain with zero bytes on stderr, (2) this branch emitting the warning for the same update while stdout stays clean JSON, and (3) no warning when --body-html is supplied, with the rich body preserved. Test draft was deleted afterwards; nothing was sent.

@clawsweeper

clawsweeper Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Aug 3, 2026
…warning

The existing-draft fetch is skipped when --to, --reply-to-message-id and
--attach are all supplied, leaving existingPayload nil. That path still
rebuilds the body, so a rich-text draft was downgraded to plain text
without the warning firing.

Extend the fetch predicate with the same condition the warning uses (no
HTML body supplied and not --quote), so every plain-only, non-quote
update inspects the stored MIME tree. The extra fetch is confined to
updates that can actually drop an HTML body. Adds a regression test for
the all-fields invocation; verified failing without the predicate change.
@mcinteerj

Copy link
Copy Markdown
Contributor Author

Fixed in 0682a12 — thanks, the finding was correct.

--to + --reply-to-message-id + --attach made all three fetch-guard conditions false, so existingPayload was nil and the warning couldn't fire on a path that still replaced the body. The fetch predicate now carries the same condition as the warning (no HTML body supplied and not --quote), so every plain-only non-quote update inspects the stored MIME tree while the extra fetch stays confined to updates that can actually drop an HTML body.

Added TestGmailDraftsUpdateCmd_WarnsWhenAllFieldsUpdateSkipsFetchGuard for that invocation; verified it fails without the predicate change and passes with it. PR body has a live Gmail transcript of the same combination — rich draft in, warning emitted, resulting MIME multipart/mixed with no text/html. Full internal/cmd suite, make fmt and go vet clean.

@mcinteerj

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

New head 0682a12 addresses the P2 finding: the existing-draft fetch predicate now carries the same condition as the warning (no HTML body supplied and not --quote), so the --to + --reply-to-message-id + --attach path no longer skips MIME inspection. Added TestGmailDraftsUpdateCmd_WarnsWhenAllFieldsUpdateSkipsFetchGuard for that invocation (verified failing without the predicate change), plus a live Gmail transcript of the same combination in the PR body.

@clawsweeper

clawsweeper Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. labels Aug 4, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P2 Normal priority bug or improvement with limited blast radius. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Aug 4, 2026
The plain-only downgrade warning added the existing-draft fetch to updates
that previously needed no read at all (--to + --reply-to-message-id +
--attach). That fetch kept the original `return fetchErr`, so a transient
Drafts.Get failure aborted an update main would have performed.

Split the predicate: a read the rebuilt message depends on (stored
recipients, reply lineage, attachment carry-forward) stays fatal; a read
wanted only for the warning is advisory and a failure costs the warning,
not the update.

Tests: AdvisoryFetchFailureStillUpdates (failing GET, PUT still sent, no
warning) and RequiredFetchFailureAborts (no --to, failing GET, no PUT).
@mcinteerj

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

New head 8dbbdfc fixes the P1: the advisory MIME inspection no longer blocks the update.

The previous head added inspectForHTMLDowngrade to the existing-draft fetch predicate but left the original return fetchErr in place, so on the --to + --reply-to-message-id + --attach path — where nothing but the warning wants that read — a transient Drafts.Get failure aborted an update main performs. The finding was correct.

Predicate and error handling are now separate:

requireExistingDraft := (!toWasSet && !c.ReplyAll) || strings.TrimSpace(replyToMessageID) == "" || preserveAttachments
inspectForHTMLDowngrade := strings.TrimSpace(htmlBody) == "" && !c.Quote
if requireExistingDraft || inspectForHTMLDowngrade {
	existing, fetchErr := svc.Users.Drafts.Get("me", draftID).Format("full").Do()
	// Advisory whenever nothing but the warning wants it: a failed inspection
	// costs the warning, never the update the caller asked for.
	if fetchErr != nil && requireExistingDraft {
		return fetchErr
	}
	if fetchErr == nil && existing != nil && existing.Message != nil {
		...
	}
}

A read the rebuilt message depends on — stored recipients (no --to), reply lineage (no --reply-to-message-id), attachment carry-forward — stays fatal, since proceeding there would silently drop data. A read wanted only for the warning does not.

Both halves have regression tests, on an httptest server that fails the draft GET and accepts the PUT:

  • TestGmailDraftsUpdateCmd_AdvisoryFetchFailureStillUpdates — all-fields invocation, GET returns 503: exit 0, exactly one PUT sent, no warning (none is possible without the payload). Verified failing on the previous head: advisory draft read failure must not abort the update: googleapi: got HTTP response code 503 with body: transient draft read failure.
  • TestGmailDraftsUpdateCmd_RequiredFetchFailureAborts — no --to, same failing GET: command errors, zero PUTs, so the relaxation didn't leak into reads that matter.

Live re-verification that the user-facing behaviour is unchanged by the split — same all-fields path, patched binary, real Gmail account (ids redacted):

$ gog gmail drafts get r6691291…3717 --json | mimetree
multipart/alternative
  text/plain
  text/html

$ gog-patched gmail drafts update r6691291…3717 \
    --to me@example.com --reply-to-message-id 19fce745…1210 --attach note.txt \
    --subject "test draft" --body "Downgraded via the all-fields path." --json >out.json
Warning: draft has an HTML body; this update replaces it with plain text only. Pass --body-html/--body-html-file to keep rich-text formatting.
exit=0
$ head -4 out.json                  <-- stdout still clean JSON
{
  "attachments": [
    {
      "filename": "note.txt",
$ gog gmail drafts get r6691291…3717 --json | mimetree
multipart/mixed                     <-- warning was truthful
  text/plain
  text/plain                        (the attachment part)

Test draft deleted afterwards; nothing was sent.

Checks on this head: goimports/gofumpt clean, go vet ./internal/cmd/ clean, golangci-lint run ./internal/cmd/... 0 issues, go test ./internal/cmd/ -count=1 passes (139s), plus the two acceptance-criteria runs from the review.

@clawsweeper

clawsweeper Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. labels Aug 4, 2026
@steipete

steipete commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Landed in #957 after maintainer review as commit c68441a, with the contributor credit preserved.

The landed version includes the original warning behavior and regression coverage, keeps advisory MIME inspection non-blocking, adds a guard so HTML nested inside an attached message does not trigger a false warning, and adds the changelog credit. Local make ci, autoreview, and the exact-head Linux/Windows/macOS/worker CI matrix all passed. Thank you, @mcinteerj.

@steipete steipete closed this Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants