Skip to content

Commit d848130

Browse files
committed
fix(google): keep a retried response's unread state matching the first pass
The retry omitted `unread` entirely, so a response folded late defaulted to unread — including an acceptance, which the first pass deliberately leaves read, and including responses first seen during the initial backfill, where nothing should be marked unread at all. A late fold was therefore noisier than a timely one. Carry the originating pass's `initialSync` on the pending entry and run the same `shouldMarkUnread` rule on retry.
1 parent 0759246 commit d848130

2 files changed

Lines changed: 54 additions & 0 deletions

File tree

‎connectors/google/src/mail/sync.test.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,50 @@ describe("drainPendingRsvpsFn — retract once the event arrives", () => {
12231223
expect(store.has("pending-rsvp:rsvp-late")).toBe(false);
12241224
});
12251225

1226+
it("applies the same unread rule the first pass would have", async () => {
1227+
const { host, store } = makeHost();
1228+
const { notes } = captureSaves(host, { noteId: "N" });
1229+
const declined = rsvpThread("rsvp-late", replyIcs("DECLINED"));
1230+
store.set("pending-rsvp:rsvp-late", {
1231+
threadId: "rsvp-late",
1232+
channelId: "INBOX",
1233+
initialSync: false,
1234+
firstSeen: new Date().toISOString(),
1235+
});
1236+
(host.tools.store.list as ReturnType<typeof vi.fn>).mockImplementation(
1237+
async (prefix: string) =>
1238+
[...store.keys()].filter((k) => k.startsWith(prefix))
1239+
);
1240+
vi.spyOn(GmailApi.prototype, "getThread").mockResolvedValue(declined);
1241+
1242+
await drainPendingRsvpsFn(host);
1243+
1244+
// A decline is worth surfacing; an acceptance is not. Retrying must not
1245+
// change that, or a late fold is noisier than a timely one.
1246+
expect(notes[0]).toMatchObject({ unread: true });
1247+
});
1248+
1249+
it("leaves a response first seen during the initial backfill read", async () => {
1250+
const { host, store } = makeHost();
1251+
const { notes } = captureSaves(host, { noteId: "N" });
1252+
const declined = rsvpThread("rsvp-late", replyIcs("DECLINED"));
1253+
store.set("pending-rsvp:rsvp-late", {
1254+
threadId: "rsvp-late",
1255+
channelId: "INBOX",
1256+
initialSync: true,
1257+
firstSeen: new Date().toISOString(),
1258+
});
1259+
(host.tools.store.list as ReturnType<typeof vi.fn>).mockImplementation(
1260+
async (prefix: string) =>
1261+
[...store.keys()].filter((k) => k.startsWith(prefix))
1262+
);
1263+
vi.spyOn(GmailApi.prototype, "getThread").mockResolvedValue(declined);
1264+
1265+
await drainPendingRsvpsFn(host);
1266+
1267+
expect(notes[0].unread).toBeUndefined();
1268+
});
1269+
12261270
it("keeps the entry and archives nothing while the event is still missing", async () => {
12271271
const { host, store } = makeHost();
12281272
captureSaves(host, { noteId: null });

‎connectors/google/src/mail/sync.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,6 +1518,12 @@ type PendingRsvp = {
15181518
*/
15191519
threadId: string;
15201520
channelId: string;
1521+
/**
1522+
* Whether the response was first seen during the initial backfill. Carried
1523+
* so the retry applies the same unread rule the first pass would have — a
1524+
* late fold must not be noisier than a timely one.
1525+
*/
1526+
initialSync: boolean;
15211527
/** ISO timestamp of the first failed fold, for {@link PENDING_RSVP_TTL_MS}. */
15221528
firstSeen: string;
15231529
};
@@ -1577,6 +1583,9 @@ export async function drainPendingRsvpsFn(host: GmailSyncHost): Promise<void> {
15771583
email: reply.attendeeEmail,
15781584
...(reply.attendeeName ? { name: reply.attendeeName } : {}),
15791585
},
1586+
...(shouldMarkUnread(reply, pending.initialSync)
1587+
? { unread: true }
1588+
: {}),
15801589
});
15811590
if (!noteId) allFolded = false;
15821591
}
@@ -1695,6 +1704,7 @@ async function saveTransformedThread(
16951704
await host.set(key, {
16961705
threadId: thread.id,
16971706
channelId,
1707+
initialSync,
16981708
// Preserved across passes so the retry window measures from the
16991709
// first failure, not from the most recent re-sync of the thread.
17001710
firstSeen: existing?.firstSeen ?? new Date().toISOString(),

0 commit comments

Comments
 (0)