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>
This commit is contained in:
1 parent
725b5fc4e6
commit
ac12a3146a
35 files changed
+1164
-129
No files matched your search
@@ -0,0 +1,62 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import Database from 'better-sqlite3';
|
||||
import { RedditRecipientAccountStatusRepository } from '../src/database/account-repositories.js';
|
||||
import { runMigrations } from '../src/database/client.js';
|
||||
import { RecipientAccountStatusService } from '../src/services/recipient-account-status.js';
|
||||
import { createMockLogger } from './mockLogger.js';
|
||||
|
||||
describe('RecipientAccountStatusService', () => {
|
||||
let db: Database.Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new Database(':memory:');
|
||||
runMigrations(db);
|
||||
});
|
||||
|
||||
afterEach(() => db.close());
|
||||
|
||||
it('checks each real username once, persists results, and skips fresh checks', async () => {
|
||||
let now = 10_000;
|
||||
const repository = new RedditRecipientAccountStatusRepository(db);
|
||||
const lookup = {
|
||||
getUserAccountStatus: vi.fn(async (username: string) => username === 'Alice' ? 'deleted' as const : 'active' as const),
|
||||
};
|
||||
const service = new RecipientAccountStatusService(
|
||||
{
|
||||
getCachedConversations: vi.fn(async () => [
|
||||
{ username: 'Alice' }, { username: 'alice' }, { username: 'Bob' },
|
||||
{ username: 'unknown' }, { username: 't2_abc' },
|
||||
]),
|
||||
} as never,
|
||||
lookup,
|
||||
repository,
|
||||
createMockLogger(),
|
||||
{ isFullyAuthenticated: () => true, invalidate: () => undefined },
|
||||
4_000,
|
||||
() => now,
|
||||
);
|
||||
|
||||
await expect(service.checkNow()).resolves.toBe(2);
|
||||
expect(lookup.getUserAccountStatus).toHaveBeenCalledTimes(2);
|
||||
expect(repository.get('alice')).toMatchObject({ status: 'deleted', checkedAt: 10_000 });
|
||||
|
||||
now = 12_000;
|
||||
await expect(service.checkNow()).resolves.toBe(0);
|
||||
now = 15_000;
|
||||
await expect(service.checkNow()).resolves.toBe(2);
|
||||
});
|
||||
|
||||
it('does no profile work while account authentication is unavailable', async () => {
|
||||
const lookup = { getUserAccountStatus: vi.fn() };
|
||||
const service = new RecipientAccountStatusService(
|
||||
{ getCachedConversations: vi.fn() } as never,
|
||||
lookup,
|
||||
new RedditRecipientAccountStatusRepository(db),
|
||||
createMockLogger(),
|
||||
{ isFullyAuthenticated: () => false, invalidate: () => undefined },
|
||||
);
|
||||
|
||||
await expect(service.checkNow()).resolves.toBe(0);
|
||||
expect(lookup.getUserAccountStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user