Files
RedditBot/tests/chat-service-username-persist.test.ts
T
jjbruntonandClaude Opus 4.7 b9df8459e3 Atomic chat-cache writes + stop tests clobbering prod cache
Three intertwined fixes for the torn-write that corrupted the prod
data/chat-cache.json mid-session:

  - ChatCache.save / persistCooldown now write via temp file + rename.
    rename(2) is atomic on the same filesystem, so a crashed or
    concurrent writer can never leave NULs in the middle of the file.
    A small serialising write queue stops two concurrent saves from
    racing to the same temp name.

  - ChatCache throws if constructed without a filePath under NODE_ENV=test.
    The old default (./data/chat-cache.json) silently routed test
    writes into the production cache — exactly how the corruption
    happened. Future test that forgets to pass a tmp path fails loudly
    instead of stomping data.

  - Cooldown / delta-media / username-persist tests now pass an
    explicit tmp ChatCache so the new guard is satisfied.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:48:33 +01:00

107 lines
3.2 KiB
TypeScript

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', () => {
function tmpCache(logger: Parameters<typeof ChatCache>[0]) {
return new ChatCache(
logger,
path.join(os.tmpdir(), `chat-cache-${Date.now()}-${Math.random()}.json`),
);
}
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,
tmpCache(logger),
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,
undefined, undefined, undefined, undefined,
tmpCache(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();
});
});