feat: expand outreach targeting and messaging

This commit is contained in:
jjbrunton committed 2026-08-10 17:31:05 +01:00
1 parent 7defff422d
commit d0a8bd0e2f
40 files changed
+890 -82

No files matched your search

+75 -19
View File
@@ -24,13 +24,18 @@ function createMockRedditApi(imageUrl: string | null = 'http://example.com/img.j
function createMockAiClient(
gender: DetectedGender = 'female',
analysis: ImageAnalysisResult = { description: 'Beautiful woman', detectedGender: 'female', attractivenessScore: null },
analysis: ImageAnalysisResult = {
description: 'Beautiful woman',
detectedGender: 'female',
attractivenessScore: null,
physiqueScore: null,
},
message: string = 'Hey gorgeous!'
): OpenRouterClient {
return {
detectGenderOnly: vi.fn(async () => gender),
analyzeImage: vi.fn(async () => analysis),
generateMessage: vi.fn(async (analysis: string) => message),
generateMessage: 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) => {
@@ -123,9 +128,60 @@ describe('ProcessorService', () => {
expect(isSkip(result)).toBe(false);
});
it('screens out a scored image outside the enabled physique range before generating a message', async () => {
aiClient = createMockAiClient('female', {
description: 'Visible full-body pose',
detectedGender: 'female',
attractivenessScore: 72,
physiqueScore: 8,
});
const config = getTestConfig({
reddit: { physiqueTargeting: { enabled: true, minScore: 3, maxScore: 6 } },
});
processor = new ProcessorService(redditApi, aiClient, config, logger, fakeFetchImage);
const result = await processor.processPost(mockPost);
expect(result).toEqual({ skipReason: 'physique score 8 outside target range 3-6' });
expect(aiClient.generateMessage).not.toHaveBeenCalled();
});
it('screens out an unscorable image when physique targeting is enabled', async () => {
const config = getTestConfig({
reddit: { physiqueTargeting: { enabled: true, minScore: 3, maxScore: 6 } },
});
processor = new ProcessorService(redditApi, aiClient, config, logger, fakeFetchImage);
const result = await processor.processPost(mockPost);
expect(result).toEqual({
skipReason: 'physique score unavailable while physique targeting is enabled',
});
expect(aiClient.generateMessage).not.toHaveBeenCalled();
});
it('retains the physique score when it falls inside the enabled range', async () => {
aiClient = createMockAiClient('female', {
description: 'Visible full-body pose',
detectedGender: 'female',
attractivenessScore: 66,
physiqueScore: 5,
});
const config = getTestConfig({
reddit: { physiqueTargeting: { enabled: true, minScore: 3, maxScore: 6 } },
});
processor = new ProcessorService(redditApi, aiClient, config, logger, fakeFetchImage);
const result = await processor.processPost(mockPost);
if (isSkip(result)) throw new Error(`expected success, got skip: ${result.skipReason}`);
expect(result.physiqueScore).toBe(5);
expect(aiClient.generateMessage).toHaveBeenCalledOnce();
});
it('skips when AI refuses', async () => {
const error = new Error('AI_REFUSAL: All models refused to analyze image');
vi.mocked(aiClient.analyzeAndGenerateMessage).mockRejectedValue(error);
vi.mocked(aiClient.analyzeImage).mockRejectedValue(error);
const result = await processor.processPost(mockPost);
expect(isSkip(result)).toBe(true);
@@ -134,14 +190,14 @@ describe('ProcessorService', () => {
it('re-throws non-AI_REFUSAL errors', async () => {
const error = new Error('Network error');
vi.mocked(aiClient.analyzeAndGenerateMessage).mockRejectedValue(error);
vi.mocked(aiClient.analyzeImage).mockRejectedValue(error);
await expect(processor.processPost(mockPost)).rejects.toThrow('Network error');
});
it('skips when generated message is too short', async () => {
aiClient = createMockAiClient('female',
{ description: 'Nice', detectedGender: 'female' },
{ description: 'Nice', detectedGender: 'female', attractivenessScore: null, physiqueScore: null },
'Short'
);
const config = getTestConfig();
@@ -154,7 +210,7 @@ describe('ProcessorService', () => {
it('skips when generated message is empty', async () => {
aiClient = createMockAiClient('female',
{ description: 'Nice', detectedGender: 'female' },
{ description: 'Nice', detectedGender: 'female', attractivenessScore: null, physiqueScore: null },
''
);
const config = getTestConfig();
@@ -188,13 +244,13 @@ describe('ProcessorService', () => {
await processor.processPost(mockPost);
expect(aiClient.analyzeAndGenerateMessage).toHaveBeenCalledWith(
expect.any(String), // base64
'image/jpeg',
expect(aiClient.generateMessage).toHaveBeenCalledWith(
'Beautiful woman',
'Test Post',
'testuser',
['Example 1: Hey', 'Example 2: Nice'],
undefined,
null,
undefined,
undefined,
[],
@@ -211,13 +267,13 @@ describe('ProcessorService', () => {
await processor.processPost(mockPost);
expect(aiClient.analyzeAndGenerateMessage).toHaveBeenCalledWith(
expect.any(String),
'image/jpeg',
expect(aiClient.generateMessage).toHaveBeenCalledWith(
'Beautiful woman',
'Test Post',
'testuser',
[],
'Be teasing',
null,
undefined,
undefined,
[],
@@ -238,13 +294,13 @@ describe('ProcessorService', () => {
await processor.processPost(mockPost);
expect(aiClient.analyzeAndGenerateMessage).toHaveBeenCalledWith(
expect.any(String),
'image/jpeg',
expect(aiClient.generateMessage).toHaveBeenCalledWith(
'Beautiful woman',
'Test Post',
'testuser',
[],
undefined,
null,
undefined,
undefined,
recentMessages,
@@ -281,13 +337,13 @@ describe('ProcessorService', () => {
await processor.processPost(mockPost);
expect(aiClient.analyzeAndGenerateMessage).toHaveBeenCalledWith(
expect.any(String),
'image/jpeg',
expect(aiClient.generateMessage).toHaveBeenCalledWith(
'Beautiful woman',
'Test Post',
'testuser',
['Example A'],
undefined,
null,
undefined,
undefined,
[],
@@ -428,7 +484,7 @@ describe('ProcessorService', () => {
await processor.processPost(tonedPost);
// 8th arg (index 7) after strategyPrompt and postFlair is toneGuidance
const callArgs = vi.mocked(aiClient.analyzeAndGenerateMessage).mock.calls[0];
const callArgs = vi.mocked(aiClient.generateMessage).mock.calls[0];
expect(callArgs[7]).toContain('STYLE TONE');
expect(callArgs[7]).toContain('commanding');
});