Add commenter targeting mode
This commit is contained in:
1 parent
0dfdda61fd
commit
e9a4c965ee
23 files changed
+374
-10
No files matched your search
@@ -434,7 +434,7 @@ openrouter:
|
||||
|
||||
const config = loadConfig();
|
||||
expect(config.reddit.subreddits).toEqual([{
|
||||
name: 'test-sub', enabled: true, type: 'image', tone: 'default',
|
||||
name: 'test-sub', enabled: true, type: 'image', targetingMode: 'post-author', tone: 'default',
|
||||
conversation: { mode: 'legacy', maxAutomatedTurns: 3, rapportTurnsBeforeMediaAsk: 3 },
|
||||
}]);
|
||||
expect(config.reddit.pollIntervalMs).toBe(5000);
|
||||
|
||||
@@ -179,6 +179,16 @@ describe('subreddits page', () => {
|
||||
});
|
||||
|
||||
describe('editing one subreddit', () => {
|
||||
it('can target qualifying commenters on an image subreddit', async () => {
|
||||
renderSubreddits();
|
||||
const dialog = await openEditor('sleepy');
|
||||
const user = userEvent.setup();
|
||||
await user.selectOptions(within(dialog).getByRole('combobox', { name: 'Recipient' }), 'comment-author');
|
||||
await user.click(within(dialog).getByRole('button', { name: 'Save subreddit' }));
|
||||
await waitFor(() => expect(saved).toHaveLength(1));
|
||||
expect(saved[0].find((item: any) => item.name === 'sleepy').targetingMode).toBe('comment-author');
|
||||
});
|
||||
|
||||
it('previews the share the chosen weight will actually produce', async () => {
|
||||
renderSubreddits();
|
||||
const dialog = await openEditor('casualconversation');
|
||||
@@ -597,4 +607,3 @@ describe('subreddits page', () => {
|
||||
expect(await findRows()).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -675,4 +675,82 @@ describe('MonitorService', () => {
|
||||
expect(result[0].id).toBe('t3_m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('comment-author targeting', () => {
|
||||
it('turns a high-confidence targeted reaction into a commenter candidate with source media intact', async () => {
|
||||
const source: RedditPost = {
|
||||
id: 'post1', name: 't3_post1', subreddit: 'scene', author: 'poster', title: 'A scene',
|
||||
url: 'https://example.com/scene.jpg', permalink: '/r/scene/comments/post1/a_scene/', thumbnail: '',
|
||||
is_self: false, created_utc: 100, over_18: true, spoiler: false,
|
||||
};
|
||||
const redditApi = createMockRedditApi([source]);
|
||||
redditApi.getBrowsingComments = vi.fn().mockResolvedValue([
|
||||
{
|
||||
fullname: 't1_match', kind: 'comment', subreddit: 'scene', author: 'reactor',
|
||||
permalink: '/r/scene/comments/post1/a_scene/match/', createdAt: 101_000, score: 2,
|
||||
text: "I'd love that to be done to me", nsfw: true, parentPostId: 'post1',
|
||||
distinguished: null, removed: false,
|
||||
},
|
||||
{
|
||||
fullname: 't1_praise', kind: 'comment', subreddit: 'scene', author: 'fan',
|
||||
permalink: '/r/scene/comments/post1/a_scene/praise/', createdAt: 102_000, score: 1,
|
||||
text: 'Great post, thanks for sharing', nsfw: true, parentPostId: 'post1',
|
||||
distinguished: null, removed: false,
|
||||
},
|
||||
]);
|
||||
const screener = {
|
||||
screenCommentReactions: vi.fn().mockResolvedValue([
|
||||
{ id: 't1_match', qualifies: true, expressedGender: 'female', confidence: 0.94, evidence: 'personal desire' },
|
||||
{ id: 't1_praise', qualifies: false, expressedGender: 'unknown', confidence: 0.98, evidence: 'generic praise' },
|
||||
]),
|
||||
};
|
||||
const config = getTestConfig({ reddit: {
|
||||
subreddits: [{ name: 'scene', type: 'image', targetingMode: 'comment-author' }],
|
||||
targetGenders: ['female'],
|
||||
} });
|
||||
monitor = new MonitorService(
|
||||
redditApi, createMockPostsRepo(), createMockMessagesRepo(), config, logger, 0, undefined, screener,
|
||||
);
|
||||
|
||||
const candidates = await monitor.fetchCandidates(config.reddit.subreddits[0]);
|
||||
|
||||
expect(candidates).toHaveLength(1);
|
||||
expect(candidates[0]).toMatchObject({
|
||||
id: 't1_match', name: 't1_match', author: 'reactor', url: source.url,
|
||||
permalink: '/r/scene/comments/post1/a_scene/match/',
|
||||
targetComment: {
|
||||
body: "I'd love that to be done to me", sourcePostId: 'post1',
|
||||
sourcePostAuthor: 'poster', expressedGender: 'female', confidence: 0.94,
|
||||
},
|
||||
});
|
||||
await monitor.fetchCandidates(config.reddit.subreddits[0]);
|
||||
expect(screener.screenCommentReactions).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fails closed on low-confidence or non-qualifying reactions', async () => {
|
||||
const source = {
|
||||
id: 'post1', name: 't3_post1', subreddit: 'scene', author: 'poster', title: 'A scene',
|
||||
url: 'https://example.com/scene.jpg', permalink: '/r/scene/comments/post1/', thumbnail: '',
|
||||
is_self: false, created_utc: 100, over_18: true, spoiler: false,
|
||||
} as RedditPost;
|
||||
const redditApi = createMockRedditApi([source]);
|
||||
redditApi.getBrowsingComments = vi.fn().mockResolvedValue([
|
||||
{ fullname: 't1_low', author: 'a', text: 'I want this as well', removed: false, createdAt: 101_000, permalink: '/low' },
|
||||
{ fullname: 't1_male', author: 'b', text: 'As a man I want this', removed: false, createdAt: 102_000, permalink: '/male' },
|
||||
]);
|
||||
const screener = { screenCommentReactions: vi.fn().mockResolvedValue([
|
||||
{ id: 't1_low', qualifies: true, expressedGender: 'female', confidence: 0.79, evidence: '' },
|
||||
{ id: 't1_male', qualifies: false, expressedGender: 'male', confidence: 0.99, evidence: '' },
|
||||
]) };
|
||||
const config = getTestConfig({ reddit: {
|
||||
subreddits: [{ name: 'scene', type: 'image', targetingMode: 'comment-author' }],
|
||||
targetGenders: ['female'],
|
||||
} });
|
||||
monitor = new MonitorService(
|
||||
redditApi, createMockPostsRepo(), createMockMessagesRepo(), config, logger, 0, undefined, screener,
|
||||
);
|
||||
|
||||
await expect(monitor.fetchCandidates(config.reddit.subreddits[0])).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -135,6 +135,24 @@ describe('OpenRouterClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('screenCommentReactions', () => {
|
||||
it('parses batched reaction verdicts and fences comments as untrusted data', async () => {
|
||||
mockCreate.mockResolvedValueOnce({
|
||||
model: 'chat-model', usage: {},
|
||||
choices: [{ message: { content: '{"results":[{"id":"t1_a","qualifies":true,"expressedGender":"female","confidence":0.92,"evidence":"wants the depicted role"}]}' } }],
|
||||
});
|
||||
|
||||
await expect(client.screenCommentReactions([
|
||||
{ id: 't1_a', text: "I'd love that to be done to me" },
|
||||
])).resolves.toEqual([
|
||||
{ id: 't1_a', qualifies: true, expressedGender: 'female', confidence: 0.92, evidence: 'wants the depicted role' },
|
||||
]);
|
||||
const request = mockCreate.mock.calls[0][0];
|
||||
expect(request.temperature).toBe(0);
|
||||
expect(request.messages[1].content).toContain('<<<UNTRUSTED COMMENT>>>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRefusal (internal)', () => {
|
||||
// We test refusal detection by checking the analyzeImage behavior
|
||||
// since isRefusal is private. We do this by mocking responses that
|
||||
@@ -638,6 +656,17 @@ 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('substitutes {rating} placeholder when score provided', async () => {
|
||||
const cfg = getTestConfig({
|
||||
openrouter: {
|
||||
|
||||
@@ -99,6 +99,27 @@ describe('ProcessorService', () => {
|
||||
expect(result.scripted).toBe(false);
|
||||
});
|
||||
|
||||
it('passes a commenter-specific context without treating the source image as theirs', async () => {
|
||||
await processor.processPost({
|
||||
...mockPost,
|
||||
id: 't1_comment',
|
||||
author: 'commenter',
|
||||
targetComment: {
|
||||
body: "I'd love that to be done to me",
|
||||
permalink: '/r/test/comments/abc/test/comment/',
|
||||
sourcePostId: 't3_test',
|
||||
sourcePostAuthor: 'testuser',
|
||||
expressedGender: 'unknown',
|
||||
confidence: 0.95,
|
||||
},
|
||||
});
|
||||
|
||||
expect(aiClient.generateMessage).toHaveBeenCalledWith(
|
||||
'Beautiful woman', 'Test Post', 'commenter', [], undefined, null, undefined, undefined, [],
|
||||
expect.stringContaining("I'd love that to be done to me"),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null when no image URL', async () => {
|
||||
redditApi = createMockRedditApi(null);
|
||||
const config = getTestConfig();
|
||||
|
||||
Reference in new issue
Block a user