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

+6
View File
@@ -11,6 +11,7 @@ describe('agent API discovery', () => {
expect(keys).toContain('POST /api/v1/control/start');
expect(keys).toContain('POST /api/v1/control/stop');
expect(keys).toContain('POST /api/v1/jobs');
expect(keys).toContain('POST /api/v1/messages');
expect(keys).toContain('GET /api/events/stream');
});
@@ -39,6 +40,7 @@ describe('agent API discovery', () => {
expect(document.paths['/api/v1/control/start'].post['x-mutates']).toBe(true);
expect(document.paths['/api/v1/jobs'].post.requestBody).toBeDefined();
expect((document.paths['/api/v1/jobs'].post.responses as Record<string, unknown>)['202']).toBeDefined();
expect(document.paths['/api/v1/messages'].post.requestBody).toBeDefined();
});
it('returns a compact capability catalogue for agents', () => {
@@ -48,5 +50,9 @@ describe('agent API discovery', () => {
access: 'admin',
mutates: true,
});
expect(result.operations.find((item) => item.operationId === 'sendRedditUserMessage')).toMatchObject({
access: 'admin',
mutates: true,
});
});
});
+4
View File
@@ -31,6 +31,10 @@ function prepared(candidate: RedditPost): ProcessedPost {
post: candidate,
imageAnalysis: 'analysis',
detectedGender: 'female',
attractivenessScore: 64,
physiqueScore: 5,
physiqueTargeting: { enabled: true, minScore: 3, maxScore: 7 },
sourceImageUrl: 'https://i.redd.it/example.jpg',
generatedMessage: 'prepared message',
strategyId: null,
experimentId: null,
+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;
+10 -1
View File
@@ -29,10 +29,11 @@ describe('RedditBot MCP server', () => {
expect(names).toEqual(expect.arrayContaining([
'get_overview', 'list_accounts', 'list_conversations', 'start_bot', 'stop_bot',
'set_bot_running', 'trigger_sync', 'send_reply', 'start_conversation',
'set_bot_running', 'trigger_sync', 'send_reply', 'send_message_to_user', 'start_conversation',
]));
expect(tools.tools.find((tool) => tool.name === 'get_overview')?.annotations?.readOnlyHint).toBe(true);
expect(tools.tools.find((tool) => tool.name === 'send_reply')?.annotations?.destructiveHint).toBe(true);
expect(tools.tools.find((tool) => tool.name === 'send_message_to_user')?.annotations?.destructiveHint).toBe(true);
});
it('routes start and stop tools to the versioned control API', async () => {
@@ -44,4 +45,12 @@ describe('RedditBot MCP server', () => {
expect(post).toHaveBeenNthCalledWith(1, '/api/v1/control/start');
expect(post).toHaveBeenNthCalledWith(2, '/api/v1/control/stop');
});
it('routes automatic user messages to the versioned messaging API', async () => {
const { client, post } = await connectedClient();
await client.callTool({ name: 'send_message_to_user', arguments: { username: 'alice', message: 'hello' } });
expect(post).toHaveBeenCalledWith('/api/v1/messages', { username: 'alice', message: 'hello' });
});
});
+20
View File
@@ -315,6 +315,26 @@ describe('OpenRouterClient', () => {
expect(result.attractivenessScore).toBeNull();
});
it('parses and removes a grounded PHYSIQUE_SCORE', async () => {
mockCreate.mockResolvedValueOnce({
choices: [{ message: { content: 'GENDER:female\nPHYSIQUE_SCORE:7\nRATING:73\n\nDescription here.' } }],
});
const result = await client.analyzeImage('base64data', 'image/jpeg');
expect(result.physiqueScore).toBe(7);
expect(result.description).toBe('Description here.');
});
it('returns null for PHYSIQUE_SCORE:UNKNOWN', async () => {
mockCreate.mockResolvedValueOnce({
choices: [{ message: { content: 'GENDER:female\nPHYSIQUE_SCORE:UNKNOWN\n\nClose-cropped portrait.' } }],
});
const result = await client.analyzeImage('base64data', 'image/jpeg');
expect(result.physiqueScore).toBeNull();
expect(result.description).toBe('Close-cropped portrait.');
});
it('handles case-insensitive GENDER prefix', async () => {
mockCreate.mockResolvedValueOnce({
choices: [{ message: { content: 'GENDER:FEMALE\n\nA woman.' } }],
+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');
});
+3 -1
View File
@@ -12,7 +12,9 @@ describe('materializePreparedPostForAccount', () => {
thumbnail: '', is_self: false, created_utc: 123, over_18: true, spoiler: false,
};
const processed: ProcessedPost = {
post, imageAnalysis: 'analysis', detectedGender: 'female', generatedMessage: 'message',
post, imageAnalysis: 'analysis', detectedGender: 'female', attractivenessScore: 64,
physiqueScore: 5, physiqueTargeting: { enabled: false, minScore: 1, maxScore: 10 },
sourceImageUrl: 'https://example.com/image.jpg', generatedMessage: 'message',
strategyId: null, experimentId: null, promptVersion: null, selectionReason: null,
modelId: 'model', sourceType: 'image', toneId: 'default', scripted: false,
};
+75
View File
@@ -0,0 +1,75 @@
import { describe, expect, it, vi } from 'vitest';
import { RedditUserMessenger } from '../src/services/reddit-user-messenger.js';
import type { RedditAccountRuntime, RedditAccountRuntimeManager } from '../src/services/reddit-account-runtime.js';
function runtime(accountId: string, redditUsername: string, conversations: Array<Record<string, unknown>>, sends = 0) {
return {
session: { accountId, redditUsername },
resources: {
accountAuth: { isFullyAuthenticated: () => true },
chatService: {
getCachedConversations: vi.fn().mockResolvedValue(conversations),
getInvitationCooldown: () => null,
sendChatMessage: vi.fn().mockResolvedValue(true),
startConversationDetailed: vi.fn().mockResolvedValue({
status: 'sent',
chat: { username: 'new-user', roomId: 'new-room', messages: [] },
}),
refreshTopChats: vi.fn().mockResolvedValue(undefined),
},
rateLimiter: {
getStats: () => ({ messagesThisHour: sends }),
throttle: vi.fn((work: () => Promise<unknown>) => work()),
},
activityLogRepo: { insert: vi.fn() },
conversationStateRepo: { get: vi.fn(), save: vi.fn() },
},
} as unknown as RedditAccountRuntime;
}
function manager(items: RedditAccountRuntime[]) {
return {
list: () => items.map((item) => ({
accountId: item.session.accountId,
redditUsername: item.session.redditUsername,
matrixUserId: item.session.accountId,
state: 'running',
updatedAt: 1,
failure: null,
})),
get: (accountId: string) => items.find((item) => item.session.accountId === accountId) ?? null,
} as unknown as RedditAccountRuntimeManager;
}
describe('RedditUserMessenger', () => {
it('prefers the account with the most recent existing interaction', async () => {
const first = runtime('one', 'owner-one', [{
username: 'Alice', roomId: 'old-room', url: '/room/old-room', lastMessageTime: 10,
messages: [],
}]);
const second = runtime('two', 'owner-two', [{
username: 'u/alice', roomId: 'recent-room', url: '/room/recent-room', lastMessageTime: 20,
messages: [],
}]);
const result = await new RedditUserMessenger(manager([first, second])).send('u/Alice', 'hello');
expect(result).toMatchObject({ accountId: 'two', conversationId: 'recent-room', reusedConversation: true });
expect(second.resources.chatService.sendChatMessage).toHaveBeenCalledWith('/room/recent-room', 'hello');
expect(first.resources.chatService.sendChatMessage).not.toHaveBeenCalled();
expect(second.resources.conversationStateRepo.save).toHaveBeenCalledWith('recent-room', expect.objectContaining({
automation: expect.objectContaining({ status: 'user-overridden' }),
}));
});
it('starts a new conversation on the running account with the lowest send count', async () => {
const busy = runtime('busy', 'z-owner', [], 4);
const available = runtime('available', 'a-owner', [], 1);
const result = await new RedditUserMessenger(manager([busy, available])).send('new-user', 'hello');
expect(result).toMatchObject({ accountId: 'available', conversationId: 'new-room', reusedConversation: false });
expect(available.resources.chatService.startConversationDetailed).toHaveBeenCalledWith('new-user', 'hello');
expect(busy.resources.chatService.startConversationDetailed).not.toHaveBeenCalled();
});
});
+1
View File
@@ -44,6 +44,7 @@ export function getTestConfig(overrides?: Partial<Config>): Config {
prioritizeRecentPosts: false,
recentWindowMinutes: 60,
maxReadyCandidates: 25,
physiqueTargeting: { enabled: false, minScore: 1, maxScore: 10 },
...overrides?.reddit,
},
openrouter: {
+40
View File
@@ -109,3 +109,43 @@ describe('config route conversation kill controls', () => {
expect(config.conversations.aiReplyScreening.enabled).toBe(false);
});
});
describe('config route physique targeting', () => {
it('hot-updates and persists the image-candidate score range', async () => {
const { app, config, saved } = harness();
const targetingRef = config.reddit.physiqueTargeting;
const response = await request(app, '/api/config', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
reddit: {
physiqueTargeting: { enabled: true, minScore: 3, maxScore: 7 },
},
}),
});
expect(response.status).toBe(200);
expect(config.reddit.physiqueTargeting).toBe(targetingRef);
expect(config.reddit.physiqueTargeting).toEqual({ enabled: true, minScore: 3, maxScore: 7 });
expect(saved.at(-1)?.reddit.physiqueTargeting).toEqual({ enabled: true, minScore: 3, maxScore: 7 });
});
it('rejects an inverted physique range without mutating configuration', async () => {
const { app, config, saved } = harness();
const response = await request(app, '/api/config', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
reddit: {
physiqueTargeting: { enabled: true, minScore: 8, maxScore: 4 },
},
}),
});
expect(response.status).toBe(400);
expect(config.reddit.physiqueTargeting).toEqual({ enabled: false, minScore: 1, maxScore: 10 });
expect(saved).toHaveLength(0);
});
});
+25 -3
View File
@@ -20,7 +20,7 @@ afterEach(async () => {
});
describe('outreach route', () => {
it('publishes active candidates and queue outcomes without prepared message content', async () => {
it('publishes prepared candidate context without leaking worker lease internals', async () => {
const candidateQueue = {
getStats: vi.fn(() => ({
total: 9,
@@ -37,7 +37,22 @@ describe('outreach route', () => {
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,
},
prepared: { generatedMessage: 'private prepared text' }, priority: 2, status: 'sending',
prepared: {
generatedMessage: 'Prepared opener for Alice',
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/post-1.jpg',
strategyId: 'direct-observation',
experimentId: 'strategies:direct-observation:abc123',
promptVersion: 'abc123',
selectionReason: 'exploit',
modelId: 'example/image-model',
toneId: 'direct',
scripted: false,
}, priority: 2, status: 'sending',
discoveredAt: 1_000, updatedAt: 2_000, availableAt: 2_000,
leaseOwner: 'worker-secret', leaseExpiresAt: 3_000, assignedAccountId: 'account-a',
attemptCount: 2, lastError: null,
@@ -64,9 +79,16 @@ describe('outreach route', () => {
postId: 'post-1', status: 'sending', hasPreparedMessage: true,
postUrl: 'https://www.reddit.com/r/example/comments/post-1/hello/',
owner: { accountId: 'account-a', redditUsername: 'operator' },
preparation: {
message: 'Prepared opener for Alice',
attractivenessScore: 68,
physiqueScore: 6,
physiqueTargeting: { enabled: true, minScore: 4, maxScore: 7 },
sourceImageUrl: 'https://i.redd.it/post-1.jpg',
strategyId: 'direct-observation',
},
}],
});
expect(JSON.stringify(body)).not.toContain('private prepared text');
expect(JSON.stringify(body)).not.toContain('worker-secret');
});