feat: track received image outcomes

This commit is contained in:
jjbrunton committed 2026-08-08 18:53:11 +01:00
1 parent 50de1c180d
commit d819a0f1e6
15 files changed
+145 -21

No files matched your search

+20 -2
View File
@@ -8,7 +8,7 @@ import { MatrixClient } from '../src/services/chat/matrix/client.js';
import { MediaService } from '../src/services/media.js';
import type { MediaDownloader } from '../src/services/media.js';
import { createMockLogger } from './mockLogger.js';
import { authenticatedAccountAuth } from './testUtils.js';
import { authenticatedAccountAuth, createTestDb, createTestRepositories } from './testUtils.js';
const creds = {
homeserver: 'https://matrix.example.test',
@@ -79,8 +79,18 @@ describe('ChatService delta sync — media attachment', () => {
const logger = createMockLogger();
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'chat-delta-media-merge-'));
const cacheFile = path.join(tmpDir, 'chat-cache.json');
const db = createTestDb();
const repos = createTestRepositories(db);
try {
const now = Date.now();
repos.postsRepo.insert({
id: 'source-post', subreddit: 'example', author: 'alice', title: 'Source',
imageUrl: null, processedAt: now - 10_000,
});
repos.messagesRepo.insert({
postId: 'source-post', recipient: 'alice', subject: 'alice', message: 'hello',
sentAt: now - 9_000, strategyId: 'direct', chatRoomId: '!room1:example.test',
});
const existing = {
version: 1,
savedAt: now - 60_000,
@@ -129,7 +139,7 @@ describe('ChatService delta sync — media attachment', () => {
const media = new MediaService(buildDownloader(), logger, tmpDir);
const cache = new ChatCache(logger, cacheFile);
const svc = new ChatService(matrix, logger, undefined, media, undefined, undefined, cache, undefined, undefined, authenticatedAccountAuth);
const svc = new ChatService(matrix, logger, undefined, media, undefined, undefined, cache, repos.messagesRepo, undefined, authenticatedAccountAuth);
await svc.loadCache();
// refreshTopChats always runs a /sync (unlike getActiveChats, which can
@@ -145,7 +155,15 @@ describe('ChatService delta sync — media attachment', () => {
expect(imageMsg).toBeDefined();
expect(imageMsg!.media).toBeDefined();
expect(imageMsg!.media!.length).toBe(1);
const performanceMessage = repos.messagesRepo.getByChatRoomId('!room1:example.test');
expect(performanceMessage).toMatchObject({ gotResponse: true, gotImage: true });
expect(repos.messagesRepo.getStrategyStats()).toEqual([
expect.objectContaining({
strategyId: 'direct', totalResponses: 1, totalImages: 1, responseRate: 100, imageRate: 100,
}),
]);
} finally {
db.close();
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
+1 -1
View File
@@ -155,7 +155,7 @@ describe('dashboard foundation', () => {
),
http.get('/api/messages', () => HttpResponse.json([{
id: 1, postId: 'post-1', recipient: 'alice', subject: 'Hello', message: 'A recent opener',
sentAt: Date.now(), gotResponse: true, responseCheckedAt: Date.now(), strategyId: null, chatRoomId: null,
sentAt: Date.now(), gotResponse: true, gotImage: false, responseCheckedAt: Date.now(), strategyId: null, chatRoomId: null,
}])),
);
@@ -36,21 +36,21 @@ describe('PerformancePage', () => {
server.use(
http.get('/api/analytics/by-subreddit', () =>
HttpResponse.json([
{ subreddit: 'askuk', totalSent: 40, totalResponses: 12, responseRate: 30 },
{ subreddit: 'tiny', totalSent: 3, totalResponses: 2, responseRate: 67 },
{ subreddit: 'askuk', totalSent: 40, totalResponses: 12, responseRate: 30, totalImages: 6, imageRate: 15 },
{ subreddit: 'tiny', totalSent: 3, totalResponses: 2, responseRate: 67, totalImages: 1, imageRate: 33 },
]),
),
http.get('/api/analytics/by-strategy', () =>
HttpResponse.json([
{ strategyId: 'friendly', totalSent: 25, totalResponses: 10, responseRate: 40, configured: true },
{ strategyId: 'new-test', totalSent: 2, totalResponses: 1, responseRate: 50 },
{ strategyId: 'friendly', totalSent: 25, totalResponses: 10, responseRate: 40, totalImages: 5, imageRate: 20, configured: true },
{ strategyId: 'new-test', totalSent: 2, totalResponses: 1, responseRate: 50, totalImages: 1, imageRate: 50 },
]),
),
http.get('/api/analytics/trend', ({ request }) => {
expect(new URL(request.url).searchParams.get('days')).toBe('30');
return HttpResponse.json([
{ date: '2026-08-07', totalSent: 10, totalResponses: 2, responseRate: 20 },
{ date: '2026-08-08', totalSent: 8, totalResponses: 4, responseRate: 50 },
{ date: '2026-08-07', totalSent: 10, totalResponses: 2, responseRate: 20, totalImages: 1, imageRate: 10 },
{ date: '2026-08-08', totalSent: 8, totalResponses: 4, responseRate: 50, totalImages: 2, imageRate: 25 },
]);
}),
http.get('/api/successful', () =>
@@ -64,6 +64,7 @@ describe('PerformancePage', () => {
responseSnippet: 'That sounds lovely!',
subreddit: 'askuk',
createdAt: Date.parse('2026-08-08T10:00:00Z'),
gotImage: true,
},
]),
),
@@ -76,12 +77,15 @@ describe('PerformancePage', () => {
expect(within(subredditTable).getByText('n = 40')).toBeInTheDocument();
expect(within(subredditTable).getByText('n = 3')).toBeInTheDocument();
expect(within(subredditTable).getByText('Low confidence')).toBeInTheDocument();
expect(within(subredditTable).getByRole('columnheader', { name: 'Images' })).toBeInTheDocument();
expect(within(subredditTable).getByText('15%')).toBeInTheDocument();
const strategyTable = screen.getByRole('table', { name: 'Strategy performance' });
expect(within(strategyTable).getByText('n = 25')).toBeInTheDocument();
expect(within(strategyTable).getByText('Configured')).toBeInTheDocument();
expect(screen.getByRole('list', { name: '30 day response trend' })).toHaveTextContent('50%');
expect(screen.getByRole('article', { name: 'Successful example from askuk' })).toHaveTextContent('A thoughtful opener');
expect(screen.getByRole('article', { name: 'Successful example from askuk' })).toHaveTextContent('That sounds lovely!');
expect(screen.getByRole('article', { name: 'Successful example from askuk' })).toHaveTextContent('Image received');
});
});
+2
View File
@@ -81,6 +81,7 @@ describe('Database Client', () => {
const columnNames = columns.map(c => c.name);
expect(columnNames).toContain('got_response');
expect(columnNames).toContain('response_checked_at');
expect(columnNames).toContain('got_image');
});
it('adds strategy_id column', () => {
@@ -157,6 +158,7 @@ describe('Database Client', () => {
expect(columnMap.get('sent_at')?.notnull).toBe(1);
expect(columnMap.get('post_id')?.notnull).toBe(0); // nullable — chat rows have no post
expect(columnMap.get('chat_room_id')).toBeDefined();
expect(columnMap.get('got_image')).toBeDefined();
});
it('successful_messages has correct schema', () => {
+8 -1
View File
@@ -199,10 +199,12 @@ describe('MessagesRepository', () => {
expect(row!.chatRoomId).toBe('!room123:reddit.com');
expect(row!.recipient).toBe('alice');
expect(row!.gotResponse).toBe(false);
expect(row!.gotImage).toBe(false);
messagesRepo.markGotResponse(id);
messagesRepo.markGotImage(id);
const updated = messagesRepo.getByChatRoomId('!room123:reddit.com');
expect(updated!.gotResponse).toBe(true);
expect(updated!.gotImage).toBe(true);
});
it('keeps unassociated chat history out of performance trends', () => {
@@ -353,6 +355,7 @@ describe('MessagesRepository', () => {
messagesRepo.markGotResponse(1);
messagesRepo.markGotResponse(3);
messagesRepo.markGotImage(1);
const stats = messagesRepo.getStrategyStats();
expect(stats.length).toBe(2);
@@ -361,11 +364,15 @@ describe('MessagesRepository', () => {
expect(variantA!.totalSent).toBe(2);
expect(variantA!.totalResponses).toBe(1);
expect(variantA!.responseRate).toBe(50);
expect(variantA!.totalImages).toBe(1);
expect(variantA!.imageRate).toBe(50);
const variantB = stats.find(s => s.strategyId === 'variant-b');
expect(variantB!.totalSent).toBe(1);
expect(variantB!.totalResponses).toBe(1);
expect(variantB!.responseRate).toBe(100);
expect(variantB!.totalImages).toBe(0);
expect(variantB!.imageRate).toBe(0);
});
it('excludes messages without strategy_id from stats', () => {