[eric] browser: make a read wait until the page stops growing, not just until it looks long enough

This commit is contained in:
ciregenz
2026-07-28 11:02:27 -07:00
parent b2198776f6
commit b99bc6cc3c
4 changed files with 103 additions and 23 deletions
+67 -2
View File
@@ -14,8 +14,14 @@ class Resp:
class Aux:
def __init__(self, text): self.txt = text; self.messages = self; self.calls = 0
async def create(self, **kw): self.calls += 1; return Resp(self.txt)
def __init__(self, text):
self.txt = text; self.messages = self; self.calls = 0; self.last_page = ""
async def create(self, **kw):
self.calls += 1
# Keep what the aux was actually shown: which page reached it IS the hydration contract.
self.last_page = str(kw.get("messages", [{}])[0].get("content", ""))
return Resp(self.txt)
def tool_returning(text):
@@ -28,6 +34,65 @@ def tool_returning(text):
PAGE = "Tyler Chen\nHe/Him · 1st\nSomething Here\nIrvine, California\nEntrepreneurs First\n" + ("filler " * 200)
def tool_returning_sequence(pages):
"""A page that changes between reads, like an SPA finishing its render."""
seen = []
async def run_tool(name, params, browser_id, tab_id):
assert name == "BrowserGetText"
seen.append(len(seen))
return {"text": pages[min(len(seen) - 1, len(pages) - 1)]}
run_tool.seen = seen
return run_tool
# Chrome that clears the 500-char floor while the actual content is still missing. This is the
# shape that made the bug invisible: it is long enough to look like a real page.
CHROME_ONLY = ("Home Feed My Network Jobs Messaging Notifications Me Work "
"Skip to main content Keyboard shortcuts Close jump menu " * 12)
HYDRATED = CHROME_ONLY + "\nTyler Chen\nSomething Here\nEntrepreneurs First\n" + ("filler " * 200)
def test_waits_for_the_page_to_stop_growing(monkeypatch):
"""The false-clean: a hydrating SPA crosses the char floor on nav and footer chrome long before
the content lands. Answering from that first passing read produces a CONFIDENT WRONG answer,
because the aux reports what it can see and nothing declines, so the INSUFFICIENT retry never
fires. Two reads have to agree the page stopped growing before the aux sees anything."""
monkeypatch.setattr(rs, "P_THIN_SETTLE_S", 0)
monkeypatch.setattr(rs, "P_STABLE_SETTLE_S", 0)
aux = Aux("His title is \"Something Here\".")
tool = tool_returning_sequence([CHROME_ONLY, HYDRATED, HYDRATED])
out = asyncio.run(rs.run_read_script(aux, "m", "find tyler chen's title", "b1", "t1", tool))
assert out == "His title is \"Something Here\"."
assert len(tool.seen) >= 3, "must re-read until two reads agree, not answer off the first"
sent = aux.last_page
assert "Tyler Chen" in sent, "the aux must be handed the HYDRATED page, not the chrome-only one"
def test_a_settled_page_still_answers_without_extra_waiting(monkeypatch):
"""The guard must not turn every read into a slow read: a page that is already done answers as
soon as two reads agree, which is immediately."""
monkeypatch.setattr(rs, "P_THIN_SETTLE_S", 0)
monkeypatch.setattr(rs, "P_STABLE_SETTLE_S", 0)
aux = Aux("answer")
tool = tool_returning_sequence([HYDRATED, HYDRATED])
assert asyncio.run(rs.run_read_script(aux, "m", "q", "b1", "t1", tool)) == "answer"
assert len(tool.seen) == 2, "a settled page costs exactly one confirming re-read"
def test_a_page_that_never_settles_still_answers_from_the_last_read(monkeypatch):
"""Something that keeps streaming forever (a live feed) must not fail closed to the loop just
for being busy; after the read budget we use the fullest page we got."""
monkeypatch.setattr(rs, "P_THIN_SETTLE_S", 0)
monkeypatch.setattr(rs, "P_STABLE_SETTLE_S", 0)
grows = [HYDRATED + ("more " * 200 * i) for i in range(1, 8)]
aux = Aux("answer")
tool = tool_returning_sequence(grows)
assert asyncio.run(rs.run_read_script(aux, "m", "q", "b1", "t1", tool)) == "answer"
assert len(tool.seen) == rs.MAX_READS
def test_flag_gate(monkeypatch):
monkeypatch.delenv("OSW_READ_SCRIPT", raising=False)
assert rs.read_script_enabled() is False
+8 -8
View File
@@ -189,7 +189,7 @@ async def test_borrow_happens_at_the_door_not_only_at_the_wall(monkeypatch):
from backend.apps.agents.browser import browser_agent
seen = []
browser_agent.p_signin_borrowed.discard("x.com")
browser_agent.signin_borrowed.discard("x.com")
monkeypatch.setattr(browser_agent.browser_session_import, "is_enabled", lambda s: True)
monkeypatch.setattr(browser_agent.browser_session_import, "has_importable_session", lambda d: True)
@@ -198,13 +198,13 @@ async def test_borrow_happens_at_the_door_not_only_at_the_wall(monkeypatch):
return si.SessionImportResult(outcome="imported", domain=domain, entries_applied=3)
monkeypatch.setattr(browser_agent.browser_session_import, "import_signin", fake_import)
await browser_agent.p_borrow_signin_before_nav("https://x.com/compose/post", "b1")
await browser_agent.borrow_signin_before_nav("https://x.com/compose/post", "b1")
assert seen == ["x.com"], "navigating to a site must borrow its sign-in first"
# Second navigate to the same site must not re-read the user's browser.
await browser_agent.p_borrow_signin_before_nav("https://x.com/home", "b1")
await browser_agent.borrow_signin_before_nav("https://x.com/home", "b1")
assert seen == ["x.com"], "a borrowed site must not be re-imported on every navigate"
browser_agent.p_signin_borrowed.discard("x.com")
browser_agent.signin_borrowed.discard("x.com")
@pytest.mark.asyncio
@@ -212,11 +212,11 @@ async def test_pre_nav_borrow_respects_the_opt_in(monkeypatch):
"""The door is the busiest path in the whole agent, so the gate has to hold there too."""
from backend.apps.agents.browser import browser_agent
browser_agent.p_signin_borrowed.discard("x.com")
browser_agent.signin_borrowed.discard("x.com")
monkeypatch.setattr(browser_agent.browser_session_import, "is_enabled", lambda s: False)
monkeypatch.setattr(browser_agent.browser_session_import, "has_importable_session",
lambda d: pytest.fail("must not probe the user's browser while opted out"))
await browser_agent.p_borrow_signin_before_nav("https://x.com/home", "b1")
await browser_agent.borrow_signin_before_nav("https://x.com/home", "b1")
@pytest.mark.asyncio
@@ -226,14 +226,14 @@ async def test_wall_handoff_asks_a_human_once_the_door_borrow_did_not_take(monke
silently returning True here would skip the prompt and strand the run."""
from backend.apps.agents.browser import browser_agent
browser_agent.p_signin_borrowed.add("acme.example")
browser_agent.signin_borrowed.add("acme.example")
monkeypatch.setattr(browser_agent.browser_session_import, "is_enabled", lambda s: True)
monkeypatch.setattr(browser_agent.browser_session_import, "import_signin",
lambda d, b: pytest.fail("must not re-import the same values"))
try:
assert await browser_agent.try_borrow_signin("acme.example", "b1", "", "") is False
finally:
browser_agent.p_signin_borrowed.discard("acme.example")
browser_agent.signin_borrowed.discard("acme.example")
def test_import_timeout_outlasts_the_hidden_window_warm():