Forget pushed event ids when Google Calendar is disconnected (Feature 16)
CI / ci (push) Successful in 3m49s
CI / ci (pull_request) Successful in 4m33s

Disconnect deleted the connection but left every pushed plan synced with
its stored event ids, though no calendar was chosen any more. It now
deletes the connection and runs the same invalidation as choosing another
calendar (forgetEvents: MarkCalendarPlansEdited, ClearCalendarEvents) in
one transaction, under the account lock. A refused revoke still changes
nothing.

Tests push three weeks, disconnect, and check each is changed with no
event id; then connect again, choose a calendar and push the whole week
to it. Also: a refused revoke keeps every week synced, and a disconnect
during a push waits for its record and then forgets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 23:07:36 -04:00
co-authored by Claude Opus 5.5
parent 26fde27f54
commit fb7d8ddff8
8 changed files with 183 additions and 35 deletions
+12 -4
View File
@@ -342,6 +342,11 @@ func (s *Service) store(ctx context.Context, sealed []byte) error {
// connection. A token Google no longer knows (the household already removed
// MealQuest's access in their Google account) counts as revoked. Any other
// revoke failure keeps the connection so the household can retry.
//
// No calendar is chosen once disconnected, so the same transaction that
// deletes the connection forgets every pushed event (forgetEvents): every
// synced plan becomes changed, and the next push after connecting again and
// choosing a calendar writes the whole week.
func (s *Service) Disconnect(ctx context.Context) error {
s.account.Lock()
defer s.account.Unlock()
@@ -359,10 +364,13 @@ func (s *Service) Disconnect(ctx context.Context) error {
if err := s.revoke(ctx, string(token)); err != nil {
return err
}
if err := db.New(s.st.Writer).DeleteIntegrationsGoogle(ctx); err != nil {
return fmt.Errorf("gcal: delete Google integration: %w", err)
}
return nil
return s.st.WithTx(ctx, func(tx *sql.Tx) error {
q := db.New(tx)
if err := q.DeleteIntegrationsGoogle(ctx); err != nil {
return fmt.Errorf("gcal: delete Google integration: %w", err)
}
return forgetEvents(ctx, q, store.Timestamp{Time: s.now().UTC().Truncate(time.Millisecond)}.String())
})
}
// RevokeError is a revoke Google refused or that failed in transit. Its text