mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
96 lines
5.1 KiB
Python
96 lines
5.1 KiB
Python
"""Soft signed-out detection, plus the two invariants behind the 2026-07-26 bug hunt.
|
|
|
|
Each test here corresponds to a bug that actually escaped to a live run, so the point is to make
|
|
that whole CLASS unwritable rather than to re-check a line:
|
|
|
|
1. Sites that browse fine while withholding the composer (bsky/stackoverflow/tiktok/threads) hit
|
|
no login URL and show no password field, so the hard-wall gate saw nothing and the run
|
|
reported "couldn't find the compose box" when the truth was "you are not signed in".
|
|
2. The composer finder budgeted ~24s of reveal work into a 15s command timeout, so heavy pages
|
|
were killed mid-ladder and threw away everything, and the last two tiers were unreachable.
|
|
"""
|
|
import os
|
|
import re
|
|
|
|
from backend.apps.agents.browser import browser_send_parse as sp
|
|
from backend.apps.agents.browser import browser_login_handoff as lh
|
|
from backend.apps.agents.core.ws_manager import BROWSER_CMD_TIMEOUTS, BROWSER_CMD_TIMEOUT_DEFAULT
|
|
|
|
P_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
P_HANDLER_TS = os.path.join(P_REPO_ROOT, "frontend", "src", "shared", "browserCommandHandler.ts")
|
|
|
|
# Page-shaped perceptions, in the interactives format the agent actually sees.
|
|
BSKY_SIGNED_OUT = '[1]<link "Sign in">\n[2]<button "Create account">\n[3]<heading "Discover">'
|
|
# The live shape that broke the first draft: a LOGGED-OUT page still advertising "Notifications".
|
|
# Vetoing on that word suppressed the detector on exactly the sites it exists for (bsky, 0 cookies,
|
|
# read as signed-in). Only controls meaningless-unless-authenticated may veto.
|
|
BSKY_SIGNED_OUT_WITH_NAV = ('[1]<link "Notifications">\n[2]<button "Sign in">\n'
|
|
'[3]<button "Create account">')
|
|
HN_SIGNED_OUT = '[1]<textbox "title">\n[2]<link "login">\n[3]<button "submit">'
|
|
SO_SIGNED_OUT = '[4]<link "Log in">\n[5]<link "Sign up">\n[6]<heading "Questions">'
|
|
X_SIGNED_IN = '[1]<button "Profile">\n[2]<textbox "What is happening?">\n[9]<button "Post">'
|
|
# A signed-IN page that still advertises a sign-up somewhere: the veto must win, or we would tell
|
|
# an authenticated user to log in again.
|
|
SIGNED_IN_WITH_PROMO = '[1]<link "Sign up">\n[2]<button "Sign out">\n[3]<textbox "Post text">'
|
|
LINKEDIN_FEED = '[1]<button "Start a post">\n[2]<link "Notifications">\n[3]<link "My Network">'
|
|
|
|
|
|
def test_soft_signed_out_pages_are_detected():
|
|
assert sp.looks_signed_out(BSKY_SIGNED_OUT)
|
|
assert sp.looks_signed_out(SO_SIGNED_OUT)
|
|
assert sp.looks_signed_out(HN_SIGNED_OUT)
|
|
|
|
|
|
def test_logged_out_page_advertising_notifications_still_reads_signed_out():
|
|
"""Regression for the veto that was too broad: a signed-OUT page may still show Notifications,
|
|
Profile or Inbox links, so those must never veto. Only 'sign out'-class controls may."""
|
|
assert sp.looks_signed_out(BSKY_SIGNED_OUT_WITH_NAV)
|
|
|
|
|
|
def test_signed_in_pages_are_never_called_signed_out():
|
|
for state in (X_SIGNED_IN, SIGNED_IN_WITH_PROMO, LINKEDIN_FEED, ""):
|
|
assert not sp.looks_signed_out(state), state[:40]
|
|
|
|
|
|
def test_signed_in_marker_vetoes_a_sign_in_link():
|
|
"""The whole point of the veto: 'Sign up' present AND 'Sign out' present means signed IN."""
|
|
assert not sp.looks_signed_out(SIGNED_IN_WITH_PROMO)
|
|
|
|
|
|
def test_soft_detection_is_separate_from_the_hard_wall():
|
|
"""A soft page must NOT read as a hard login wall: the hard gate declines the send outright,
|
|
and mislabelling here would change behaviour on pages we can still act on."""
|
|
assert not sp.looks_like_login_wall("https://bsky.app/", BSKY_SIGNED_OUT)
|
|
assert sp.looks_signed_out(BSKY_SIGNED_OUT)
|
|
|
|
|
|
def test_handoff_ignores_soft_pages_unless_explicitly_allowed():
|
|
"""allow_soft is off by default because the pause interrupts the user; the caller turns it on
|
|
only once the agent is demonstrably stuck."""
|
|
assert lh.login_wall_domain("https://bsky.app/", BSKY_SIGNED_OUT) is None
|
|
assert lh.login_wall_domain("https://bsky.app/", BSKY_SIGNED_OUT, allow_soft=True) == "bsky.app"
|
|
|
|
|
|
def test_handoff_still_catches_hard_walls_without_soft():
|
|
assert lh.login_wall_domain("https://www.instagram.com/accounts/login/", "") == "instagram.com"
|
|
|
|
|
|
def test_find_composer_timeout_exceeds_its_own_in_page_budget():
|
|
"""INVARIANT that seals the linkedin bug: the reveal ladder self-caps at a deadline, and the
|
|
command carrying it must outlast that deadline. If someone lowers the timeout (or raises the
|
|
in-page budget) past each other, heavy pages silently die mid-ladder again."""
|
|
budget_ms = p_in_page_budget_ms()
|
|
timeout_s = BROWSER_CMD_TIMEOUTS.get("find_composer", BROWSER_CMD_TIMEOUT_DEFAULT)
|
|
assert timeout_s * 1000 > budget_ms, (
|
|
f"find_composer timeout {timeout_s}s must exceed the in-page deadline {budget_ms}ms")
|
|
|
|
|
|
def p_in_page_budget_ms() -> int:
|
|
"""The deadline the in-page finder gives itself, read from the renderer source so the two sides
|
|
of the invariant can never drift apart silently."""
|
|
with open(P_HANDLER_TS, encoding="utf-8") as fh:
|
|
src = fh.read()
|
|
m = re.search(r"const DEADLINE = Date\.now\(\) \+ (\d+)", src)
|
|
assert m, "in-page finder deadline not found; did the reveal ladder change?"
|
|
return int(m.group(1))
|