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

+26 -4
View File
@@ -113,6 +113,7 @@ describe('dashboard foundation', () => {
it('shows the live outreach stages and the potential posts being burned down', async () => {
installSuccessfulApi();
const user = userEvent.setup();
server.use(
http.get('/api/login/status', () => HttpResponse.json({ ready: true, loggedIn: true, status: 'authenticated' })),
http.get('/api/chats/sync', () => HttpResponse.json({
@@ -131,6 +132,17 @@ describe('dashboard foundation', () => {
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?',
imageAnalysis: 'VISIBLE_FACTS:\nBODY: VISIBLE - full-body pose',
detectedGender: 'female', attractivenessScore: 68, physiqueScore: 6,
physiqueTargeting: { enabled: true, minScore: 4, maxScore: 7 },
sourceImageUrl: 'https://i.redd.it/candidate-1.jpg',
strategyId: 'direct-observation', experimentId: 'strategies:direct-observation:abc123',
promptVersion: 'abc123', selectionReason: 'exploit', modelId: 'example/image-model',
toneId: 'direct', scripted: false,
},
note: null, owner: null,
}],
})),
@@ -141,11 +153,21 @@ describe('dashboard foundation', () => {
expect(await screen.findByRole('heading', { name: 'Outreach funnel' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '3 active posts' })).toBeInTheDocument();
expect(screen.getByRole('list', { name: 'Active outreach candidates' })).toHaveTextContent('A potential post');
expect(screen.getByRole('link', { name: '[25F] A potential post' })).toHaveAttribute(
'href',
'https://www.reddit.com/r/example/comments/candidate-1/',
);
expect(screen.getAllByRole('link', { name: '[25F] A potential post' }))
.toEqual(expect.arrayContaining([
expect.objectContaining({ href: 'https://www.reddit.com/r/example/comments/candidate-1/' }),
]));
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('68/100');
expect(preparation).toHaveTextContent('6/10');
expect(preparation).toHaveTextContent('Target 4–7');
expect(preparation).toHaveTextContent('example/image-model');
const sourceImage = within(preparation).getByRole('img', { name: 'Source post: [25F] A potential post' });
expect(sourceImage).toHaveClass('is-blurred');
await user.click(within(preparation).getByRole('button', { name: 'Reveal source image: [25F] A potential post' }));
expect(sourceImage).not.toHaveClass('is-blurred');
});
it('hydrates media filters from the routed URL', async () => {
+46 -1
View File
@@ -13,7 +13,12 @@ import { SettingsRoute } from '../../apps/dashboard/src/features/settings/settin
const config = {
rateLimit: { minDelayBetweenMessagesMs: 300_000, maxMessagesPerHour: 8, jitterMs: 60_000 },
browser: { dryRun: true },
reddit: { prioritizeRecentPosts: false, recentWindowMinutes: 60, maxReadyCandidates: 25 },
reddit: {
prioritizeRecentPosts: false,
recentWindowMinutes: 60,
maxReadyCandidates: 25,
physiqueTargeting: { enabled: false, minScore: 1, maxScore: 10 },
},
subreddits: [{ name: 'casualconversation', type: 'chat', weight: 2, tone: 'default' }],
tones: ['default', 'dominant', 'submissive', 'playful', 'direct', 'gentle', 'kink-friendly', 'teasing'],
advanced: { openrouter: { chatModel: 'example/chat-model' } },
@@ -204,6 +209,46 @@ describe('settings and access', () => {
expect(screen.queryByText('Settings saved.')).not.toBeInTheDocument();
});
it('validates and saves the physique targeting range', async () => {
installConfigApi();
let requestBody: { reddit?: typeof config.reddit } | undefined;
server.use(http.put('/api/config', async ({ request }) => {
requestBody = await request.json() as typeof requestBody;
return HttpResponse.json({
success: true,
config: {
rateLimit: config.rateLimit,
browser: config.browser,
reddit: requestBody?.reddit ?? config.reddit,
},
subreddits: config.subreddits,
});
}));
const user = userEvent.setup();
renderSettings('/settings?section=monitoring');
await user.click(await screen.findByRole('checkbox', { name: 'Enable physique targeting' }));
const minimum = screen.getByRole('spinbutton', { name: 'Minimum physique score' });
const maximum = screen.getByRole('spinbutton', { name: 'Maximum physique score' });
await user.clear(minimum);
await user.type(minimum, '7');
await user.clear(maximum);
await user.type(maximum, '4');
await user.click(screen.getByRole('button', { name: 'Save settings' }));
expect(screen.getByText('Minimum physique score cannot exceed maximum physique score.')).toBeInTheDocument();
expect(requestBody).toBeUndefined();
await user.clear(maximum);
await user.type(maximum, '9');
await user.click(screen.getByRole('button', { name: 'Save settings' }));
await waitFor(() => expect(requestBody?.reddit?.physiqueTargeting).toEqual({
enabled: true,
minScore: 7,
maxScore: 9,
}));
});
it('validates subreddit rows and confirms disabling dry run before saving behaviour', async () => {
installConfigApi();
let updateRequests = 0;