From f7d75307ee5b23aa4c0ebdcb1dc8dbf2d760eee2 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 30 Jul 2026 19:44:57 -0500 Subject: [PATCH] fix(tasks): distinguish a failed Trigger lookup from an empty schedule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit enrichTasks had four paths that all returned {recent_runs: [], upcoming: [], timezone: null}: no schedule id, the runs/timezone lookup threw, the payload retrieval threw, and a live schedule with no runs yet. Consumers could not tell a failure from a genuinely empty result. Observed 2026-07-29: two tasks reported upcoming: [] while the Trigger API showed both schedules active with a correct cron and nextRun. The false negative was read as a dead schedule and a duplicate task was created. Adds trigger_lookup_failed, true only when the lookup errored. A task with no trigger_schedule_id is false — there was nothing to look up. Implements docs#284. chat#1918 --- lib/tasks/__tests__/enrichTasks.test.ts | 28 +++++++++++++++++++++++++ lib/tasks/enrichTasks.ts | 27 ++++++++++++++++++++---- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/lib/tasks/__tests__/enrichTasks.test.ts b/lib/tasks/__tests__/enrichTasks.test.ts index 5ba06b33..41f9b488 100644 --- a/lib/tasks/__tests__/enrichTasks.test.ts +++ b/lib/tasks/__tests__/enrichTasks.test.ts @@ -79,6 +79,7 @@ describe("enrichTasks", () => { upcoming: ["2026-03-27T09:00:00Z", "2026-04-03T09:00:00Z"], owner_email: "owner@example.com", timezone: "America/New_York", + trigger_lookup_failed: false, }, ]); expect(fetchTriggerRuns).toHaveBeenCalledWith({ "filter[schedule]": "sched_abc" }, 5); @@ -99,6 +100,7 @@ describe("enrichTasks", () => { upcoming: [], owner_email: null, timezone: null, + trigger_lookup_failed: false, }, ]); expect(fetchTriggerRuns).not.toHaveBeenCalled(); @@ -118,6 +120,7 @@ describe("enrichTasks", () => { upcoming: [], owner_email: null, timezone: null, + trigger_lookup_failed: true, }, ]); }); @@ -136,8 +139,33 @@ describe("enrichTasks", () => { upcoming: [], owner_email: null, timezone: "UTC", + trigger_lookup_failed: false, }, ]); expect(retrieveTaskRun).not.toHaveBeenCalled(); }); + + it("flags trigger_lookup_failed when the Trigger lookup throws (chat#1918)", async () => { + vi.mocked(fetchTriggerRuns).mockRejectedValue(new Error("Trigger API 503")); + vi.mocked(retrieveScheduleTimezone).mockResolvedValue(null as never); + vi.mocked(selectAccountEmails).mockResolvedValue([] as never); + const [enriched] = await enrichTasks([mockTask]); + expect(enriched.trigger_lookup_failed).toBe(true); + expect(enriched.upcoming).toEqual([]); + }); + + it("does NOT flag a live schedule that legitimately has no runs yet", async () => { + vi.mocked(fetchTriggerRuns).mockResolvedValue([] as never); + vi.mocked(retrieveScheduleTimezone).mockResolvedValue("UTC" as never); + vi.mocked(selectAccountEmails).mockResolvedValue([] as never); + const [enriched] = await enrichTasks([mockTask]); + expect(enriched.trigger_lookup_failed).toBe(false); + expect(enriched.upcoming).toEqual([]); + }); + + it("does NOT flag a task that has no trigger_schedule_id to look up", async () => { + vi.mocked(selectAccountEmails).mockResolvedValue([] as never); + const [enriched] = await enrichTasks([{ ...mockTask, trigger_schedule_id: null }]); + expect(enriched.trigger_lookup_failed).toBe(false); + }); }); diff --git a/lib/tasks/enrichTasks.ts b/lib/tasks/enrichTasks.ts index b8eece49..5be7ab12 100644 --- a/lib/tasks/enrichTasks.ts +++ b/lib/tasks/enrichTasks.ts @@ -12,12 +12,20 @@ export type EnrichedTask = ScheduledAction & { owner_email: string | null; /** IANA timezone read from the Trigger.dev schedule (source of truth); null when unavailable. */ timezone: string | null; + /** + * True when the Trigger.dev lookup errored, so `recent_runs` / `upcoming` / + * `timezone` are not authoritative. Without this, a failed lookup and a live + * schedule with no runs yet were both an empty `upcoming` — which got a live + * schedule misdiagnosed as dead and a duplicate task created (chat#1918). + */ + trigger_lookup_failed: boolean; }; interface TriggerInfo { recent_runs: TriggerRun[]; upcoming: string[]; timezone: string | null; + trigger_lookup_failed: boolean; } type TriggerInfoEntry = readonly [string, TriggerInfo]; @@ -34,7 +42,10 @@ export async function enrichTasks(tasks: ScheduledAction[]): Promise