mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 02:07:45 +02:00
[eric] browser: send-script completes on X/IG/FB (submit-button vocab beyond LinkedIn Send)
This commit is contained in:
@@ -473,13 +473,24 @@ def delta_state(text: str, seen_lines: set[str]) -> str:
|
||||
# A button row whose name is exactly a Send control (not "Send InMail credit" or "Send a message to X"); used to hand the model the Send button after it types, so it never burns turns hunting a button that's right there.
|
||||
P_SEND_ROW_RE = re.compile(r'\[(\d+)\]\*?<\s*button\s+"([^"]*)"', re.I)
|
||||
|
||||
# The submit-button labels across the popular composers, so the fast send-path COMPLETES on
|
||||
# X/IG/FB/Threads/YouTube, not just LinkedIn. EXACT match + button-only (P_SEND_ROW_RE) is the
|
||||
# safety: "Post" never matches "Post a job", a "Share" menuitem isn't a <button so it's excluded,
|
||||
# and every caller is post-fill (a hint, or the send-script's receipt-gated click), so a rare
|
||||
# opener-vs-submit mismatch fails safe (the composer just doesn't clear), never a false send.
|
||||
P_SEND_LABELS = frozenset({
|
||||
"send", "send now", "send message", # LinkedIn / Gmail / DMs
|
||||
"post", "post all", "tweet", "reply", # X / Threads compose + reply
|
||||
"publish", "comment", "share", # articles / YouTube+FB comments / shares
|
||||
})
|
||||
|
||||
|
||||
def send_index_in_state(state_text: str):
|
||||
"""(index, name) of a real Send button in an interactives list, or None.
|
||||
Strict exact match so it never grabs an upsell or a profile 'Send a message' link."""
|
||||
"""(index, name) of a real submit button in an interactives list, or None. Strict exact
|
||||
match against P_SEND_LABELS so it never grabs an upsell or a profile 'Send a message' link."""
|
||||
for line in (state_text or "").splitlines():
|
||||
m = P_SEND_ROW_RE.search(line)
|
||||
if m and m.group(2).strip().lower() in ("send", "send now", "send message"):
|
||||
if m and m.group(2).strip().lower() in P_SEND_LABELS:
|
||||
return int(m.group(1)), m.group(2)
|
||||
return None
|
||||
|
||||
|
||||
@@ -1524,6 +1524,20 @@ def test_send_index_handoff_points_only_at_a_real_send_button():
|
||||
assert send_index_in_state("") is None
|
||||
|
||||
|
||||
def test_send_index_finds_the_popular_composers_submit_buttons():
|
||||
# the generalization: the fast send-path must COMPLETE on X/IG/FB/YouTube, so the submit
|
||||
# finder knows Post/Reply/Tweet/etc, not just LinkedIn's "Send".
|
||||
from backend.apps.agents.browser.browser_agent import send_index_in_state
|
||||
assert send_index_in_state('[3]<textbox "Post your reply">\n[8]<button "Reply">') == (8, "Reply")
|
||||
assert send_index_in_state('[2]<textbox "What is happening?">\n[9]<button "Post">') == (9, "Post")
|
||||
assert send_index_in_state('[4]<button "Tweet">') == (4, "Tweet")
|
||||
assert send_index_in_state('[7]<button "Comment">') == (7, "Comment")
|
||||
# exact + button-only keeps the safety: not a look-alike, not a menuitem, not an opener phrase
|
||||
assert send_index_in_state('[5]<button "Post a job">') is None
|
||||
assert send_index_in_state('[6]<menuitem "Share">') is None # button-only excludes the Drive-Share false positive
|
||||
assert send_index_in_state('[8]<button "Reply to Maya">') is None
|
||||
|
||||
|
||||
def test_strip_lone_surrogates():
|
||||
from backend.apps.agents.browser.browser_agent import strip_lone_surrogates, format_tool_result
|
||||
# an orphan UTF-16 surrogate (half an emoji from the webview) is what crashes the turn at .encode('utf-8'); it must be swapped, not carried through
|
||||
|
||||
@@ -69,8 +69,11 @@ async def test_surface_gate_declines_when_no_composer_in_perception():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_surface_gate_fires_on_a_NON_linkedin_composer():
|
||||
"""The whole generalization: the same send-script fires on ANY site whose
|
||||
perception carries a composer (here X's 'Post your reply'), no per-site URL gate."""
|
||||
"""The whole generalization: the same send-script fires on ANY site whose perception
|
||||
carries a composer (here X's 'Post your reply'), no per-site URL gate, AND completes
|
||||
by clicking X's real submit button ('Reply') BY INDEX. Before the submit-vocabulary
|
||||
was generalized this only 'passed' because the mock succeeds on the by-name 'Send'
|
||||
fallback that doesn't exist on real X, so assert the real index path is taken."""
|
||||
X_FILLED = '[3]<textbox "Post your reply" value="[test] hello world r9-os">\n[8]<button "Reply">'
|
||||
X_SENT = '[3]<textbox "Post your reply">\n[1]<link "Home">'
|
||||
ex, calls = make_exec([X_FILLED, X_FILLED, X_SENT])
|
||||
@@ -78,6 +81,9 @@ async def test_surface_gate_fires_on_a_NON_linkedin_composer():
|
||||
payload_in_textbox, payload_source=TASK,
|
||||
current_url="https://x.com/messages/123")
|
||||
assert r is not None and r["sent"] is True
|
||||
# the send went via BrowserClickIndex on X's real Reply submit (8), not the by-name 'Send' crutch
|
||||
assert ("BrowserClickIndex", {"index": 8}) in calls["clicks"]
|
||||
assert not any(t == "BrowserClickByName" for t, _ in calls["clicks"])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user