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