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

+44
View File
@@ -197,6 +197,50 @@ describe('CandidateQueueRepository', () => {
expect(reasons).toEqual([{ last_error: 'Cleared manually by operator' }]);
});
it('edits only prepared unsent messages and clears generated attribution', () => {
queue.enqueue([post('editable', 'alice', 200), post('waiting', 'bob', 100)], 'image', 1, 1_000);
const editable = queue.claimForScreening('screen', 1_000, 1_001)!;
queue.markReady(editable.postId, 'screen', prepared(editable.post), 1_002);
db.prepare(`
INSERT INTO processed_posts (account_id, id, subreddit, author, title, generated_message, processed_at)
VALUES ('account-a', ?, 'test', 'alice', 'title', 'prepared message', 1)
`).run(editable.postId);
expect(queue.updatePreparedMessage(editable.postId, 'Operator-written opener', 1_003)).toBe('updated');
expect(queue.listActive().find((candidate) => candidate.postId === editable.postId)?.prepared).toMatchObject({
generatedMessage: 'Operator-written opener',
strategyId: null,
experimentId: null,
promptVersion: null,
selectionReason: null,
modelId: 'operator-edited',
operatorEdited: true,
});
expect((db.prepare(`SELECT generated_message FROM processed_posts WHERE id = ?`).get(editable.postId) as {
generated_message: string;
}).generated_message).toBe('Operator-written opener');
expect(queue.updatePreparedMessage('waiting', 'Nope', 1_004)).toBe('not-editable');
expect(queue.updatePreparedMessage('missing', 'Nope', 1_004)).toBe('not-found');
queue.claimReady('account-a', 'send', 1_000, 1_005);
expect(queue.updatePreparedMessage(editable.postId, 'Too late', 1_006)).toBe('not-editable');
});
it('removes one unsent candidate but refuses to cancel an in-flight send', () => {
queue.enqueue([post('remove', 'alice')], 'image', 1, 1_000);
expect(queue.cancelUnsent('remove', 1_001)).toBe('cancelled');
expect(queue.getStats()).toMatchObject({ byStatus: { rejected: 1 } });
expect(queue.cancelUnsent('remove', 1_002)).toBe('not-cancellable');
expect(queue.cancelUnsent('missing', 1_002)).toBe('not-found');
queue.enqueue([post('sending', 'bob')], 'image', 1, 1_003);
const candidate = queue.claimForScreening('screen', 1_000, 1_004)!;
queue.markReady(candidate.postId, 'screen', prepared(candidate.post), 1_005);
queue.claimReady('account-a', 'send', 1_000, 1_006);
expect(queue.cancelUnsent('sending', 1_007)).toBe('not-cancellable');
expect(queue.listActive()).toEqual([expect.objectContaining({ postId: 'sending', status: 'sending' })]);
});
it('retries a rate-limited prepared candidate and preserves its generated message', () => {
queue.enqueue([post('p1', 'alice')], 'image', 1, 1_000);
const screening = queue.claimForScreening('screen', 100, 1_001)!;