mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-30 21:44:50 +02:00
[eric] browser: make a read wait until the page stops growing, not just until it looks long enough
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user