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>
75 lines
2.3 KiB
TypeScript
75 lines
2.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { MediaSyncService } from '../src/services/media-sync.js';
|
|
import { createMockLogger } from './mockLogger.js';
|
|
|
|
describe('MediaSyncService account authentication gate', () => {
|
|
it('does not touch chat while the global account context is unauthenticated', async () => {
|
|
const chat = {
|
|
refreshTopChats: vi.fn(),
|
|
fetchAllMessages: vi.fn(),
|
|
};
|
|
const auth = { isFullyAuthenticated: vi.fn(() => false) };
|
|
const service = new MediaSyncService(
|
|
chat as never,
|
|
createMockLogger(),
|
|
30 * 60 * 1000,
|
|
auth,
|
|
);
|
|
|
|
await expect(service.triggerSync()).resolves.toBe(false);
|
|
expect(chat.refreshTopChats).not.toHaveBeenCalled();
|
|
expect(chat.fetchAllMessages).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('reports the actual number of rooms changed by an incremental sync', async () => {
|
|
const chat = {
|
|
refreshTopChats: vi.fn().mockResolvedValue(3),
|
|
fetchAllMessages: vi.fn(),
|
|
};
|
|
const service = new MediaSyncService(
|
|
chat as never,
|
|
createMockLogger(),
|
|
30 * 60 * 1000,
|
|
{ isFullyAuthenticated: () => true },
|
|
);
|
|
|
|
await expect(service.triggerSync()).resolves.toBe(true);
|
|
expect(service.getStatus().lastSyncChats).toBe(3);
|
|
});
|
|
|
|
it('requires a complete historical backfill for a full media sync', async () => {
|
|
const chat = {
|
|
refreshTopChats: vi.fn(),
|
|
fetchAllMessages: vi.fn().mockResolvedValue([]),
|
|
};
|
|
const service = new MediaSyncService(
|
|
chat as never,
|
|
createMockLogger(),
|
|
30 * 60 * 1000,
|
|
{ isFullyAuthenticated: () => true },
|
|
);
|
|
|
|
await service.triggerSync(true);
|
|
expect(chat.fetchAllMessages).toHaveBeenCalledWith({ requireComplete: true });
|
|
});
|
|
|
|
it('returns failure and preserves the operator-visible error when sync work is incomplete', async () => {
|
|
const chat = {
|
|
refreshTopChats: vi.fn().mockRejectedValue(new Error('media history incomplete')),
|
|
fetchAllMessages: vi.fn(),
|
|
};
|
|
const service = new MediaSyncService(
|
|
chat as never,
|
|
createMockLogger(),
|
|
30 * 60 * 1000,
|
|
{ isFullyAuthenticated: () => true },
|
|
);
|
|
|
|
await expect(service.triggerSync()).resolves.toBe(false);
|
|
expect(service.getStatus()).toMatchObject({
|
|
lastSyncAt: null,
|
|
lastError: 'media history incomplete',
|
|
});
|
|
});
|
|
});
|