Summary
gog calendar update --remove-zoom on an event whose description contains only the gog-managed Zoom block leaves the description block intact, even though the Zoom meeting is correctly cancelled on Zoom's side. Recipients of the event would still see a clickable "Join Zoom Meeting" link pointing at a now-cancelled meeting.
The bug surfaces only when the gog-managed Zoom block is the entire description. Events that have additional user content before/after the block work correctly (block stripped, surrounding content preserved).
Repro (against main, 5afc19f)
# 1. Create an event with --with-zoom (description is the Zoom block only)
gog calendar create primary \
-a <your-acct>@gmail.com \
--summary "Repro" \
--from "<future-time>" --to "<future-time + 30m>" \
--with-zoom \
--json | jq -r '.event.id' # capture EVENT_ID
# 2. Verify the description contains only the gog-zoom-meeting block
gog calendar event primary $EVENT_ID -a <your-acct>@gmail.com --json | jq -r '.event.description'
# 3. --remove-zoom
gog calendar update primary $EVENT_ID -a <your-acct>@gmail.com --remove-zoom
# 4. Re-fetch and observe: description still contains the Zoom block, but the
# Zoom meeting is cancelled on Zoom's side (audit line confirmed action=delete)
gog calendar event primary $EVENT_ID -a <your-acct>@gmail.com --json | jq -r '.event.description'
Expected: description becomes empty (or whitespace-only).
Actual: description unchanged; still contains the gog-zoom-meeting block referencing the cancelled meeting.
Reproduces 100% of the time on macOS, against main HEAD 5afc19f, against a consumer Gmail account.
Differential (verified)
| Test setup |
--remove-zoom result |
| Event description was only the gog-managed Zoom block |
❌ Block remains; description unchanged |
| Event description had user content + the gog-managed Zoom block |
✅ Block stripped; user content preserved |
Root cause (suspected)
In internal/cmd/calendar_edit.go, the --remove-zoom path at ~line 973 correctly produces an empty description via:
patch.Description = applyZoomDescriptionBlock(descriptionForPatch(existing, patch), "")
But the subsequent merge at calendar_edit.go:994-996 treats empty as "no change requested":
if strings.TrimSpace(patch.Description) != "" {
merged.Description = patch.Description
}
When the post-strip description is empty (only-Zoom-block case), this branch is skipped and the existing description survives the merge.
This is a pre-existing patch-merge gap that the new description-mode path exposes — not a regression introduced by #590. The TrimSpace != "" check is reasonable for "user didn't pass --description" cases but doesn't differentiate from "user wants description cleared," which --remove-zoom newly relies on.
Suggested fix direction
Distinguish between "user didn't set description" and "user wants description cleared." The Google Go client uses ForceSendFields for forcing zero-value strings (vs NullFields for pointer/struct fields), so the likely candidate for clearing the Description string field is patch.ForceSendFields = append(patch.ForceSendFields, "Description") when the post-strip result is empty AND we know we intend to clear (e.g., --remove-zoom path).
For reference, the legacy ConferenceData clearing at line 974-977 uses NullFields correctly — but that's because ConferenceData is a pointer-to-struct, not a string:
if existing != nil && existing.ConferenceData != nil && isZoomConferenceData(existing.ConferenceData) {
patch.ConferenceData = nil
patch.NullFields = append(patch.NullFields, "ConferenceData")
}
So the Description field needs analogous intent (force-clear), but via the string-appropriate ForceSendFields mechanism.
Impact
- Functional: users running
--remove-zoom may believe the Zoom info is gone from the event, but recipients still see the (now-dead) Zoom link in the event description
- Data hygiene: stale Zoom URLs persist in Calendar events long after the meetings are cancelled
- Audit trail mismatch: the
[zoom] action=delete audit line claims success; the Calendar-side state contradicts it
Discovered during
Live-capture validation of PR #590 against real Zoom Pro + real Google Calendar (consumer Gmail). Same setup used to validate the original architecture finding that drove the description-mode pivot. Test infrastructure still in place; happy to re-test any proposed fix against the same setup.
Summary
gog calendar update --remove-zoomon an event whose description contains only the gog-managed Zoom block leaves the description block intact, even though the Zoom meeting is correctly cancelled on Zoom's side. Recipients of the event would still see a clickable "Join Zoom Meeting" link pointing at a now-cancelled meeting.The bug surfaces only when the gog-managed Zoom block is the entire description. Events that have additional user content before/after the block work correctly (block stripped, surrounding content preserved).
Repro (against main,
5afc19f)Expected: description becomes empty (or whitespace-only).
Actual: description unchanged; still contains the gog-zoom-meeting block referencing the cancelled meeting.
Reproduces 100% of the time on macOS, against main HEAD
5afc19f, against a consumer Gmail account.Differential (verified)
Root cause (suspected)
In
internal/cmd/calendar_edit.go, the--remove-zoompath at ~line 973 correctly produces an empty description via:But the subsequent merge at
calendar_edit.go:994-996treats empty as "no change requested":When the post-strip description is empty (only-Zoom-block case), this branch is skipped and the existing description survives the merge.
This is a pre-existing patch-merge gap that the new description-mode path exposes — not a regression introduced by #590. The
TrimSpace != ""check is reasonable for "user didn't pass--description" cases but doesn't differentiate from "user wants description cleared," which--remove-zoomnewly relies on.Suggested fix direction
Distinguish between "user didn't set description" and "user wants description cleared." The Google Go client uses
ForceSendFieldsfor forcing zero-value strings (vsNullFieldsfor pointer/struct fields), so the likely candidate for clearing the Description string field ispatch.ForceSendFields = append(patch.ForceSendFields, "Description")when the post-strip result is empty AND we know we intend to clear (e.g.,--remove-zoompath).For reference, the legacy ConferenceData clearing at line 974-977 uses
NullFieldscorrectly — but that's becauseConferenceDatais a pointer-to-struct, not a string:So the Description field needs analogous intent (force-clear), but via the string-appropriate
ForceSendFieldsmechanism.Impact
--remove-zoommay believe the Zoom info is gone from the event, but recipients still see the (now-dead) Zoom link in the event description[zoom] action=deleteaudit line claims success; the Calendar-side state contradicts itDiscovered during
Live-capture validation of PR #590 against real Zoom Pro + real Google Calendar (consumer Gmail). Same setup used to validate the original architecture finding that drove the description-mode pivot. Test infrastructure still in place; happy to re-test any proposed fix against the same setup.