mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 01:37:43 +02:00
[eric] browser: fill the one field a disabled submit is waiting on, then re-ask the page
This commit is contained in:
@@ -105,9 +105,50 @@ async def complete_send(
|
||||
# title field is filled, and every run blind-tapped a coordinate and claimed success.
|
||||
# Handing back untouched is what lets the model do the one thing that CAN fix this,
|
||||
# fill the rest of the form, and it costs a run that was never going to send anyway.
|
||||
logger.info(f"[browser-sendscript] submit {str(p_v.get('name'))!r} is present but DISABLED; "
|
||||
f"the form is incomplete, handing to the model without clicking anything")
|
||||
return {"clicked": False, "sent": False, "log": log,
|
||||
# The form is incomplete, and on a two-field form we can usually say WHICH field. The
|
||||
# composer we filled is the compose-shaped one (reddit's "Post text"); the one still
|
||||
# empty is what the submit is waiting on (reddit's "Title"). Fill that too and re-ask.
|
||||
# Measured: reddit's create-post flow could never complete, because the send path is
|
||||
# single-composer by construction and reddit needs a title before its submit enables.
|
||||
#
|
||||
# Deliberately narrow: EXACTLY one other empty textbox, so there is no guessing about
|
||||
# which field gets the payload, and the submit must ENABLE on its own afterwards. If it
|
||||
# stays disabled we fall through to the same honest hand-off as before, so the failure
|
||||
# mode is unchanged and nothing is ever clicked on a form the site still refuses.
|
||||
p_state = await fresh_list()
|
||||
# Exclude the composer BY INDEX, not by looking for the payload in the row: the row
|
||||
# regex captures the accessible NAME only, so a filled box and an empty one are
|
||||
# indistinguishable by name and every field would read as "still empty".
|
||||
p_empty = [(i, n) for i, n in browser_send_parse.P_COMPOSER_ROW_RE.findall(p_state)
|
||||
if int(i) != composer_index
|
||||
and not browser_send_parse.P_AUTH_FIELD_NAME_RE.search(n or "")]
|
||||
if len(p_empty) == 1:
|
||||
p_idx, p_name = int(p_empty[0][0]), p_empty[0][1]
|
||||
logger.info(f"[browser-sendscript] submit disabled and one field is still empty "
|
||||
f"({p_name!r}); filling it and re-checking the submit")
|
||||
await execute_tool("BrowserClickIndex", {"index": p_idx, "text": payload},
|
||||
browser_id, tab_id)
|
||||
log.append({"tool": "BrowserClickIndex", "input": {"index": p_idx, "text": payload},
|
||||
"ok": True, "result_summary": f"filled required field {p_name!r}"[:200],
|
||||
"elapsed_ms": 0})
|
||||
r_ev2 = await execute_tool(
|
||||
"BrowserEvaluate",
|
||||
{"expression": browser_submit_click.container_submit_expression(payload)},
|
||||
browser_id, tab_id)
|
||||
p_v2 = browser_submit_click.parse_eval_value(r_ev2)
|
||||
if isinstance(p_v2, dict) and p_v2.get("ok") and p_v2.get("xPct") is not None:
|
||||
logger.info("[browser-sendscript] the submit enabled once the required field was filled")
|
||||
r_send = await execute_tool(
|
||||
"BrowserClickPoint",
|
||||
{"xPercent": float(p_v2["xPct"]), "yPercent": float(p_v2["yPct"])},
|
||||
browser_id, tab_id)
|
||||
send_name = str(p_v2.get("name") or "submit")
|
||||
via = "container-after-required-field"
|
||||
p_v = p_v2
|
||||
if via == "index" and not (isinstance(p_v, dict) and p_v.get("ok")):
|
||||
logger.info(f"[browser-sendscript] submit {str(p_v.get('name'))!r} is present but DISABLED; "
|
||||
f"the form is incomplete, handing to the model without clicking anything")
|
||||
return {"clicked": False, "sent": False, "log": log,
|
||||
"note": (f"The {str(p_v.get('name')) or 'submit'} button is visible but disabled, so this "
|
||||
f"form is not ready to send: something it requires is still empty (often a "
|
||||
f"title or subject), or the editor never registered the typed text. Fill the "
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""A form whose submit is gated on a second field must still be sendable.
|
||||
|
||||
Measured live 2026-08-04: reddit's create-post flow could never complete. `/r/test/submit` exposes
|
||||
`textbox "Title"` (required, and its submit stays DISABLED until filled) and `textbox "Post text"`.
|
||||
Only the second matches composer vocabulary, so the send script filled the body, the title stayed
|
||||
empty, the submit never enabled, and the run handed off every time. Every other supported surface is
|
||||
a single box, so this was the one shape the single-composer design could not express.
|
||||
|
||||
The rule is deliberately narrow, and the narrowness is the safety:
|
||||
- EXACTLY one other empty non-auth textbox, so there is never a guess about which field to fill
|
||||
- the submit must ENABLE ON ITS OWN afterwards; we re-ask the page rather than assume
|
||||
- if it stays disabled, the old honest hand-off is unchanged and nothing is clicked
|
||||
|
||||
Auth fields are excluded via the same P_AUTH_FIELD_NAME_RE the login-wall gate uses, so a password
|
||||
or email box beside a composer can never be treated as "the field the submit is waiting on".
|
||||
"""
|
||||
|
||||
import inspect
|
||||
|
||||
from backend.apps.agents.browser import browser_send_parse as sp
|
||||
from backend.apps.agents.browser import browser_send_script as ss
|
||||
|
||||
SRC = inspect.getsource(ss)
|
||||
|
||||
|
||||
def p_missing(state, composer_index):
|
||||
"""The selection rule under test, exactly as the send script applies it."""
|
||||
return [(i, n) for i, n in sp.P_COMPOSER_ROW_RE.findall(state)
|
||||
if int(i) != composer_index and not sp.P_AUTH_FIELD_NAME_RE.search(n or "")]
|
||||
|
||||
|
||||
def test_the_reddit_shape_identifies_exactly_one_missing_field():
|
||||
"""The perception verbatim. The composer is excluded BY INDEX: the row regex captures only the
|
||||
accessible name, so a filled box and an empty one look identical by name, and matching on the
|
||||
payload made every field read as still-empty (this test caught that)."""
|
||||
state = '[3]<textbox "Title">\n[7]<textbox "Post text">'
|
||||
missing = p_missing(state, composer_index=7)
|
||||
assert len(missing) == 1 and missing[0][1] == "Title", missing
|
||||
|
||||
|
||||
def test_an_auth_field_is_never_mistaken_for_the_missing_field():
|
||||
"""A password box beside a composer must not be filled with the user's post."""
|
||||
state = '[3]<textbox "Password">\n[7]<textbox "Post text">'
|
||||
assert p_missing(state, composer_index=7) == [], "an auth field must never be the target"
|
||||
|
||||
|
||||
def test_two_empty_fields_stay_ambiguous_and_are_not_guessed():
|
||||
"""Three-field forms are a different problem; guessing which one gates the submit is how you
|
||||
type a post into someone's phone-number box."""
|
||||
state = '[3]<textbox "Title">\n[5]<textbox "Flair">\n[7]<textbox "Post text">'
|
||||
assert len(p_missing(state, composer_index=7)) == 2, "two candidates must not collapse to a pick"
|
||||
|
||||
|
||||
def test_the_submit_must_re_enable_on_its_own_before_any_click():
|
||||
"""The safety property. Filling a field is reversible; clicking submit is not, so the page has
|
||||
to confirm the form is now valid rather than us assuming it."""
|
||||
i = SRC.index("filling it and re-checking the submit")
|
||||
block = SRC[i:i + 1500]
|
||||
assert "container_submit_expression" in block, "must re-ask the page whether the submit enabled"
|
||||
assert block.index("container_submit_expression") < block.index("BrowserClickPoint"), \
|
||||
"the re-check must happen BEFORE the click"
|
||||
Reference in New Issue
Block a user