mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
192 lines
9.4 KiB
Python
192 lines
9.4 KiB
Python
from backend.apps.agents.browser.browser_fast_path import (
|
|
normalize_for_classifier,
|
|
parse_verdict_and_brief,
|
|
compose_task,
|
|
dispatch_failed,
|
|
fast_path_eligible,
|
|
recovery_task,
|
|
)
|
|
|
|
|
|
def test_browsy_first_messages_are_eligible():
|
|
for p in (
|
|
"go to tyler chen's linkedin hes in entrepreneurs first and text him 'hi'",
|
|
"open hacker news and tell me the top story",
|
|
"look up on amazon how much a herman miller aeron costs",
|
|
"check https://example.com/pricing and summarize the tiers",
|
|
):
|
|
assert fast_path_eligible(p, "agent", "dash1", True, False), p
|
|
|
|
|
|
def test_non_browsy_or_gated_messages_fall_through():
|
|
assert not fast_path_eligible("write me a poem about autumn", "agent", "dash1", True, False)
|
|
assert not fast_path_eligible("fix the bug in agent_manager.py", "agent", "dash1", True, False)
|
|
browsy = "open hacker news and tell me the top story"
|
|
assert not fast_path_eligible(browsy, "chat", "dash1", True, False)
|
|
assert not fast_path_eligible(browsy, "agent", None, True, False)
|
|
assert not fast_path_eligible(browsy, "agent", "dash1", False, False)
|
|
assert not fast_path_eligible(browsy, "agent", "dash1", True, True)
|
|
assert not fast_path_eligible("", "agent", "dash1", True, False)
|
|
|
|
|
|
def test_verdict_parsing_is_strict():
|
|
v, brief = parse_verdict_and_brief("READ\nENTRY: https://news.ycombinator.com\n1. read top story")
|
|
assert v == "read" and brief.startswith("ENTRY:") and "top story" in brief
|
|
assert parse_verdict_and_brief("ACT\nENTRY: https://x.com")[0] == "act"
|
|
assert parse_verdict_and_brief("yes") == ("act", "")
|
|
assert parse_verdict_and_brief("NO") == ("no", "")
|
|
assert parse_verdict_and_brief("Maybe\nENTRY: x") == ("no", "")
|
|
assert parse_verdict_and_brief("") == ("no", "")
|
|
long_brief = "ACT\n" + "x" * 2000
|
|
assert len(parse_verdict_and_brief(long_brief)[1]) == 700
|
|
|
|
|
|
def test_fast_read_entry_extraction_and_thin_detection():
|
|
from backend.apps.agents.browser.browser_fast_read import extract_entry_url, page_is_thin
|
|
brief = "ENTRY: https://news.ycombinator.com/\n1. Load the page\n2. Read the top story"
|
|
assert extract_entry_url(brief) == "https://news.ycombinator.com/"
|
|
assert extract_entry_url("1. just steps, no entry") == ""
|
|
assert extract_entry_url("entry: HTTPS://example.com/a).") == "HTTPS://example.com/a"
|
|
assert page_is_thin("")
|
|
assert page_is_thin("HTTP error 403 fetching https://x.com")
|
|
assert page_is_thin("Refused to fetch http://169.254.169.254: private address")
|
|
assert page_is_thin("Title\n\nshort body")
|
|
assert not page_is_thin("Title\n\n" + "real content " * 60)
|
|
|
|
|
|
def test_compose_task_keeps_user_words_first():
|
|
assert compose_task("go to hn", "") == "go to hn"
|
|
composed = compose_task("go to hn", "ENTRY: https://news.ycombinator.com")
|
|
assert composed.startswith("go to hn\n\n[routing brief")
|
|
assert composed.endswith("ENTRY: https://news.ycombinator.com")
|
|
|
|
|
|
def test_dispatch_failure_detection_is_fail_closed():
|
|
# The result dict's structured `done` is the signal now (set true only when the sub-agent called Done with success AND the honesty gate agreed).
|
|
assert dispatch_failed({})
|
|
assert dispatch_failed(None)
|
|
assert dispatch_failed({"summary": "Error: browser card was deleted"})
|
|
assert dispatch_failed({"summary": "couldn't find the thread", "done": False})
|
|
assert dispatch_failed({"summary": "the browser became unresponsive", "error": "x"})
|
|
assert dispatch_failed({"summary": "clicked around a bit"}) # no done -> failed
|
|
assert not dispatch_failed({"summary": "Sent it, it's in the thread now.", "done": True})
|
|
assert not dispatch_failed({"summary": "Done, I took care of that for you.", "done": True})
|
|
|
|
|
|
def test_recovery_task_verifies_before_repeating():
|
|
t = recovery_task("text bob 'hi' on linkedin", "OUTCOME: NOT DONE - hung before confirming send")
|
|
assert "text bob 'hi' on linkedin" in t
|
|
assert "hung before confirming" in t
|
|
assert "do NOT repeat it" in t
|
|
assert "no report (the browser died)" in recovery_task("go to hn", "")
|
|
|
|
|
|
def test_text_normalizes_to_message_without_phone_number():
|
|
assert (
|
|
normalize_for_classifier("go to maya's linkedin and text her thanks")
|
|
== "go to maya's linkedin and message her thanks"
|
|
)
|
|
assert normalize_for_classifier("keep texting until he replies").startswith("keep message")
|
|
sms = "text 4085551234 saying im running late"
|
|
assert normalize_for_classifier(sms) == sms
|
|
count = "count messages containing the exact text r10-os"
|
|
assert "message r10-os" in normalize_for_classifier(count)
|
|
|
|
|
|
def test_dispatch_refused_when_no_dashboard_connected(monkeypatch):
|
|
import asyncio
|
|
from backend.apps.agents.browser.browser_agent import run_browser_agents
|
|
from backend.apps.agents.core import ws_manager as wsm
|
|
|
|
# Dispatch now waits briefly for a momentary WS drop to reconnect; with a genuinely-closed window that wait just elapses and it still refuses without dispatching an agent or burning a turn. Zero the wait so the test is instant.
|
|
monkeypatch.setattr(wsm, "P_WS_RECONNECT_WAIT_S", 0.0)
|
|
assert not wsm.ws_manager.global_connections
|
|
results = asyncio.run(run_browser_agents(tasks=[{"task": "go to example.com"}], model="sonnet"))
|
|
assert len(results) == 1
|
|
assert results[0]["summary"].startswith("Error: no dashboard window is connected")
|
|
assert dispatch_failed(results[0])
|
|
|
|
|
|
def test_send_probe_verdict_parsing_order_and_fail_closed():
|
|
from backend.apps.agents.browser.browser_fast_path import probe_verdict
|
|
assert probe_verdict("OUTCOME: PAYLOAD-NOT-FOUND") == "not-found"
|
|
assert probe_verdict("checked thread. OUTCOME: PAYLOAD-FOUND at 10:43 PM") == "found"
|
|
assert probe_verdict("the browser became unresponsive") == "unknown"
|
|
assert probe_verdict("") == "unknown"
|
|
# a report quoting BOTH tokens must not read as found
|
|
assert probe_verdict("PAYLOAD-FOUND? no: PAYLOAD-NOT-FOUND") == "not-found"
|
|
|
|
|
|
def test_send_probe_task_is_read_only_and_names_payload():
|
|
from backend.apps.agents.browser.browser_fast_path import send_probe_task
|
|
t = send_probe_task("dm tyler", "[test] hello r45-os")
|
|
assert "READ-ONLY" in t and "[test] hello r45-os" in t
|
|
assert "PAYLOAD-FOUND" in t and "PAYLOAD-NOT-FOUND" in t
|
|
|
|
|
|
def test_recovery_task_clearance_only_when_verified():
|
|
from backend.apps.agents.browser.browser_fast_path import recovery_task
|
|
hedged = recovery_task("dm tyler", "browser died")
|
|
cleared = recovery_task("dm tyler", "browser died", verified_undelivered=True)
|
|
assert "FIRST verify" in hedged and "NOT yet delivered" not in hedged
|
|
assert "NOT yet delivered" in cleared and "exactly ONCE" in cleared
|
|
|
|
|
|
def test_send_probe_replies_are_honest():
|
|
from backend.apps.agents.browser.browser_fast_path import (
|
|
already_sent_reply, unverifiable_reply,
|
|
)
|
|
a = already_sent_reply("[test] hi r46-os", "OUTCOME: PAYLOAD-FOUND at 11:02 PM")
|
|
assert "did NOT send it again" in a and "r46-os" in a
|
|
u = unverifiable_reply("[test] hi r46-os", "browser became unresponsive")
|
|
assert "not retrying" in u.lower() and "r46-os" in u
|
|
|
|
|
|
def test_entry_url_extracted_from_brief():
|
|
from backend.apps.agents.browser.browser_fast_path import entry_url_from_brief
|
|
brief = (
|
|
"ENTRY: https://www.linkedin.com/search/results/people/?keywords=tyler%20chen\n"
|
|
"1. Open the first matching profile\n2. Click Message\n3. Type the text"
|
|
)
|
|
assert entry_url_from_brief(brief) == (
|
|
"https://www.linkedin.com/search/results/people/?keywords=tyler%20chen"
|
|
)
|
|
# case-insensitive, mid-brief, trailing punctuation stripped
|
|
assert entry_url_from_brief("steps...\nentry: https://news.ycombinator.com/.") == "https://news.ycombinator.com/"
|
|
assert entry_url_from_brief("no entry line here") == ""
|
|
assert entry_url_from_brief("") == ""
|
|
# never a non-http scheme
|
|
assert entry_url_from_brief("ENTRY: javascript:alert(1)") == ""
|
|
|
|
|
|
def test_results_url_shapes():
|
|
from backend.apps.agents.browser.browser_agent import RESULTS_URL_RE
|
|
hits = [
|
|
"https://www.linkedin.com/search/results/people/?keywords=tyler+chen",
|
|
"https://www.google.com/search?q=anything",
|
|
"https://www.reddit.com/search/?q=cats",
|
|
"https://example.com/find?term=x",
|
|
]
|
|
misses = [
|
|
"https://www.linkedin.com/in/tylerchen1200/",
|
|
"https://news.ycombinator.com/",
|
|
"https://www.linkedin.com/messaging/thread/abc123/",
|
|
]
|
|
for u in hits:
|
|
assert RESULTS_URL_RE.search(u), u
|
|
for u in misses:
|
|
assert not RESULTS_URL_RE.search(u), u
|
|
|
|
|
|
def test_connected_integration_names_route_native_not_browser():
|
|
"""ENG-225: 'make a Slack skill' with Slack connected must NOT read browser-ish; the same words
|
|
with nothing connected keep the cookie-borrow browser path."""
|
|
from backend.apps.agents.browser import browser_fast_path as fp
|
|
haik = ("Send a note in the Miscellaneous Slack channel. Better yet, make a Slack skill that "
|
|
"activates every time any of the Slack tools are called and prepends [AI].")
|
|
assert fp.browsy_beyond_connected(haik, ["slack"]) is False
|
|
assert fp.browsy_beyond_connected(haik, []) is True
|
|
assert fp.browsy_beyond_connected("go to slack.com and read the pricing page", ["slack"]) is True
|
|
assert fp.browsy_beyond_connected("add a row to my Notion tracker", ["notion"]) is False
|
|
assert fp.browsy_beyond_connected("send a linkedin message to my recruiter", ["slack", "notion"]) is True
|