From e95a8d42824d59ae12241347ef54e793c300c8c1 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Mon, 30 Mar 2026 23:09:08 -0700 Subject: [PATCH] refactor: improve pollSessions error handling and test coverage --- muxplex/frontend/app.js | 9 +- muxplex/frontend/tests/test_app.mjs | 145 ++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 4a24aef..459c4bf 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -173,7 +173,9 @@ async function api(method, path, body, baseUrl) { } const res = await fetch(url, opts); if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`); + const err = new Error(`HTTP ${res.status}: ${res.statusText}`); + err.status = res.status; + throw err; } return res; } @@ -248,7 +250,7 @@ function setConnectionStatus(level) { */ async function pollSessions() { // Falls back to local-only if _sources is empty - if (!_sources || _sources.length === 0) { + if (_sources.length === 0) { try { const res = await api('GET', '/api/sessions'); const sessions = await res.json(); @@ -284,8 +286,7 @@ async function pollSessions() { delete source.nextRetryAt; return { source, sessions }; } catch (err) { - const msg = err.message || ''; - if (msg.includes('401') || msg.includes('403')) { + if (err.status === 401 || err.status === 403) { source.status = 'auth_required'; } else { source.status = 'unreachable'; diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 5c050c4..cc26083 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -2190,4 +2190,149 @@ test('pollSessions fetches from all sources and merges results', async () => { app._setSources([]); }); +test('pollSessions skips sources with nextRetryAt in the future', async () => { + const mockStatusEl = { textContent: '', className: '' }; + const mockGrid = { innerHTML: '' }; + const mockEmptyState = { style: {}, classList: { add: () => {}, remove: () => {} } }; + + const origGetById = globalThis.document.getElementById; + const origQSA = globalThis.document.querySelectorAll; + globalThis.document.getElementById = (id) => { + if (id === 'connection-status') return mockStatusEl; + if (id === 'session-grid') return mockGrid; + if (id === 'empty-state') return mockEmptyState; + return null; + }; + globalThis.document.querySelectorAll = () => []; + + const futureRetry = Date.now() + 60000; // 60s in the future + app._setSources([ + { url: '', name: 'Local', type: 'local', status: 'authenticated', backoffMs: 2000 }, + { + url: 'https://remote.example.com', + name: 'Remote', + type: 'remote', + status: 'unreachable', + backoffMs: 4000, + nextRetryAt: futureRetry, + }, + ]); + + const fetchCalls = []; + globalThis.fetch = async (url) => { + fetchCalls.push(url); + return { ok: true, json: async () => [{ name: 'local-session' }] }; + }; + + await app.pollSessions(); + + assert.ok(fetchCalls.some((url) => url === '/api/sessions'), 'should fetch local sessions'); + assert.ok( + !fetchCalls.some((url) => url === 'https://remote.example.com/api/sessions'), + 'should skip remote source still in backoff', + ); + + globalThis.document.getElementById = origGetById; + globalThis.document.querySelectorAll = origQSA; + globalThis.fetch = undefined; + app._setSources([]); +}); + +test('pollSessions sets auth_required on 401 response', async () => { + const mockStatusEl = { textContent: '', className: '' }; + const mockGrid = { innerHTML: '' }; + const mockEmptyState = { style: {}, classList: { add: () => {}, remove: () => {} } }; + + const origGetById = globalThis.document.getElementById; + const origQSA = globalThis.document.querySelectorAll; + globalThis.document.getElementById = (id) => { + if (id === 'connection-status') return mockStatusEl; + if (id === 'session-grid') return mockGrid; + if (id === 'empty-state') return mockEmptyState; + return null; + }; + globalThis.document.querySelectorAll = () => []; + + const sources = [ + { url: '', name: 'Local', type: 'local', status: 'authenticated', backoffMs: 2000 }, + { + url: 'https://remote.example.com', + name: 'Remote', + type: 'remote', + status: 'authenticated', + backoffMs: 2000, + }, + ]; + app._setSources(sources); + + globalThis.fetch = async (url) => { + if (url === '/api/sessions') { + return { ok: true, json: async () => [{ name: 'local-session' }] }; + } + // Remote returns 401 + return { ok: false, status: 401, statusText: 'Unauthorized' }; + }; + + await app.pollSessions(); + + const remoteSrc = sources.find((s) => s.url === 'https://remote.example.com'); + assert.strictEqual(remoteSrc.status, 'auth_required', 'remote source should be marked auth_required'); + assert.ok(!remoteSrc.nextRetryAt, 'auth_required should not set nextRetryAt'); + + globalThis.document.getElementById = origGetById; + globalThis.document.querySelectorAll = origQSA; + globalThis.fetch = undefined; + app._setSources([]); +}); + +test('pollSessions sets unreachable and applies exponential backoff on network error', async () => { + const mockStatusEl = { textContent: '', className: '' }; + const mockGrid = { innerHTML: '' }; + const mockEmptyState = { style: {}, classList: { add: () => {}, remove: () => {} } }; + + const origGetById = globalThis.document.getElementById; + const origQSA = globalThis.document.querySelectorAll; + globalThis.document.getElementById = (id) => { + if (id === 'connection-status') return mockStatusEl; + if (id === 'session-grid') return mockGrid; + if (id === 'empty-state') return mockEmptyState; + return null; + }; + globalThis.document.querySelectorAll = () => []; + + const sources = [ + { url: '', name: 'Local', type: 'local', status: 'authenticated', backoffMs: 2000 }, + { + url: 'https://remote.example.com', + name: 'Remote', + type: 'remote', + status: 'authenticated', + backoffMs: 2000, + }, + ]; + app._setSources(sources); + + const beforePoll = Date.now(); + globalThis.fetch = async (url) => { + if (url === '/api/sessions') { + return { ok: true, json: async () => [] }; + } + return { ok: false, status: 500, statusText: 'Internal Server Error' }; + }; + + await app.pollSessions(); + + const remoteSrc = sources.find((s) => s.url === 'https://remote.example.com'); + assert.strictEqual(remoteSrc.status, 'unreachable', 'remote source should be marked unreachable'); + // backoffMs doubles from 2000 → 4000 + assert.strictEqual(remoteSrc.backoffMs, 4000, 'backoff should double from 2000 to 4000'); + assert.ok(remoteSrc.nextRetryAt > beforePoll, 'nextRetryAt should be set to a future timestamp'); + assert.ok(remoteSrc.nextRetryAt <= beforePoll + 4000 + 100, 'nextRetryAt should be roughly now + 4000ms'); + + globalThis.document.getElementById = origGetById; + globalThis.document.querySelectorAll = origQSA; + globalThis.fetch = undefined; + app._setSources([]); +}); +