Skip to content

Commit ab7b2ed

Browse files
kodes-agencyclaude
andauthored
fix(review): compare a reply's update time to the whole second in head checks (#1081)
A stored reply time can hold whole seconds where the newest-page list returns fractions of a second. The head check compared them to the millisecond, so such a reply read as changed on every look and the signal never cleared. Both sides are now compared at whole-second precision. A reply whose update time moved by a second or more still signals. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent ad6d814 commit ab7b2ed

2 files changed

Lines changed: 37 additions & 3 deletions

File tree

‎src/contexts/review/domain/head-check.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,35 @@ describe('compareHead: what a head page would persist', () => {
167167
expect(result.replyChangedCount).toBe(0)
168168
})
169169

170+
it.each([
171+
['2026-10-20T12:00:00.000Z', '2026-10-20T12:00:00.789Z'],
172+
['2026-10-20T12:00:00.999Z', '2026-10-20T12:00:00.001Z'],
173+
])('ignores reply fractions on both sides (%s, %s)', (storedTime, listTime) => {
174+
const result = compare({
175+
reviews: [google({ replyText: 'Thanks', replyUpdatedAt: new Date(listTime) })],
176+
copies: [
177+
stored({ reply: { state: 'live', providerUpdatedAt: new Date(storedTime) } }),
178+
],
179+
})
180+
181+
expect(result).toMatchObject({ replyChangedCount: 0, wouldPersist: 0 })
182+
})
183+
184+
it.each([1000, 2000])('signals a real reply change %s ms apart', (difference) => {
185+
const storedTime = new Date('2026-10-20T12:00:00.789Z')
186+
const result = compare({
187+
reviews: [
188+
google({
189+
replyText: 'Thanks',
190+
replyUpdatedAt: new Date(storedTime.getTime() + difference),
191+
}),
192+
],
193+
copies: [stored({ reply: { state: 'live', providerUpdatedAt: storedTime } })],
194+
})
195+
196+
expect(result).toMatchObject({ replyChangedCount: 1, wouldPersist: 1 })
197+
})
198+
170199
it('does not count no reply on both sides', () => {
171200
const result = compare({
172201
reviews: [google()],

‎src/contexts/review/domain/head-check.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,13 @@ export type HeadComparison = Readonly<{
9090
hasMore: boolean
9191
}>
9292

93-
const sameInstant = (a: Date | null, b: Date | null): boolean =>
94-
(a === null && b === null) || (a !== null && b !== null && a.getTime() === b.getTime())
93+
// A stored reply time can hold whole seconds where the newest-page list returns
94+
// fractions of a second, so the two are compared to the second.
95+
const sameSecond = (a: Date | null, b: Date | null): boolean =>
96+
(a === null && b === null) ||
97+
(a !== null &&
98+
b !== null &&
99+
Math.floor(a.getTime() / 1000) === Math.floor(b.getTime() / 1000))
95100

96101
/** Google's updateTime is newer than the stored one (or there is none stored). */
97102
const updatedSince = (google: HeadGoogleReview, copy: HeadStoredCopy): boolean => {
@@ -123,7 +128,7 @@ const isReplyChanged = (google: HeadGoogleReview, copy: HeadStoredCopy): boolean
123128
const storedHasReply = copy.reply?.state === 'live'
124129
if (googleHasReply !== storedHasReply) return true
125130
if (!googleHasReply) return false
126-
return !sameInstant(google.replyUpdatedAt, copy.reply?.providerUpdatedAt ?? null)
131+
return !sameSecond(google.replyUpdatedAt, copy.reply?.providerUpdatedAt ?? null)
127132
}
128133

129134
const needsLeaseRefresh = (copy: HeadStoredCopy, now: Date): boolean =>

0 commit comments

Comments
 (0)