[eric] browser: fill the one field a disabled submit is waiting on, then re-ask the page

This commit is contained in:
ciregenz
2026-08-04 20:14:50 -07:00
parent 7dc5f58651
commit e445ca3eeb
2 changed files with 105 additions and 3 deletions
@@ -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 "
+61
View File
@@ -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"