From ac511e417f9c457454ce39cc41c6af77f5683cd4 Mon Sep 17 00:00:00 2001 From: capJavert Date: Wed, 8 Jul 2026 10:15:19 +0000 Subject: [PATCH] fix: don't mark headline backfill done when no sources resolve dailyHeadlines can race onboarding: when it runs before the user's onboarding tags are saved, resolveHeadlineChannelSourcesForUser returns an empty list and the user was permanently marked as backfilled without ever getting their channel digest follows. Since the resolve query is cheap, skip markBackfilled on an empty result so a later call can retry once tags exist (or once channels clear the minimum post threshold). --- __tests__/highlights.ts | 46 +++++++++++++++++++-- src/common/channelDigest/headlineFollows.ts | 1 - 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/__tests__/highlights.ts b/__tests__/highlights.ts index 2db6f0a8d5..de133f2e2e 100644 --- a/__tests__/highlights.ts +++ b/__tests__/highlights.ts @@ -715,7 +715,7 @@ describe('query dailyHeadlines', () => { expect(action).not.toBeNull(); }); - it('should mark backfilled without seeding when no channel clears the minimum post count', async () => { + it('should not seed and not mark backfilled when no channel clears the minimum post count', async () => { loggedUser = '1'; await saveDigestSource('backend_digest'); @@ -735,7 +735,7 @@ describe('query dailyHeadlines', () => { userId: '1', type: UserActionType.DailyHeadlinesBackfilled, }); - expect(action).not.toBeNull(); + expect(action).toBeNull(); }); it('should not seed when the user was already backfilled', async () => { @@ -788,7 +788,7 @@ describe('query dailyHeadlines', () => { expect(action).not.toBeNull(); }); - it('should mark backfilled without seeding when the user has no onboarding tags', async () => { + it('should not seed and not mark backfilled when the user has no onboarding tags', async () => { loggedUser = '1'; await saveDigestSource('backend_digest'); @@ -806,7 +806,45 @@ describe('query dailyHeadlines', () => { userId: '1', type: UserActionType.DailyHeadlinesBackfilled, }); - expect(action).not.toBeNull(); + expect(action).toBeNull(); + }); + + it('should seed on a later call once onboarding tags exist', async () => { + loggedUser = '1'; + + await saveDigestSource('backend_digest'); + await saveChannelDigest('backend', 'backend_digest', 'backend'); + await saveChannelPosts('mh', ['backend'], 'nodejs', 3); + await saveDigestPost('bd-new', 'backend_digest', hoursAgo(2)); + await refreshKeywordChannel(); + + const first = await client.query(DAILY_HEADLINES_BACKFILL_QUERY); + expect(first.errors).toBeFalsy(); + expect( + await con.getRepository(UserAction).findOneBy({ + userId: '1', + type: UserActionType.DailyHeadlinesBackfilled, + }), + ).toBeNull(); + + await followKeyword('nodejs'); + + const second = await client.query(DAILY_HEADLINES_BACKFILL_QUERY); + expect(second.errors).toBeFalsy(); + const follows = await con + .getRepository(ContentPreferenceSource) + .find({ where: { userId: '1', feedId: '1' } }); + expect(follows).toHaveLength(1); + expect(follows[0]).toMatchObject({ + referenceId: 'backend_digest', + status: ContentPreferenceStatus.Follow, + }); + expect( + await con.getRepository(UserAction).findOneBy({ + userId: '1', + type: UserActionType.DailyHeadlinesBackfilled, + }), + ).not.toBeNull(); }); }); }); diff --git a/src/common/channelDigest/headlineFollows.ts b/src/common/channelDigest/headlineFollows.ts index 6496cf6d9c..87174bbd17 100644 --- a/src/common/channelDigest/headlineFollows.ts +++ b/src/common/channelDigest/headlineFollows.ts @@ -138,7 +138,6 @@ export const seedHeadlineChannelsForUser = async ({ }); if (!sourceIds.length) { - await markBackfilled({ manager: con, userId }); return { seeded: false, sourceIds: [] }; }