From a62a2de51f13a3484bf3c8917e943e406f93ebd7 Mon Sep 17 00:00:00 2001 From: Ido Shamun <1993245+idoshamun@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:03:06 +0300 Subject: [PATCH] fix(cdc): re-enrich scheduled freeform posts when they become visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A scheduled freeform post's tags/contentQuality are captured once, when it's created/scheduled, and are never refreshed at publish time — publishScheduledPost only flips visibility. So a post enriched during a broken enrichment window (e.g. the empty-tags bug) goes live broken, and correcting the source afterwards has no effect because nothing re-sends. On the visible false->true transition for a freeform post, emit the same content-requested event used at creation. yggdrasil resolves it to the existing entry (deterministic id), flips it back to processing, and re-enriches against current models, so the post publishes with correct tags/contentQuality. The existing op=u re-request is gated on a content delta, which a scheduled publish never has, so this path was uncovered. --- __tests__/workers/cdc/primary.ts | 55 ++++++++++++++++++++++++++++++++ src/workers/cdc/primary.ts | 11 +++++++ 2 files changed, 66 insertions(+) diff --git a/__tests__/workers/cdc/primary.ts b/__tests__/workers/cdc/primary.ts index bad9e2dc52..60eb75475d 100644 --- a/__tests__/workers/cdc/primary.ts +++ b/__tests__/workers/cdc/primary.ts @@ -1781,6 +1781,61 @@ describe('post', () => { expect(notifyContentRequested).toHaveBeenCalledTimes(0); }); + it('should re-request enrichment when a scheduled freeform post becomes visible', async () => { + const before = { + ...base, + type: PostType.Freeform, + content: '1'.repeat(FREEFORM_POST_MINIMUM_CONTENT_LENGTH), + visible: false, + }; + const after = { + ...before, + visible: true, + }; + + await expectSuccessfulBackground( + worker, + mockChangeMessage({ + after, + before, + op: 'u', + table: 'post', + }), + ); + + expect(notifyFreeformContentRequested).toHaveBeenCalledTimes(1); + expect( + jest.mocked(notifyFreeformContentRequested).mock.calls[0][1].payload + .after, + ).toEqual(after); + }); + + it('should not re-request enrichment when a freeform post that becomes visible is too short', async () => { + const before = { + ...base, + type: PostType.Freeform, + title: '', + content: '', + visible: false, + }; + const after = { + ...before, + visible: true, + }; + + await expectSuccessfulBackground( + worker, + mockChangeMessage({ + after, + before, + op: 'u', + table: 'post', + }), + ); + + expect(notifyFreeformContentRequested).toHaveBeenCalledTimes(0); + }); + it('should notify when yggdrasil id is available on creation', async () => { const after = { ...base, diff --git a/src/workers/cdc/primary.ts b/src/workers/cdc/primary.ts index f562db75b2..3ef97e068b 100644 --- a/src/workers/cdc/primary.ts +++ b/src/workers/cdc/primary.ts @@ -1198,6 +1198,17 @@ const onPostChange = async ( data.payload.after!, data.payload.before!, ); + if (data.payload.after!.type === PostType.Freeform) { + // A scheduled freeform post just went live. Its tags/contentQuality were + // captured once when it was scheduled and are never refreshed at publish, + // so a post enriched during a broken window (e.g. empty tags) goes live + // broken. Re-request enrichment — same content-requested emit as creation — + // so yggdrasil re-enriches against current models before it's shown. + const freeform = data as ChangeMessage; + if (isFreeformPostLongEnough(freeform)) { + await notifyFreeformContentRequested(logger, freeform); + } + } if ( data.payload.after!.type === PostType.Freeform && data.payload.after!.authorId