From 28d32d21beb239fe53db30801fae16c28d6d374e Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Tue, 31 Mar 2026 03:53:34 -0700 Subject: [PATCH] feat: closeSession skips DELETE for remote sessions - Guard DELETE /api/sessions/current with if (!_viewingSourceUrl) check - Reset _viewingSourceUrl to '' after the conditional DELETE - For remote sessions, the remote instance doesn't need to know we stopped watching - just disconnect WebSocket and update local UI Tests added: - closeSession does NOT fire DELETE for remote session (non-empty _viewingSourceUrl) - closeSession still fires DELETE /api/sessions/current for local session Task: task-8-close-session-remote-aware --- muxplex/frontend/app.js | 7 ++- muxplex/frontend/tests/test_app.mjs | 76 +++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 542b1b2..5326634 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -1190,8 +1190,11 @@ function closeSession() { if (window._closeTerminal) window._closeTerminal(); - // Fire-and-forget DELETE - api('DELETE', '/api/sessions/current').catch(() => {}); + // Fire-and-forget DELETE — skip for remote sessions (they don't need to know we stopped watching) + if (!_viewingSourceUrl) { + api('DELETE', '/api/sessions/current').catch(function() {}); + } + _viewingSourceUrl = ''; const expanded = $('view-expanded'); const overview = $('view-overview'); diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 44ac523..a0a33de 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -1168,6 +1168,82 @@ test('closeSession fires DELETE /api/sessions/current', async () => { globalThis.document.getElementById = origGetById; }); +test('closeSession does NOT fire DELETE for remote session (non-empty _viewingSourceUrl)', async () => { + const origFetch = globalThis.fetch; + const origGetById = globalThis.document.getElementById; + const origQS = globalThis.document.querySelector; + const origSetTimeout = globalThis.setTimeout; + + // Setup to call openSession with remote sourceUrl + globalThis.fetch = async () => ({ ok: true }); + globalThis.document.getElementById = () => ({ textContent: '', style: {}, classList: { remove: () => {}, add: () => {} } }); + globalThis.document.querySelector = () => null; + globalThis.setTimeout = () => {}; + globalThis.window._openTerminal = () => {}; + globalThis.window._closeTerminal = () => {}; + + // Open a remote session - this sets _viewingSourceUrl = 'http://work:8088' + await app.openSession('remote-sess', { sourceUrl: 'http://work:8088' }); + + // Restore setTimeout so Promise-based yielding works + globalThis.setTimeout = origSetTimeout; + + // Reset fetch tracking + const fetchCalls = []; + globalThis.fetch = async (url, opts) => { fetchCalls.push({ url, opts }); return { ok: true }; }; + + // Close session + await app.closeSession(); + // yield microtask queue for any fire-and-forget calls + await new Promise((r) => setTimeout(r, 0)); + + const deleteCall = fetchCalls.find((c) => c.url === '/api/sessions/current' && c.opts && c.opts.method === 'DELETE'); + assert.ok(!deleteCall, 'closeSession should NOT fire DELETE for remote session'); + + globalThis.fetch = origFetch; + globalThis.document.getElementById = origGetById; + globalThis.document.querySelector = origQS; + globalThis.setTimeout = origSetTimeout; +}); + +test('closeSession still fires DELETE /api/sessions/current for local session', async () => { + const origFetch = globalThis.fetch; + const origGetById = globalThis.document.getElementById; + const origQS = globalThis.document.querySelector; + const origSetTimeout = globalThis.setTimeout; + + // Setup to call openSession without sourceUrl (local session) + globalThis.fetch = async () => ({ ok: true }); + globalThis.document.getElementById = () => ({ textContent: '', style: {}, classList: { remove: () => {}, add: () => {} } }); + globalThis.document.querySelector = () => null; + globalThis.setTimeout = () => {}; + globalThis.window._openTerminal = () => {}; + globalThis.window._closeTerminal = () => {}; + + // Open a local session - this sets _viewingSourceUrl = '' + await app.openSession('local-sess', {}); + + // Restore setTimeout so Promise-based yielding works + globalThis.setTimeout = origSetTimeout; + + // Reset fetch tracking + const fetchCalls = []; + globalThis.fetch = async (url, opts) => { fetchCalls.push({ url, opts }); return { ok: true }; }; + + // Close session + await app.closeSession(); + // yield microtask queue for fire-and-forget DELETE + await new Promise((r) => setTimeout(r, 0)); + + const deleteCall = fetchCalls.find((c) => c.url === '/api/sessions/current' && c.opts && c.opts.method === 'DELETE'); + assert.ok(deleteCall, 'closeSession should fire DELETE /api/sessions/current for local session'); + + globalThis.fetch = origFetch; + globalThis.document.getElementById = origGetById; + globalThis.document.querySelector = origQS; + globalThis.setTimeout = origSetTimeout; +}); + // ─── Command Palette ─────────────────────────────────────────────────────────