fix(browse): honest probe budget, bounded daemon log, single refusal source, liveness + reinstall coverage

Five hardening items in the browse CLI and its tests:

- probeHealthWithBackoff's advertised ~8s budget could really run ~10s: the
  final 2s probe could start 1ms before the deadline, and every call site
  had JUST run a failed probe yet the loop re-probed immediately.
  Iterations now start with the sleep and each probe's timeout clamps to
  the remaining budget (isServerHealthy takes an injectable timeout).
- browse-daemon.log is append-mode across every respawn with no size cap,
  so a crash-respawn loop fills the disk. The path is now built in one
  place (daemonLogPath — the Unix fd path and the Windows launcher string
  had two spellings) and daemon start rotates a >10MB log to
  browse-daemon.log.1, single generation, matching the repo's 10MB
  rotation convention. Rotation is exported + injectable and behaviorally
  unit-tested.
- The two "healthy daemon already running" refusal blocks in connect had
  already drifted (one lost the tabs/cookies/logins explainer) — extracted
  refuseHeadedOverLiveDaemon as the single source.
- process-liveness: pinned the EPERM-means-alive contract (PID 1 on POSIX,
  PID 4 on Windows — signalable-or-EPERM, both alive). A probe that reads
  EPERM as dead is the false negative that leaked agents.
- runBoundedChromiumReinstall had zero coverage: now exercised end-to-end
  against a stub bunx on a prepended PATH — exit 0, install-exit-N with
  stderr tail, the detached group-kill timeout path (child of the child
  dies too), and spawn-error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-16 14:13:15 -07:00
co-authored by Claude Fable 5
parent 412ad5c1f9
commit aa6c73821f
4 changed files with 174 additions and 14 deletions
@@ -54,6 +54,15 @@ describe('process liveness probe (Windows terminal-agent leak)', () => {
expect(isProcessAlive(2147483646)).toBe(false);
});
test('2b. EPERM means ALIVE: an unsignalable-but-existing PID is not dead (T2)', () => {
// signal-0 to a process we lack permission over throws EPERM — the
// process EXISTS, we just can't signal it. Treating EPERM as "dead" is
// the false negative that leaked agents. PID 1 (launchd/init) on POSIX
// and PID 4 (System) on Windows always exist and are either signalable
// or EPERM — both must read as alive.
expect(isProcessAlive(process.platform === 'win32' ? 4 : 1)).toBe(true);
});
test('3. isProcessAlive spawns NO subprocess on ANY platform (signal-0, #1952)', () => {
// The heart of the bug: a liveness probe that forks is slow enough to
// time out, and a timed-out probe silently answers "dead". Signal 0