mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
152 lines
7.4 KiB
Python
152 lines
7.4 KiB
Python
"""A question about a post must never become another post.
|
|
|
|
A verification prompt quotes the very text it is asking about, so it looks identical to a send task:
|
|
quoted payload plus a composer equals fire. Measured live on a real LinkedIn account 2026-07-28:
|
|
|
|
"say whether anything containing "<text>" is still there. Change nothing."
|
|
|
|
posted <text> to the feed. Only "verify whether" and "check whether" were in the guard, so the
|
|
question shape was matched by two exact phrasings rather than by what it actually is.
|
|
|
|
The guard fails SAFE in the other direction: a false match only means the send goes through the
|
|
model path instead of the script, which costs turns, never a wrong action.
|
|
"""
|
|
from backend.apps.agents.browser import browser_send_parse as sp
|
|
|
|
# Every one of these is somebody asking a QUESTION about content, with the content quoted.
|
|
QUESTIONS = [
|
|
'say whether anything containing "hello there" is still there. Change nothing.',
|
|
'check whether "hello there" is still on my profile',
|
|
'verify whether the post "hello there" went through',
|
|
'tell me if "hello there" is still published',
|
|
'confirm whether "hello there" posted',
|
|
'is "hello there" still there?',
|
|
'is the post "hello there" still live',
|
|
'find out if "hello there" is still up',
|
|
'look for "hello there" without posting anything',
|
|
'this is read-only, do not post: is "hello there" there',
|
|
'do not change anything, just say if "hello there" is present',
|
|
]
|
|
|
|
# Real send intents that must STILL fire the script; over-widening the guard would quietly disable
|
|
# the whole fast write path, which is the failure mode on the other side.
|
|
SENDS = [
|
|
'post this, exactly: "hello there"',
|
|
'tweet "hello there"',
|
|
'message Tyler and say "hello there"',
|
|
'reply to that thread with "hello there"',
|
|
'comment "hello there" on the first post',
|
|
'send "hello there" to my brother on whatsapp',
|
|
'put "hello there" in a new linkedin post',
|
|
]
|
|
|
|
|
|
def test_every_question_shape_declines():
|
|
missed = [q for q in QUESTIONS if not sp.is_readonly(q)]
|
|
assert not missed, f"these questions would be treated as sends: {missed}"
|
|
|
|
|
|
def test_the_exact_prompt_that_posted_for_real_is_caught():
|
|
"""The literal string that put a test post on a real LinkedIn feed."""
|
|
assert sp.is_readonly(
|
|
'Go to linkedin.com and say whether anything containing "x" is still there. Change nothing.')
|
|
|
|
|
|
def test_real_sends_still_fire():
|
|
blocked = [s for s in SENDS if sp.is_readonly(s)]
|
|
assert not blocked, f"the guard swallowed real send intents: {blocked}"
|
|
|
|
|
|
def test_the_guard_is_case_and_spacing_insensitive():
|
|
assert sp.is_readonly('IS "hello" STILL THERE?')
|
|
assert sp.is_readonly("Tell me if it posted")
|
|
|
|
|
|
def test_empty_and_junk_are_not_readonly():
|
|
"""An empty task is not a question; treating it as read-only would silently disable the script
|
|
on a malformed input rather than letting the normal gates decide."""
|
|
assert not sp.is_readonly("")
|
|
assert not sp.is_readonly(" ")
|
|
|
|
|
|
# --- surface targeting: a post is not a comment ---------------------------------------------
|
|
|
|
def test_a_post_task_rejects_a_comment_box():
|
|
"""Measured: on LinkedIn's feed the capped listing starved the post modal of its own composer,
|
|
so the only compose-shaped textbox left was a stranger's comment box. Filling it is the wrong
|
|
action on the wrong content, not a slower route to the right one."""
|
|
assert sp.surface_mismatch('post this, exactly: "hi"', "Text editor for creating comment")
|
|
assert sp.surface_mismatch("start a post saying hi", "Add a comment")
|
|
assert sp.surface_mismatch("tweet hello", "Post your reply")
|
|
|
|
|
|
def test_a_comment_task_keeps_its_comment_box():
|
|
"""One-directional by design: asking to comment must still land in a comment box."""
|
|
assert not sp.surface_mismatch("comment on the first post saying hi", "Text editor for creating comment")
|
|
assert not sp.surface_mismatch("reply to that thread with hi", "Add a comment")
|
|
assert not sp.surface_mismatch("respond to his post", "Post your reply")
|
|
|
|
|
|
def test_a_post_task_keeps_a_real_post_composer():
|
|
assert not sp.surface_mismatch('post this, exactly: "hi"', "Post text")
|
|
assert not sp.surface_mismatch("start a post", "Share your thoughts")
|
|
assert not sp.surface_mismatch("tweet hello", "What is happening?")
|
|
|
|
|
|
def test_a_task_with_no_post_intent_is_left_alone():
|
|
"""Messaging a person is neither posting nor commenting; the guard must not touch it."""
|
|
assert not sp.surface_mismatch("text tyler hello", "Write a message")
|
|
assert not sp.surface_mismatch("", "Add a comment")
|
|
|
|
|
|
# --- the opener is a surface too --------------------------------------------------------------
|
|
|
|
def test_a_post_task_rejects_a_comment_OPENER():
|
|
"""Measured in the dry-run coverage sweep: on linkedin.com with "start a post", the only opener
|
|
listed was 'Comment'. The script opened a stranger's comment box, found no post composer inside
|
|
it, and declined. The composer already had this guard; the opener did not, so the wrong surface
|
|
got opened one step earlier and burned the single reversible-opener hop."""
|
|
assert sp.surface_mismatch("start a post saying hi", "Comment")
|
|
assert sp.surface_mismatch('post this, exactly: "hi"', "Reply")
|
|
|
|
|
|
def test_a_comment_task_keeps_its_comment_opener():
|
|
"""One-directional, same as the composer rule: asking to comment must still open a comment box."""
|
|
assert not sp.surface_mismatch("comment on the first post saying hi", "Comment")
|
|
assert not sp.surface_mismatch("reply to that thread with hi", "Reply")
|
|
|
|
|
|
def test_a_post_task_keeps_a_real_post_opener():
|
|
# "New message" and "Message" used to sit in this list as merely not-a-comment. They are DM
|
|
# openers on every site we drive, and taking one for a post task is how instagram came within
|
|
# a dry-run flag of DMing a stranger. See test_browser_wrong_surface.py.
|
|
for opener in ("Post", "Compose", "Create post", "Write"):
|
|
assert not sp.surface_mismatch('post this, exactly: "hi"', opener), opener
|
|
|
|
|
|
# --- LinkedIn names its editors, and the names are the only thing telling them apart -----------
|
|
|
|
def p_row(name: str) -> str:
|
|
return f'[12]*<textbox "{name}" value="">'
|
|
|
|
|
|
def test_linkedins_post_editor_is_recognised_as_a_composer():
|
|
"""Measured: landing on LinkedIn's own compose surface listed exactly ONE textbox, named
|
|
"Text editor for creating content", and the composer picker scored zero because no pattern
|
|
matched it. The whole site failed 3/3 on a page that was showing the right box."""
|
|
from backend.apps.agents.browser import browser_send_parse as sp
|
|
assert sp.composer_index_in_state(p_row("Text editor for creating content")) is not None
|
|
|
|
|
|
def test_linkedins_comment_editor_is_still_refused_for_a_post_task():
|
|
"""The names differ by one word, so widening the picker must not swallow the comment box.
|
|
It matches now, and surface_mismatch is what rejects it: recognising a box and choosing it are
|
|
different jobs, and only the second one is allowed to be wrong here."""
|
|
from backend.apps.agents.browser import browser_send_parse as sp
|
|
row = p_row("Text editor for creating comment")
|
|
assert sp.composer_index_in_state(row) is not None
|
|
assert sp.surface_mismatch('start a post saying "hello there"',
|
|
"Text editor for creating comment") is True
|
|
assert sp.surface_mismatch('start a post saying "hello there"',
|
|
"Text editor for creating content") is False
|