683 lines
23 KiB
TypeScript
683 lines
23 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import type { Database } from 'better-sqlite3';
|
|
import { createTestDb, createTestRepositories } from '../tests/testUtils.js';
|
|
import {
|
|
PostsRepository,
|
|
MessagesRepository,
|
|
SuccessfulMessagesRepository,
|
|
ConversationStateRepository,
|
|
} from '../src/database/repositories.js';
|
|
import { buildConversationState } from '../src/services/conversation-state.js';
|
|
|
|
describe('PostsRepository', () => {
|
|
let postsRepo: PostsRepository;
|
|
let messagesRepo: MessagesRepository;
|
|
let db: Database;
|
|
|
|
beforeEach(() => {
|
|
const repos = createTestRepositories();
|
|
postsRepo = repos.postsRepo;
|
|
messagesRepo = repos.messagesRepo;
|
|
db = repos.db;
|
|
});
|
|
|
|
it('inserts and retrieves a post by id', () => {
|
|
postsRepo.insert({
|
|
id: 'post1',
|
|
subreddit: 'test',
|
|
author: 'user1',
|
|
title: 'Test Post',
|
|
postUrl: 'https://www.reddit.com/r/test/comments/post1/test_post/',
|
|
imageUrl: 'http://example.com/img.jpg',
|
|
processedAt: Date.now(),
|
|
});
|
|
|
|
const post = postsRepo.getById('post1');
|
|
expect(post).toBeDefined();
|
|
expect(post!.id).toBe('post1');
|
|
expect(post!.subreddit).toBe('test');
|
|
expect(post!.author).toBe('user1');
|
|
expect(post!.title).toBe('Test Post');
|
|
expect(post!.postUrl).toBe('https://www.reddit.com/r/test/comments/post1/test_post/');
|
|
expect(post!.imageUrl).toBe('http://example.com/img.jpg');
|
|
expect(post!.messageSent).toBe(false);
|
|
expect(post!.imageAnalysis).toBeNull();
|
|
expect(post!.error).toBeNull();
|
|
});
|
|
|
|
it('checks if a post is processed', () => {
|
|
expect(postsRepo.isProcessed('nonexistent')).toBe(false);
|
|
|
|
postsRepo.insert({
|
|
id: 'post2',
|
|
subreddit: 'test',
|
|
author: 'user2',
|
|
title: 'Another Post',
|
|
imageUrl: null,
|
|
processedAt: Date.now(),
|
|
});
|
|
|
|
expect(postsRepo.isProcessed('post2')).toBe(true);
|
|
});
|
|
|
|
it('marks a message as sent', () => {
|
|
postsRepo.insert({
|
|
id: 'post3',
|
|
subreddit: 'test',
|
|
author: 'user3',
|
|
title: 'Sent Post',
|
|
imageUrl: 'http://example.com/img.jpg',
|
|
processedAt: Date.now(),
|
|
});
|
|
|
|
postsRepo.markMessageSent('post3');
|
|
const post = postsRepo.getById('post3');
|
|
expect(post!.messageSent).toBe(true);
|
|
});
|
|
|
|
it('updates analysis and generated message', () => {
|
|
postsRepo.insert({
|
|
id: 'post4',
|
|
subreddit: 'test',
|
|
author: 'user4',
|
|
title: 'Analysis Post',
|
|
imageUrl: 'http://example.com/img.jpg',
|
|
processedAt: Date.now(),
|
|
});
|
|
|
|
postsRepo.updateAnalysis('post4', 'Beautiful woman with blonde hair', 'Hey gorgeous...');
|
|
const post = postsRepo.getById('post4');
|
|
expect(post!.imageAnalysis).toBe('Beautiful woman with blonde hair');
|
|
expect(post!.generatedMessage).toBe('Hey gorgeous...');
|
|
});
|
|
|
|
it('sets error on a post', () => {
|
|
postsRepo.insert({
|
|
id: 'post5',
|
|
subreddit: 'test',
|
|
author: 'user5',
|
|
title: 'Error Post',
|
|
imageUrl: 'http://example.com/img.jpg',
|
|
processedAt: Date.now(),
|
|
});
|
|
|
|
postsRepo.setError('post5', 'Failed to download image');
|
|
const post = postsRepo.getById('post5');
|
|
expect(post!.error).toBe('Failed to download image');
|
|
});
|
|
|
|
it('gets posts by author (case insensitive)', () => {
|
|
postsRepo.insert({ id: 'p1', subreddit: 'a', author: 'TestUser', title: 'T1', imageUrl: null, processedAt: Date.now() });
|
|
postsRepo.insert({ id: 'p2', subreddit: 'b', author: 'testuser', title: 'T2', imageUrl: null, processedAt: Date.now() });
|
|
postsRepo.insert({ id: 'p3', subreddit: 'c', author: 'TESTUSER', title: 'T3', imageUrl: null, processedAt: Date.now() });
|
|
|
|
const results = postsRepo.getByAuthor('testuser');
|
|
expect(results.length).toBe(3);
|
|
// Should be ordered by processed_at DESC
|
|
expect(results[0].id).toBe('p3');
|
|
expect(results[2].id).toBe('p1');
|
|
});
|
|
|
|
it('limits getByAuthor to 10 results', () => {
|
|
for (let i = 0; i < 15; i++) {
|
|
postsRepo.insert({
|
|
id: `pa${i}`,
|
|
subreddit: 'test',
|
|
author: 'bulkuser',
|
|
title: `Bulk ${i}`,
|
|
imageUrl: null,
|
|
processedAt: Date.now() + i,
|
|
});
|
|
}
|
|
const results = postsRepo.getByAuthor('bulkuser');
|
|
expect(results.length).toBe(10);
|
|
});
|
|
|
|
it('returns undefined for non-existent post', () => {
|
|
expect(postsRepo.getById('nonexistent')).toBeUndefined();
|
|
});
|
|
|
|
it('maps database columns to camelCase correctly', () => {
|
|
postsRepo.insert({
|
|
id: 'post6',
|
|
subreddit: 'camelcase',
|
|
author: 'user6',
|
|
title: 'Camel Case Test',
|
|
imageUrl: 'http://example.com/test.png',
|
|
processedAt: 1234567890,
|
|
});
|
|
|
|
const post = postsRepo.getById('post6');
|
|
expect(post).toMatchObject({
|
|
id: 'post6',
|
|
subreddit: 'camelcase',
|
|
author: 'user6',
|
|
title: 'Camel Case Test',
|
|
imageUrl: 'http://example.com/test.png',
|
|
processedAt: 1234567890,
|
|
messageSent: false,
|
|
imageAnalysis: null,
|
|
generatedMessage: null,
|
|
error: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('MessagesRepository', () => {
|
|
let postsRepo: PostsRepository;
|
|
let messagesRepo: MessagesRepository;
|
|
let db: Database;
|
|
|
|
beforeEach(() => {
|
|
const repos = createTestRepositories();
|
|
postsRepo = repos.postsRepo;
|
|
messagesRepo = repos.messagesRepo;
|
|
db = repos.db;
|
|
});
|
|
|
|
it('returns only sent timestamps within a rolling window', () => {
|
|
postsRepo.insert({ id: 'window-post', subreddit: 'test', author: 'window-user', title: 'window', imageUrl: null, processedAt: 1 });
|
|
messagesRepo.insert({ postId: 'window-post', recipient: 'window-user', subject: 's', message: 'm', sentAt: 100 });
|
|
messagesRepo.insert({ postId: 'window-post', recipient: 'window-user', subject: 's', message: 'm', sentAt: 200 });
|
|
|
|
expect(messagesRepo.getSentTimestampsSince(150)).toEqual([200]);
|
|
});
|
|
|
|
it('insertChat stores a chat-originated send and getByChatRoomId returns it', () => {
|
|
const id = messagesRepo.insertChat({
|
|
recipient: 'alice',
|
|
subject: 'alice',
|
|
message: 'hey there',
|
|
sentAt: 1700000000000,
|
|
chatRoomId: '!room123:reddit.com',
|
|
});
|
|
expect(id).toBeGreaterThan(0);
|
|
|
|
const row = messagesRepo.getByChatRoomId('!room123:reddit.com');
|
|
expect(row).toBeDefined();
|
|
expect(row!.postId).toBeNull();
|
|
expect(row!.chatRoomId).toBe('!room123:reddit.com');
|
|
expect(row!.recipient).toBe('alice');
|
|
expect(row!.gotResponse).toBe(false);
|
|
expect(row!.gotImage).toBe(false);
|
|
|
|
messagesRepo.markGotImage(id);
|
|
const updated = messagesRepo.getByChatRoomId('!room123:reddit.com');
|
|
expect(updated!.gotResponse).toBe(true);
|
|
expect(updated!.gotImage).toBe(true);
|
|
});
|
|
|
|
it('keeps unassociated chat history out of performance trends', () => {
|
|
messagesRepo.insertChat({
|
|
recipient: 'bob',
|
|
subject: 'bob',
|
|
message: 'hi',
|
|
sentAt: Date.now(),
|
|
chatRoomId: '!r1:reddit.com',
|
|
});
|
|
const recent = messagesRepo.getRecent(10);
|
|
expect(recent.some((m) => m.chatRoomId === '!r1:reddit.com')).toBe(true);
|
|
expect(messagesRepo.getResponseRateTrend()).toEqual([]);
|
|
});
|
|
|
|
it('inserts and retrieves a message', () => {
|
|
const messageId = messagesRepo.insert({
|
|
postId: 'post1',
|
|
recipient: 'recipient1',
|
|
subject: 'Hello',
|
|
message: 'Hey there!',
|
|
sentAt: Date.now(),
|
|
strategyId: 'variant-a',
|
|
});
|
|
|
|
expect(messageId).toBeGreaterThan(0);
|
|
|
|
const msg = messagesRepo.getByPostId('post1');
|
|
expect(msg).toBeDefined();
|
|
expect(msg!.postId).toBe('post1');
|
|
expect(msg!.recipient).toBe('recipient1');
|
|
expect(msg!.subject).toBe('Hello');
|
|
expect(msg!.message).toBe('Hey there!');
|
|
expect(msg!.strategyId).toBe('variant-a');
|
|
expect(msg!.gotResponse).toBe(false);
|
|
});
|
|
|
|
it('retrieves recent messages', () => {
|
|
const baseTime = Date.now();
|
|
for (let i = 0; i < 5; i++) {
|
|
messagesRepo.insert({
|
|
postId: `post${i}`,
|
|
recipient: `recipient${i}`,
|
|
subject: `Subject ${i}`,
|
|
message: `Message ${i}`,
|
|
// Later i => more recent sentAt, so msg4 should come back first.
|
|
sentAt: baseTime + i * 1000,
|
|
});
|
|
}
|
|
|
|
const recent = messagesRepo.getRecent(3);
|
|
expect(recent.length).toBe(3);
|
|
// Should be ordered by sent_at DESC
|
|
expect(recent[0].recipient).toBe('recipient4');
|
|
});
|
|
|
|
it('marks a message as having gotten a response', () => {
|
|
messagesRepo.insert({
|
|
postId: 'post1',
|
|
recipient: 'recipient1',
|
|
subject: 'Hello',
|
|
message: 'Hey!',
|
|
sentAt: Date.now(),
|
|
});
|
|
|
|
const msg = messagesRepo.getByPostId('post1');
|
|
expect(msg!.gotResponse).toBe(false);
|
|
|
|
messagesRepo.markGotResponse(msg!.id);
|
|
const updated = messagesRepo.getByPostId('post1');
|
|
expect(updated!.gotResponse).toBe(true);
|
|
expect(updated!.responseCheckedAt).toBeDefined();
|
|
});
|
|
|
|
it('marks a message as checked without response', () => {
|
|
messagesRepo.insert({
|
|
postId: 'post1',
|
|
recipient: 'recipient1',
|
|
subject: 'Hello',
|
|
message: 'Hey!',
|
|
sentAt: Date.now(),
|
|
});
|
|
|
|
const msg = messagesRepo.getByPostId('post1');
|
|
messagesRepo.markChecked(msg!.id);
|
|
|
|
const updated = messagesRepo.getByPostId('post1');
|
|
expect(updated!.gotResponse).toBe(false);
|
|
expect(updated!.responseCheckedAt).toBeDefined();
|
|
});
|
|
|
|
it('gets the latest sent message by recipient with joined post info', () => {
|
|
postsRepo.insert({
|
|
id: 'post-a',
|
|
subreddit: 'gonewild',
|
|
author: 'alice',
|
|
title: 'First post',
|
|
imageUrl: null,
|
|
processedAt: Date.now(),
|
|
});
|
|
postsRepo.insert({
|
|
id: 'post-b',
|
|
subreddit: 'gonewild',
|
|
author: 'alice',
|
|
title: 'Second post',
|
|
postUrl: 'https://www.reddit.com/r/gonewild/comments/post-b/second_post/',
|
|
imageUrl: null,
|
|
processedAt: Date.now(),
|
|
});
|
|
|
|
const base = Date.now();
|
|
messagesRepo.insert({ postId: 'post-a', recipient: 'alice', subject: 's', message: 'm', sentAt: base });
|
|
messagesRepo.insert({ postId: 'post-b', recipient: 'alice', subject: 's', message: 'm', sentAt: base + 1000, chatRoomId: 'room-b' });
|
|
|
|
const latest = messagesRepo.getLatestByRecipient('alice');
|
|
expect(latest).toBeDefined();
|
|
expect(latest!.postId).toBe('post-b');
|
|
expect(latest!.subreddit).toBe('gonewild');
|
|
expect(latest!.postTitle).toBe('Second post');
|
|
expect(latest!.postUrl).toBe('https://www.reddit.com/r/gonewild/comments/post-b/second_post/');
|
|
expect(messagesRepo.getByChatRoomId('room-b')).toMatchObject({ postId: 'post-b', postTitle: 'Second post' });
|
|
|
|
expect(messagesRepo.getLatestByRecipient('ALICE')).toBeDefined();
|
|
expect(messagesRepo.getLatestByRecipient('nobody')).toBeUndefined();
|
|
});
|
|
|
|
it('checks if a message has been sent to a recipient', () => {
|
|
expect(messagesRepo.hasSentTo('someone')).toBe(false);
|
|
|
|
messagesRepo.insert({
|
|
postId: 'post1',
|
|
recipient: 'recipient1',
|
|
subject: 'Hello',
|
|
message: 'Hey!',
|
|
sentAt: Date.now(),
|
|
});
|
|
|
|
expect(messagesRepo.hasSentTo('recipient1')).toBe(true);
|
|
expect(messagesRepo.hasSentTo('someone')).toBe(false);
|
|
});
|
|
|
|
it('gets strategy stats from database', () => {
|
|
const now = Date.now();
|
|
|
|
messagesRepo.insert({ postId: 'p1', recipient: 'r1', subject: 's', message: 'm', sentAt: now, strategyId: 'variant-a' });
|
|
messagesRepo.insert({ postId: 'p2', recipient: 'r2', subject: 's', message: 'm', sentAt: now, strategyId: 'variant-a' });
|
|
messagesRepo.insert({ postId: 'p3', recipient: 'r3', subject: 's', message: 'm', sentAt: now, strategyId: 'variant-b' });
|
|
|
|
messagesRepo.markGotResponse(1);
|
|
messagesRepo.markGotResponse(3);
|
|
messagesRepo.markGotImage(1);
|
|
|
|
const stats = messagesRepo.getStrategyStats();
|
|
expect(stats.length).toBe(2);
|
|
|
|
const variantA = stats.find(s => s.strategyId === 'variant-a');
|
|
expect(variantA!.totalSent).toBe(2);
|
|
expect(variantA!.totalResponses).toBe(1);
|
|
expect(variantA!.responseRate).toBe(50);
|
|
expect(variantA!.totalImages).toBe(1);
|
|
expect(variantA!.imageRate).toBe(50);
|
|
|
|
const variantB = stats.find(s => s.strategyId === 'variant-b');
|
|
expect(variantB!.totalSent).toBe(1);
|
|
expect(variantB!.totalResponses).toBe(1);
|
|
expect(variantB!.responseRate).toBe(100);
|
|
expect(variantB!.totalImages).toBe(0);
|
|
expect(variantB!.imageRate).toBe(0);
|
|
});
|
|
|
|
it('excludes messages without strategy_id from stats', () => {
|
|
messagesRepo.insert({ postId: 'p1', recipient: 'r1', subject: 's', message: 'm', sentAt: Date.now() });
|
|
|
|
const stats = messagesRepo.getStrategyStats();
|
|
expect(stats.length).toBe(0);
|
|
});
|
|
|
|
it('calculates 0 response rate when no messages sent', () => {
|
|
// No messages inserted
|
|
const stats = messagesRepo.getStrategyStats();
|
|
expect(stats.length).toBe(0);
|
|
});
|
|
|
|
it('gets unchecked messages with correct time filters', () => {
|
|
const now = Date.now();
|
|
|
|
// Old message (2 hours ago, no check) - should be returned
|
|
messagesRepo.insert({ postId: 'p1', recipient: 'r1', subject: 's', message: 'm', sentAt: now - 2 * 3600000 });
|
|
|
|
// Recent message (30 min ago) - should NOT be returned (too young)
|
|
messagesRepo.insert({ postId: 'p2', recipient: 'r2', subject: 's', message: 'm', sentAt: now - 30 * 60000 });
|
|
|
|
// Already checked message - should NOT be returned
|
|
messagesRepo.insert({ postId: 'p3', recipient: 'r3', subject: 's', message: 'm', sentAt: now - 2 * 3600000 });
|
|
messagesRepo.markChecked(3);
|
|
|
|
const unchecked = messagesRepo.getUncheckedMessages(60 * 60 * 1000, 6 * 60 * 60 * 1000, 10);
|
|
expect(unchecked.length).toBe(1);
|
|
expect(unchecked[0].postId).toBe('p1');
|
|
});
|
|
|
|
it('maps sent_at as number and response_checked_at as number | null', () => {
|
|
messagesRepo.insert({
|
|
postId: 'post1',
|
|
recipient: 'r1',
|
|
subject: 's',
|
|
message: 'm',
|
|
sentAt: 1234567890,
|
|
});
|
|
|
|
const msg = messagesRepo.getByPostId('post1');
|
|
expect(typeof msg!.sentAt).toBe('number');
|
|
expect(msg!.responseCheckedAt).toBeNull();
|
|
});
|
|
|
|
it('returns optional fields as undefined when not present', () => {
|
|
messagesRepo.insert({
|
|
postId: 'post1',
|
|
recipient: 'r1',
|
|
subject: 's',
|
|
message: 'm',
|
|
sentAt: Date.now(),
|
|
});
|
|
|
|
const msg = messagesRepo.getByPostId('post1');
|
|
expect(msg!.subreddit).toBeUndefined();
|
|
expect(msg!.postTitle).toBeUndefined();
|
|
expect(msg!.postImageUrl).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('SuccessfulMessagesRepository', () => {
|
|
let successfulMessagesRepo: SuccessfulMessagesRepository;
|
|
let postsRepo: PostsRepository;
|
|
let messagesRepo: MessagesRepository;
|
|
let db: Database;
|
|
|
|
beforeEach(() => {
|
|
const repos = createTestRepositories();
|
|
successfulMessagesRepo = repos.successfulMessagesRepo;
|
|
postsRepo = repos.postsRepo;
|
|
messagesRepo = repos.messagesRepo;
|
|
db = repos.db;
|
|
});
|
|
|
|
it('inserts and retrieves a successful message', () => {
|
|
successfulMessagesRepo.insert({
|
|
sentMessageId: 1,
|
|
originalMessage: 'Hey gorgeous!',
|
|
imageAnalysis: 'Blonde woman, rating 8/10',
|
|
postTitle: 'My selfie',
|
|
responseSnippet: 'Aww thanks!',
|
|
subreddit: 'gonewild',
|
|
});
|
|
|
|
const recent = successfulMessagesRepo.getRecent(5);
|
|
expect(recent.length).toBe(1);
|
|
expect(recent[0].originalMessage).toBe('Hey gorgeous!');
|
|
expect(recent[0].imageAnalysis).toBe('Blonde woman, rating 8/10');
|
|
expect(recent[0].postTitle).toBe('My selfie');
|
|
expect(recent[0].responseSnippet).toBe('Aww thanks!');
|
|
expect(recent[0].subreddit).toBe('gonewild');
|
|
});
|
|
|
|
it('checks if a message already exists', () => {
|
|
successfulMessagesRepo.insert({
|
|
sentMessageId: 1,
|
|
originalMessage: 'Unique message',
|
|
imageAnalysis: null,
|
|
postTitle: null,
|
|
responseSnippet: null,
|
|
subreddit: null,
|
|
});
|
|
|
|
expect(successfulMessagesRepo.existsByMessage('Unique message')).toBe(true);
|
|
expect(successfulMessagesRepo.existsByMessage('Non-existent')).toBe(false);
|
|
});
|
|
|
|
it('gets weighted examples for a subreddit', () => {
|
|
successfulMessagesRepo.insert({ sentMessageId: 1, originalMessage: 'Msg 1', imageAnalysis: null, postTitle: null, responseSnippet: null, subreddit: 'gonewild' });
|
|
successfulMessagesRepo.insert({ sentMessageId: 2, originalMessage: 'Msg 2', imageAnalysis: null, postTitle: null, responseSnippet: null, subreddit: 'gonewild' });
|
|
successfulMessagesRepo.insert({ sentMessageId: 3, originalMessage: 'Msg 3', imageAnalysis: null, postTitle: null, responseSnippet: null, subreddit: 'homemadesfW' });
|
|
|
|
const examples = successfulMessagesRepo.getWeightedExamples('gonewild', 3);
|
|
// Returns up to `limit` examples weighted toward the target subreddit but
|
|
// including off-subreddit fallback rows when there's headroom.
|
|
expect(examples.length).toBe(3);
|
|
expect(examples[0].subreddit).toBe('gonewild');
|
|
expect(examples[1].subreddit).toBe('gonewild');
|
|
});
|
|
|
|
it('falls back to recent when no subreddit match', () => {
|
|
successfulMessagesRepo.insert({ sentMessageId: 1, originalMessage: 'Msg 1', imageAnalysis: null, postTitle: null, responseSnippet: null, subreddit: 'gonewild' });
|
|
successfulMessagesRepo.insert({ sentMessageId: 2, originalMessage: 'Msg 2', imageAnalysis: null, postTitle: null, responseSnippet: null, subreddit: 'other' });
|
|
|
|
const examples = successfulMessagesRepo.getWeightedExamples('nonexistent', 3);
|
|
expect(examples.length).toBe(2);
|
|
});
|
|
|
|
it('counts successful messages', () => {
|
|
expect(successfulMessagesRepo.count()).toBe(0);
|
|
|
|
successfulMessagesRepo.insert({ sentMessageId: 1, originalMessage: 'Msg 1', imageAnalysis: null, postTitle: null, responseSnippet: null, subreddit: null });
|
|
successfulMessagesRepo.insert({ sentMessageId: 2, originalMessage: 'Msg 2', imageAnalysis: null, postTitle: null, responseSnippet: null, subreddit: null });
|
|
|
|
expect(successfulMessagesRepo.count()).toBe(2);
|
|
});
|
|
|
|
it('handles null fields correctly', () => {
|
|
successfulMessagesRepo.insert({
|
|
sentMessageId: null,
|
|
originalMessage: 'No context',
|
|
imageAnalysis: null,
|
|
postTitle: null,
|
|
responseSnippet: null,
|
|
subreddit: null,
|
|
});
|
|
|
|
const recent = successfulMessagesRepo.getRecent(1);
|
|
expect(recent[0].sentMessageId).toBeNull();
|
|
expect(recent[0].imageAnalysis).toBeNull();
|
|
expect(recent[0].postTitle).toBeNull();
|
|
expect(recent[0].responseSnippet).toBeNull();
|
|
expect(recent[0].subreddit).toBeNull();
|
|
});
|
|
|
|
it('returns only examples attributable to a post, strategy, or subreddit', () => {
|
|
postsRepo.insert({
|
|
id: 'source-post',
|
|
subreddit: 'example',
|
|
author: 'alice',
|
|
title: 'Source post',
|
|
imageUrl: null,
|
|
processedAt: Date.now(),
|
|
});
|
|
const sentMessageId = messagesRepo.insert({
|
|
postId: 'source-post',
|
|
recipient: 'alice',
|
|
subject: 'alice',
|
|
message: 'Attributed opener',
|
|
sentAt: Date.now(),
|
|
strategyId: 'direct',
|
|
});
|
|
successfulMessagesRepo.insert({
|
|
sentMessageId,
|
|
originalMessage: 'Attributed opener',
|
|
imageAnalysis: null,
|
|
postTitle: 'Source post',
|
|
responseSnippet: 'Hello',
|
|
subreddit: null,
|
|
});
|
|
successfulMessagesRepo.insert({
|
|
sentMessageId: null,
|
|
originalMessage: 'Legacy unknown opener',
|
|
imageAnalysis: null,
|
|
postTitle: 'Chat with t2_unknown',
|
|
responseSnippet: 'Hello',
|
|
subreddit: null,
|
|
});
|
|
|
|
expect(successfulMessagesRepo.getAttributedRecent(10)).toEqual([
|
|
expect.objectContaining({ originalMessage: 'Attributed opener' }),
|
|
]);
|
|
expect(successfulMessagesRepo.getRecent(10)).toHaveLength(2);
|
|
});
|
|
|
|
it('returns most recent examples first', () => {
|
|
for (let i = 0; i < 5; i++) {
|
|
successfulMessagesRepo.insert({
|
|
sentMessageId: i,
|
|
originalMessage: `Msg ${i}`,
|
|
imageAnalysis: null,
|
|
postTitle: null,
|
|
responseSnippet: null,
|
|
subreddit: null,
|
|
});
|
|
}
|
|
|
|
const recent = successfulMessagesRepo.getRecent(3);
|
|
expect(recent[0].originalMessage).toBe('Msg 4');
|
|
expect(recent[2].originalMessage).toBe('Msg 2');
|
|
});
|
|
});
|
|
|
|
describe('ConversationStateRepository', () => {
|
|
it('persists state by room id and replaces it on later saves', () => {
|
|
const db = createTestDb();
|
|
const repo = new ConversationStateRepository(db);
|
|
const original = buildConversationState([{ sender: 'Me', text: '32M UK, married.', isSelf: true }]);
|
|
repo.save('!room:reddit.com', original);
|
|
|
|
expect(repo.get('!room:reddit.com')).toMatchObject({
|
|
persona: { age: 32, location: 'UK', relationshipStatus: 'married' },
|
|
});
|
|
|
|
const updated = buildConversationState([{ sender: 'Me', text: '34M UK.', isSelf: true }]);
|
|
repo.save('!room:reddit.com', updated);
|
|
expect(repo.get('!room:reddit.com')?.persona.age).toBe(34);
|
|
expect(repo.get('missing')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Repository edge cases', () => {
|
|
it('handles special characters in post titles', () => {
|
|
const repos = createTestRepositories();
|
|
repos.postsRepo.insert({
|
|
id: 'special',
|
|
subreddit: 'test',
|
|
author: "user's \"quotes\"",
|
|
title: 'Title with <html> & "special" chars',
|
|
imageUrl: null,
|
|
processedAt: Date.now(),
|
|
});
|
|
|
|
const post = repos.postsRepo.getById('special');
|
|
expect(post!.author).toBe("user's \"quotes\"");
|
|
expect(post!.title).toBe('Title with <html> & "special" chars');
|
|
});
|
|
|
|
it('handles very long messages', () => {
|
|
const repos = createTestRepositories();
|
|
const longMessage = 'x'.repeat(10000);
|
|
|
|
repos.messagesRepo.insert({
|
|
postId: 'post1',
|
|
recipient: 'r1',
|
|
subject: 's',
|
|
message: longMessage,
|
|
sentAt: Date.now(),
|
|
});
|
|
|
|
const msg = repos.messagesRepo.getByPostId('post1');
|
|
expect(msg!.message.length).toBe(10000);
|
|
});
|
|
|
|
it('handles empty string fields', () => {
|
|
const repos = createTestRepositories();
|
|
repos.postsRepo.insert({
|
|
id: 'empty',
|
|
subreddit: '',
|
|
author: '',
|
|
title: '',
|
|
imageUrl: '',
|
|
processedAt: Date.now(),
|
|
});
|
|
|
|
const post = repos.postsRepo.getById('empty');
|
|
expect(post!.subreddit).toBe('');
|
|
expect(post!.author).toBe('');
|
|
expect(post!.title).toBe('');
|
|
});
|
|
|
|
it('handles duplicate post insert (should throw due to PRIMARY KEY)', () => {
|
|
const repos = createTestRepositories();
|
|
repos.postsRepo.insert({ id: 'dup', subreddit: 't', author: 'a', title: 't', imageUrl: null, processedAt: Date.now() });
|
|
|
|
expect(() => {
|
|
repos.postsRepo.insert({ id: 'dup', subreddit: 't', author: 'a', title: 't', imageUrl: null, processedAt: Date.now() });
|
|
}).toThrow();
|
|
});
|
|
|
|
it('handles messages with strategy_id null', () => {
|
|
const repos = createTestRepositories();
|
|
const id = repos.messagesRepo.insert({
|
|
postId: 'p1',
|
|
recipient: 'r1',
|
|
subject: 's',
|
|
message: 'm',
|
|
sentAt: Date.now(),
|
|
});
|
|
|
|
const msg = repos.messagesRepo.getByPostId('p1');
|
|
expect(msg!.strategyId).toBeNull();
|
|
});
|
|
});
|