import { describe, expect, it } from 'vitest'; import type { RedditPost } from '../src/reddit/api.js'; import { DEFAULT_GROWTH_THREAD_POLICY, screenGrowthThread, } from '../src/services/growth-thread-screening.js'; const NOW = 1_700_000_000_000; function post(overrides: Partial = {}): RedditPost { return { id: 'abc', name: 't3_abc', subreddit: 'cats', author: 'someone', title: 'My cat keeps knocking things off shelves', url: 'https://www.reddit.com/r/cats/comments/abc/', permalink: '/r/cats/comments/abc/', thumbnail: '', is_self: true, // 90 minutes old, comfortably inside the default window. created_utc: (NOW - 90 * 60_000) / 1_000, over_18: false, spoiler: false, num_comments: 20, ...overrides, } as RedditPost; } function reason(overrides: Partial, policy = DEFAULT_GROWTH_THREAD_POLICY): string | null { const screening = screenGrowthThread(post(overrides), policy, NOW); return screening.eligible ? null : screening.reason; } describe('screenGrowthThread', () => { it('accepts a thread with traction inside the window', () => { expect(screenGrowthThread(post(), DEFAULT_GROWTH_THREAD_POLICY, NOW)).toEqual({ eligible: true }); }); describe('threads that cannot accept a comment', () => { it.each([ [{ locked: true }, /locked/], [{ archived: true }, /archived/], [{ stickied: true }, /stickied/], [{ pinned: true }, /stickied/], [{ removed_by_category: 'moderator' }, /removed/], [{ distinguished: 'moderator' }, /moderator or admin/], [{ distinguished: 'admin' }, /moderator or admin/], [{ author: '[deleted]' }, /author is deleted/], [{ title: ' ' }, /no title/], ])('rejects %o', (overrides, expected) => { expect(reason(overrides)).toMatch(expected); }); it('allows an ordinary distinguished value through', () => { expect(reason({ distinguished: null })).toBeNull(); }); }); describe('age window', () => { it('rejects a thread with no traction yet', () => { expect(reason({ created_utc: (NOW - 5 * 60_000) / 1_000 })).toMatch(/younger than 15m/); }); it('rejects a thread where a comment would be buried', () => { expect(reason({ created_utc: (NOW - 10 * 3_600_000) / 1_000 })).toMatch(/older than 6h/); }); it('rejects a thread dated in the future', () => { expect(reason({ created_utc: (NOW + 60_000) / 1_000 })).toMatch(/future/); }); it('accepts the window boundaries', () => { expect(reason({ created_utc: (NOW - 15 * 60_000) / 1_000 })).toBeNull(); expect(reason({ created_utc: (NOW - 6 * 3_600_000) / 1_000 })).toBeNull(); }); }); describe('comment count', () => { it('rejects a dead thread', () => { expect(reason({ num_comments: 2 })).toMatch(/fewer than 3 comments/); expect(reason({ num_comments: undefined })).toMatch(/fewer than 3 comments/); }); it('rejects a thread that is already saturated', () => { expect(reason({ num_comments: 400 })).toMatch(/more than 150 comments/); }); it('accepts the count boundaries', () => { expect(reason({ num_comments: 3 })).toBeNull(); expect(reason({ num_comments: 150 })).toBeNull(); }); }); describe('NSFW', () => { it('rejects NSFW threads by default', () => { expect(reason({ over_18: true })).toMatch(/marked NSFW/); }); it('allows them when the account opts in', () => { expect(reason({ over_18: true }, { ...DEFAULT_GROWTH_THREAD_POLICY, allowNsfw: true })) .toBeNull(); }); }); it('honours a custom policy', () => { const strict = { minAgeMs: 60 * 60_000, maxAgeMs: 2 * 60 * 60_000, minComments: 30, maxComments: 50, allowNsfw: false, }; expect(reason({ num_comments: 20 }, strict)).toMatch(/fewer than 30/); expect(reason({ num_comments: 40 }, strict)).toBeNull(); }); });