fix(feed): resolve home feed back-button scroll-to-top trap
- Separated obstacle detection from feed marker validation - Prevented blind BACK button presses when markers are missing mid-scroll - Added TDD verification for feed navigation stability - Cleaned up debug artifacts and temporary test output
This commit is contained in:
86
tests/tdd/test_home_feed_back_button_trap.py
Normal file
86
tests/tdd/test_home_feed_back_button_trap.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop
|
||||
|
||||
def test_feed_markers_missing_prevents_back_button_trap():
|
||||
"""
|
||||
TDD Test: When the bot is on the feed but no feed markers (like buttons) are visible
|
||||
(e.g., due to a tall image or mid-scroll), it must NOT press the Android 'back' button,
|
||||
because pressing back on the Home Feed forces a jump to the top of the feed and a refresh.
|
||||
It should only press back if it explicitly detects an obstacle (e.g., a bottom sheet).
|
||||
"""
|
||||
device = MagicMock()
|
||||
# Return XML that has NO feed markers and NO obstacles
|
||||
device.dump_hierarchy.return_value = '<?xml version="1.0"?><hierarchy><node text="some tall post" /></hierarchy>'
|
||||
|
||||
configs = MagicMock()
|
||||
configs.args.ignore_close_friends = False
|
||||
configs.args.carousel_percentage = 0
|
||||
configs.args.interaction_users_amount = "1"
|
||||
|
||||
# We want to break the loop after one pass. We can patch _humanized_scroll to raise an Exception.
|
||||
class LoopBreak(Exception): pass
|
||||
|
||||
with patch("GramAddict.core.bot_flow._humanized_scroll", side_effect=LoopBreak) as mock_scroll:
|
||||
with patch("GramAddict.core.bot_flow.sleep"):
|
||||
with patch("GramAddict.core.bot_flow.is_ad", return_value=False):
|
||||
with patch("GramAddict.core.bot_flow.TelepathicEngine") as MockEng:
|
||||
MockEng.get_instance.return_value._extract_semantic_nodes.return_value = [1] # prevent zero-node crash
|
||||
|
||||
dopamine = MagicMock()
|
||||
dopamine.is_app_session_over.return_value = False
|
||||
dopamine.wants_to_doomscroll.return_value = False
|
||||
cog_stack = {"dopamine": dopamine}
|
||||
|
||||
try:
|
||||
zero_engine = MagicMock()
|
||||
nav_graph = MagicMock()
|
||||
session_state = MagicMock()
|
||||
session_state.check_limit.return_value = [False, False]
|
||||
_run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, "home_feed", cog_stack)
|
||||
except LoopBreak:
|
||||
pass
|
||||
|
||||
# It must NOT press back, because it's just lost in the feed without explicit obstacles.
|
||||
try:
|
||||
device.press.assert_not_called()
|
||||
except AssertionError:
|
||||
pytest.fail("Agent incorrectly pressed BACK when no obstacle was present. This triggers the scroll-to-top trap!")
|
||||
mock_scroll.assert_called_once()
|
||||
|
||||
def test_explicit_obstacle_triggers_back_button():
|
||||
"""
|
||||
TDD Test: When the bot detects an explicit obstacle (e.g., dialog_container),
|
||||
it MUST press the back button to try and dismiss it.
|
||||
"""
|
||||
device = MagicMock()
|
||||
# Return XML that HAS an obstacle
|
||||
device.dump_hierarchy.return_value = '<?xml version="1.0"?><hierarchy><node resource-id="dialog_container" /></hierarchy>'
|
||||
|
||||
configs = MagicMock()
|
||||
configs.args.ignore_close_friends = False
|
||||
|
||||
class LoopBreak(Exception): pass
|
||||
|
||||
with patch("GramAddict.core.bot_flow.sleep"):
|
||||
with patch("GramAddict.core.bot_flow.is_ad", return_value=False):
|
||||
with patch("GramAddict.core.bot_flow.TelepathicEngine") as MockEng:
|
||||
MockEng.get_instance.return_value._extract_semantic_nodes.return_value = [1]
|
||||
dopamine = MagicMock()
|
||||
dopamine.is_app_session_over.return_value = False
|
||||
dopamine.wants_to_doomscroll.return_value = False
|
||||
cog_stack = {"dopamine": dopamine}
|
||||
|
||||
try:
|
||||
zero_engine = MagicMock()
|
||||
nav_graph = MagicMock()
|
||||
session_state = MagicMock()
|
||||
session_state.check_limit.return_value = [False, False]
|
||||
# Make device.press raise LoopBreak so we can verify it was called and break the infinite loop
|
||||
device.press.side_effect = LoopBreak
|
||||
_run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, "home_feed", cog_stack)
|
||||
except LoopBreak:
|
||||
pass
|
||||
|
||||
# device.press("back") SHOULD be called
|
||||
device.press.assert_called_with("back")
|
||||
@@ -16,7 +16,7 @@ def test_modal_guard_blocks_nav_intent_on_failed_xml():
|
||||
with open(FAILED_XML_PATH, "r") as f:
|
||||
xml_content = f.read()
|
||||
|
||||
engine = TelepathicEngine.get_instance()
|
||||
engine = TelepathicEngine()
|
||||
|
||||
# Intent that SHOULD be blocked because Home Tab is obscured by the comment sheet
|
||||
intent = "tap home tab"
|
||||
@@ -36,7 +36,7 @@ def test_zone_enforcement_blocks_mid_screen_tab_hallucination():
|
||||
Tests that even if VLM is triggered (e.g. no modal detected but low confidence),
|
||||
any result for a 'tab' intent that is in the middle of the screen is rejected.
|
||||
"""
|
||||
engine = TelepathicEngine.get_instance()
|
||||
engine = TelepathicEngine()
|
||||
|
||||
# Minimal XML
|
||||
xml = "<?xml version='1.0' ?><hierarchy><node index='0' text='Add comment' bounds='[42,2224][1038,2350]' visible-to-user='true' /></hierarchy>"
|
||||
|
||||
@@ -68,7 +68,7 @@ def test_reels_loop_repost_execution(mock_device):
|
||||
<node resource-id="com.instagram.android:id/direct_share_button" content-desc="Share" />
|
||||
</hierarchy>'''
|
||||
|
||||
mock_device.deviceV2.dump_hierarchy.return_value = reels_xml
|
||||
mock_device.dump_hierarchy.return_value = reels_xml
|
||||
mock_device.dump_hierarchy.return_value = reels_xml
|
||||
|
||||
# Repost Sheet XML
|
||||
@@ -111,7 +111,7 @@ def test_reels_loop_repost_execution(mock_device):
|
||||
return state['current']
|
||||
|
||||
mock_device.dump_hierarchy.side_effect = side_effect_func
|
||||
mock_device.deviceV2.dump_hierarchy.side_effect = side_effect_func
|
||||
mock_device.dump_hierarchy.side_effect = side_effect_func
|
||||
|
||||
# We need to change the state when transition is called
|
||||
original_execute = mock_cognitive_stack["nav_graph"]._execute_transition
|
||||
|
||||
Reference in New Issue
Block a user