Improve Reddit chat and media reliability
This commit is contained in:
1 parent
a44768c089
commit
345ec99348
14 files changed
+323
-676
No files matched your search
@@ -85,7 +85,7 @@ describe('ChatService text delta sync', () => {
|
||||
const [conversation] = await service.getCachedConversations();
|
||||
expect(conversation.messages).toEqual([
|
||||
{ sender: 'alice', text: 'hello', timestamp: now - 1_000, isSelf: false },
|
||||
{ sender: 'Me', text: 'AI-assisted reply', timestamp: now, isSelf: true },
|
||||
{ eventId: '$sent', sender: 'Me', text: 'AI-assisted reply', timestamp: now, isSelf: true },
|
||||
]);
|
||||
|
||||
const persisted = JSON.parse(await fs.readFile(cacheFile, 'utf-8')) as {
|
||||
@@ -93,4 +93,139 @@ describe('ChatService text delta sync', () => {
|
||||
};
|
||||
expect(persisted.chats[0].messages).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('reconciles an optimistic opening message with its Matrix event', async () => {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'chat-text-reconcile-'));
|
||||
temporaryDirectories.push(directory);
|
||||
const cacheFile = path.join(directory, 'chat-cache.json');
|
||||
const serverTimestamp = Date.now();
|
||||
await fs.writeFile(cacheFile, JSON.stringify({
|
||||
version: 1,
|
||||
savedAt: serverTimestamp,
|
||||
chats: [{
|
||||
username: 'alice',
|
||||
roomName: 'alice',
|
||||
roomId: '!alice:example.test',
|
||||
lastMessageTime: serverTimestamp + 80,
|
||||
lastMessageSnippet: 'hello',
|
||||
isFromUs: true,
|
||||
unread: false,
|
||||
messages: [{
|
||||
eventId: '$opening',
|
||||
sender: 'Me',
|
||||
text: 'hello',
|
||||
timestamp: serverTimestamp + 80,
|
||||
isSelf: true,
|
||||
}],
|
||||
}],
|
||||
nextBatch: 's1',
|
||||
lastDeltaSyncAt: serverTimestamp,
|
||||
}), 'utf-8');
|
||||
|
||||
const logger = createMockLogger();
|
||||
const matrix = new MatrixClient({
|
||||
homeserver: 'https://matrix.example.test',
|
||||
accessToken: 'token',
|
||||
userId: '@me:example.test',
|
||||
capturedAt: serverTimestamp,
|
||||
}, logger, { minRequestIntervalMs: 0 });
|
||||
matrix.sync = vi.fn(async () => ({
|
||||
next_batch: 's2',
|
||||
rooms: {
|
||||
join: {
|
||||
'!alice:example.test': {
|
||||
timeline: {
|
||||
events: [{
|
||||
event_id: '$opening',
|
||||
type: 'm.room.message',
|
||||
sender: '@me:example.test',
|
||||
origin_server_ts: serverTimestamp,
|
||||
content: { msgtype: 'm.text', body: 'hello' },
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as typeof matrix.sync;
|
||||
|
||||
const service = new ChatService(
|
||||
matrix,
|
||||
logger,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
new ChatCache(logger, cacheFile),
|
||||
undefined,
|
||||
undefined,
|
||||
authenticatedAccountAuth,
|
||||
);
|
||||
await service.loadCache();
|
||||
await service.refreshTopChats();
|
||||
|
||||
const [conversation] = await service.getCachedConversations();
|
||||
expect(conversation.messages).toEqual([{
|
||||
eventId: '$opening',
|
||||
sender: 'Me',
|
||||
text: 'hello',
|
||||
timestamp: serverTimestamp,
|
||||
isSelf: true,
|
||||
}]);
|
||||
});
|
||||
|
||||
it('repairs a legacy cached opening-message duplicate', async () => {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'chat-text-legacy-repair-'));
|
||||
temporaryDirectories.push(directory);
|
||||
const cacheFile = path.join(directory, 'chat-cache.json');
|
||||
const serverTimestamp = Date.now();
|
||||
const message = { sender: 'Me', text: 'hello', isSelf: true };
|
||||
await fs.writeFile(cacheFile, JSON.stringify({
|
||||
version: 1,
|
||||
savedAt: serverTimestamp,
|
||||
chats: [{
|
||||
username: 'alice',
|
||||
roomName: 'alice',
|
||||
roomId: '!alice:example.test',
|
||||
lastMessageTime: serverTimestamp,
|
||||
lastMessageSnippet: 'hello',
|
||||
isFromUs: true,
|
||||
unread: false,
|
||||
messages: [
|
||||
{ ...message, timestamp: serverTimestamp + 80 },
|
||||
{ ...message, timestamp: serverTimestamp },
|
||||
],
|
||||
}],
|
||||
nextBatch: 's1',
|
||||
lastDeltaSyncAt: serverTimestamp,
|
||||
}), 'utf-8');
|
||||
|
||||
const logger = createMockLogger();
|
||||
const matrix = new MatrixClient({
|
||||
homeserver: 'https://matrix.example.test',
|
||||
accessToken: 'token',
|
||||
userId: '@me:example.test',
|
||||
capturedAt: serverTimestamp,
|
||||
}, logger, { minRequestIntervalMs: 0 });
|
||||
const service = new ChatService(
|
||||
matrix,
|
||||
logger,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
new ChatCache(logger, cacheFile),
|
||||
undefined,
|
||||
undefined,
|
||||
authenticatedAccountAuth,
|
||||
);
|
||||
|
||||
await service.loadCache();
|
||||
|
||||
const [conversation] = await service.getCachedConversations();
|
||||
expect(conversation.messages).toEqual([{ ...message, timestamp: serverTimestamp }]);
|
||||
const persisted = JSON.parse(await fs.readFile(cacheFile, 'utf-8')) as {
|
||||
chats: Array<{ messages: unknown[] }>;
|
||||
};
|
||||
expect(persisted.chats[0].messages).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -94,6 +94,9 @@ describe('ChatService username persistence', () => {
|
||||
const chat = await svc.startConversation('alice', 'hello', 't2_abc123');
|
||||
|
||||
expect(chat?.username).toBe('alice');
|
||||
expect(chat?.messages).toEqual([
|
||||
expect.objectContaining({ eventId: '$event', text: 'hello', isSelf: true }),
|
||||
]);
|
||||
expect(usernameResolver.resolveUsernameToT2).not.toHaveBeenCalled();
|
||||
expect(matrix.sendInitialDirectMessage).toHaveBeenCalledWith(
|
||||
'@t2_abc123:reddit.com',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import express from 'express';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { MediaResponseSchema } from '../../packages/api-contracts/src/media.js';
|
||||
import type { WebContext } from '../../src/web/context.js';
|
||||
import { createMediaRouter } from '../../src/web/routes/media.js';
|
||||
|
||||
@@ -63,4 +64,35 @@ describe('media route username presentation', () => {
|
||||
expect(JSON.stringify(body)).not.toContain('t2_missing');
|
||||
expect(body[0]).toMatchObject({ chatUsername: null, sender: 'Unknown Reddit user' });
|
||||
});
|
||||
|
||||
it('keeps the internal account ID out of the public media item', async () => {
|
||||
const context = {
|
||||
logger: { error: vi.fn() },
|
||||
services: {
|
||||
media: { getMediaItems: vi.fn().mockResolvedValue([{
|
||||
accountId: 't2_owner', id: 'one', chatUsername: 'alice', chatRoomId: 'room-one', sender: 'alice',
|
||||
timestamp: 1, originalUrl: 'mxc://one', fileName: 'one.jpg', relativePath: 'one.jpg', createdAt: 2,
|
||||
}]) },
|
||||
chat: {
|
||||
getCachedConversations: vi.fn().mockResolvedValue([]),
|
||||
resolveSenderDisplay: vi.fn((identity: string) => identity),
|
||||
},
|
||||
redditAccounts: {
|
||||
get: vi.fn().mockReturnValue({ accountId: 't2_owner', redditUsername: 'owner' }),
|
||||
},
|
||||
},
|
||||
} as unknown as WebContext;
|
||||
const app = express().use('/api', createMediaRouter(context));
|
||||
|
||||
const response = await request(app, '/api/media');
|
||||
const body: unknown = await response.json();
|
||||
|
||||
expect(MediaResponseSchema.parse(body)).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'one',
|
||||
owner: { accountId: 't2_owner', redditUsername: 'owner' },
|
||||
}),
|
||||
]);
|
||||
expect(body).not.toEqual([expect.objectContaining({ accountId: expect.anything() })]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user