Add outreach candidate editing controls

This commit is contained in:
Jamie Brunton committed 2026-08-14 09:12:54 +01:00
1 parent c75e71f26e
commit 79c508be2b
19 files changed
+434 -12

No files matched your search

+63 -6
View File
@@ -3,7 +3,9 @@ import type { AddressInfo } from 'node:net';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
OutreachClearResponseSchema,
OutreachCandidateDeleteResponseSchema,
OutreachFunnelResponseSchema,
OutreachMessageUpdateResponseSchema,
} from '../../packages/api-contracts/src/index.js';
import type { WebContext } from '../../src/web/context.js';
import { createOutreachRouter } from '../../src/web/routes/outreach.js';
@@ -48,13 +50,14 @@ describe('outreach route', () => {
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',
strategyId: null,
experimentId: null,
promptVersion: null,
selectionReason: null,
modelId: 'operator-edited',
toneId: 'direct',
scripted: false,
operatorEdited: true,
}, priority: 2, status: 'sending',
discoveredAt: 1_000, updatedAt: 2_000, availableAt: 2_000,
leaseOwner: 'worker-secret', leaseExpiresAt: 3_000, assignedAccountId: 'account-a',
@@ -89,7 +92,8 @@ describe('outreach route', () => {
physiqueScore: 6,
physiqueTargeting: { enabled: true, minScore: 1, maxScore: 6 },
sourceImageUrl: 'https://i.redd.it/post-1.jpg',
strategyId: 'direct-observation',
strategyId: null,
operatorEdited: true,
},
}],
});
@@ -136,4 +140,57 @@ describe('outreach route', () => {
expect.objectContaining({ total: 7, retainedSending: 1 }),
);
});
it('edits a prepared message and removes an individual unsent candidate', async () => {
const updatePreparedMessage = vi.fn(() => 'updated' as const);
const cancelUnsent = vi.fn(() => 'cancelled' as const);
const activity = { push: vi.fn() };
const broadcaster = { push: vi.fn() };
const context = {
services: { candidateQueue: { updatePreparedMessage, cancelUnsent } }, activity, broadcaster,
} as unknown as WebContext;
const app = express().use(express.json()).use('/api', createOutreachRouter(context));
const editResponse = await request(app, '/api/outreach/post%2Fone/message', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: ' A revised opener ' }),
});
expect(OutreachMessageUpdateResponseSchema.parse(await editResponse.json())).toEqual({
success: true, postId: 'post/one', message: 'A revised opener',
});
expect(updatePreparedMessage).toHaveBeenCalledWith('post/one', 'A revised opener');
const deleteResponse = await request(app, '/api/outreach/post%2Fone', { method: 'DELETE' });
expect(OutreachCandidateDeleteResponseSchema.parse(await deleteResponse.json())).toEqual({
success: true, postId: 'post/one',
});
expect(cancelUnsent).toHaveBeenCalledWith('post/one');
expect(activity.push).toHaveBeenCalledTimes(2);
});
it('rejects unsafe edits and stale mutations that reached the sending boundary', async () => {
const updatePreparedMessage = vi.fn(() => 'not-editable' as const);
const cancelUnsent = vi.fn(() => 'not-cancellable' as const);
const context = {
services: { candidateQueue: { updatePreparedMessage, cancelUnsent } },
activity: { push: vi.fn() }, broadcaster: { push: vi.fn() },
} as unknown as WebContext;
const app = express().use(express.json()).use('/api', createOutreachRouter(context));
const unsafe = await request(app, '/api/outreach/p1/message', {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'We should meet tonight' }),
});
expect(unsafe.status).toBe(400);
expect(updatePreparedMessage).not.toHaveBeenCalled();
const staleEdit = await request(app, '/api/outreach/p1/message', {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'A safe revised message' }),
});
expect(staleEdit.status).toBe(409);
const staleDelete = await request(app, '/api/outreach/p1', { method: 'DELETE' });
expect(staleDelete.status).toBe(409);
});
});