Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions .infra/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ export const workers: Worker[] = [
subscription: 'api.new-notification-push',
},
{
topic: 'api.v1.post-highlighted',
subscription: 'api.new-highlight-real-time',
topic: 'api.v1.highlight-created',
subscription: 'api.new-highlight-real-time-v2',
},
{
topic: 'api.v1.source-privacy-updated',
Expand Down Expand Up @@ -312,8 +312,12 @@ export const workers: Worker[] = [
subscription: 'api.post-added-user-notification',
},
{
topic: 'api.v1.post-highlighted',
subscription: 'api.major-highlight-tweet',
topic: 'api.v1.post-visible',
subscription: 'api.agentic-digest-tweet',
},
{
topic: 'api.v1.highlight-created',
subscription: 'api.major-highlight-tweet-v2',
},
{
topic: 'api.v1.source-post-moderation-submitted',
Expand Down Expand Up @@ -452,8 +456,8 @@ export const workers: Worker[] = [
subscription: 'api.recruiter-new-candidate-notification',
},
{
topic: 'api.v1.post-highlighted',
subscription: 'api.major-headline-added-notification',
topic: 'api.v1.highlight-created',
subscription: 'api.major-headline-added-notification-v2',
},
{
topic: 'gondul.v1.candidate-application-scored',
Expand Down Expand Up @@ -523,8 +527,8 @@ export const workers: Worker[] = [
subscription: 'api.generate-channel-digest',
},
{
topic: 'api.v1.generate-channel-highlight',
subscription: 'api.generate-channel-highlight',
topic: 'api.v1.generate-highlights',
subscription: 'api.generate-highlights-v2',
},
{
topic: 'api.v1.user-deletion-requested',
Expand Down
39 changes: 2 additions & 37 deletions __tests__/cron/channelHighlights.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { DataSource } from 'typeorm';
import createOrGetConnection from '../../src/db';
import { ChannelHighlightDefinition } from '../../src/entity/ChannelHighlightDefinition';
import * as typedPubsub from '../../src/common/typedPubsub';
import channelHighlights from '../../src/cron/channelHighlights';
import { crons } from '../../src/cron/index';
Expand All @@ -14,7 +13,6 @@ beforeAll(async () => {
describe('channelHighlights cron', () => {
afterEach(async () => {
jest.restoreAllMocks();
await con.getRepository(ChannelHighlightDefinition).clear();
});

it('should be registered', () => {
Expand All @@ -25,50 +23,20 @@ describe('channelHighlights cron', () => {
expect(registeredCron).toBeDefined();
});

it('should enqueue active highlight definitions', async () => {
it('should enqueue a global highlight generation run', async () => {
const triggerTypedEventSpy = jest
.spyOn(typedPubsub, 'triggerTypedEvent')
.mockResolvedValue();

await con.getRepository(ChannelHighlightDefinition).save([
{
channel: 'backend',
mode: 'shadow',
candidateHorizonHours: 72,
maxItems: 10,
},
{
channel: 'vibes',
mode: 'shadow',
candidateHorizonHours: 72,
maxItems: 10,
},
{
channel: 'disabled',
mode: 'disabled',
candidateHorizonHours: 72,
maxItems: 10,
},
]);

const startedAt = Date.now();
await channelHighlights.handler(con, {} as never, {} as never);
const completedAt = Date.now();

expect(triggerTypedEventSpy.mock.calls).toEqual([
[
{},
'api.v1.generate-channel-highlight',
'api.v1.generate-highlights',
{
channel: 'backend',
scheduledAt: expect.any(String),
},
],
[
{},
'api.v1.generate-channel-highlight',
{
channel: 'vibes',
scheduledAt: expect.any(String),
},
],
Expand All @@ -79,8 +47,5 @@ describe('channelHighlights cron', () => {
);
expect(scheduledAt).toBeGreaterThanOrEqual(startedAt);
expect(scheduledAt).toBeLessThanOrEqual(completedAt);
expect(triggerTypedEventSpy.mock.calls[0][2].scheduledAt).toBe(
triggerTypedEventSpy.mock.calls[1][2].scheduledAt,
);
});
});
36 changes: 32 additions & 4 deletions __tests__/cron/cleanChannelHighlights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { expectSuccessfulCron } from '../helpers';
import { crons } from '../../src/cron/index';
import { ChannelHighlightRun } from '../../src/entity/ChannelHighlightRun';
import { PostHighlight } from '../../src/entity/PostHighlight';
import { PostHighlightChannel } from '../../src/entity/PostHighlightChannel';
import { ArticlePost, Source } from '../../src/entity';
import { PostType } from '../../src/entity/posts/Post';
import { createSource } from '../fixture/source';
Expand Down Expand Up @@ -93,7 +94,8 @@ describe('cleanChannelHighlights cron', () => {

afterEach(async () => {
await con.getRepository(ChannelHighlightRun).clear();
await con.getRepository(PostHighlight).clear();
await con.getRepository(PostHighlightChannel).clear();
await con.createQueryBuilder().delete().from(PostHighlight).execute();
await con
.createQueryBuilder()
.delete()
Expand All @@ -114,7 +116,7 @@ describe('cleanChannelHighlights cron', () => {
});

it('should delete retired highlights older than 30 days and expired runs', async () => {
await con.getRepository(PostHighlight).save([
const highlights = await con.getRepository(PostHighlight).save([
{
channel: 'vibes',
postId: 'active-highlight',
Expand All @@ -137,6 +139,26 @@ describe('cleanChannelHighlights cron', () => {
retiredAt: sub(new Date(), { days: 1 }),
},
]);
await con.getRepository(PostHighlightChannel).save([
{
highlightId: highlights[0].id,
channel: 'vibes',
placedAt: highlights[0].highlightedAt,
retiredAt: null,
},
{
highlightId: highlights[1].id,
channel: 'vibes',
placedAt: highlights[1].highlightedAt,
retiredAt: new Date('2026-01-02T10:00:00.000Z'),
},
{
highlightId: highlights[2].id,
channel: 'vibes',
placedAt: highlights[2].highlightedAt,
retiredAt: sub(new Date(), { days: 1 }),
},
]);
await con.getRepository(ChannelHighlightRun).save([
{
channel: 'vibes',
Expand Down Expand Up @@ -164,21 +186,27 @@ describe('cleanChannelHighlights cron', () => {

await expectSuccessfulCron(cleanChannelHighlights);

const highlights = await con.getRepository(PostHighlight).find({
const remainingHighlights = await con.getRepository(PostHighlight).find({
order: { postId: 'ASC' },
});
const remainingPlacements = await con
.getRepository(PostHighlightChannel)
.find({
order: { highlightId: 'ASC' },
});
const runs = await con.getRepository(ChannelHighlightRun).find({
order: { scheduledAt: 'ASC' },
});

expect(highlights).toEqual([
expect(remainingHighlights).toEqual([
expect.objectContaining({
postId: 'active-highlight',
}),
expect.objectContaining({
postId: 'recently-retired-highlight',
}),
]);
expect(remainingPlacements).toHaveLength(2);
expect(runs).toHaveLength(1);
expect(runs[0].completedAt).not.toBeNull();
});
Expand Down
49 changes: 37 additions & 12 deletions __tests__/highlights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
PostHighlight,
PostHighlightSignificance,
} from '../src/entity/PostHighlight';
import { PostHighlightChannel } from '../src/entity/PostHighlightChannel';
import { PostType } from '../src/entity/posts/Post';
import { sourcesFixture } from './fixture/source';

Expand Down Expand Up @@ -87,11 +88,42 @@ const createTestPosts = async () => {
]);
};

const saveHighlights = async (
items: Array<
Partial<PostHighlight> & {
postId: string;
channel: string;
highlightedAt: Date;
headline: string;
channels?: string[];
}
>,
) => {
const highlights = await con.getRepository(PostHighlight).save(items);

await con.getRepository(PostHighlightChannel).save(
highlights.flatMap((highlight, index) =>
(items[index].channels || [items[index].channel]).map((channel) => ({
highlightId: highlight.id,
channel,
placedAt: highlight.highlightedAt,
retiredAt:
items[index].retiredAt instanceof Date
? items[index].retiredAt
: null,
})),
),
);

return highlights;
};

beforeEach(async () => {
jest.resetAllMocks();
await con.getRepository(ChannelDigest).clear();
await con.getRepository(ChannelHighlightDefinition).clear();
await con.getRepository(PostHighlight).clear();
await con.getRepository(PostHighlightChannel).clear();
await con.createQueryBuilder().delete().from(PostHighlight).execute();
await con.getRepository(ArticlePost).delete(['h1', 'h2', 'h3', 'h4']);
await con
.getRepository(Source)
Expand Down Expand Up @@ -233,7 +265,7 @@ describe('query postHighlights', () => {

it('should return highlights ordered by highlightedAt desc', async () => {
await createTestPosts();
await con.getRepository(PostHighlight).save([
await saveHighlights([
{
postId: 'h2',
channel: 'happening-now',
Expand Down Expand Up @@ -277,7 +309,7 @@ describe('query postHighlights', () => {

it('should filter by channel', async () => {
await createTestPosts();
await con.getRepository(PostHighlight).save([
await saveHighlights([
{
postId: 'h1',
channel: 'happening-now',
Expand Down Expand Up @@ -307,7 +339,7 @@ describe('query postHighlights', () => {

it('should hide retired highlights', async () => {
await createTestPosts();
await con.getRepository(PostHighlight).save([
await saveHighlights([
{
postId: 'h1',
channel: 'happening-now',
Expand Down Expand Up @@ -339,16 +371,9 @@ describe('query postHighlights', () => {
});

describe('query majorHeadlines', () => {
it('should return only breaking and major headlines deduplicated by postId', async () => {
it('should return only live breaking and major canonical highlights', async () => {
await createTestPosts();
await con.getRepository(PostHighlight).save([
{
postId: 'h1',
channel: 'agentic',
highlightedAt: new Date('2026-03-19T10:30:00.000Z'),
headline: 'Major agentic headline',
significance: PostHighlightSignificance.Major,
},
{
postId: 'h1',
channel: 'vibes',
Expand Down
25 changes: 18 additions & 7 deletions __tests__/sitemaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { keywordsFixture } from './fixture/keywords';
import { ONE_DAY_IN_SECONDS } from '../src/common/constants';
import { ChannelHighlightDefinition } from '../src/entity/ChannelHighlightDefinition';
import { PostHighlight } from '../src/entity/PostHighlight';
import { PostHighlightChannel } from '../src/entity/PostHighlightChannel';
let app: FastifyInstance;
let con: DataSource;
const previousSitemapLimit = process.env.SITEMAP_LIMIT;
Expand Down Expand Up @@ -530,7 +531,7 @@ describe('GET /sitemaps/highlights.xml', () => {
order: 0,
},
]);
await con.getRepository(PostHighlight).save([
const highlights = await con.getRepository(PostHighlight).save([
{
postId: 'p1',
channel: 'career',
Expand All @@ -543,17 +544,27 @@ describe('GET /sitemaps/highlights.xml', () => {
highlightedAt: new Date('2026-04-12T09:00:00.000Z'),
headline: 'Career latest',
},
]);
await con.getRepository(PostHighlightChannel).save([
{
postId: 'p1',
highlightId: highlights[0].id,
channel: 'career',
placedAt: new Date('2026-04-10T10:00:00.000Z'),
},
{
highlightId: highlights[0].id,
channel: 'backend',
highlightedAt: new Date('2026-04-09T08:00:00.000Z'),
headline: 'Backend live',
placedAt: new Date('2026-04-09T08:00:00.000Z'),
},
{
postId: 'p4',
highlightId: highlights[1].id,
channel: 'career',
placedAt: new Date('2026-04-12T09:00:00.000Z'),
},
{
highlightId: highlights[1].id,
channel: 'backend',
highlightedAt: new Date('2026-04-13T08:00:00.000Z'),
headline: 'Backend retired',
placedAt: new Date('2026-04-13T08:00:00.000Z'),
retiredAt: new Date('2026-04-13T08:30:00.000Z'),
},
]);
Expand Down
Loading
Loading