feat: support concurrent Reddit accounts
This commit is contained in:
1 parent
d819a0f1e6
commit
16a31335d0
51 files changed
+3241
-250
No files matched your search
@@ -0,0 +1,155 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user