Files
openswarm/backend/tests/test_browser_delete_script.py
T

156 lines
6.7 KiB
Python

"""BrowserDeleteItem tool logic (the resolve JS is proven live; this pins the pure pieces and
the resolve->trusted-click->verify orchestration with a mocked tool runner)."""
import json
import pytest
from backend.apps.agents.browser import browser_delete_script as d
def test_flag_default_off(monkeypatch):
monkeypatch.delenv("OSW_DELETE_SCRIPT", raising=False)
assert d.delete_tool_enabled() is False
monkeypatch.setenv("OSW_DELETE_SCRIPT", "1")
assert d.delete_tool_enabled() is True
def test_expression_embeds_target_json_safe():
# a target with quotes must not break out of the JS string literal
tricky = 'he said "hi"; alert(1)'
expr = d.resolve_expression("more", tricky)
assert json.dumps(tricky) in expr # embedded as a JSON literal
assert 'const TARGET = ' + json.dumps(tricky) in expr
assert "const STEP = \"more\"" in expr
def make_exec(step_results, click_ok=True, verify_after_refresh=None):
"""Tool-runner mock: BrowserEvaluate returns the scripted resolve result for the step it was
called with (read out of the expression); BrowserClickPoint records real-input clicks; the
location probe and BrowserNavigate model the refresh-reverify pass."""
calls = {"clicks": [], "steps": [], "navs": []}
async def execute(tool, params, bid, tid):
if tool == "BrowserEvaluate":
expr = params["expression"]
if "location.href" in expr:
return {"value": {"href": "https://site.test/profile"}}
step = expr.split('const STEP = "', 1)[1].split('"', 1)[0]
calls["steps"].append(step)
if step == "verify" and calls["navs"] and verify_after_refresh is not None:
return {"value": verify_after_refresh}
if step not in step_results: # menu-flow fixtures: no direct control on the item
return {"value": {"ok": False, "stage": step, "optional": True, "msg": "none"}}
return {"value": step_results[step]}
if tool == "BrowserClickPoint":
calls["clicks"].append((params["xPercent"], params["yPercent"]))
return {"ok": True} if click_ok else {"error": "no webview"}
if tool == "BrowserNavigate":
calls["navs"].append(params["url"])
return {"ok": True}
return {"ok": True}
return execute, calls
POS = {"xPct": 50.0, "yPct": 40.0}
@pytest.mark.asyncio
async def test_full_flow_clicks_each_stage_with_real_input():
ex, calls = make_exec({
"more": {"ok": True, "stage": "more", **POS},
"menuitem": {"ok": True, "stage": "menuitem", **POS},
"confirm": {"ok": True, "stage": "confirm", **POS},
"verify": {"ok": True, "stage": "verify"},
})
r = await d.run_delete("coffee notes abc123", "b1", "", ex)
assert r == {"removed": True, "stage": "done", "msg": "item removed"}
assert len(calls["clicks"]) == 3 # more, menuitem, confirm: all real-input clicks
assert calls["steps"][-1] == "verify"
@pytest.mark.asyncio
async def test_verify_still_present_is_honest():
# still-present survives the refresh re-verify too: honest not-removed, one refresh attempted
ex, calls = make_exec({
"more": {"ok": True, "stage": "more", **POS},
"menuitem": {"ok": True, "stage": "menuitem", **POS},
"confirm": {"ok": True, "stage": "confirm", **POS},
"verify": {"ok": False, "stage": "verify"},
})
r = await d.run_delete("coffee notes abc123", "b1", "", ex)
assert r["removed"] is False and r["stage"] == "done"
assert calls["navs"] == ["https://site.test/profile"]
@pytest.mark.asyncio
async def test_stale_tile_flips_removed_after_refresh():
"""Shreddit keeps the dead tile mounted until a reload; the refresh re-verify turns a real
server-side delete into removed=True instead of an honest-but-wrong still-present."""
ex, calls = make_exec({
"more": {"ok": True, "stage": "more", **POS},
"menuitem": {"ok": True, "stage": "menuitem", **POS},
"confirm": {"ok": True, "stage": "confirm", **POS},
"verify": {"ok": False, "stage": "verify"},
}, verify_after_refresh={"ok": True, "stage": "verify"})
r = await d.run_delete("coffee notes abc123", "b1", "", ex)
assert r["removed"] is True
assert len(calls["navs"]) == 1
@pytest.mark.asyncio
async def test_missing_confirm_is_optional_verify_decides():
ex, calls = make_exec({
"more": {"ok": True, "stage": "more", **POS},
"menuitem": {"ok": True, "stage": "menuitem", **POS},
"confirm": {"ok": False, "stage": "confirm", "optional": True, "msg": "no confirm dialog appeared"},
"verify": {"ok": True, "stage": "verify"},
})
r = await d.run_delete("coffee notes abc123", "b1", "", ex)
assert r["removed"] is True
assert len(calls["clicks"]) == 2 # confirm never clicked
@pytest.mark.asyncio
async def test_direct_delete_skips_menu_walk():
"""Row-action sites (Gmail): a literal Delete control on the item is clicked straight; the
menu ladder never runs and a page-wide non-dialog 'Delete' earns NO bonus confirm click."""
ex, calls = make_exec({
"direct": {"ok": True, "stage": "direct", "label": "delete", **POS},
"confirm": {"ok": True, "stage": "confirm", "fromDialog": False, **POS},
"verify": {"ok": True, "stage": "verify"},
})
r = await d.run_delete("note to self abc123", "b1", "", ex)
assert r["removed"] is True
assert len(calls["clicks"]) == 1
assert "more" not in calls["steps"] and "menuitem" not in calls["steps"]
@pytest.mark.asyncio
async def test_direct_delete_confirms_only_in_dialog():
ex, calls = make_exec({
"direct": {"ok": True, "stage": "direct", "label": "move to trash", **POS},
"confirm": {"ok": True, "stage": "confirm", "fromDialog": True, **POS},
"verify": {"ok": True, "stage": "verify"},
})
r = await d.run_delete("note to self abc123", "b1", "", ex)
assert r["removed"] is True
assert len(calls["clicks"]) == 2 # direct + real-dialog confirm
@pytest.mark.asyncio
async def test_stage_failure_reports_stage_and_never_removed():
ex, calls = make_exec({
"more": {"ok": False, "stage": "find", "msg": "target text not on this page"},
})
r = await d.run_delete("coffee notes abc123", "b1", "", ex)
assert r == {"removed": False, "stage": "find", "msg": "target text not on this page"}
assert not calls["clicks"]
@pytest.mark.asyncio
async def test_unreadable_resolve_is_honest_failure():
async def execute(tool, params, bid, tid):
return {"error": "eval blew up"}
r = await d.run_delete("coffee notes abc123", "b1", "", execute)
assert r["removed"] is False and r["stage"] == "eval"