[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