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
@@ -540,6 +540,36 @@ describe('RedditApiClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserAccountStatus', () => {
|
||||
it('distinguishes active, suspended and deleted accounts', async () => {
|
||||
const mockAxios = { get: vi.fn() };
|
||||
vi.mocked(axios.create).mockReturnValue(mockAxios as unknown as AxiosInstance);
|
||||
const client2 = new RedditApiClient(getTestConfig(), logger);
|
||||
|
||||
mockAxios.get.mockResolvedValueOnce({ data: { data: {} } });
|
||||
await expect(client2.getUserAccountStatus('active-user')).resolves.toBe('active');
|
||||
|
||||
mockAxios.get.mockResolvedValueOnce({ data: { data: { is_suspended: true } } });
|
||||
await expect(client2.getUserAccountStatus('suspended-user')).resolves.toBe('suspended');
|
||||
|
||||
mockAxios.get.mockRejectedValueOnce({ isAxiosError: true, response: { status: 404 } });
|
||||
await expect(client2.getUserAccountStatus('deleted-user')).resolves.toBe('deleted');
|
||||
});
|
||||
|
||||
it('does not turn temporary or malformed responses into deletion signals', async () => {
|
||||
const mockAxios = { get: vi.fn() };
|
||||
vi.mocked(axios.create).mockReturnValue(mockAxios as unknown as AxiosInstance);
|
||||
const client2 = new RedditApiClient(getTestConfig(), logger);
|
||||
|
||||
const temporary = { isAxiosError: true, response: { status: 503 } };
|
||||
mockAxios.get.mockRejectedValueOnce(temporary);
|
||||
await expect(client2.getUserAccountStatus('alice')).rejects.toBe(temporary);
|
||||
|
||||
mockAxios.get.mockResolvedValueOnce({ data: {} });
|
||||
await expect(client2.getUserAccountStatus('alice')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNewPosts', () => {
|
||||
it('throws a permanent-disable signal for a plain 404 subreddit', async () => {
|
||||
const error = { isAxiosError: true, response: { status: 404 } };
|
||||
|
||||
Reference in new issue
Block a user