From 91e4757916a3b87d0955810595663bdce1ba2ad9 Mon Sep 17 00:00:00 2001 From: "cyrus@tinyhumans.ai" Date: Tue, 30 Jun 2026 19:10:33 +0530 Subject: [PATCH 1/2] fix(meet): mint unique correlationId per upcoming-meeting join MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Upcoming table's Join button passed the deterministic `calendar_event_id` (_) as the join correlationId. That correlationId is echoed back at call-end and copied verbatim into `MeetCallRecord.request_id` — which is both the recent-calls list key/React key and the per-call detail filename. Re-joining the same calendar meeting therefore produced two call records sharing one request_id: the history list double-highlighted both rows and the second call's transcript overwrote / was unreachable. Mint a fresh `crypto.randomUUID()` per join (mirroring MeetComposer and the background auto-join in calendar.rs). `calendar_event_id` stays the dedup/per-event-policy key and button-disable state only — never the call identity. Closes #4338 --- app/src/components/meetings/UpcomingTable.tsx | 17 +++++++-- .../meetings/__tests__/UpcomingTable.test.tsx | 35 ++++++++++++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/app/src/components/meetings/UpcomingTable.tsx b/app/src/components/meetings/UpcomingTable.tsx index 002a365500..1e7c02a9f3 100644 --- a/app/src/components/meetings/UpcomingTable.tsx +++ b/app/src/components/meetings/UpcomingTable.tsx @@ -346,7 +346,20 @@ export function UpcomingTable({ const handleJoin = async (meeting: UpcomingMeeting) => { if (!meeting.meet_url) return; const platform = meeting.platform ?? inferPlatformFromUrl(meeting.meet_url) ?? undefined; - log('[upcoming] joining %s platform=%s', meeting.calendar_event_id, platform); + // Mint a fresh correlation id per join. It becomes the call record's + // `request_id` (recent-calls list key + per-call detail filename), so it + // MUST be unique per join — reusing the deterministic `calendar_event_id` + // collapsed re-joins of the same event onto one request_id, overwriting the + // earlier call's transcript and double-highlighting the history row (#4338). + // `calendar_event_id` stays the dedup/policy key only (handleJoinPolicyChange, + // setJoiningId), mirroring the background auto-join in calendar.rs. + const correlationId = crypto.randomUUID(); + log( + '[upcoming] joining %s platform=%s correlationId=%s', + meeting.calendar_event_id, + platform, + correlationId + ); setJoiningId(meeting.calendar_event_id); try { await joinMeetViaBackendBot({ @@ -356,7 +369,7 @@ export function UpcomingTable({ systemPrompt: personaDescription || undefined, mascotId: mascotId || undefined, listenOnly: true, - correlationId: meeting.calendar_event_id, + correlationId, riveColors, }); } catch (err) { diff --git a/app/src/components/meetings/__tests__/UpcomingTable.test.tsx b/app/src/components/meetings/__tests__/UpcomingTable.test.tsx index 9ba6893db1..8db3f96334 100644 --- a/app/src/components/meetings/__tests__/UpcomingTable.test.tsx +++ b/app/src/components/meetings/__tests__/UpcomingTable.test.tsx @@ -183,12 +183,37 @@ describe('UpcomingTable', () => { await waitFor(() => expect(joinMock).toHaveBeenCalledOnce()); expect(joinMock).toHaveBeenCalledWith( - expect.objectContaining({ - meetUrl: 'https://meet.google.com/abc-def-ghi', - listenOnly: true, - correlationId: 'evt-1', - }) + expect.objectContaining({ meetUrl: 'https://meet.google.com/abc-def-ghi', listenOnly: true }) ); + // The correlation id MUST be a freshly-minted unique id, NOT the + // deterministic calendar_event_id — reusing the event id collapsed + // re-joins onto one request_id (#4338). + const { correlationId } = joinMock.mock.calls[0][0] as { correlationId: string }; + expect(correlationId).toBeTruthy(); + expect(correlationId).not.toBe('evt-1'); + }); + + it('mints a unique correlationId per join so re-joining the same event does not collide (#4338)', async () => { + joinMock.mockResolvedValue({ + meetUrl: 'https://meet.google.com/abc-def-ghi', + platform: 'gmeet', + }); + // Same meeting (same calendar_event_id) returned across reloads. + listMock.mockResolvedValue([makeMeeting()]); + renderWithProviders(); + + const joinBtn = await screen.findByRole('button', { name: /^join$/i }); + fireEvent.click(joinBtn); + await waitFor(() => expect(joinMock).toHaveBeenCalledOnce()); + fireEvent.click(joinBtn); + await waitFor(() => expect(joinMock).toHaveBeenCalledTimes(2)); + + const first = (joinMock.mock.calls[0][0] as { correlationId: string }).correlationId; + const second = (joinMock.mock.calls[1][0] as { correlationId: string }).correlationId; + expect(first).not.toBe('evt-1'); + expect(second).not.toBe('evt-1'); + // Two joins of the same calendar event must yield distinct correlation ids. + expect(first).not.toBe(second); }); it('does not show a join button for meetings without a conferencing URL', async () => { From 9a286bb424e1a20a867140713244cb836515d05a Mon Sep 17 00:00:00 2001 From: "cyrus@tinyhumans.ai" Date: Wed, 1 Jul 2026 13:50:14 +0530 Subject: [PATCH 2/2] test(meet): wait for Join button to re-enable before second click Addresses CodeRabbit: the regression test clicked Join a second time before handleJoin's finally cleared joiningId, so the (disabled) button could swallow the click and make the two-joins assertion timing-dependent. Wait for the button to re-enable first. --- app/src/components/meetings/__tests__/UpcomingTable.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/components/meetings/__tests__/UpcomingTable.test.tsx b/app/src/components/meetings/__tests__/UpcomingTable.test.tsx index 8db3f96334..26504ec87b 100644 --- a/app/src/components/meetings/__tests__/UpcomingTable.test.tsx +++ b/app/src/components/meetings/__tests__/UpcomingTable.test.tsx @@ -205,6 +205,10 @@ describe('UpcomingTable', () => { const joinBtn = await screen.findByRole('button', { name: /^join$/i }); fireEvent.click(joinBtn); await waitFor(() => expect(joinMock).toHaveBeenCalledOnce()); + // handleJoin disables the row via `joiningId` until its `finally` runs; + // wait for the button to re-enable before the second click so it isn't + // swallowed by the disabled state (timing-dependent otherwise). + await waitFor(() => expect((joinBtn as HTMLButtonElement).disabled).toBe(false)); fireEvent.click(joinBtn); await waitFor(() => expect(joinMock).toHaveBeenCalledTimes(2));