Persist t2→username mappings in reddit_users table
Adds a durable mapping so the UI never has to show raw t2_xxx IDs or MXIDs just because the in-memory cache is cold after a restart. ChatService seeds from the table on construction and writes through every resolution path; cached chats whose username is still t2_xxx get upgraded from the DB on loadCache. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
160172403b
commit
1b6dfd67e3
7 files changed
+407
-18
No files matched your search
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import Database from 'better-sqlite3';
|
||||
import { runMigrations } from '../src/database/client.js';
|
||||
import { RedditUsersRepository } from '../src/database/repositories.js';
|
||||
import { ChatService } from '../src/services/chat/service.js';
|
||||
import { ChatCache } from '../src/services/chat/cache.js';
|
||||
import { MatrixClient } from '../src/services/chat/matrix/client.js';
|
||||
import { createMockLogger } from './mockLogger.js';
|
||||
|
||||
const creds = {
|
||||
homeserver: 'https://matrix.example.test',
|
||||
accessToken: 'tok',
|
||||
userId: '@me:example.test',
|
||||
capturedAt: Date.now(),
|
||||
};
|
||||
|
||||
function buildMatrix() {
|
||||
const logger = createMockLogger();
|
||||
const fetchImpl = (() =>
|
||||
Promise.resolve(
|
||||
new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } }),
|
||||
)) as typeof fetch;
|
||||
return new MatrixClient(creds, logger, { minRequestIntervalMs: 0, fetchImpl });
|
||||
}
|
||||
|
||||
describe('ChatService username persistence', () => {
|
||||
it('seeds the in-memory cache from RedditUsersRepository on construction', () => {
|
||||
const db = new Database(':memory:');
|
||||
runMigrations(db);
|
||||
const repo = new RedditUsersRepository(db);
|
||||
repo.upsert({ t2Id: 't2_abc', username: 'alice' });
|
||||
|
||||
const logger = createMockLogger();
|
||||
const svc = new ChatService(
|
||||
buildMatrix(),
|
||||
logger,
|
||||
undefined, undefined, undefined, undefined, undefined, undefined,
|
||||
repo,
|
||||
);
|
||||
|
||||
expect(svc.resolveSenderDisplay('@t2_abc:reddit.com')).toBe('alice');
|
||||
expect(svc.resolveSenderDisplay('t2_abc')).toBe('alice');
|
||||
db.close();
|
||||
});
|
||||
|
||||
it('falls back to t2 form when no mapping is known', () => {
|
||||
const logger = createMockLogger();
|
||||
const svc = new ChatService(buildMatrix(), logger);
|
||||
expect(svc.resolveSenderDisplay('@t2_unknown:reddit.com')).toBe('t2_unknown');
|
||||
});
|
||||
|
||||
it('upgrades cached chats with t2_ usernames from the db map on loadCache', async () => {
|
||||
const db = new Database(':memory:');
|
||||
runMigrations(db);
|
||||
const repo = new RedditUsersRepository(db);
|
||||
repo.upsert({ t2Id: 't2_abc', username: 'alice' });
|
||||
|
||||
const logger = createMockLogger();
|
||||
const tmpFile = path.join(os.tmpdir(), `chat-cache-${Date.now()}-${Math.random()}.json`);
|
||||
const cache = new ChatCache(logger, tmpFile);
|
||||
cache.set([
|
||||
{
|
||||
username: 't2_abc',
|
||||
roomName: 't2_abc',
|
||||
lastMessageTime: Date.now(),
|
||||
lastMessageSnippet: 'hi',
|
||||
isFromUs: false,
|
||||
unread: false,
|
||||
url: 'https://chat.reddit.com/room/x',
|
||||
roomId: '!room:reddit.com',
|
||||
messages: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const svc = new ChatService(
|
||||
buildMatrix(),
|
||||
logger,
|
||||
undefined, undefined, undefined, undefined,
|
||||
cache,
|
||||
undefined,
|
||||
repo,
|
||||
);
|
||||
|
||||
await svc.loadCache();
|
||||
const chats = await svc.getCachedConversations();
|
||||
expect(chats[0].username).toBe('alice');
|
||||
expect(chats[0].roomName).toBe('alice');
|
||||
db.close();
|
||||
});
|
||||
});
|
||||
@@ -103,8 +103,8 @@ describe('Database Client', () => {
|
||||
const tables = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
.all() as { name: string }[];
|
||||
// processed_posts, sent_messages, successful_messages, activity_log
|
||||
expect(tables.length).toBe(4);
|
||||
// processed_posts, sent_messages, successful_messages, activity_log, reddit_users
|
||||
expect(tables.length).toBe(5);
|
||||
});
|
||||
|
||||
it('processed_posts has correct schema', () => {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import Database from 'better-sqlite3';
|
||||
import { runMigrations } from '../src/database/client.js';
|
||||
import { RedditUsersRepository } from '../src/database/repositories.js';
|
||||
|
||||
describe('RedditUsersRepository', () => {
|
||||
let db: Database.Database;
|
||||
let repo: RedditUsersRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new Database(':memory:');
|
||||
db.pragma('foreign_keys = ON');
|
||||
runMigrations(db);
|
||||
repo = new RedditUsersRepository(db);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
it('upserts a new user and reads it back', () => {
|
||||
repo.upsert({ t2Id: 't2_abc', username: 'alice' });
|
||||
const got = repo.getByT2('t2_abc');
|
||||
expect(got?.username).toBe('alice');
|
||||
expect(got?.t2Id).toBe('t2_abc');
|
||||
});
|
||||
|
||||
it('normalizes a bare t2 id when looking up or writing', () => {
|
||||
repo.upsert({ t2Id: 'abc', username: 'alice' });
|
||||
expect(repo.getByT2('abc')?.username).toBe('alice');
|
||||
expect(repo.getByT2('t2_abc')?.username).toBe('alice');
|
||||
});
|
||||
|
||||
it('updates the username when the same t2 is upserted twice', () => {
|
||||
repo.upsert({ t2Id: 't2_abc', username: 'old_name' });
|
||||
repo.upsert({ t2Id: 't2_abc', username: 'new_name' });
|
||||
expect(repo.getByT2('t2_abc')?.username).toBe('new_name');
|
||||
});
|
||||
|
||||
it('refuses to store t2_-prefixed pseudo-usernames', () => {
|
||||
repo.upsert({ t2Id: 't2_abc', username: 't2_abc' });
|
||||
expect(repo.getByT2('t2_abc')).toBeNull();
|
||||
});
|
||||
|
||||
it('refuses to store MXID-shaped pseudo-usernames', () => {
|
||||
repo.upsert({ t2Id: 't2_abc', username: '@t2_abc:reddit.com' });
|
||||
expect(repo.getByT2('t2_abc')).toBeNull();
|
||||
});
|
||||
|
||||
it('looks up by username case-insensitively', () => {
|
||||
repo.upsert({ t2Id: 't2_abc', username: 'Alice' });
|
||||
expect(repo.getByUsername('alice')?.t2Id).toBe('t2_abc');
|
||||
expect(repo.getByUsername('ALICE')?.t2Id).toBe('t2_abc');
|
||||
});
|
||||
|
||||
it('preserves an existing mxid when a later upsert passes null', () => {
|
||||
repo.upsert({ t2Id: 't2_abc', username: 'alice', mxid: '@t2_abc:reddit.com' });
|
||||
repo.upsert({ t2Id: 't2_abc', username: 'alice' });
|
||||
expect(repo.getByT2('t2_abc')?.mxid).toBe('@t2_abc:reddit.com');
|
||||
});
|
||||
|
||||
it('bulk-inserts via upsertMany', () => {
|
||||
repo.upsertMany([
|
||||
{ t2Id: 't2_a', username: 'alice' },
|
||||
{ t2Id: 't2_b', username: 'bob' },
|
||||
]);
|
||||
const all = repo.getAll();
|
||||
expect(all.map((u) => u.username).sort()).toEqual(['alice', 'bob']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user