Files
gstack/browse/test/sidepanel-patient-autoconnect.test.ts
T
Garry Tan ad669b238a feat(sidebar): patient tryAutoConnect — poll forever with ascending status, abort only on 401
The 15s give-up message ("Browse server not ready. Reload sidebar to retry.")
fired on every cold start where the daemon took >15s to bind — common on
Conductor workspaces, CI runners, and any system under load. The user
already opened the sidebar; telling them to give up is the wrong default.

Now polls every 2s indefinitely with ascending status messages:
  *   0 - 15s : silent (handles the happy path on a warm laptop)
  *  15 - 60s : "Waiting for browse server..."
  *  60s - 5m : "Still waiting — browse server may be slow to start."
  *      > 5m : "Browse server still not responding after 5 min. Try `$B status`."

Loop aborts on three signals only:
  * state transitions out of IDLE (connect succeeded or user navigated)
  * autoConnectAborted sticky flag set on unrecoverable error
  * the panel itself unloading (browser handles this; pagehide cleanup
    arrives with T8 of the larger plan)

401 from /pty-session sets the sticky flag with a clear "Auth invalid —
reload the sidebar or restart your gstack session." message. Without the
flag, the loop would re-call connect() every 2s and spam the same error;
with it, the user sees the message once and the loop holds. forceRestart()
clears the flag so clicking Restart is the explicit "try again" escape hatch.

Bumped poll interval 200ms → 2000ms — the legacy tight loop burned CPU
for no reason. 2s is plenty fast for a "did the daemon come up yet" check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:10:17 -07:00

71 lines
2.9 KiB
TypeScript

import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
// v1.44 patient autoConnect — static-grep invariants for the polling loop.
//
// Pre-v1.44 the sidebar gave up at 15s with "Browse server not ready.
// Reload sidebar to retry." Cold-start the browse server takes ~3-8s on a
// healthy laptop, longer on Conductor workspaces / slow CI, so the user
// frequently saw the failure message even when nothing was wrong. The
// fix: poll forever with ascending status messages and only abort on
// explicit unrecoverable signals (401 auth invalid).
const CLIENT_JS = path.resolve(
new URL(import.meta.url).pathname,
'..',
'..',
'..',
'extension',
'sidepanel-terminal.js',
);
describe('sidepanel tryAutoConnect patience (v1.44+)', () => {
test('1. no 15s give-up message', () => {
const src = fs.readFileSync(CLIENT_JS, 'utf-8');
// The v0.x give-up string must NOT reappear — it's the message users
// saw on every cold start and the whole point of v1.44 was to delete it.
expect(src).not.toContain('Browse server not ready. Reload sidebar to retry.');
});
test('2. ascending status messages at 15s / 60s / 5min', () => {
const src = fs.readFileSync(CLIENT_JS, 'utf-8');
expect(src).toContain('Waiting for browse server...');
expect(src).toContain('Still waiting');
expect(src).toContain('still not responding after 5 min');
});
test('3. sticky abort flag prevents loop spam on 401', () => {
const src = fs.readFileSync(CLIENT_JS, 'utf-8');
expect(src).toContain('autoConnectAborted');
// The mint failure branch must short-circuit on 401 specifically.
expect(src).toMatch(/minted\.error.*startsWith\('401'\)/);
// tryAutoConnect tick must respect the flag.
expect(src).toMatch(/if \(autoConnectAborted\) return/);
});
test('4. forceRestart re-arms the loop by clearing the abort flag', () => {
const src = fs.readFileSync(CLIENT_JS, 'utf-8');
// forceRestart is the user's "try again" escape hatch — must reset
// the sticky flag or 401-once means stuck-forever.
const block = sliceBetween(src, 'function forceRestart', 'function repaintIfLive');
expect(block).toContain('autoConnectAborted = false');
});
test('5. poll interval is 2s, not the legacy 200ms tight loop', () => {
const src = fs.readFileSync(CLIENT_JS, 'utf-8');
// 200ms ticks burned CPU and made the give-up window land too fast.
// 2s is the v1.44 cadence — verify the tight-loop literal is gone.
expect(src).toContain('setTimeout(tick, 2000)');
expect(src).not.toContain('setTimeout(tick, 200)');
});
});
function sliceBetween(source: string, start: string, end: string): string {
const i = source.indexOf(start);
if (i === -1) throw new Error(`marker not found: ${start}`);
const j = source.indexOf(end, i + start.length);
if (j === -1) throw new Error(`end marker not found: ${end}`);
return source.slice(i, j);
}