Add recent outreach rejections stream

This commit is contained in:
jjbrunton committed 2026-08-14 15:45:57 +01:00
1 parent 0c21202253
commit 94049ed9c4
9 files changed
+170 -16

No files matched your search

+15
View File
@@ -245,6 +245,21 @@ describe('CandidateQueueRepository', () => {
expect(reasons).toEqual([{ last_error: 'Cleared manually by operator' }]);
});
it('lists the latest 50 rejected candidates by their terminal update time', () => {
queue.enqueue([post('older', 'alice'), post('newer', 'bob')], 'image', 1, 1_000);
const older = queue.claimForScreening('older-worker', 1_000, 1_001)!;
queue.reject(older.postId, 'older-worker', 'No gender tag in title/body', 1_002);
const newer = queue.claimForScreening('newer-worker', 1_000, 1_003)!;
queue.reject(newer.postId, 'newer-worker', 'Seller signals detected', 1_004);
queue.enqueue([post('manual', 'carol')], 'image', 1, 1_005);
queue.cancelUnsent('manual', 1_006);
expect(queue.listRecentRejected()).toEqual([
expect.objectContaining({ postId: 'newer', lastError: 'Seller signals detected', updatedAt: 1_004 }),
expect.objectContaining({ postId: 'older', lastError: 'No gender tag in title/body', updatedAt: 1_002 }),
]);
});
it('edits only prepared unsent messages and clears generated attribution', () => {
queue.enqueue([post('editable', 'alice', 200), post('waiting', 'bob', 100)], 'image', 1, 1_000);
const editable = queue.claimForScreening('screen', 1_000, 1_001)!;
+11 -2
View File
@@ -130,6 +130,11 @@ describe('dashboard foundation', () => {
stages: { discovered: 1, screening: 0, ready: 1, sending: 0, retryable: 1 },
outcomes: { sent: 12, rejected: 7 },
rejectionReasons: [{ reason: 'Seller signals detected', count: 5 }, { reason: 'No gender tag in title/body', count: 2 }],
recentRejections: [{
postId: 'rejected-1', subreddit: 'example', author: 'bob', title: '[24F] Rejected post',
postUrl: 'https://www.reddit.com/r/example/comments/rejected-1/', sourceType: 'chat',
sourceContent: 'This is the content that was screened.', rejectedAt: Date.parse('2026-08-09T07:45:00Z'), reason: 'Seller signals detected',
}],
oldestActiveAt: Date.parse('2026-08-09T07:00:00Z'),
candidates: [{
postId: 'candidate-1', subreddit: 'example', author: 'alice', title: '[25F] A potential post',
@@ -170,9 +175,12 @@ describe('dashboard foundation', () => {
expect(await screen.findByRole('heading', { name: 'Outreach funnel' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '3 active posts' })).toBeInTheDocument();
await user.click(screen.getByRole('tab', { name: 'Rejections 7' }));
expect(screen.getByRole('tabpanel', { name: 'Rejections 7' })).toHaveTextContent('Seller signals detected');
await user.click(screen.getByRole('tab', { name: 'Rejection reasons 7' }));
expect(screen.getByRole('tabpanel', { name: 'Rejection reasons 7' })).toHaveTextContent('Seller signals detected');
expect(screen.getByRole('list', { name: 'Outreach rejection reasons' })).toHaveTextContent('5');
await user.click(screen.getByRole('tab', { name: 'Recent rejections 1' }));
expect(screen.getByRole('list', { name: 'Recent rejected outreach candidates' })).toHaveTextContent('This is the content that was screened.');
expect(screen.getByText('Rejected because')).toBeInTheDocument();
await user.click(screen.getByRole('tab', { name: 'Active queue 3' }));
expect(screen.getByRole('list', { name: 'Active outreach candidates' })).toHaveTextContent('A potential post');
expect(screen.getAllByRole('link', { name: '[25F] A potential post' }))
@@ -225,6 +233,7 @@ describe('dashboard foundation', () => {
stages: { discovered: cleared ? 0 : 2, screening: 0, ready: 0, sending: 1, retryable: 0 },
outcomes: { sent: 12, rejected: cleared ? 9 : 7 },
rejectionReasons: [{ reason: 'Cleared manually by operator', count: cleared ? 2 : 0 }].filter(({ count }) => count > 0),
recentRejections: [],
oldestActiveAt: Date.now() - 60_000,
candidates: [],
})),
+13
View File
@@ -35,6 +35,14 @@ describe('outreach route', () => {
rejectionReasons: [{ reason: 'Seller signals detected', count: 1 }],
})),
getOldestActiveAt: vi.fn(() => 1_000),
listRecentRejected: vi.fn(() => [{
postId: 'rejected-1', subreddit: 'example', author: 'bob', sourceType: 'chat',
post: {
id: 'rejected-1', subreddit: 'example', author: 'bob', title: 'A rejected post',
permalink: '/r/example/comments/rejected-1/', selftext: 'Scanned post content',
},
updatedAt: 3_000, lastError: 'Seller signals detected',
}]),
listActive: vi.fn(() => [{
postId: 'post-1', subreddit: 'example', author: 'alice', sourceType: 'image',
post: {
@@ -89,6 +97,11 @@ describe('outreach route', () => {
stages: { discovered: 2, screening: 1, ready: 1, sending: 1, retryable: 1 },
outcomes: { sent: 2, rejected: 1 },
rejectionReasons: [{ reason: 'Seller signals detected', count: 1 }],
recentRejections: [{
postId: 'rejected-1', subreddit: 'example', author: 'bob', title: 'A rejected post',
postUrl: 'https://www.reddit.com/r/example/comments/rejected-1/', sourceType: 'chat',
sourceContent: 'Scanned post content', rejectedAt: 3_000, reason: 'Seller signals detected',
}],
candidates: [{
postId: 'post-1', status: 'sending', hasPreparedMessage: true,
postUrl: 'https://www.reddit.com/r/example/comments/post-1/hello/',