156 lines
6.8 KiB
TypeScript
156 lines
6.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import Database from 'better-sqlite3';
|
|
import { RedditAccountsRepository } from '../src/database/account-repositories.js';
|
|
import { runMigrations } from '../src/database/client.js';
|
|
import {
|
|
ActivityLogRepository,
|
|
ConversationOutcomesRepository,
|
|
ConversationStateRepository,
|
|
DeliveryOutboxRepository,
|
|
MessagesRepository,
|
|
PostsRepository,
|
|
SuccessfulMessagesRepository,
|
|
} from '../src/database/repositories.js';
|
|
import { buildConversationState } from '../src/services/conversation-state.js';
|
|
|
|
const ACCOUNT_A = '@t2_account_a:reddit.com';
|
|
const ACCOUNT_B = '@t2_account_b:reddit.com';
|
|
|
|
describe('account-bound domain repositories', () => {
|
|
let db: Database.Database;
|
|
|
|
beforeEach(() => {
|
|
db = new Database(':memory:');
|
|
db.pragma('foreign_keys = ON');
|
|
runMigrations(db);
|
|
const accounts = new RedditAccountsRepository(db);
|
|
for (const [accountId, username] of [[ACCOUNT_A, 'account-a'], [ACCOUNT_B, 'account-b']] as const) {
|
|
accounts.upsertVerified({
|
|
accountId,
|
|
redditUsername: username,
|
|
matrixUserId: accountId,
|
|
cookiesJson: '[]',
|
|
matrixCredentialsJson: '{}',
|
|
now: 1,
|
|
});
|
|
}
|
|
});
|
|
|
|
afterEach(() => db.close());
|
|
|
|
function insertSamePost(posts: PostsRepository, title: string): void {
|
|
posts.insert({
|
|
id: 'same-post',
|
|
subreddit: 'same-subreddit',
|
|
author: 'same-recipient',
|
|
title,
|
|
imageUrl: null,
|
|
processedAt: 1,
|
|
});
|
|
}
|
|
|
|
it('isolates posts and messages with the same Reddit IDs and recipients', () => {
|
|
const postsA = new PostsRepository(db, ACCOUNT_A);
|
|
const postsB = new PostsRepository(db, ACCOUNT_B);
|
|
const messagesA = new MessagesRepository(db, ACCOUNT_A);
|
|
const messagesB = new MessagesRepository(db, ACCOUNT_B);
|
|
insertSamePost(postsA, 'owned by A');
|
|
insertSamePost(postsB, 'owned by B');
|
|
|
|
postsA.markMessageSent('same-post');
|
|
expect(postsA.getById('same-post')).toMatchObject({ title: 'owned by A', messageSent: true });
|
|
expect(postsB.getById('same-post')).toMatchObject({ title: 'owned by B', messageSent: false });
|
|
|
|
const messageA = messagesA.insert({
|
|
postId: 'same-post', recipient: 'same-recipient', subject: 'A', message: 'from A', sentAt: 10,
|
|
});
|
|
const messageB = messagesB.insert({
|
|
postId: 'same-post', recipient: 'same-recipient', subject: 'B', message: 'from B', sentAt: 20,
|
|
});
|
|
messagesA.insertChat({
|
|
recipient: 'only-a', subject: 'A only', message: 'private', sentAt: 30, chatRoomId: 'same-room',
|
|
});
|
|
|
|
expect(messagesA.getByPostId('same-post')).toMatchObject({ id: messageA, message: 'from A' });
|
|
expect(messagesB.getByPostId('same-post')).toMatchObject({ id: messageB, message: 'from B' });
|
|
expect(messagesA.hasSentTo('only-a')).toBe(true);
|
|
expect(messagesB.hasSentTo('only-a')).toBe(false);
|
|
expect(messagesA.getByChatRoomId('same-room')?.recipient).toBe('only-a');
|
|
expect(messagesB.getByChatRoomId('same-room')).toBeUndefined();
|
|
|
|
messagesA.markGotResponse(messageB);
|
|
expect(messagesB.getByPostId('same-post')?.gotResponse).toBe(false);
|
|
expect(messagesA.getRecent()).toHaveLength(2);
|
|
expect(messagesB.getRecent()).toHaveLength(1);
|
|
});
|
|
|
|
it('isolates conversation state with the same conversation ID', () => {
|
|
const statesA = new ConversationStateRepository(db, ACCOUNT_A);
|
|
const statesB = new ConversationStateRepository(db, ACCOUNT_B);
|
|
statesA.save('same-room', buildConversationState([
|
|
{ sender: 'account-a', text: 'hello from A', isSelf: true },
|
|
]));
|
|
statesB.save('same-room', buildConversationState([
|
|
{ sender: 'recipient', text: 'stop', isSelf: false },
|
|
]));
|
|
|
|
expect(statesA.get('same-room')?.boundaries).toEqual([]);
|
|
expect(statesB.get('same-room')?.boundaries).toContain('does not want further sexual escalation');
|
|
});
|
|
|
|
it('isolates outbox claims, successful examples, activity and outcomes', () => {
|
|
const postsA = new PostsRepository(db, ACCOUNT_A);
|
|
const postsB = new PostsRepository(db, ACCOUNT_B);
|
|
insertSamePost(postsA, 'A');
|
|
insertSamePost(postsB, 'B');
|
|
const messagesA = new MessagesRepository(db, ACCOUNT_A);
|
|
const messagesB = new MessagesRepository(db, ACCOUNT_B);
|
|
const messageA = messagesA.insert({
|
|
postId: 'same-post', recipient: 'same-recipient', subject: 'A', message: 'A', sentAt: 1,
|
|
});
|
|
const messageB = messagesB.insert({
|
|
postId: 'same-post', recipient: 'same-recipient', subject: 'B', message: 'B', sentAt: 2,
|
|
});
|
|
|
|
const outboxA = new DeliveryOutboxRepository(db, ACCOUNT_A);
|
|
const outboxB = new DeliveryOutboxRepository(db, ACCOUNT_B);
|
|
const items = [outboxA, outboxB].map((outbox) =>
|
|
outbox.discover({
|
|
postId: 'same-post', recipient: 'same-recipient', subject: 'same', message: 'same',
|
|
idempotencyKey: 'same-key', now: 1,
|
|
}),
|
|
);
|
|
expect(outboxA.count()).toBe(1);
|
|
expect(outboxB.count()).toBe(1);
|
|
expect(outboxA.markPrepared(items[0].id)).toBe(true);
|
|
expect(outboxB.markPrepared(items[1].id)).toBe(true);
|
|
expect(outboxA.claimNext('worker-a', 10, 1)?.id).toBe(items[0].id);
|
|
expect(outboxB.get(items[1].id)?.status).toBe('prepared');
|
|
expect(outboxB.claimNext('worker-b', 10, 1)?.id).toBe(items[1].id);
|
|
|
|
const examplesA = new SuccessfulMessagesRepository(db, ACCOUNT_A);
|
|
const examplesB = new SuccessfulMessagesRepository(db, ACCOUNT_B);
|
|
examplesA.insert({ sentMessageId: messageA, originalMessage: 'same text', imageAnalysis: null, postTitle: null, responseSnippet: null, subreddit: null });
|
|
expect(examplesA.existsByMessage('same text')).toBe(true);
|
|
expect(examplesB.existsByMessage('same text')).toBe(false);
|
|
examplesB.insert({ sentMessageId: messageB, originalMessage: 'same text', imageAnalysis: null, postTitle: null, responseSnippet: null, subreddit: null });
|
|
expect(examplesA.count()).toBe(1);
|
|
expect(examplesB.count()).toBe(1);
|
|
|
|
const activityA = new ActivityLogRepository(db, ACCOUNT_A);
|
|
const activityB = new ActivityLogRepository(db, ACCOUNT_B);
|
|
activityA.insert({ timestamp: 1, type: 'sent', message: 'A activity' });
|
|
activityB.insert({ timestamp: 2, type: 'sent', message: 'B activity' });
|
|
expect(activityA.getRecent(10).map((item) => item.message)).toEqual(['A activity']);
|
|
expect(activityB.getRecent(10).map((item) => item.message)).toEqual(['B activity']);
|
|
|
|
const outcomesA = new ConversationOutcomesRepository(db, ACCOUNT_A);
|
|
const outcomesB = new ConversationOutcomesRepository(db, ACCOUNT_B);
|
|
outcomesA.record(messageA, 'engaged', 'A response');
|
|
outcomesB.record(messageB, 'declined', 'B response');
|
|
expect(outcomesA.get(messageA)?.outcome).toBe('engaged');
|
|
expect(outcomesA.get(messageB)).toBeUndefined();
|
|
expect(outcomesB.get(messageB)?.outcome).toBe('declined');
|
|
});
|
|
});
|