From 92287c0a9a18f022c90a3364ae185ae921359b8e Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Sat, 8 Aug 2026 15:40:55 +0100 Subject: [PATCH] Stop Matrix refresh loop when login is unavailable --- src/services/chat/service.ts | 22 ++++++++- tests/chat-service-username-persist.test.ts | 54 ++++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/services/chat/service.ts b/src/services/chat/service.ts index 3a324f0..0506e67 100644 --- a/src/services/chat/service.ts +++ b/src/services/chat/service.ts @@ -25,6 +25,13 @@ export interface TokenRefresher { refresh(): Promise; } +class MatrixAuthenticationUnavailableError extends Error { + constructor(cause: unknown) { + super('Matrix authentication could not be restored', { cause }); + this.name = 'MatrixAuthenticationUnavailableError'; + } +} + /** * Optional Reddit-API hook for resolving t2 IDs to real usernames. Without * it, conversations show up as `t2_abc123` (stable but ugly). With it, the @@ -174,7 +181,11 @@ export class ChatService { } catch (err) { if (err instanceof MatrixAuthError && this.tokenRefresher) { this.logger.warn('[chat] Matrix token rejected — refreshing via browser session'); - await this.tokenRefresher.refresh(); + try { + await this.tokenRefresher.refresh(); + } catch (refreshError) { + throw new MatrixAuthenticationUnavailableError(refreshError); + } return await fn(); } throw err; @@ -331,6 +342,7 @@ export class ChatService { try { await this.loadDetails(chat); } catch (err) { + if (err instanceof MatrixAuthenticationUnavailableError) throw err; this.logger.error(`[chat] backfill: failed details for ${chat.roomName}`, err); } done++; @@ -1062,6 +1074,10 @@ export class ChatService { }) as { state_key?: string } | undefined; t2 = otherMember?.state_key?.match(/^@(t2_[^:]+):/)?.[1]; } catch (err) { + if (err instanceof MatrixAuthenticationUnavailableError) { + this.logger.warn('[chat] username enrichment paused until Reddit login is restored'); + return; + } this.logger.debug(`[chat] /state lookup failed for ${chat.roomId}:`, err); } if (!t2) { @@ -1074,6 +1090,10 @@ export class ChatService { )?.sender; t2 = otherSender?.match(/^@(t2_[^:]+):/)?.[1]; } catch (err) { + if (err instanceof MatrixAuthenticationUnavailableError) { + this.logger.warn('[chat] username enrichment paused until Reddit login is restored'); + return; + } this.logger.debug(`[chat] /messages lookup failed for ${chat.roomId}:`, err); } } diff --git a/tests/chat-service-username-persist.test.ts b/tests/chat-service-username-persist.test.ts index a504b5a..afefccb 100644 --- a/tests/chat-service-username-persist.test.ts +++ b/tests/chat-service-username-persist.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import os from 'os'; import path from 'path'; import Database from 'better-sqlite3'; @@ -6,7 +6,7 @@ import { runMigrations } from '../src/database/client.js'; import { RedditUsersRepository } from '../src/database/repositories.js'; 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 { MatrixAuthError, MatrixClient } from '../src/services/chat/matrix/client.js'; import { createMockLogger } from './mockLogger.js'; const creds = { @@ -103,4 +103,54 @@ describe('ChatService username persistence', () => { expect(chats[0].roomName).toBe('alice'); db.close(); }); + + it('aborts username enrichment after one failed token refresh', async () => { + const logger = createMockLogger(); + const cache = tmpCache(logger); + cache.set([ + { + username: 'unknown', + roomName: 'unknown', + lastMessageTime: Date.now(), + lastMessageSnippet: '', + isFromUs: false, + unread: false, + roomId: '!first:reddit.com', + messages: [], + }, + { + username: 'unknown', + roomName: 'unknown', + lastMessageTime: Date.now(), + lastMessageSnippet: '', + isFromUs: false, + unread: false, + roomId: '!second:reddit.com', + messages: [], + }, + ]); + + const matrix = buildMatrix(); + matrix.getRoomFullState = vi.fn().mockRejectedValue(new MatrixAuthError('expired')); + matrix.getRoomMessages = vi.fn().mockRejectedValue(new MatrixAuthError('expired')); + const tokenRefresher = { + refresh: vi.fn().mockRejectedValue(new Error('interactive login required')), + }; + const usernameResolver = { resolveT2Ids: vi.fn().mockResolvedValue({}) }; + const svc = new ChatService( + matrix, + logger, + undefined, + undefined, + tokenRefresher, + usernameResolver, + cache, + ); + + await svc.enrichUsernames(); + + expect(tokenRefresher.refresh).toHaveBeenCalledOnce(); + expect(matrix.getRoomFullState).toHaveBeenCalledOnce(); + expect(matrix.getRoomMessages).not.toHaveBeenCalled(); + }); });