Add durable candidate funnel

This commit is contained in:
jjbrunton committed 2026-08-09 15:08:06 +01:00
1 parent 5abac31c28
commit df35648920
20 files changed
+1128 -142

No files matched your search

+14 -6
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MonitorService } from '../src/services/monitor.js';
import { MonitorService, parseSelfDeclaredAge } from '../src/services/monitor.js';
import {
RedditRateLimitError,
RedditSubredditUnavailableError,
@@ -24,6 +24,7 @@ function createMockPostsRepo(processedIds: Set<string> = new Set()): PostsReposi
isProcessed: vi.fn((id: string) => processedIds.has(id)),
isProcessedAnywhere: vi.fn((id: string) => processedIds.has(id)),
insert: vi.fn(),
insertIfAbsent: vi.fn(() => true),
updateAnalysis: vi.fn(),
markMessageSent: vi.fn(),
setError: vi.fn(),
@@ -168,7 +169,7 @@ describe('MonitorService', () => {
expect(redditApi.isSellerProfile).toHaveBeenCalledWith(
'seller', config.reddit.sellerKeywords, config.reddit.sellerDomains, sellerPost,
);
expect(postsRepo.insert).toHaveBeenCalledWith(expect.objectContaining({ id: 't3_seller' }));
expect(postsRepo.insertIfAbsent).toHaveBeenCalledWith(expect.objectContaining({ id: 't3_seller' }));
expect(postsRepo.setError).toHaveBeenCalledWith('t3_seller', 'Seller signals detected');
});
@@ -192,7 +193,7 @@ describe('MonitorService', () => {
await expect(monitor.getUnprocessedPosts()).resolves.toEqual([selected]);
expect(logger.logs).toContainEqual(expect.objectContaining({
level: 'info',
message: 'r/sub1 listing: 4 fetched; 3 screened; 0 already processed; 1 already messaged; 1 seller signals; 0 missing gender tag; 0 gender not targeted; 1 eligible (selected); 0 outside recent window; 1 not evaluated after selected recent candidate',
message: 'r/sub1 listing: 4 fetched; 3 screened; 0 already processed; 1 already messaged; 1 seller signals; 0 missing gender tag; 0 gender not targeted; 0 missing age tag; 0 underage; 1 eligible (selected); 0 outside recent window; 1 not evaluated after selected recent candidate',
}));
});
@@ -224,7 +225,7 @@ describe('MonitorService', () => {
);
});
it('finishes the seller and gender checks for one candidate before moving to the next', async () => {
it('runs free gender and age checks before seller profile requests', async () => {
const newer: RedditPost = {
id: 't3_newer', subreddit: 'chat-sub', author: 'newer', title: '[30M] hello',
url: '', permalink: '/r/chat-sub/newer', thumbnail: '', is_self: true,
@@ -249,11 +250,18 @@ describe('MonitorService', () => {
await expect(monitor.getUnprocessedPosts()).resolves.toEqual([eligible]);
expect(vi.mocked(redditApi.isSellerProfile).mock.calls.map(([username]) => username))
.toEqual(['newer', 'eligible']);
expect(postsRepo.insert).toHaveBeenCalledWith(expect.objectContaining({ id: 't3_newer' }));
.toEqual(['eligible']);
expect(postsRepo.insertIfAbsent).toHaveBeenCalledWith(expect.objectContaining({ id: 't3_newer' }));
expect(postsRepo.setError).toHaveBeenCalledWith('t3_newer', 'Gender tag "male" not targeted');
});
it('parses adult subreddit age tags without treating unrelated numbers as ages', () => {
expect(parseSelfDeclaredAge('28 [F4M] Online')).toBe(28);
expect(parseSelfDeclaredAge('[25F] hello')).toBe(25);
expect(parseSelfDeclaredAge('F 31 looking to chat')).toBe(31);
expect(parseSelfDeclaredAge('F4M, 100 percent bored')).toBeNull();
});
it('tries next subreddit when first has no valid posts', async () => {
const post1: RedditPost = {
id: 't3_1', subreddit: 'sub2', author: 'user1', title: 'Post',