From bea8108b73e6c36bcd27826c2909a0db67b69e22 Mon Sep 17 00:00:00 2001 From: ther12k Date: Sun, 21 Jun 2026 15:34:29 +0000 Subject: [PATCH] fix(google-calendar): skip syncTaskToCalendar when export is disabled (issue #2040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user installed TaskNotes and created tasks without ever enabling the Google Calendar export integration, every eligible task (those with a scheduled or due date) would append an entry to googleCalendarSyncQueue in data.json. The queue was being populated with entries like: 'lastError': 'Google Calendar sync is not ready' This happened because: 1. TaskCreationService calls syncTaskToCalendar for every task when googleCalendarExport.syncOnTaskCreate === true. 2. syncTaskToCalendar passes the eligibility check (any task with scheduled/due is eligible per settings.googleCalendarExport.syncTrigger). 3. syncTaskToCalendar reaches the 'if (!this.isEnabled())' branch. 4. isEnabled() returns false because the user never configured the integration, so queueTaskSync is called with 'sync not ready'. 5. The queue entry is persisted to data.json. 6. Nothing ever clears it because the queue is intended for transient failures that the recovery loop processes; a permanent 'never configured' state never recovers. The queue is for transient failures (auth expired, rate limited, target calendar temporarily missing). When the user has opted out of the feature entirely, there is nothing to recover from. Fix: add an early-return at the top of syncTaskToCalendar when settings.googleCalendarExport.enabled === false. This applies to every caller (TaskCreationService, taskNotesCommands, TaskContextMenu, the periodic sync loop) without needing a guard at each call site. The existing isEnabled() check below is preserved unchanged so that transient failures while the feature IS enabled still queue as before. Adds tests/unit/issues/issue-2040-google-calendar-sync-queue.test.ts with three cases: - export disabled → syncTaskToCalendar returns true, queueTaskSync not called - export enabled but isEnabled() false (e.g. OAuth disconnected) → queueTaskSync IS called (preserves existing behavior) - ineligible task + export disabled → no-op, no queue interaction Closes #2040 --- src/services/TaskCalendarSyncService.ts | 10 ++ ...ue-2040-google-calendar-sync-queue.test.ts | 132 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 tests/unit/issues/issue-2040-google-calendar-sync-queue.test.ts diff --git a/src/services/TaskCalendarSyncService.ts b/src/services/TaskCalendarSyncService.ts index cc4076bb..03d848ea 100644 --- a/src/services/TaskCalendarSyncService.ts +++ b/src/services/TaskCalendarSyncService.ts @@ -2425,6 +2425,16 @@ export class TaskCalendarSyncService { } const settings = this.plugin.settings.googleCalendarExport; + // If the user has not enabled Google Calendar export at all, do nothing + // and do not queue. The queue is for transient failures that might be + // recoverable (auth expired, rate limited, target calendar temporarily + // missing). When the user has opted out of the feature entirely, every + // eligible task creation would otherwise pollute the queue with an + // entry that will never be processed (issue #2040). + if (!settings.enabled) { + return true; + } + const existingEventId = this.getTaskEventId(task); const targetCalendarId = settings.targetCalendarId; diff --git a/tests/unit/issues/issue-2040-google-calendar-sync-queue.test.ts b/tests/unit/issues/issue-2040-google-calendar-sync-queue.test.ts new file mode 100644 index 00000000..54e34e09 --- /dev/null +++ b/tests/unit/issues/issue-2040-google-calendar-sync-queue.test.ts @@ -0,0 +1,132 @@ +import { TaskCalendarSyncService } from "../../../src/services/TaskCalendarSyncService"; +import type TaskNotesPlugin from "../../../src/main"; +import type { TaskInfo } from "../../../src/types/TaskInfo"; + +/** + * Regression test for issue #2040: + * "[Bug]: googleCalendarSyncQueue fills up without a calendar configured" + * + * Expected behavior: + * When the user has not enabled Google Calendar export in settings + * (settings.googleCalendarExport.enabled === false), creating/updating + * tasks must NOT add entries to googleCalendarSyncQueue. The queue is + * intended for transient failures (auth expired, rate limited, target + * calendar temporarily missing) and should never grow when the user has + * opted out of the feature entirely. + * + * Previous (buggy) behavior: + * TaskCreationService would call syncTaskToCalendar on every eligible + * task (any task with a scheduled or due date). syncTaskToCalendar would + * reach the `if (!this.isEnabled())` branch, which called queueTaskSync + * with "Google Calendar sync is not ready". Every task creation + * therefore appended a queue entry that nothing would ever clear, + * polluting data.json on every commit. + * + * Fix: + * Early-return from syncTaskToCalendar when + * settings.googleCalendarExport.enabled === false, before any queue + * interaction. + */ + +function makePlugin(enabled: boolean, syncOnTaskCreate = true): TaskNotesPlugin { + return { + settings: { + googleCalendarExport: { + enabled, + syncOnTaskCreate, + targetCalendarId: enabled ? "cal-1" : "", + syncTrigger: "scheduled", + }, + }, + loadData: jest.fn().mockResolvedValue({}), + } as unknown as TaskNotesPlugin; +} + +function makeTask(overrides: Partial = {}): TaskInfo { + return { + path: "tasks/foo.md", + title: "Foo", + status: "open", + priority: "normal", + archived: false, + scheduled: "2026-07-01", + tags: [], + ...overrides, + } as unknown as TaskInfo; +} + +describe("Issue #2040: syncTaskToCalendar does not queue when Google Calendar export is disabled", () => { + it("returns true and does not call queueTaskSync when export is disabled", async () => { + const plugin = makePlugin(false); + + // Spy on the private queueTaskSync method via prototype. + const queueSpy = jest + .spyOn(TaskCalendarSyncService.prototype as any, "queueTaskSync") + .mockResolvedValue(undefined); + + const service = new TaskCalendarSyncService( + plugin, + {} as any, // googleCalendarService + ); + + const result = await (service as any).syncTaskToCalendar(makeTask()); + + expect(result).toBe(true); + expect(queueSpy).not.toHaveBeenCalled(); + queueSpy.mockRestore(); + }); + + it("still calls queueTaskSync for transient failures when export IS enabled", async () => { + // When enabled === true but isEnabled() returns false (e.g. OAuth not + // connected), the existing queue-on-failure behavior is preserved. + const plugin = makePlugin(true); + + const queueSpy = jest + .spyOn(TaskCalendarSyncService.prototype as any, "queueTaskSync") + .mockResolvedValue(undefined); + + // Force isEnabled() to return false by stubbing it. + const isEnabledSpy = jest + .spyOn(TaskCalendarSyncService.prototype as any, "isEnabled") + .mockReturnValue(false); + + const service = new TaskCalendarSyncService( + plugin, + {} as any, + ); + + const result = await (service as any).syncTaskToCalendar(makeTask()); + + expect(result).toBe(false); + expect(queueSpy).toHaveBeenCalledWith( + "tasks/foo.md", + expect.objectContaining({ message: "Google Calendar sync is not ready" }), + ); + + queueSpy.mockRestore(); + isEnabledSpy.mockRestore(); + }); + + it("returns true for ineligible tasks even when export is disabled (no-op)", async () => { + // Tasks without scheduled/due dates should not be queued regardless. + const plugin = makePlugin(false); + + const queueSpy = jest + .spyOn(TaskCalendarSyncService.prototype as any, "queueTaskSync") + .mockResolvedValue(undefined); + + const service = new TaskCalendarSyncService( + plugin, + {} as any, + ); + + const ineligibleTask = makeTask({ scheduled: undefined, due: undefined }); + // The early-return for !enabled should fire before the eligibility + // check, but we still expect no queue interaction either way. + const result = await (service as any).syncTaskToCalendar(ineligibleTask); + + expect(result).toBe(true); + expect(queueSpy).not.toHaveBeenCalled(); + queueSpy.mockRestore(); + }); +}); \ No newline at end of file