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