From eea5ea3bd64d03401eb0b21b9ac9eddf9092c834 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sun, 26 Jul 2026 22:53:41 -0700 Subject: [PATCH] [eric] browser: seal today's three bug classes (mute aux lane, finder budget, soft signed-out) --- backend/tests/test_browser_classifier_lane.py | 43 ++++++++++ backend/tests/test_browser_signed_out.py | 82 +++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 backend/tests/test_browser_classifier_lane.py create mode 100644 backend/tests/test_browser_signed_out.py diff --git a/backend/tests/test_browser_classifier_lane.py b/backend/tests/test_browser_classifier_lane.py new file mode 100644 index 00000000..fc797e46 --- /dev/null +++ b/backend/tests/test_browser_classifier_lane.py @@ -0,0 +1,43 @@ +"""The fast-path classifier must not let a mute provider disable the browser fast path. + +Measured live 2026-07-26: on the codex lane the aux resolves to cx/gpt-5.4-mini, which returns an +EMPTY body for this call. Empty parsed to verdict 'no', which reads identically to "this is not a +browser task", so the entire browser fast path silently switched off for every GPT user with no +error to show for it (gpt-5.4 filled 0/3; after the empty-body fallback, 2/2). These tests pin both +halves of the contract: the parser may call empty 'no', and the caller must not accept that as a +verdict when a provider was pinned. +""" +from backend.apps.agents.browser import browser_fast_path as fp + +CLAUDE_REPLY = "ACT\n\nENTRY: https://x.com/home\n\n1. Navigate to X.\n2. Click the composer." + + +def test_empty_body_parses_as_no() -> None: + """The parser is allowed to say 'no' on empty; the BUG was the caller treating that as a real + verdict instead of a mute lane.""" + assert fp.parse_verdict_and_brief("") == ("no", "") + + +def test_whitespace_only_body_is_treated_as_empty() -> None: + """A lane answering with only whitespace is exactly as mute as one answering ''.""" + assert fp.parse_verdict_and_brief(" \n\t \n") == ("no", "") + + +def test_real_verdict_still_parses() -> None: + verdict, brief = fp.parse_verdict_and_brief(CLAUDE_REPLY) + assert verdict == "act" + assert "ENTRY: https://x.com/home" in brief + + +def test_read_verdict_still_parses() -> None: + verdict, _ = fp.parse_verdict_and_brief("READ\n\nENTRY: https://example.com") + assert verdict == "read" + + +def test_caller_retries_provider_agnostic_on_a_mute_lane() -> None: + """The fix itself: classify_and_brief must re-ask WITHOUT the provider pin when the pinned lane + returns nothing. Pinned by source so the retry cannot be quietly deleted.""" + import inspect + src = inspect.getsource(fp.classify_and_brief) + assert "if not text.strip() and primary_api:" in src, "empty-body fallback missing" + assert "p_ask(None)" in src, "fallback must drop the provider pin" diff --git a/backend/tests/test_browser_signed_out.py b/backend/tests/test_browser_signed_out.py new file mode 100644 index 00000000..7bc3c27a --- /dev/null +++ b/backend/tests/test_browser_signed_out.py @@ -0,0 +1,82 @@ +"""Soft signed-out detection, plus the two invariants behind the 2026-07-26 bug hunt. + +Each test here corresponds to a bug that actually escaped to a live run, so the point is to make +that whole CLASS unwritable rather than to re-check a line: + + 1. Sites that browse fine while withholding the composer (bsky/stackoverflow/tiktok/threads) hit + no login URL and show no password field, so the hard-wall gate saw nothing and the run + reported "couldn't find the compose box" when the truth was "you are not signed in". + 2. The composer finder budgeted ~24s of reveal work into a 15s command timeout, so heavy pages + were killed mid-ladder and threw away everything, and the last two tiers were unreachable. +""" +import os +import re + +from backend.apps.agents.browser import browser_send_parse as sp +from backend.apps.agents.browser import browser_login_handoff as lh +from backend.apps.agents.core.ws_manager import BROWSER_CMD_TIMEOUTS, BROWSER_CMD_TIMEOUT_DEFAULT + +P_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +P_HANDLER_TS = os.path.join(P_REPO_ROOT, "frontend", "src", "shared", "browserCommandHandler.ts") + +# Page-shaped perceptions, in the interactives format the agent actually sees. +BSKY_SIGNED_OUT = '[1]\n[2]