Fix commenter-targeted outreach attribution
This commit is contained in:
1 parent
b5304e24e3
commit
f77157d303
19 files changed
+328
-49
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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 () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user