import { describe, expect, it, vi } from 'vitest'; import type { ConversationOutcomesRepository, MessagesRepository, SentMessageRecord, SuccessfulMessagesRepository, } from '../src/database/repositories.js'; import { recordConversationReply } from '../src/services/conversation-outcome-recorder.js'; import { createMockLogger } from './mockLogger.js'; function message(): SentMessageRecord { return { id: 1, postId: 'post-1', recipient: 'alice', subject: 'alice', message: 'Hello', sentAt: 1, gotResponse: false, gotImage: false, responseCheckedAt: null, strategyId: 'direct', chatRoomId: 'room-1', subreddit: 'example', }; } function dependencies() { return { messages: { markGotResponse: vi.fn() } as unknown as MessagesRepository, successful: { insert: vi.fn(), existsByMessage: vi.fn(() => false), deleteBySentMessageId: vi.fn(() => false), } as unknown as SuccessfulMessagesRepository, outcomes: { record: vi.fn(), suppressRecipient: vi.fn() } as unknown as ConversationOutcomesRepository, logger: createMockLogger(), }; } describe('recordConversationReply', () => { it('records a first reply but excludes it from learning until engagement is reciprocal', () => { const deps = dependencies(); expect(recordConversationReply(message(), 'Thanks!', { messageCount: 2, reciprocal: false }, deps)).toBe('unknown'); expect(deps.messages.markGotResponse).toHaveBeenCalledWith(1); expect(deps.successful.insert).not.toHaveBeenCalled(); }); it('stores only reciprocal engaged conversations as learning examples', () => { const deps = dependencies(); expect(recordConversationReply(message(), 'Tell me more', { messageCount: 4, reciprocal: true, learningEligible: true, }, deps)).toBe('engaged'); expect(deps.successful.insert).toHaveBeenCalledWith(expect.objectContaining({ sentMessageId: 1 })); }); it('permanently suppresses a recipient after a boundary response', () => { const deps = dependencies(); expect(recordConversationReply(message(), 'Stop messaging me', { messageCount: 2 }, deps)).toBe('boundary'); expect(deps.outcomes.suppressRecipient).toHaveBeenCalledWith('alice', 'boundary'); expect(deps.successful.insert).not.toHaveBeenCalled(); expect(deps.successful.deleteBySentMessageId).toHaveBeenCalledWith(1); }); it('removes an earlier learning example when the full conversation later declines', () => { const deps = dependencies(); expect(recordConversationReply(message(), "I'm good", { messageCount: 8, reciprocal: true, learningEligible: true, mediaWasRequested: true, responses: ['Sure', 'Zero interest in sending you anything lol', "I'm good"], }, deps)).toBe('declined'); expect(deps.successful.deleteBySentMessageId).toHaveBeenCalledWith(1); expect(deps.successful.insert).not.toHaveBeenCalled(); }); });