[eric] browser: send-script completes on X/IG/FB (submit-button vocab beyond LinkedIn Send)

This commit is contained in:
ciregenz
2026-07-15 11:54:49 -07:00
parent 1f74cc13de
commit 1e68aa5d7b
3 changed files with 36 additions and 5 deletions
+14 -3
View File
@@ -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
+14
View File
@@ -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
+8 -2
View File
@@ -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