Fix commenter-targeted outreach attribution

This commit is contained in:
jjbrunton committed 2026-08-14 12:45:16 +01:00
1 parent b5304e24e3
commit f77157d303
19 files changed
+328 -49

No files matched your search

+47
View File
@@ -98,6 +98,53 @@ describe('CandidateQueueRepository', () => {
expect(queue.countActive()).toBe(2);
});
it('requeues only unedited commenter preparations from the legacy opener contract', () => {
const legacyComment = {
...post('comment-legacy', 'alice', 300),
targetComment: {
body: 'I wish that was me', permalink: '/r/test/comment-legacy/c1',
sourcePostId: 'source', sourcePostAuthor: 'source-author',
expressedGender: 'unknown' as const, confidence: 0.93,
},
};
const currentComment = {
...post('comment-current', 'bob', 200),
targetComment: {
body: 'I want that too', permalink: '/r/test/comment-current/c2',
sourcePostId: 'source', sourcePostAuthor: 'source-author',
expressedGender: 'unknown' as const, confidence: 0.91,
},
};
const editedComment = {
...post('comment-edited', 'dana', 150),
targetComment: {
body: 'I would love that', permalink: '/r/test/comment-edited/c3',
sourcePostId: 'source', sourcePostAuthor: 'source-author',
expressedGender: 'unknown' as const, confidence: 0.9,
},
};
queue.enqueue([legacyComment, currentComment, editedComment, post('post-author', 'carol', 100)], 'image', 1, 1_000);
for (let index = 0; index < 4; index += 1) {
const worker = `worker-${index}`;
const candidate = queue.claimForScreening(worker, 100, 1_010)!;
const contract = candidate.postId === 'comment-current'
? 'comment-author-v2'
: candidate.postId === 'post-author' ? 'post-author-v1' : undefined;
queue.markReady(candidate.postId, worker, {
...prepared(candidate.post), openerContract: contract,
operatorEdited: candidate.postId === 'comment-edited',
}, 1_011);
}
expect(queue.requeueLegacyCommentPreparations(1_020)).toBe(1);
expect(queue.listActive()).toEqual(expect.arrayContaining([
expect.objectContaining({ postId: 'comment-legacy', status: 'discovered', prepared: null }),
expect.objectContaining({ postId: 'comment-current', status: 'ready' }),
expect.objectContaining({ postId: 'comment-edited', status: 'ready' }),
expect.objectContaining({ postId: 'post-author', status: 'ready' }),
]));
});
it('admits, screens, and sends the newest posts before older higher-priority posts', () => {
queue.enqueue([
post('old-high-priority', 'alice', 100),
+27
View File
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';
import { validateCommentTargetedOpener } from '../src/services/comment-targeted-opener.js';
describe('validateCommentTargetedOpener', () => {
it('accepts an opener grounded in the expressed experience', () => {
expect(validateCommentTargetedOpener(
'I saw your comment about wishing that was you being pinned down. What part of that idea gets to you most?',
)).toEqual({ safe: true });
});
it.each([
'Your tits look incredible in that shot, I saw your comment.',
'You look so good kneeling there, your comment caught my eye.',
'I saw your comment. Send me another photo like that.',
'Your post caught my eye and I liked what you said.',
'I saw your comment. What made you decide to look up at the camera like that?',
])('rejects source-image attribution: %s', (message) => {
expect(validateCommentTargetedOpener(message).safe).toBe(false);
});
it('rejects an image-only opener even when it avoids explicit anatomy', () => {
expect(validateCommentTargetedOpener('That scene is incredibly hot.')).toEqual({
safe: false,
reason: 'does not ground the opener in what the commenter expressed',
});
});
});
+11 -2
View File
@@ -133,12 +133,16 @@ describe('dashboard foundation', () => {
candidates: [{
postId: 'candidate-1', subreddit: 'example', author: 'alice', title: '[25F] A potential post',
postUrl: 'https://www.reddit.com/r/example/comments/candidate-1/', sourceType: 'image',
targetComment: {
body: 'I wish that was me being pinned down',
permalink: 'https://www.reddit.com/r/example/comments/candidate-1/comment-1/',
},
status: 'ready', priority: 2, postCreatedAt: Date.parse('2026-08-09T06:50:00Z'),
discoveredAt: Date.parse('2026-08-09T07:00:00Z'), updatedAt: Date.parse('2026-08-09T07:30:00Z'),
availableAt: Date.parse('2026-08-09T07:30:00Z'), attemptCount: 1, hasPreparedMessage: true,
physiqueScore: 6,
preparation: {
message: 'You look incredible in that pose — what inspired this post?',
message: 'I saw your comment about wishing that was you being pinned down. What part gets to you most?',
imageAnalysis: 'VISIBLE_FACTS:\nBODY: VISIBLE - full-body pose',
detectedGender: 'female', attractivenessScore: 68, physiqueScore: 6,
physiqueTargeting: { enabled: true, minScore: 1, maxScore: 6 },
@@ -172,7 +176,12 @@ describe('dashboard foundation', () => {
]));
expect(within(screen.getByRole('list', { name: 'Active outreach candidates' })).getByText('Message prepared')).toBeInTheDocument();
const preparation = screen.getByRole('region', { name: 'Prepared outreach for u/alice' });
expect(preparation).toHaveTextContent('You look incredible in that pose');
expect(preparation).toHaveTextContent('I saw your comment about wishing that was you');
expect(preparation).toHaveTextContent('Comment that qualified this recipient');
expect(preparation).toHaveTextContent('I wish that was me being pinned down');
expect(within(preparation).getByRole('link', { name: 'View comment on Reddit ↗' }))
.toHaveAttribute('href', 'https://www.reddit.com/r/example/comments/candidate-1/comment-1/');
expect(screen.getByText('Commenter')).toBeInTheDocument();
expect(preparation).toHaveTextContent('68/100');
expect(preparation).toHaveTextContent('6/10');
expect(preparation).toHaveTextContent('Current target 1–6');
+43 -9
View File
@@ -656,15 +656,49 @@ describe('OpenRouterClient', () => {
expect(prompt).toContain('different opening words');
});
it('states that a comment-target recipient is not the person depicted', async () => {
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: 'That comment caught my attention.' } }] });
await client.generateMessage(
'Analysis', 'Title', 'Commenter', [], undefined, null, null, undefined, [],
'<<<UNTRUSTED PUBLIC COMMENT>>>\nI want that too\n<<<END COMMENT>>>',
);
const prompt = mockCreate.mock.calls[0][0].messages[0].content as string;
expect(prompt).toContain('The recipient commented on somebody else\'s post');
expect(prompt).toContain('Do not imply that the image depicts the recipient');
it('uses a separate commenter contract instead of the image-author prompt', async () => {
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: 'I saw your comment about wanting that done to you.' } }] });
await client.generateCommentTargetedMessage({
recipient: 'Commenter',
comment: 'I want that too',
sourcePostTitle: 'Source title',
sourceImageAnalysis: 'The source subject is kneeling',
toneGuidance: 'Be direct about her body.',
});
const systemPrompt = mockCreate.mock.calls[0][0].messages[0].content as string;
const userPrompt = mockCreate.mock.calls[0][0].messages[1].content as string;
expect(systemPrompt).toContain('The RECIPIENT is the commenter');
expect(systemPrompt).toContain('The SOURCE POST SUBJECT is a different person');
expect(userPrompt).toContain('I want that too');
expect(`${systemPrompt}\n${userPrompt}`).not.toContain('Write a message for Commenter about');
});
it('regenerates a draft that treats the commenter as the person pictured', async () => {
mockCreate
.mockResolvedValueOnce({ choices: [{ message: { content: 'Your body looks incredible, send me another photo.' } }] })
.mockResolvedValueOnce({ choices: [{ message: { content: 'I saw your comment about wanting to be pinned down. That idea suits you.' } }] });
await expect(client.generateCommentTargetedMessage({
recipient: 'Commenter',
comment: "I wish that was me being pinned down",
sourcePostTitle: 'Source title',
sourceImageAnalysis: 'The source subject is kneeling',
})).resolves.toContain('saw your comment');
expect(mockCreate).toHaveBeenCalledTimes(2);
expect(mockCreate.mock.calls[1][0].messages[1].content).toContain('REWRITE REQUIRED');
});
it('fails closed when regenerated drafts still misattribute the source image', async () => {
mockCreate.mockResolvedValue({ choices: [{ message: { content: 'You look amazing in that photo and your body is perfect.' } }] });
await expect(client.generateCommentTargetedMessage({
recipient: 'Commenter',
comment: 'I want that too',
sourcePostTitle: 'Source title',
sourceImageAnalysis: 'The source subject is kneeling',
})).rejects.toThrow('Comment-targeted opener rejected');
expect(mockCreate).toHaveBeenCalledTimes(2);
});
it('substitutes {rating} placeholder when score provided', async () => {
+11 -4
View File
@@ -36,6 +36,7 @@ function createMockAiClient(
detectGenderOnly: vi.fn(async () => gender),
analyzeImage: vi.fn(async () => analysis),
generateMessage: vi.fn(async () => message),
generateCommentTargetedMessage: vi.fn(async () => message),
generateReply: vi.fn(async () => 'Reply'),
detectUpsell: vi.fn(() => ({ isUpsell: false })),
analyzeAndGenerateMessage: vi.fn(async (base64: string, mimeType: string, title: string, author: string, examples?: string[], strategyPrompt?: string, postFlair?: string | null, toneGuidance?: string) => {
@@ -114,10 +115,16 @@ describe('ProcessorService', () => {
},
});
expect(aiClient.generateMessage).toHaveBeenCalledWith(
'Beautiful woman', 'Test Post', 'commenter', [], undefined, null, undefined, undefined, [],
expect.stringContaining("I'd love that to be done to me"),
);
expect(aiClient.generateCommentTargetedMessage).toHaveBeenCalledWith({
recipient: 'commenter',
comment: "I'd love that to be done to me",
sourcePostTitle: 'Test Post',
sourceImageAnalysis: 'Beautiful woman',
strategyGuidance: undefined,
toneGuidance: undefined,
recentMessages: [],
});
expect(aiClient.generateMessage).not.toHaveBeenCalled();
});
it('returns null when no image URL', async () => {
+10
View File
@@ -41,6 +41,12 @@ describe('outreach route', () => {
id: 'post-1', subreddit: 'example', author: 'alice', author_fullname: 't2_alice',
title: '[25F] Hello', url: 'https://i.redd.it/post-1.jpg', permalink: '/r/example/comments/post-1/hello/',
thumbnail: '', is_self: false, created_utc: 100, over_18: true, spoiler: false,
targetComment: {
body: 'I wish that was me',
permalink: '/r/example/comments/post-1/hello/comment-1/',
sourcePostId: 'post-1', sourcePostAuthor: 'source-author',
expressedGender: 'unknown', confidence: 0.94,
},
},
prepared: {
generatedMessage: 'Prepared opener for Alice',
@@ -85,6 +91,10 @@ describe('outreach route', () => {
candidates: [{
postId: 'post-1', status: 'sending', hasPreparedMessage: true,
postUrl: 'https://www.reddit.com/r/example/comments/post-1/hello/',
targetComment: {
body: 'I wish that was me',
permalink: 'https://www.reddit.com/r/example/comments/post-1/hello/comment-1/',
},
owner: { accountId: 'account-a', redditUsername: 'operator' },
preparation: {
message: 'Prepared opener for Alice',