From cfb7dd481815c4ba24051c10d5930b9e648f1785 Mon Sep 17 00:00:00 2001 From: dotta Date: Sat, 28 Mar 2026 10:04:46 -0500 Subject: [PATCH] Harden optimistic comment IDs Co-Authored-By: Paperclip --- ui/src/lib/optimistic-issue-comments.test.ts | 26 +++++++++++++++++++- ui/src/lib/optimistic-issue-comments.ts | 10 +++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/ui/src/lib/optimistic-issue-comments.test.ts b/ui/src/lib/optimistic-issue-comments.test.ts index 56e95e5b..39e2adb9 100644 --- a/ui/src/lib/optimistic-issue-comments.test.ts +++ b/ui/src/lib/optimistic-issue-comments.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { applyOptimisticIssueCommentUpdate, createOptimisticIssueComment, @@ -7,6 +7,11 @@ import { } from "./optimistic-issue-comments"; describe("optimistic issue comments", () => { + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + it("creates a pending optimistic comment for the current user", () => { const comment = createOptimisticIssueComment({ companyId: "company-1", @@ -22,6 +27,25 @@ describe("optimistic issue comments", () => { expect(comment.authorAgentId).toBeNull(); }); + it("falls back when crypto.randomUUID is unavailable", () => { + vi.stubGlobal("crypto", {}); + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_746_000_000_000); + const mathSpy = vi.spyOn(Math, "random").mockReturnValue(0.123456789); + + const comment = createOptimisticIssueComment({ + companyId: "company-1", + issueId: "issue-1", + body: "Working on it", + authorUserId: "board-1", + }); + + expect(comment.id).toBe("optimistic-1746000000000-4fzzzxjy"); + expect(comment.clientId).toBe(comment.id); + + nowSpy.mockRestore(); + mathSpy.mockRestore(); + }); + it("merges optimistic comments into the server thread in chronological order", () => { const merged = mergeIssueComments( [ diff --git a/ui/src/lib/optimistic-issue-comments.ts b/ui/src/lib/optimistic-issue-comments.ts index 20bc1bfe..07384d1a 100644 --- a/ui/src/lib/optimistic-issue-comments.ts +++ b/ui/src/lib/optimistic-issue-comments.ts @@ -16,6 +16,14 @@ function toTimestamp(value: Date | string) { return new Date(value).getTime(); } +function createOptimisticCommentId() { + const randomUuid = globalThis.crypto?.randomUUID?.(); + if (randomUuid) { + return `optimistic-${randomUuid}`; + } + return `optimistic-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; +} + export function sortIssueComments(comments: T[]) { return [...comments].sort((a, b) => { const createdAtDiff = toTimestamp(a.createdAt) - toTimestamp(b.createdAt); @@ -31,7 +39,7 @@ export function createOptimisticIssueComment(params: { authorUserId: string | null; }): OptimisticIssueComment { const now = new Date(); - const clientId = `optimistic-${crypto.randomUUID()}`; + const clientId = createOptimisticCommentId(); return { id: clientId, clientId,