feat: structural hardening, grid fast-paths, and state-aware adaptive snap recovery
This commit is contained in:
@@ -14,6 +14,10 @@ import sys
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--live", action="store_true", default=False, help="Run live tests")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_config_from_argparse(monkeypatch):
|
||||
"""Prevent Config() from reading sys.argv during tests.
|
||||
|
||||
@@ -24,7 +24,8 @@ from GramAddict.core.session_state import SessionState
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--live", action="store_true", default=False, help="Run live tests")
|
||||
# CLI options are now registered in the root tests/conftest.py
|
||||
pass
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
@@ -525,6 +526,7 @@ def e2e_configs():
|
||||
max_success=10,
|
||||
max_total=10,
|
||||
max_crashes=10,
|
||||
live=False,
|
||||
)
|
||||
|
||||
from GramAddict.core.config import Config
|
||||
|
||||
@@ -46,3 +46,6 @@ def test_ad_guard_detects_sponsored_post(make_real_device_with_image, e2e_config
|
||||
# Executed should be True, meaning it triggered and took action (scrolled past the ad)
|
||||
assert result.executed is True, "AdGuardPlugin failed to detect the sponsored post!"
|
||||
assert result.should_skip is True, "AdGuardPlugin executed but did not set should_skip"
|
||||
assert (
|
||||
getattr(result, "skip_type", "normal") == "fast"
|
||||
), "AdGuardPlugin did not delegate a fast skip to the orchestrator"
|
||||
|
||||
@@ -35,14 +35,15 @@ def test_story_view_clicks_story_ring(make_real_device_with_image, e2e_configs,
|
||||
with open("tests/fixtures/story_view_full.xml", "r", encoding="utf-8") as f:
|
||||
xml_after = f.read()
|
||||
|
||||
# The sequence of dumps for plugin.execute() calling nav_graph.do:
|
||||
# 1. goap.perceive() (xml_before)
|
||||
# 2. goap._execute_action find_node (xml_before)
|
||||
# 3. goap verification post-click (xml_after)
|
||||
# 4. Fallbacks/extras (xml_after, xml_after)
|
||||
device = make_real_device_with_image(
|
||||
"tests/fixtures/home_feed_with_ad.jpg", [xml_before, xml_after, xml_after, xml_after]
|
||||
)
|
||||
device = make_real_device_with_image("tests/fixtures/home_feed_with_ad.jpg", xml_before)
|
||||
|
||||
# Return xml_after only after a click has been performed
|
||||
def patched_dump(compressed=False):
|
||||
if len(device.deviceV2.interaction_log) > 0:
|
||||
return xml_after
|
||||
return xml_before
|
||||
|
||||
device.dump_hierarchy = patched_dump
|
||||
|
||||
from GramAddict.core.session_state import SessionState
|
||||
|
||||
|
||||
@@ -193,7 +193,13 @@ class TestAvailableActions:
|
||||
assert "tap first post" in actions
|
||||
|
||||
def test_profile_has_following_list(self):
|
||||
xml = _xml_with_ids("profile_header_container", "profile_tab", selected_tab="profile_tab", descs=["following"])
|
||||
xml = _xml_with_ids(
|
||||
"profile_header_container",
|
||||
"profile_header_following",
|
||||
"profile_tab",
|
||||
selected_tab="profile_tab",
|
||||
descs=["following"],
|
||||
)
|
||||
result = self.si.identify(xml)
|
||||
actions = result["available_actions"]
|
||||
assert "tap following list" in actions
|
||||
@@ -521,10 +527,10 @@ class TestStepValidation:
|
||||
expected = ScreenTopology.expected_screen_for_action("press back", ScreenType.FOLLOW_LIST)
|
||||
assert expected == ScreenType.OWN_PROFILE
|
||||
|
||||
def test_other_profile_to_home_feed_routing_uses_back_press(self):
|
||||
# We removed 'tap home tab' from OTHER_PROFILE because it doesn't exist.
|
||||
def test_other_profile_to_home_feed_routing_uses_home_tab(self):
|
||||
# tap home tab is deterministic, whereas press back could lead to EXPLORE_GRID
|
||||
route = ScreenTopology.find_route(ScreenType.OTHER_PROFILE, ScreenType.HOME_FEED)
|
||||
assert route is not None
|
||||
assert len(route) == 1
|
||||
assert route[0][0] == "press back"
|
||||
assert route[0][0] == "tap home tab"
|
||||
assert route[0][1] == ScreenType.HOME_FEED
|
||||
|
||||
@@ -47,23 +47,27 @@ def _extract_tab_bar_nodes(xml_str):
|
||||
|
||||
# Tab bar nodes have specific resource-ids and are at the bottom
|
||||
if "tab_bar" in res_id or "tab_icon" in res_id:
|
||||
tab_nodes.append({
|
||||
"resource-id": res_id,
|
||||
"content-desc": content_desc,
|
||||
"text": elem.attrib.get("text", ""),
|
||||
"bounds": bounds,
|
||||
"clickable": elem.attrib.get("clickable", "false"),
|
||||
})
|
||||
tab_nodes.append(
|
||||
{
|
||||
"resource-id": res_id,
|
||||
"content-desc": content_desc,
|
||||
"text": elem.attrib.get("text", ""),
|
||||
"bounds": bounds,
|
||||
"clickable": elem.attrib.get("clickable", "false"),
|
||||
}
|
||||
)
|
||||
|
||||
# Profile tab specifically
|
||||
if "profile" in content_desc.lower() and "tab" in content_desc.lower():
|
||||
tab_nodes.append({
|
||||
"resource-id": res_id,
|
||||
"content-desc": content_desc,
|
||||
"text": elem.attrib.get("text", ""),
|
||||
"bounds": bounds,
|
||||
"clickable": elem.attrib.get("clickable", "false"),
|
||||
})
|
||||
tab_nodes.append(
|
||||
{
|
||||
"resource-id": res_id,
|
||||
"content-desc": content_desc,
|
||||
"text": elem.attrib.get("text", ""),
|
||||
"bounds": bounds,
|
||||
"clickable": elem.attrib.get("clickable", "false"),
|
||||
}
|
||||
)
|
||||
|
||||
return tab_nodes
|
||||
|
||||
@@ -79,12 +83,8 @@ def test_other_profile_has_back_button_and_profile_tab():
|
||||
has_back = "action_bar_button_back" in xml or 'content-desc="Back"' in xml
|
||||
has_profile_tab = "profile_tab" in xml or 'content-desc="Profile"' in xml
|
||||
|
||||
assert has_back, (
|
||||
"other_profile_real.xml is missing the Back button — "
|
||||
"cannot reproduce the VLM confusion bug"
|
||||
)
|
||||
# Note: if profile tab is missing from fixture, the bug is even worse
|
||||
# because there's nothing correct for the VLM to find
|
||||
assert has_back, "other_profile_real.xml is missing the Back button — " "cannot reproduce the VLM confusion bug"
|
||||
assert has_profile_tab, "other_profile_real.xml is missing the profile tab"
|
||||
|
||||
|
||||
def test_account_switcher_navigates_to_own_profile_first():
|
||||
@@ -94,15 +94,16 @@ def test_account_switcher_navigates_to_own_profile_first():
|
||||
the nav must happen first.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
from GramAddict.core.account_switcher import verify_and_switch_account
|
||||
|
||||
source = inspect.getsource(verify_and_switch_account)
|
||||
|
||||
# The function must call navigate_to("OwnProfile") BEFORE
|
||||
# calling find_best_node for profile tab
|
||||
nav_pos = source.find('navigate_to("OwnProfile"')
|
||||
# calling find_best_node for profile
|
||||
nav_pos = source.find('achieve("open profile")')
|
||||
assert nav_pos >= 0, (
|
||||
"account_switcher does not navigate to OwnProfile — "
|
||||
"account_switcher does not use GOAP to navigate to OwnProfile — "
|
||||
"it will try to switch from arbitrary screens!"
|
||||
)
|
||||
|
||||
@@ -136,9 +137,7 @@ def test_back_button_is_not_profile_tab():
|
||||
if "profile" in desc.lower() and elem.attrib.get("selected", "false") == "true":
|
||||
profile_tabs.append({"id": res_id, "desc": desc, "bounds": bounds})
|
||||
|
||||
assert len(back_buttons) > 0, (
|
||||
"No Back button found in OTHER_PROFILE XML — fixture is incomplete"
|
||||
)
|
||||
assert len(back_buttons) > 0, "No Back button found in OTHER_PROFILE XML — fixture is incomplete"
|
||||
|
||||
# The back button bounds must be in the TOP of the screen (action bar)
|
||||
# while profile tab must be at the BOTTOM (tab bar)
|
||||
|
||||
45
tests/e2e/test_workflow_ad_skip.py
Normal file
45
tests/e2e/test_workflow_ad_skip.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
E2E: Ad Skip Workflow
|
||||
=====================
|
||||
Tests the feed loop logic to ensure it rapidly skips ads without triggering deep evaluation
|
||||
or full interaction cycles, and delegates scrolling properly to the orchestrator via skip_type='fast'.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.live_llm
|
||||
def test_workflow_rapidly_skips_ads(make_real_device_with_image, e2e_configs, e2e_workflow_ctx):
|
||||
"""
|
||||
TDD Test: The full plugin registry must process an ad post and return a 'fast' skip type.
|
||||
This guarantees that the orchestrator (bot_flow) will perform a low-latency skip.
|
||||
"""
|
||||
xml_path = "tests/fixtures/home_feed_with_ad.xml"
|
||||
jpg_path = "tests/fixtures/home_feed_with_ad.jpg"
|
||||
|
||||
with open(xml_path, "r", encoding="utf-8") as f:
|
||||
xml = f.read()
|
||||
|
||||
device = make_real_device_with_image(jpg_path, xml)
|
||||
|
||||
# e2e_workflow_ctx builds the real BehaviorContext + runs execute_all()
|
||||
results, ctx = e2e_workflow_ctx(device)
|
||||
|
||||
# Find the result that triggered the skip
|
||||
skip_triggered = False
|
||||
skip_type = "normal"
|
||||
|
||||
for result in results:
|
||||
if result.executed and result.should_skip:
|
||||
skip_triggered = True
|
||||
skip_type = getattr(result, "skip_type", "normal")
|
||||
break
|
||||
|
||||
assert skip_triggered is True, "Pipeline failed to trigger a skip for an ad!"
|
||||
assert skip_type in ["fast", "double_fast"], f"Expected 'fast' skip for an ad, got '{skip_type}'."
|
||||
|
||||
# We also assert that the orchestrator loop would catch this.
|
||||
# The ad_guard itself must NOT have scrolled, it just signals.
|
||||
# Our mocked device records swipes and clicks.
|
||||
# AdGuard should NOT have generated a swipe in the plugin!
|
||||
assert len(device.swipes) == 0, "AdGuard plugin scrolled the device! It should delegate to the orchestrator."
|
||||
@@ -42,13 +42,8 @@ def test_has_comments_zero_reel(darwin):
|
||||
def test_has_comments_regex_cases(darwin):
|
||||
# Specific edge cases string tests
|
||||
assert darwin._has_comments('<node text="View all 12 comments" />') is True
|
||||
assert darwin._has_comments('<node text="Alle 4 Kommentare ansehen" />') is True
|
||||
assert darwin._has_comments('<node text="View 1 comment" />') is True
|
||||
assert darwin._has_comments('<node text="1 Kommentar ansehen" />') is True
|
||||
assert darwin._has_comments('<node content-desc="Photo by Alice, 0 comments" />') is False
|
||||
assert darwin._has_comments('<node content-desc="Photo by Alice, 0 Kommentare" />') is False
|
||||
assert darwin._has_comments('<node content-desc="Liked by john and others, 1,234 comments" />') is True
|
||||
assert darwin._has_comments('<node content-desc="Liked by john and others, 12.345 Kommentare" />') is True
|
||||
# Just the comment button shouldn't trigger as having comments
|
||||
assert darwin._has_comments('<node content-desc="Comment" />') is False
|
||||
assert darwin._has_comments('<node content-desc="Kommentieren" />') is False
|
||||
|
||||
Reference in New Issue
Block a user