Skip to content
Merged
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
5 changes: 4 additions & 1 deletion src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2250,7 +2250,10 @@ export async function listNotificationDeliveriesForRecipient(
.select()
.from(notificationDeliveries)
.where(and(...conditions))
.orderBy(desc(notificationDeliveries.createdAt))
// #8895: a fan-out (e.g. issue_watch_match to many miners) stamps createdAt via nowIso() per row, so
// several deliveries can tie on the millisecond timestamp; the unique text id is a deterministic
// secondary sort so display order and the limit-boundary row are stable, not engine-defined.
.orderBy(desc(notificationDeliveries.createdAt), desc(notificationDeliveries.id))
.limit(Math.min(Math.max(options.limit ?? 50, 1), 100));
return rows.map(toNotificationDeliveryRecord);
}
Expand Down
18 changes: 18 additions & 0 deletions test/unit/notifications-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,24 @@ describe("evaluateNotificationEvent", () => {
const rows = await listNotificationDeliveriesForRecipient(env, "miner");
expect(rows.find((row) => row.dedupKey === "over-limit")?.status).toBe("suppressed");
});

it("returns a deterministic order for deliveries that tie on createdAt (#8895)", async () => {
const env = createTestEnv();
// Two deliveries stamped with an identical createdAt (a fan-out within one millisecond). Without a
// secondary sort key the tie order is engine-defined; desc(id) makes it deterministic. Controlled text
// ids (inserted a-before-z) prove the query reorders to id-desc rather than echoing insertion order.
const ts = "2026-07-10T00:00:00.000Z";
const insert =
"INSERT INTO notification_deliveries (id, dedup_key, channel, recipient_login, event_type, repo_full_name, pull_number, title, body, deeplink, actor_login, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
await env.DB.prepare(insert)
.bind("delivery-a", "k-a", "badge", "miner", "pull_request_changes_requested", "owner/repo", 1, "t", "b", "https://x", "reviewer", "delivered", ts)
.run();
await env.DB.prepare(insert)
.bind("delivery-z", "k-z", "badge", "miner", "pull_request_changes_requested", "owner/repo", 2, "t", "b", "https://x", "reviewer", "delivered", ts)
.run();
const rows = await listNotificationDeliveriesForRecipient(env, "miner");
expect(rows.map((row) => row.id)).toEqual(["delivery-z", "delivery-a"]);
});
});

describe("deliverNotification", () => {
Expand Down