diff --git a/src/lib/bounty-query.test.ts b/src/lib/bounty-query.test.ts index 9288c63..1fb154f 100644 --- a/src/lib/bounty-query.test.ts +++ b/src/lib/bounty-query.test.ts @@ -172,6 +172,30 @@ describe("applyBountyQuery", () => { ); expect(result.items.map((b) => b.id)).toEqual(["z", "y", "x"]); }); + + it("puts an already-expired deadline after every still-open one (#703)", () => { + const mixed = [ + bounty({ id: "expired-recent", deadline: new Date(Date.now() - 3600000).toISOString(), reward: 1 }), + bounty({ id: "expired-long-ago", deadline: new Date(Date.now() - 86400000 * 30).toISOString(), reward: 2 }), + bounty({ id: "open-far", deadline: new Date(Date.now() + 86400000 * 7).toISOString(), reward: 3 }), + bounty({ id: "open-soon", deadline: new Date(Date.now() + 3600000).toISOString(), reward: 4 }), + bounty({ id: "no-deadline", deadline: null, reward: 5 }), + ]; + const result = applyBountyQuery( + mixed, + { status: "all", difficulty: "all", asset: "all", sort: "deadline-asc", page: 1 }, + 10, + ); + // Open bounties (soonest first), then expired ones, then no-deadline last — + // not a raw timestamp sort, which would put "expired-long-ago" first. + expect(result.items.map((b) => b.id)).toEqual([ + "open-soon", + "open-far", + "expired-long-ago", + "expired-recent", + "no-deadline", + ]); + }); }); describe("buildBountyQueryString", () => { diff --git a/src/lib/bounty-query.ts b/src/lib/bounty-query.ts index 8551bb7..78ab12c 100644 --- a/src/lib/bounty-query.ts +++ b/src/lib/bounty-query.ts @@ -136,14 +136,25 @@ function sortBounties(bounties: Bounty[], sort: SortOption): Bounty[] { case "reward-asc": sorted.sort((a, b) => a.reward - b.reward); break; - case "deadline-asc": + case "deadline-asc": { + // Captured once so every comparison in this sort agrees on "now" (#703). + const now = Date.now(); sorted.sort((a, b) => { if (!a.deadline && !b.deadline) return 0; if (!a.deadline) return 1; if (!b.deadline) return -1; - return new Date(a.deadline).getTime() - new Date(b.deadline).getTime(); + const aTime = new Date(a.deadline).getTime(); + const bTime = new Date(b.deadline).getTime(); + const aExpired = aTime < now; + const bExpired = bTime < now; + // "Soonest" means soonest upcoming deadline. An already-expired + // deadline is never "soonest" — it ranks after every still-open one, + // regardless of how its raw timestamp compares. + if (aExpired !== bExpired) return aExpired ? 1 : -1; + return aTime - bTime; }); break; + } case "default": break; }