feat: expand outreach targeting and messaging
This commit is contained in:
1 parent
7defff422d
commit
d0a8bd0e2f
40 files changed
+890
-82
No files matched your search
+75
-19
@@ -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');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user