From 2405851436bf56c75df331441560ffbf16c4c453 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 6 Mar 2026 09:03:27 -0600 Subject: [PATCH] Normalize derived issue timestamps to avoid 500s --- .../src/__tests__/issues-user-context.test.ts | 19 +++++++++++++ server/src/services/issues.ts | 27 +++++++++++++------ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/server/src/__tests__/issues-user-context.test.ts b/server/src/__tests__/issues-user-context.test.ts index 0df54c3a..80c7d37b 100644 --- a/server/src/__tests__/issues-user-context.test.ts +++ b/server/src/__tests__/issues-user-context.test.ts @@ -91,4 +91,23 @@ describe("deriveIssueUserContext", () => { expect(context.myLastTouchAt?.toISOString()).toBe("2026-03-06T11:30:00.000Z"); expect(context.isUnreadForMe).toBe(false); }); + + it("handles SQL timestamp strings without throwing", () => { + const context = deriveIssueUserContext( + makeIssue({ + createdByUserId: "user-1", + createdAt: new Date("2026-03-06T09:00:00.000Z"), + }), + "user-1", + { + myLastCommentAt: "2026-03-06T10:00:00.000Z", + myLastReadAt: null, + lastExternalCommentAt: "2026-03-06T11:00:00.000Z", + }, + ); + + expect(context.myLastTouchAt?.toISOString()).toBe("2026-03-06T10:00:00.000Z"); + expect(context.lastExternalCommentAt?.toISOString()).toBe("2026-03-06T11:00:00.000Z"); + expect(context.isUnreadForMe).toBe(true); + }); }); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index bb4efd81..cb258e23 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -79,8 +79,8 @@ type IssueUserCommentStats = { type IssueUserContextInput = { createdByUserId: string | null; assigneeUserId: string | null; - createdAt: Date; - updatedAt: Date; + createdAt: Date | string; + updatedAt: Date | string; }; function sameRunLock(checkoutRunId: string | null, actorRunId: string | null) { @@ -179,18 +179,29 @@ export function deriveIssueUserContext( issue: IssueUserContextInput, userId: string, stats: - | { myLastCommentAt: Date | null; myLastReadAt: Date | null; lastExternalCommentAt: Date | null } + | { + myLastCommentAt: Date | string | null; + myLastReadAt: Date | string | null; + lastExternalCommentAt: Date | string | null; + } | null | undefined, ) { - const myLastCommentAt = stats?.myLastCommentAt ?? null; - const myLastReadAt = stats?.myLastReadAt ?? null; - const createdTouchAt = issue.createdByUserId === userId ? issue.createdAt : null; - const assignedTouchAt = issue.assigneeUserId === userId ? issue.updatedAt : null; + const normalizeDate = (value: Date | string | null | undefined) => { + if (!value) return null; + if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value; + const parsed = new Date(value); + return Number.isNaN(parsed.getTime()) ? null : parsed; + }; + + const myLastCommentAt = normalizeDate(stats?.myLastCommentAt); + const myLastReadAt = normalizeDate(stats?.myLastReadAt); + const createdTouchAt = issue.createdByUserId === userId ? normalizeDate(issue.createdAt) : null; + const assignedTouchAt = issue.assigneeUserId === userId ? normalizeDate(issue.updatedAt) : null; const myLastTouchAt = [myLastCommentAt, myLastReadAt, createdTouchAt, assignedTouchAt] .filter((value): value is Date => value instanceof Date) .sort((a, b) => b.getTime() - a.getTime())[0] ?? null; - const lastExternalCommentAt = stats?.lastExternalCommentAt ?? null; + const lastExternalCommentAt = normalizeDate(stats?.lastExternalCommentAt); const isUnreadForMe = Boolean( myLastTouchAt && lastExternalCommentAt &&