mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-21 12:12:22 +02:00
146 lines
8.0 KiB
Python
146 lines
8.0 KiB
Python
"""Structural composer finder + reveal-and-find reach + cross-nav retry, verified without a
|
|
live webview: a mock executor scripts BrowserFindComposer results so the send-script's
|
|
structural fallback path (find -> reveal -> re-perceive-on-nav -> fill) is exercised end to
|
|
end, including the flag gating and the safety that reveal only forwards under the flag."""
|
|
import pytest
|
|
|
|
from backend.apps.agents.browser import browser_send_script as ss
|
|
from backend.apps.agents.browser.browser_agent import send_submit_index_in_state, payload_in_textbox
|
|
|
|
TASK = "go to tyler chen's linkedin hes in entrepreneurs first and text him '[test] hello world r9-os'"
|
|
NAMELESS = '[1]<link "Home">\n[2]<button "Search">' # no name-matched composer, no opener
|
|
|
|
|
|
def make_struct_exec(fc_result, list_states=None):
|
|
"""execute_tool mock for the structural-finder path: list calls return no-composer
|
|
states; a BrowserFindComposer call returns the scripted structural result."""
|
|
calls = {"clicks": [], "find": 0}
|
|
states = list(list_states or [NAMELESS] * 6)
|
|
i = {"n": 0}
|
|
|
|
# fc_result may be a single dict (returned every call) or a list of dicts (returned in
|
|
# sequence, last one repeating) to script the cross-nav retry.
|
|
fc_seq = fc_result if isinstance(fc_result, list) else [fc_result]
|
|
|
|
async def execute(tool, params, bid, tid):
|
|
if tool == "BrowserListInteractives":
|
|
s = states[min(i["n"], len(states) - 1)]
|
|
i["n"] += 1
|
|
return {"text": s}
|
|
if tool == "BrowserFindComposer":
|
|
r = fc_seq[min(calls["find"], len(fc_seq) - 1)]
|
|
calls["find"] += 1
|
|
calls["clicks"].append((tool, params))
|
|
return r
|
|
calls["clicks"].append((tool, params))
|
|
return {"ok": True}
|
|
return execute, calls
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_structural_finder_fills_when_name_detector_misses(monkeypatch):
|
|
"""The generalization: no AX-named composer and no opener, but the in-page finder
|
|
ranks an editable region, fills + reads it back, and the script reports filled-ready
|
|
(dry-run stops before the send). This is what unblocks Reddit-style contenteditables."""
|
|
monkeypatch.setenv("OSW_COMPOSER_STRUCT", "1")
|
|
monkeypatch.setenv("OSW_SENDSCRIPT_DRYRUN", "1")
|
|
ex, calls = make_struct_exec({"found": True, "filled": True, "role": "contenteditable",
|
|
"selector": '[data-osw-composer="1"]', "score": 6.2, "nearSubmit": True})
|
|
r = await ss.run_send_script(TASK, "b1", "", NAMELESS, ex, send_submit_index_in_state,
|
|
payload_in_textbox, payload_source=TASK,
|
|
current_url="https://www.reddit.com/r/test/comments/x/")
|
|
assert r is not None and r["sent"] is False # filled, stopped before the send
|
|
assert calls["find"] == 1
|
|
assert any(c[0] == "BrowserFindComposer" for c in calls["clicks"])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_structural_finder_declines_when_no_editable(monkeypatch):
|
|
"""The finder honestly finds nothing usable (a page with only a search box) -> decline
|
|
to the model path, never a false fire."""
|
|
monkeypatch.setenv("OSW_COMPOSER_STRUCT", "1")
|
|
ex, calls = make_struct_exec({"found": False})
|
|
r = await ss.run_send_script(TASK, "b1", "", NAMELESS, ex, send_submit_index_in_state,
|
|
payload_in_textbox, payload_source=TASK, current_url="https://example.com/")
|
|
assert r is None
|
|
assert calls["find"] == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_structural_kill_switch_really_kills_it(monkeypatch):
|
|
"""The finder went default-ON (composer reach 3/9 -> 4/9 on an honest re-sweep), so the thing worth
|
|
pinning is no longer the default but the escape hatch: =0 must fall all the way back to the
|
|
proven name path with the finder never invoked, so a site it upsets is fixable without a build."""
|
|
monkeypatch.setenv("OSW_COMPOSER_STRUCT", "0")
|
|
ex, calls = make_struct_exec({"found": True, "filled": True, "selector": "x", "role": "textarea"})
|
|
r = await ss.run_send_script(TASK, "b1", "", NAMELESS, ex, send_submit_index_in_state,
|
|
payload_in_textbox, payload_source=TASK, current_url="https://example.com/")
|
|
assert r is None
|
|
assert calls["find"] == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reveal_flag_passed_to_finder_when_enabled(monkeypatch):
|
|
"""OSW_COMPOSER_REVEAL=1 lets the finder take a reversible reveal action: the send-script
|
|
must forward reveal=True to BrowserFindComposer (the composer isn't painted yet)."""
|
|
monkeypatch.setenv("OSW_COMPOSER_STRUCT", "1")
|
|
monkeypatch.setenv("OSW_COMPOSER_REVEAL", "1")
|
|
monkeypatch.setenv("OSW_SENDSCRIPT_DRYRUN", "1")
|
|
ex, calls = make_struct_exec({"found": True, "filled": True, "role": "contenteditable",
|
|
"selector": '[data-osw-composer="1"]', "score": 6.0,
|
|
"nearSubmit": True, "reveals": ["trigger"]})
|
|
r = await ss.run_send_script(TASK, "b1", "", NAMELESS, ex, send_submit_index_in_state,
|
|
payload_in_textbox, payload_source=TASK,
|
|
current_url="https://www.linkedin.com/feed/")
|
|
assert r is not None and r["sent"] is False
|
|
find_call = next(c for c in calls["clicks"] if c[0] == "BrowserFindComposer")
|
|
assert find_call[1].get("reveal") is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cross_nav_retry_after_open_first_navigates(monkeypatch):
|
|
"""open-first reveal navigates to the item page (killing the finder's context) so the first
|
|
call finds nothing; the send-script re-perceives and calls the finder a SECOND time on the
|
|
destination, where the composer now fills. Proves the cross-nav retry."""
|
|
monkeypatch.setenv("OSW_COMPOSER_STRUCT", "1")
|
|
monkeypatch.setenv("OSW_COMPOSER_REVEAL", "1")
|
|
monkeypatch.setenv("OSW_SENDSCRIPT_DRYRUN", "1")
|
|
seq = [
|
|
{"found": False, "reveals": ["trigger:noop", "open-first", "scroll"]}, # navigated away
|
|
{"found": True, "filled": True, "role": "contenteditable",
|
|
"selector": '[data-osw-composer="1"]', "score": 6.0, "reveals": ["scroll"]},
|
|
]
|
|
ex, calls = make_struct_exec(seq)
|
|
r = await ss.run_send_script(TASK, "b1", "", NAMELESS, ex, send_submit_index_in_state,
|
|
payload_in_textbox, payload_source=TASK,
|
|
current_url="https://www.reddit.com/r/test")
|
|
assert r is not None and r["sent"] is False
|
|
assert calls["find"] == 2 # retried on the destination
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cross_nav_no_retry_when_reveal_did_not_navigate(monkeypatch):
|
|
"""If the finder did NOT fire open-first (a same-doc trigger/scroll that just failed), there
|
|
is no destination to re-perceive, so it declines after ONE call, no wasted retry."""
|
|
monkeypatch.setenv("OSW_COMPOSER_STRUCT", "1")
|
|
monkeypatch.setenv("OSW_COMPOSER_REVEAL", "1")
|
|
ex, calls = make_struct_exec({"found": False, "reveals": ["trigger:noop", "open-first:noop", "scroll"]})
|
|
r = await ss.run_send_script(TASK, "b1", "", NAMELESS, ex, send_submit_index_in_state,
|
|
payload_in_textbox, payload_source=TASK, current_url="https://example.com/")
|
|
assert r is None
|
|
assert calls["find"] == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_reveal_kill_switch_leaves_the_finder_scanning_only(monkeypatch):
|
|
"""Reveal also went default-ON (it is the only tier that reaches youtube's comment box, which
|
|
exists only after a scroll and a click). =0 must still call the finder but forbid it from
|
|
touching anything: scan what is painted, click no trigger."""
|
|
monkeypatch.setenv("OSW_COMPOSER_STRUCT", "1")
|
|
monkeypatch.setenv("OSW_COMPOSER_REVEAL", "0")
|
|
ex, calls = make_struct_exec({"found": False})
|
|
await ss.run_send_script(TASK, "b1", "", NAMELESS, ex, send_submit_index_in_state,
|
|
payload_in_textbox, payload_source=TASK, current_url="https://example.com/")
|
|
find_call = next(c for c in calls["clicks"] if c[0] == "BrowserFindComposer")
|
|
assert find_call[1].get("reveal") is False
|