From f04e48457e04fb314186af349aaa572a299946dd Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 2 Apr 2026 19:13:36 -0700 Subject: [PATCH] feat: sidebar debug visibility + auth race tests - Show attempt count in loading screen ("Connecting... attempt 3") - After 5 failed attempts, show debug details (port, connected, token) so stuck users can see exactly what's failing - Add 4 tests: getPort includes token, tryConnect uses token, dead state exists with MAX_RECONNECT_ATTEMPTS, reconnectAttempts visible --- browse/test/sidebar-ux.test.ts | 38 ++++++++++++++++++++++++++++++++++ extension/sidepanel.js | 19 +++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/browse/test/sidebar-ux.test.ts b/browse/test/sidebar-ux.test.ts index 2765e1e9..23bab9bb 100644 --- a/browse/test/sidebar-ux.test.ts +++ b/browse/test/sidebar-ux.test.ts @@ -1323,3 +1323,41 @@ describe('extension dispatches gstack-extension-ready event', () => { expect(contentSrc).toContain("new CustomEvent('gstack-extension-ready')"); }); }); + +describe('sidebar auth race prevention', () => { + const bgSrc = fs.readFileSync(path.join(ROOT, '..', 'extension', 'background.js'), 'utf-8'); + const spSrc = fs.readFileSync(path.join(ROOT, '..', 'extension', 'sidepanel.js'), 'utf-8'); + + test('getPort response includes authToken (not just port + connected)', () => { + // The auth race: sidepanel calls getPort, gets {port, connected} but no token. + // All subsequent requests fail 401. Token must be in the getPort response. + const getPortHandler = bgSrc.slice( + bgSrc.indexOf("msg.type === 'getPort'"), + bgSrc.indexOf("msg.type === 'setPort'"), + ); + expect(getPortHandler).toContain('token: authToken'); + }); + + test('tryConnect uses token from getPort response', () => { + // Sidepanel must pass resp.token to updateConnection, not null + const start = spSrc.indexOf('function tryConnect()'); + const end = spSrc.indexOf('\ntryConnect();', start); // top-level call after the function + const tryConnectFn = spSrc.slice(start, end); + expect(tryConnectFn).toContain('resp.token'); + expect(tryConnectFn).not.toContain('updateConnection(url, null)'); + }); +}); + +describe('sidebar debug visibility when stuck', () => { + const spSrc = fs.readFileSync(path.join(ROOT, '..', 'extension', 'sidepanel.js'), 'utf-8'); + + test('connection state machine has a dead state with user-visible message', () => { + expect(spSrc).toContain("'dead'"); + expect(spSrc).toContain('MAX_RECONNECT_ATTEMPTS'); + }); + + test('reconnect attempt counter is visible in the UI', () => { + // The banner should show attempt count so user knows something is happening + expect(spSrc).toContain('reconnectAttempts'); + }); +}); diff --git a/extension/sidepanel.js b/extension/sidepanel.js index e39943f0..64c86255 100644 --- a/extension/sidepanel.js +++ b/extension/sidepanel.js @@ -1392,12 +1392,31 @@ document.getElementById('conn-copy').addEventListener('click', () => { }); // Try to connect immediately, retry every 2s until connected +let connectAttempts = 0; function tryConnect() { + connectAttempts++; + const loadingEl = document.getElementById('chat-loading'); + if (loadingEl) { + const detail = connectAttempts <= 1 ? 'Connecting...' + : `Connecting... (attempt ${connectAttempts})`; + const p = loadingEl.querySelector('p'); + if (p) p.textContent = detail; + } chrome.runtime.sendMessage({ type: 'getPort' }, (resp) => { if (resp && resp.port && resp.connected) { const url = `http://127.0.0.1:${resp.port}`; updateConnection(url, resp.token || null); } else { + // Show debug info after 5 failed attempts + if (connectAttempts >= 5 && loadingEl) { + const p = loadingEl.querySelector('p'); + if (p) { + const port = resp?.port || '?'; + const connected = resp?.connected || false; + const hasToken = !!(resp?.token); + p.textContent = `Waiting for server... port:${port} connected:${connected} token:${hasToken} (attempt ${connectAttempts})`; + } + } setTimeout(tryConnect, 2000); } });