import { describe, expect, it, vi } from 'vitest'; import { RedditUserMessenger } from '../src/services/reddit-user-messenger.js'; import type { RedditAccountRuntime, RedditAccountRuntimeManager } from '../src/services/reddit-account-runtime.js'; function runtime(accountId: string, redditUsername: string, conversations: Array>, sends = 0) { return { session: { accountId, redditUsername }, resources: { accountAuth: { isFullyAuthenticated: () => true }, chatService: { getCachedConversations: vi.fn().mockResolvedValue(conversations), getInvitationCooldown: () => null, sendChatMessage: vi.fn().mockResolvedValue(true), startConversationDetailed: vi.fn().mockResolvedValue({ status: 'sent', chat: { username: 'new-user', roomId: 'new-room', messages: [] }, }), refreshTopChats: vi.fn().mockResolvedValue(undefined), }, rateLimiter: { getStats: () => ({ messagesThisHour: sends }), throttle: vi.fn((work: () => Promise) => work()), }, activityLogRepo: { insert: vi.fn() }, conversationStateRepo: { get: vi.fn(), save: vi.fn() }, }, } as unknown as RedditAccountRuntime; } function manager(items: RedditAccountRuntime[]) { return { list: () => items.map((item) => ({ accountId: item.session.accountId, redditUsername: item.session.redditUsername, matrixUserId: item.session.accountId, state: 'running', updatedAt: 1, failure: null, })), get: (accountId: string) => items.find((item) => item.session.accountId === accountId) ?? null, } as unknown as RedditAccountRuntimeManager; } describe('RedditUserMessenger', () => { it('prefers the account with the most recent existing interaction', async () => { const first = runtime('one', 'owner-one', [{ username: 'Alice', roomId: 'old-room', url: '/room/old-room', lastMessageTime: 10, messages: [], }]); const second = runtime('two', 'owner-two', [{ username: 'u/alice', roomId: 'recent-room', url: '/room/recent-room', lastMessageTime: 20, messages: [], }]); const result = await new RedditUserMessenger(manager([first, second])).send('u/Alice', 'hello'); expect(result).toMatchObject({ accountId: 'two', conversationId: 'recent-room', reusedConversation: true }); expect(second.resources.chatService.sendChatMessage).toHaveBeenCalledWith('/room/recent-room', 'hello'); expect(first.resources.chatService.sendChatMessage).not.toHaveBeenCalled(); expect(second.resources.conversationStateRepo.save).toHaveBeenCalledWith('recent-room', expect.objectContaining({ automation: expect.objectContaining({ status: 'user-overridden' }), })); }); it('starts a new conversation on the running account with the lowest send count', async () => { const busy = runtime('busy', 'z-owner', [], 4); const available = runtime('available', 'a-owner', [], 1); const result = await new RedditUserMessenger(manager([busy, available])).send('new-user', 'hello'); expect(result).toMatchObject({ accountId: 'available', conversationId: 'new-room', reusedConversation: false }); expect(available.resources.chatService.startConversationDetailed).toHaveBeenCalledWith('new-user', 'hello'); expect(busy.resources.chatService.startConversationDetailed).not.toHaveBeenCalled(); }); });