Files
RedditBot/tests/chat-service-auth-gate.test.ts
T
Jamie BruntonandClaude Opus 5 ac12a3146a feat: recover missing conversations, fix media attachment, add manual inbox tab
Missing conversations: recipient Reddit accounts that are deleted or suspended
left conversations unaccounted for. A new RecipientAccountStatusService polls
Reddit for the users behind cached conversations and records their account
status durably, with progressive backfill and delta-media fixes in ChatService
so history and images are no longer dropped mid-sync.

Media: attachMedia now surfaces download failures as MediaAttachmentError so the
sync cursor can be retried instead of silently losing an image, recognizes image
bytes when Matrix returns a generic content type, and keeps separate gallery
occurrences when the same asset appears in more than one event.

Manual inbox tab: the conversations list now carries the reply-loop status, so
the inbox can separate conversations the operator is driving by hand from the
ones automation is still handling. A Manual filter pill and a per-row chip
explain why each conversation was handed over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 11:03:33 +01:00

113 lines
3.5 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import os from 'node:os';
import path from 'node:path';
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';
describe('ChatService account authentication gate', () => {
it('does not make Matrix requests before full account authentication', async () => {
const logger = createMockLogger();
const matrix = new MatrixClient({
homeserver: 'https://matrix.example.test',
accessToken: 'cached-but-unverified',
userId: '@t2_account:reddit.com',
capturedAt: 1,
}, logger, { minRequestIntervalMs: 0 });
matrix.sync = vi.fn();
const cache = new ChatCache(
logger,
path.join(os.tmpdir(), `chat-auth-${Date.now()}-${Math.random()}.json`),
);
const auth = { isFullyAuthenticated: vi.fn(() => false) };
const service = new ChatService(
matrix,
logger,
undefined,
undefined,
undefined,
undefined,
cache,
undefined,
undefined,
auth,
);
await expect(service.refreshTopChats()).rejects.toThrow(
'Matrix authentication could not be restored',
);
expect(matrix.sync).not.toHaveBeenCalled();
});
it('coalesces concurrent delta refresh requests before they reach Matrix', async () => {
const logger = createMockLogger();
const matrix = new MatrixClient({
homeserver: 'https://matrix.example.test',
accessToken: 'verified',
userId: '@t2_account:reddit.com',
capturedAt: 1,
}, logger, { minRequestIntervalMs: 0 });
let finishSync!: (value: { next_batch: string }) => void;
matrix.sync = vi.fn(() => new Promise((resolve) => {
finishSync = resolve;
})) as typeof matrix.sync;
const cache = new ChatCache(
logger,
path.join(os.tmpdir(), `chat-auth-${Date.now()}-${Math.random()}.json`),
);
const service = new ChatService(
matrix,
logger,
undefined,
undefined,
undefined,
undefined,
cache,
undefined,
undefined,
{ isFullyAuthenticated: () => true, invalidate: vi.fn() },
);
const first = service.refreshTopChats();
const second = service.refreshTopChats();
await vi.waitFor(() => expect(matrix.sync).toHaveBeenCalledOnce());
finishSync({ next_batch: 's1' });
await expect(Promise.all([first, second])).resolves.toEqual([0, 0]);
expect(matrix.sync).toHaveBeenCalledOnce();
});
it('reports failed sync termination to progress listeners', async () => {
const logger = createMockLogger();
const matrix = new MatrixClient({
homeserver: 'https://matrix.example.test',
accessToken: 'unverified',
userId: '@t2_account:reddit.com',
capturedAt: 1,
}, logger, { minRequestIntervalMs: 0 });
const service = new ChatService(
matrix,
logger,
undefined,
undefined,
undefined,
undefined,
new ChatCache(logger, path.join(os.tmpdir(), `chat-auth-${Date.now()}-${Math.random()}.json`)),
undefined,
undefined,
{ isFullyAuthenticated: () => false, invalidate: vi.fn() },
);
const listener = vi.fn();
service.setProgressListener(listener);
await expect(service.refreshTopChats()).rejects.toThrow();
expect(listener).toHaveBeenLastCalledWith(null, {
status: 'failed',
error: 'Matrix authentication could not be restored',
});
});
});