fix: prevent duplicate outreach and follow-ups
This commit is contained in:
1 parent
acc610c2bf
commit
5c01d7d9aa
9 files changed
+274
-14
No files matched your search
@@ -2,7 +2,11 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import { ConversationAutomationService } from '../src/services/conversation-automation.js';
|
||||
import type { ConversationState } from '../src/services/conversation-state.js';
|
||||
|
||||
function harness(sellerKeywords: string[] = [], sellerDomains: string[] = []) {
|
||||
function harness(
|
||||
sellerKeywords: string[] = [],
|
||||
sellerDomains: string[] = [],
|
||||
openerSentAt: number | null = 0,
|
||||
) {
|
||||
let saved: ConversationState | undefined;
|
||||
const chat = {
|
||||
refreshTopChats: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -14,15 +18,24 @@ function harness(sellerKeywords: string[] = [], sellerDomains: string[] = []) {
|
||||
get: vi.fn(() => saved),
|
||||
save: vi.fn((_id: string, state: ConversationState) => { saved = structuredClone(state); }),
|
||||
};
|
||||
const openers = {
|
||||
getByChatRoomId: vi.fn(() => openerSentAt === null
|
||||
? undefined
|
||||
: { sentAt: openerSentAt }),
|
||||
};
|
||||
const claims = {
|
||||
tryClaim: vi.fn(() => true),
|
||||
markSent: vi.fn(() => true),
|
||||
};
|
||||
const rateLimiter = {
|
||||
throttle: vi.fn(async (operation: () => Promise<boolean>) => operation()),
|
||||
};
|
||||
const logger = { info: vi.fn() };
|
||||
const service = new ConversationAutomationService(
|
||||
chat as never, ai as never, states as never, rateLimiter as never, logger as never,
|
||||
chat as never, ai as never, states as never, openers as never, claims, rateLimiter as never, logger as never,
|
||||
false, sellerKeywords, sellerDomains,
|
||||
);
|
||||
return { service, chat, ai, states, getSaved: () => saved };
|
||||
return { service, chat, ai, states, openers, claims, getSaved: () => saved };
|
||||
}
|
||||
|
||||
function incoming(turn: number, text = `reply ${turn}`, media?: Array<{ originalUrl: string; localPath: string; fileName: string }>) {
|
||||
@@ -98,4 +111,41 @@ describe('conversation automation', () => {
|
||||
expect(chat.sendChatMessage).toHaveBeenCalledTimes(1);
|
||||
expect(ai.generateMediaAsk).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not reply when the room has no recorded opener', async () => {
|
||||
const { service, chat, ai, claims } = harness([], [], null);
|
||||
|
||||
expect(await service.processConversation(incoming(1))).toBeNull();
|
||||
expect(ai.generateMediaAsk).not.toHaveBeenCalled();
|
||||
expect(chat.sendChatMessage).not.toHaveBeenCalled();
|
||||
expect(claims.tryClaim).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not reply to an inbound event older than the recorded opener', async () => {
|
||||
const { service, chat, ai, claims } = harness([], [], 4);
|
||||
|
||||
expect(await service.processConversation(incoming(1))).toBeNull();
|
||||
expect(ai.generateMediaAsk).not.toHaveBeenCalled();
|
||||
expect(chat.sendChatMessage).not.toHaveBeenCalled();
|
||||
expect(claims.tryClaim).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('claims an inbound event durably before sending and marks it sent afterwards', async () => {
|
||||
const { service, chat, claims } = harness();
|
||||
|
||||
await service.processConversation(incoming(1));
|
||||
|
||||
expect(claims.tryClaim).toHaveBeenCalledWith('room-alice', 'in-1');
|
||||
expect(claims.tryClaim.mock.invocationCallOrder[0])
|
||||
.toBeLessThan(chat.sendChatMessage.mock.invocationCallOrder[0]);
|
||||
expect(claims.markSent).toHaveBeenCalledWith('room-alice', 'in-1');
|
||||
});
|
||||
|
||||
it('does not send when another worker already claimed the inbound event', async () => {
|
||||
const { service, chat, claims } = harness();
|
||||
claims.tryClaim.mockReturnValue(false);
|
||||
|
||||
expect(await service.processConversation(incoming(1))).toBeNull();
|
||||
expect(chat.sendChatMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user