From 2075d3e016317cbb02155a3a4e72aca4725fcf33 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:37:34 -0700 Subject: [PATCH] test(initiative): pin one email when stall-nudge runs race Four concurrent nudgeStalledInitiative runs inside one episode must write one AttentionItem and send one email. Only this test kills the mutant that drops the upsertedId guard in recordForRecipients (4 emails instead of 1); the sequential two-run test still passes under it because its second run returns early on the existing row. Co-Authored-By: Claude Opus 5.5 Co-authored-by: Keel Agent-Model: claude/claude-opus-5-5/high --- .../__tests__/service/initiativeStallNudge.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/__tests__/service/initiativeStallNudge.test.js b/backend/__tests__/service/initiativeStallNudge.test.js index a41a0e56fb..eb97a2ded9 100644 --- a/backend/__tests__/service/initiativeStallNudge.test.js +++ b/backend/__tests__/service/initiativeStallNudge.test.js @@ -99,6 +99,17 @@ describe('initiative stall nudge', () => { expect(String(sendNeedsYouEmail.mock.calls[0][0].recipientUserId)).toBe(String(owner._id)); }); + it('concurrent runs inside one episode send one email', async () => { + const initiative = initiativeFor({ userId: owner._id }, [link('t1')]); + const tasks = [taskRow('t1', 'claimed', [merge(hoursAgo(50))])]; + + const results = await Promise.all([1, 2, 3, 4].map(() => run(initiative, tasks))); + + results.forEach((r) => expect(r).toMatchObject({ state: 'nudged', recipientUserId: String(owner._id) })); + expect(await stallRows()).toHaveLength(1); + expect(sendNeedsYouEmail).toHaveBeenCalledTimes(1); + }); + it('an owner change inside one episode does not nudge the new owner too', async () => { const tasks = [taskRow('t1', 'claimed', [merge(hoursAgo(50))])]; const before = initiativeFor({ userId: owner._id }, [link('t1')]);