Files
RedditBot/tests/chat-service-progressive-backfill.test.ts
T

194 lines
7.0 KiB
TypeScript

import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import { describe, expect, it, vi } from 'vitest';
import { ChatCache } from '../src/services/chat/cache.js';
import { MatrixClient } from '../src/services/chat/matrix/client.js';
import type { MatrixMessagesResponse } from '../src/services/chat/matrix/types.js';
import { ChatService } from '../src/services/chat/service.js';
import { createMockLogger } from './mockLogger.js';
import { authenticatedAccountAuth, createTestDb, createTestRepositories } from './testUtils.js';
const credentials = {
homeserver: 'https://matrix.example.test',
accessToken: 'token',
userId: '@me:reddit.com',
capturedAt: Date.now(),
};
describe('ChatService progressive full backfill', () => {
it('records replies for performance only when the room has source context', async () => {
const logger = createMockLogger();
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'chat-performance-context-'));
const db = createTestDb();
const repos = createTestRepositories(db);
try {
repos.postsRepo.insert({
id: 'post-alice',
subreddit: 'example',
author: 'alice',
title: 'Source post',
imageUrl: null,
processedAt: 1_000,
});
repos.messagesRepo.insert({
postId: 'post-alice',
recipient: 'alice',
subject: 'alice',
message: 'Hello Alice',
sentAt: Date.now(),
strategyId: 'direct',
chatRoomId: '!alice:reddit.com',
});
const matrix = new MatrixClient(credentials, logger, { minRequestIntervalMs: 0 });
matrix.sync = vi.fn(async () => ({ next_batch: 's1', rooms: { join: {} } })) as typeof matrix.sync;
matrix.getJoinedRooms = vi.fn(async () => ['!alice:reddit.com', '!bob:reddit.com']);
matrix.getRoomMessages = vi.fn(async (roomId: string) => ({
chunk: [
{
event_id: `$reply-${roomId}`,
type: 'm.room.message',
sender: roomId === '!alice:reddit.com' ? '@t2_alice:reddit.com' : '@t2_bob:reddit.com',
origin_server_ts: 2_000,
content: { body: 'Hi there' },
},
{
event_id: `$first-${roomId}`,
type: 'm.room.message',
sender: '@me:reddit.com',
origin_server_ts: 1_000,
content: { body: roomId === '!alice:reddit.com' ? 'Hello Alice' : 'Hello Bob' },
},
],
start: 'start',
}));
const service = new ChatService(
matrix,
logger,
repos.successfulMessagesRepo,
undefined,
undefined,
{ resolveT2Ids: vi.fn(async () => ({ t2_alice: 'alice', t2_bob: 'bob' })) },
new ChatCache(logger, path.join(tmpDir, 'chat-cache.json')),
repos.messagesRepo,
undefined,
authenticatedAccountAuth,
);
await service.fetchAllMessages();
expect(repos.messagesRepo.getByChatRoomId('!alice:reddit.com')?.gotResponse).toBe(true);
expect(repos.messagesRepo.getByChatRoomId('!bob:reddit.com')).toBeUndefined();
expect(repos.messagesRepo.getResponseRateTrend()).toEqual([
expect.objectContaining({ totalSent: 1, totalResponses: 1, responseRate: 100 }),
]);
// A single reply counts toward response performance, but the
// reciprocal-engagement policy excludes it from learning examples.
expect(repos.successfulMessagesRepo.getRecent()).toEqual([]);
} finally {
db.close();
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
it('publishes room summaries and recent timeline messages before deep history finishes', async () => {
const logger = createMockLogger();
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'chat-progressive-backfill-'));
let finishHistory!: (response: MatrixMessagesResponse) => void;
const pendingHistory = new Promise<MatrixMessagesResponse>((resolve) => {
finishHistory = resolve;
});
let finishUsernameResolution!: (usernames: Record<string, string>) => void;
const pendingUsernames = new Promise<Record<string, string>>((resolve) => {
finishUsernameResolution = resolve;
});
try {
const matrix = new MatrixClient(credentials, logger, { minRequestIntervalMs: 0 });
matrix.sync = vi.fn(async () => ({
next_batch: 's1',
rooms: {
join: {
'!alice:reddit.com': {
summary: { 'm.heroes': ['@t2_alice:reddit.com'] },
timeline: {
events: [
{
event_id: '$first',
type: 'm.room.message',
sender: '@me:reddit.com',
origin_server_ts: 1_000,
content: { body: 'Hello Alice' },
},
{
event_id: '$reply',
type: 'm.room.message',
sender: '@t2_alice:reddit.com',
origin_server_ts: 2_000,
content: { body: 'Hi there' },
},
],
},
},
},
},
})) as typeof matrix.sync;
matrix.getJoinedRooms = vi.fn(async () => ['!alice:reddit.com']);
matrix.getRoomMessages = vi.fn(() => pendingHistory);
const usernameResolver = {
resolveT2Ids: vi.fn(() => pendingUsernames),
};
const cachePath = path.join(tmpDir, 'chat-cache.json');
const service = new ChatService(
matrix,
logger,
undefined,
undefined,
undefined,
usernameResolver,
new ChatCache(logger, cachePath),
undefined,
undefined,
authenticatedAccountAuth,
);
const backfill = service.fetchAllMessages();
await vi.waitFor(() => expect(usernameResolver.resolveT2Ids).toHaveBeenCalledOnce());
const visibleChats = await service.getCachedConversations();
expect(visibleChats).toHaveLength(1);
expect(visibleChats[0]).toMatchObject({
roomId: '!alice:reddit.com',
username: 't2_alice',
lastMessageSnippet: 'Hi there',
messages: [
{ text: 'Hello Alice', isSelf: true },
{ text: 'Hi there', isSelf: false },
],
});
expect(usernameResolver.resolveT2Ids).toHaveBeenCalledWith(['t2_alice']);
const persisted = JSON.parse(await fs.readFile(cachePath, 'utf-8')) as { chats: unknown[] };
expect(persisted.chats).toHaveLength(1);
finishUsernameResolution({ t2_alice: 'alice' });
await vi.waitFor(() => expect(matrix.getRoomMessages).toHaveBeenCalledOnce());
await vi.waitFor(async () => {
const enrichedChats = await service.getCachedConversations();
expect(enrichedChats[0].username).toBe('alice');
});
finishHistory({ chunk: [], start: 'start' });
await backfill;
} finally {
finishUsernameResolution({});
finishHistory({ chunk: [], start: 'start' });
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
});