From 3f176d222636e34c0ac334662d606ea971b58944 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 18:47:46 -0700 Subject: [PATCH] test(browse): align dual-listener and terminal-agent static guards with the current source Two static-grep guards pinned superseded source shapes and failed once the suite actually ran them: the tunnel dispatch gate is args-aware since the --out disk-write ban (canDispatchOverTunnel takes command AND args), and lazy PTY spawn routes through the maybeSpawnPty helper since v1.44. The updated assertions pin the current, stricter shapes (open() never spawns; the helper is the only spawnClaude caller). Contributed by @time-attack (PR #2230, dual-listener + terminal-agent hunks). Co-Authored-By: Claude Fable 5 --- browse/test/dual-listener.test.ts | 4 +++- browse/test/terminal-agent.test.ts | 22 +++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/browse/test/dual-listener.test.ts b/browse/test/dual-listener.test.ts index 9520fb13f..f78e3a2ec 100644 --- a/browse/test/dual-listener.test.ts +++ b/browse/test/dual-listener.test.ts @@ -220,7 +220,9 @@ describe('/command tunnel command allowlist', () => { 'return handleCommand(body, tokenInfo)' ); expect(commandBlock).toContain("surface === 'tunnel'"); - expect(commandBlock).toContain('canDispatchOverTunnel(body?.command)'); + // Args-aware since the --out (disk write) tunnel ban: the dispatch gate + // takes both the command and its args. + expect(commandBlock).toContain('canDispatchOverTunnel(body?.command, body?.args)'); expect(commandBlock).toContain('disallowed_command'); expect(commandBlock).toContain('is not allowed over the tunnel surface'); expect(commandBlock).toContain('status: 403'); diff --git a/browse/test/terminal-agent.test.ts b/browse/test/terminal-agent.test.ts index d908052d2..18064ce01 100644 --- a/browse/test/terminal-agent.test.ts +++ b/browse/test/terminal-agent.test.ts @@ -144,17 +144,33 @@ describe('Source-level guard: terminal-agent', () => { test('lazy spawn: claude PTY is spawned in message handler, not on upgrade', () => { // The whole point of lazy-spawn (codex finding #8) is that the WS - // upgrade itself does NOT call spawnClaude. Spawn happens on first - // message frame. + // upgrade itself does NOT spawn claude. Spawn happens on first + // message frame (binary input or the v1.44 explicit `start` frame), + // routed through the maybeSpawnPty helper, which is the only caller + // of spawnClaude. const upgradeBlock = AGENT_SRC.slice( AGENT_SRC.indexOf("if (url.pathname === '/ws')"), AGENT_SRC.indexOf("websocket: {"), ); expect(upgradeBlock).not.toContain('spawnClaude('); + expect(upgradeBlock).not.toContain('maybeSpawnPty('); // Spawn must be invoked from the message handler (lazy on first byte). const messageHandler = AGENT_SRC.slice(AGENT_SRC.indexOf('message(ws, raw)')); - expect(messageHandler).toContain('spawnClaude('); + expect(messageHandler).toContain('maybeSpawnPty('); expect(messageHandler).toContain('!session.spawned'); + // The open() upgrade handler must not spawn — it only creates the + // (spawned: false) session record or re-attaches a detached one. + const openBlock = AGENT_SRC.slice( + AGENT_SRC.indexOf('open(ws)'), + AGENT_SRC.indexOf('message(ws, raw)'), + ); + expect(openBlock).not.toContain('spawnClaude('); + expect(openBlock).not.toContain('maybeSpawnPty('); + // And the helper itself is where spawnClaude actually happens, gated + // on session.spawned so it stays a single-shot lazy spawn. + const helperBlock = AGENT_SRC.slice(AGENT_SRC.indexOf('function maybeSpawnPty')); + expect(helperBlock).toContain('spawnClaude('); + expect(helperBlock).toContain('if (session.spawned) return true;'); }); test('process.on uncaughtException + unhandledRejection handlers exist', () => {