feat: persist settings and enrich conversation context
This commit is contained in:
1 parent
a36723a325
commit
2c100d72b6
84 files changed
+2248
-644
No files matched your search
@@ -0,0 +1,180 @@
|
||||
import express from 'express';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { WebContext } from '../../src/web/context.js';
|
||||
import { createConversationsRouter } from '../../src/web/routes/conversations.js';
|
||||
|
||||
const servers: Array<ReturnType<express.Express['listen']>> = [];
|
||||
|
||||
async function request(app: express.Express, path: string): Promise<Response> {
|
||||
const server = app.listen(0);
|
||||
servers.push(server);
|
||||
await new Promise<void>((resolve) => server.once('listening', resolve));
|
||||
const { port } = server.address() as AddressInfo;
|
||||
return fetch(`http://127.0.0.1:${port}${path}`);
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(servers.splice(0).map((server) => new Promise<void>((resolve) => server.close(() => resolve()))));
|
||||
});
|
||||
|
||||
describe('conversations route', () => {
|
||||
it('reports an unavailable chat service instead of presenting it as an empty inbox', async () => {
|
||||
const context = {
|
||||
logger: { error: vi.fn() },
|
||||
services: {},
|
||||
} as unknown as WebContext;
|
||||
const app = express().use('/api', createConversationsRouter(context));
|
||||
|
||||
const response = await request(app, '/api/conversations');
|
||||
|
||||
expect(response.status).toBe(503);
|
||||
expect(await response.json()).toEqual({ error: 'Chat service not initialized' });
|
||||
});
|
||||
|
||||
it('reports a cache read failure instead of presenting it as an empty inbox', async () => {
|
||||
const logger = { error: vi.fn() };
|
||||
const context = {
|
||||
logger,
|
||||
services: {
|
||||
chat: {
|
||||
getCachedConversations: vi.fn().mockRejectedValue(new Error('cache unavailable')),
|
||||
},
|
||||
},
|
||||
} as unknown as WebContext;
|
||||
const app = express().use('/api', createConversationsRouter(context));
|
||||
|
||||
const response = await request(app, '/api/conversations');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(await response.json()).toEqual({ error: 'Failed to fetch conversations' });
|
||||
expect(logger.error).toHaveBeenCalledWith('Failed to fetch conversations:', expect.any(Error));
|
||||
});
|
||||
|
||||
it('marks conversations that have received images in the durable media index', async () => {
|
||||
const conversation = {
|
||||
username: 'alice',
|
||||
roomName: 'alice',
|
||||
roomId: 'room-alice',
|
||||
lastMessageTime: 1_786_000_000_000,
|
||||
lastMessageSnippet: '[Image]',
|
||||
isFromUs: false,
|
||||
unread: true,
|
||||
};
|
||||
const context = {
|
||||
logger: { error: vi.fn() },
|
||||
services: {
|
||||
chat: { getCachedConversations: vi.fn().mockResolvedValue([conversation]) },
|
||||
media: {
|
||||
getMediaItems: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'image-one',
|
||||
chatUsername: 'alice',
|
||||
chatRoomId: 'room-alice',
|
||||
sender: 'alice',
|
||||
timestamp: 1_786_000_000_000,
|
||||
originalUrl: 'mxc://reddit.com/image-one',
|
||||
fileName: 'image-one.jpg',
|
||||
relativePath: 'image-one.jpg',
|
||||
createdAt: 1_786_000_000_100,
|
||||
},
|
||||
]),
|
||||
},
|
||||
},
|
||||
} as unknown as WebContext;
|
||||
const app = express().use('/api', createConversationsRouter(context));
|
||||
|
||||
const legacyResponse = await request(app, '/api/conversations');
|
||||
const response = await request(app, '/api/conversations?includeImageMetadata=true');
|
||||
|
||||
expect(legacyResponse.status).toBe(200);
|
||||
expect(await legacyResponse.json()).toEqual([conversation]);
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual([{ ...conversation, hasReceivedImages: true }]);
|
||||
});
|
||||
|
||||
it('serves conversation details without starting a delta sync', async () => {
|
||||
const conversation = {
|
||||
username: 'alice',
|
||||
roomName: 'alice',
|
||||
roomId: 'room-alice',
|
||||
lastMessageTime: 1_786_000_000_000,
|
||||
lastMessageSnippet: 'Hello',
|
||||
isFromUs: false,
|
||||
unread: false,
|
||||
messages: [{
|
||||
sender: 'alice',
|
||||
text: 'Hello',
|
||||
timestamp: 1_786_000_000_000,
|
||||
isSelf: false,
|
||||
}],
|
||||
};
|
||||
const refreshTopChats = vi.fn();
|
||||
const loadConversationDetails = vi.fn();
|
||||
const context = {
|
||||
logger: { error: vi.fn(), warn: vi.fn() },
|
||||
repos: {
|
||||
messages: { getLatestByRecipient: vi.fn().mockReturnValue(undefined) },
|
||||
},
|
||||
services: {
|
||||
chat: {
|
||||
getSyncStatus: vi.fn().mockReturnValue({
|
||||
syncing: false,
|
||||
syncingKind: null,
|
||||
progress: null,
|
||||
}),
|
||||
getCachedConversations: vi.fn().mockResolvedValue([conversation]),
|
||||
refreshTopChats,
|
||||
loadConversationDetails,
|
||||
saveCacheExternal: vi.fn(),
|
||||
},
|
||||
},
|
||||
} as unknown as WebContext;
|
||||
const app = express().use('/api', createConversationsRouter(context));
|
||||
|
||||
const response = await request(app, '/api/conversations/room-alice');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect((await response.json()).username).toBe('alice');
|
||||
expect(refreshTopChats).not.toHaveBeenCalled();
|
||||
expect(loadConversationDetails).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('attaches the exact room origin with its saved URL, title, and image', async () => {
|
||||
const conversation = {
|
||||
username: 'alice', roomName: 'alice', roomId: 'room-alice',
|
||||
lastMessageTime: 10, lastMessageSnippet: 'Hello', isFromUs: true, unread: false,
|
||||
messages: [{ sender: 'me', text: 'Hello', timestamp: 10, isSelf: true }],
|
||||
};
|
||||
const roomOrigin = {
|
||||
id: 7, postId: 'post-exact', recipient: 'alice', subject: 'alice', message: 'Hello',
|
||||
sentAt: 9, gotResponse: false, responseCheckedAt: null, strategyId: null,
|
||||
chatRoomId: 'room-alice', subreddit: 'example', postTitle: 'Exact source post',
|
||||
postUrl: 'https://www.reddit.com/r/example/comments/post-exact/exact_source_post/',
|
||||
postImageUrl: 'https://i.redd.it/exact-source.jpg',
|
||||
};
|
||||
const latestRecipientOrigin = { ...roomOrigin, postId: 'wrong-latest-post', postTitle: 'Wrong post' };
|
||||
const context = {
|
||||
logger: { error: vi.fn(), warn: vi.fn() },
|
||||
repos: { messages: {
|
||||
getByChatRoomId: vi.fn().mockReturnValue(roomOrigin),
|
||||
getLatestByRecipient: vi.fn().mockReturnValue(latestRecipientOrigin),
|
||||
} },
|
||||
services: { chat: {
|
||||
getSyncStatus: vi.fn().mockReturnValue({ syncing: false, syncingKind: null, progress: null }),
|
||||
getCachedConversations: vi.fn().mockResolvedValue([conversation]),
|
||||
loadConversationDetails: vi.fn(), saveCacheExternal: vi.fn(),
|
||||
} },
|
||||
} as unknown as WebContext;
|
||||
const app = express().use('/api', createConversationsRouter(context));
|
||||
|
||||
const response = await request(app, '/api/conversations/room-alice');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect((await response.json()).triggeringPost).toEqual({
|
||||
id: 'post-exact', subreddit: 'example', title: 'Exact source post',
|
||||
url: roomOrigin.postUrl, imageUrl: roomOrigin.postImageUrl, sentAt: 9,
|
||||
});
|
||||
expect(context.repos.messages.getLatestByRecipient).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user