Add outreach candidate editing controls
This commit is contained in:
1 parent
c75e71f26e
commit
79c508be2b
19 files changed
+434
-12
No files matched your search
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user