Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions lib/tasks/__tests__/enrichTasks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -99,6 +100,7 @@ describe("enrichTasks", () => {
upcoming: [],
owner_email: null,
timezone: null,
trigger_lookup_failed: false,
},
]);
expect(fetchTriggerRuns).not.toHaveBeenCalled();
Expand All @@ -118,6 +120,7 @@ describe("enrichTasks", () => {
upcoming: [],
owner_email: null,
timezone: null,
trigger_lookup_failed: true,
},
]);
});
Expand All @@ -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);
});
});
27 changes: 23 additions & 4 deletions lib/tasks/enrichTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -34,7 +42,10 @@ export async function enrichTasks(tasks: ScheduledAction[]): Promise<EnrichedTas
const scheduleId = task.trigger_schedule_id;

if (!scheduleId) {
return [task.id, { recent_runs: [], upcoming: [], timezone: null }] as const;
return [
task.id,
{ recent_runs: [], upcoming: [], timezone: null, trigger_lookup_failed: false },
] as const;
}

try {
Expand Down Expand Up @@ -64,11 +75,19 @@ export async function enrichTasks(tasks: ScheduledAction[]): Promise<EnrichedTas

return [
task.id,
{ recent_runs: recentRuns, upcoming, timezone: timezone ?? null },
{
recent_runs: recentRuns,
upcoming,
timezone: timezone ?? null,
trigger_lookup_failed: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A failed latest-run lookup is still reported as successful, leaving upcoming empty with trigger_lookup_failed: false. Propagate that failure into this flag (or avoid treating upcoming as authoritative), otherwise consumers can again classify a live schedule as dead when payload retrieval is unavailable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/tasks/enrichTasks.ts, line 82:

<comment>A failed latest-run lookup is still reported as successful, leaving `upcoming` empty with `trigger_lookup_failed: false`. Propagate that failure into this flag (or avoid treating `upcoming` as authoritative), otherwise consumers can again classify a live schedule as dead when payload retrieval is unavailable.</comment>

<file context>
@@ -64,11 +75,19 @@ export async function enrichTasks(tasks: ScheduledAction[]): Promise<EnrichedTas
+            recent_runs: recentRuns,
+            upcoming,
+            timezone: timezone ?? null,
+            trigger_lookup_failed: false,
+          },
         ] as const;
</file context>

},
] as const;
} catch {
// Trigger.dev API failed — return task without trigger enrichment
return [task.id, { recent_runs: [], upcoming: [], timezone: null }] as const;
// Trigger.dev API failed — the enrichment fields are unknown, not empty.
return [
task.id,
{ recent_runs: [], upcoming: [], timezone: null, trigger_lookup_failed: true },
] as const;
}
}),
);
Expand Down
Loading