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:
@@ -16,7 +16,7 @@ def mock_device():
|
||||
device.deviceV2 = MagicMock()
|
||||
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
|
||||
# Link facade method to deviceV2 mock so old tests keep working
|
||||
device.dump_hierarchy = device.deviceV2.dump_hierarchy
|
||||
device.dump_hierarchy = device.dump_hierarchy
|
||||
return device
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ def test_align_active_post(mock_device):
|
||||
</hierarchy>'''
|
||||
res = _align_active_post(mock_device)
|
||||
# The header is at 850px. Target is 250px. Diff is 600px. It should swipe.
|
||||
assert mock_device.deviceV2.swipe.called
|
||||
assert mock_device.swipe.called
|
||||
|
||||
def test_feed_loop_boredom_change_feed(mock_device, mock_cognitive_stack):
|
||||
mock_cognitive_stack["dopamine"].is_app_session_over.return_value = False
|
||||
@@ -74,7 +74,7 @@ def test_feed_loop_context_lost(mock_device, mock_cognitive_stack):
|
||||
mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
|
||||
mock_cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
|
||||
|
||||
mock_device.deviceV2.dump_hierarchy.return_value = "<hierarchy></hierarchy>" # Blind
|
||||
mock_device.dump_hierarchy.return_value = "<hierarchy></hierarchy>" # Blind
|
||||
|
||||
# Needs telepathic engine mock
|
||||
with patch('GramAddict.core.bot_flow.TelepathicEngine') as MockTelepathic, patch('GramAddict.core.bot_flow.dump_ui_state'):
|
||||
@@ -105,7 +105,7 @@ def test_feed_loop_zero_nodes(mock_device, mock_cognitive_stack):
|
||||
|
||||
_run_zero_latency_feed_loop(mock_device, mock_cognitive_stack["zero_engine"], mock_cognitive_stack["nav_graph"], configs, session_state, "HomeFeed", mock_cognitive_stack)
|
||||
|
||||
assert mock_device.deviceV2.press.called_with("back")
|
||||
assert mock_device.press.called_with("back")
|
||||
assert mock_scroll.called
|
||||
|
||||
def test_feed_loop_ad_skip(mock_device, mock_cognitive_stack):
|
||||
@@ -113,7 +113,7 @@ def test_feed_loop_ad_skip(mock_device, mock_cognitive_stack):
|
||||
mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
|
||||
mock_cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
|
||||
|
||||
mock_device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
mock_device.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
<hierarchy>
|
||||
<node resource-id="com.instagram.android:id/row_feed_button_like" />
|
||||
<node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="ad_account" />
|
||||
@@ -142,7 +142,7 @@ def test_stories_loop_success(mock_device, mock_cognitive_stack):
|
||||
configs.args.stories = "1"
|
||||
session_state = MagicMock()
|
||||
|
||||
mock_device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
mock_device.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
<hierarchy>
|
||||
<node resource-id="com.instagram.android:id/story_viewer" />
|
||||
</hierarchy>'''
|
||||
@@ -162,7 +162,7 @@ def test_stories_loop_boredom(mock_device, mock_cognitive_stack):
|
||||
with patch('GramAddict.core.bot_flow.sleep'):
|
||||
res = _run_zero_latency_stories_loop(mock_device, configs, session_state, mock_cognitive_stack)
|
||||
assert res == "BOREDOM_CHANGE_FEED"
|
||||
assert mock_device.deviceV2.press.called_with("back")
|
||||
assert mock_device.press.called_with("back")
|
||||
|
||||
def test_start_bot_interrupt():
|
||||
from GramAddict.core.bot_flow import start_bot
|
||||
@@ -215,7 +215,7 @@ def test_feed_loop_deep_engagement(mock_device, mock_cognitive_stack):
|
||||
session_state.check_limit.side_effect = lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
|
||||
|
||||
# Needs to report a structure that has NO ad, HAS content, and HAS feed markers.
|
||||
mock_device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
mock_device.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
<hierarchy>
|
||||
<node resource-id="com.instagram.android:id/row_feed_button_like" />
|
||||
<node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="legit_user" />
|
||||
@@ -271,7 +271,7 @@ def test_feed_loop_repost(mock_device, mock_cognitive_stack):
|
||||
session_state = MagicMock()
|
||||
session_state.check_limit.side_effect = lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
|
||||
|
||||
mock_device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
mock_device.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
<hierarchy>
|
||||
<node resource-id="com.instagram.android:id/row_feed_button_like" />
|
||||
<node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="legit_user" />
|
||||
@@ -311,7 +311,7 @@ def test_profile_learning_percentage_trigger(mock_device, mock_cognitive_stack):
|
||||
session_state = MagicMock()
|
||||
session_state.check_limit.side_effect = lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
|
||||
|
||||
mock_device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
mock_device.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
<hierarchy>
|
||||
<node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="legit_user" />
|
||||
<node resource-id="com.instagram.android:id/row_feed_photo_imageview" content-desc="test image" />
|
||||
|
||||
@@ -79,7 +79,7 @@ def test_execute_proof_of_resonance_close_comments():
|
||||
|
||||
# Simulated UI Dump: No 'bottom_sheet_container', but neither 'row_feed' nor 'button_like'
|
||||
# Which occurs when IG renames it to 'fragment_container_view' or similar wrapper
|
||||
device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
device.dump_hierarchy.return_value = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy>
|
||||
<node class="android.widget.FrameLayout" bounds="[0,0][1080,2400]">
|
||||
<!-- Random UI generic sheet classes the Bot doesn't track -->
|
||||
@@ -95,4 +95,4 @@ def test_execute_proof_of_resonance_close_comments():
|
||||
# Assert: Instead of checking string names for "bottom_sheet_container",
|
||||
# it should verify the presence of 'row_feed' to confirm we are back in Home!
|
||||
# If not in Home, it presses back twice.
|
||||
assert device.deviceV2.press.call_count == 2
|
||||
assert device.press.call_count == 2
|
||||
|
||||
@@ -89,7 +89,7 @@ def test_ghost_typing_stealth_chunking():
|
||||
_adb_inject_text(mock_device, "hello world")
|
||||
|
||||
# Assert space was correctly mapped to %s for native consumption
|
||||
mock_device.deviceV2.shell.assert_called_with(["input", "text", "hello%sworld"])
|
||||
mock_device.shell.assert_called_with(["input", "text", "hello%sworld"])
|
||||
|
||||
def test_ghost_typing_special_character_escaping():
|
||||
"""
|
||||
@@ -101,4 +101,4 @@ def test_ghost_typing_special_character_escaping():
|
||||
_adb_inject_text(mock_device, "it's cool")
|
||||
|
||||
# assert single quote was escaped
|
||||
mock_device.deviceV2.shell.assert_called_with(["input", "text", "it\\'s%scool"])
|
||||
mock_device.shell.assert_called_with(["input", "text", "it\\'s%scool"])
|
||||
|
||||
@@ -34,7 +34,7 @@ def test_recovery_from_dm_view(mock_device):
|
||||
call_counts["dumps"] += 1
|
||||
|
||||
# If app_start hasn't been called, we are still locked in the DM screen
|
||||
if not mock_device.deviceV2.app_start.called:
|
||||
if not mock_device.app_start.called:
|
||||
return dm_xml
|
||||
else:
|
||||
# After forced app_start, we land on Home.
|
||||
@@ -69,6 +69,6 @@ def test_recovery_from_dm_view(mock_device):
|
||||
assert success is True
|
||||
assert nav.current_state == "ReelsFeed"
|
||||
# Verify hard recovery was triggered
|
||||
mock_device.deviceV2.app_start.assert_called_with("com.instagram.android", use_monkey=True)
|
||||
mock_device.app_start.assert_called_with("com.instagram.android", use_monkey=True)
|
||||
# 15 perception dumps + 15 execute dumps + verified dumps + retry dumps
|
||||
assert call_counts["dumps"] >= 16
|
||||
|
||||
@@ -12,7 +12,7 @@ def test_qnavgraph_same_state_navigation_bug():
|
||||
mock_device.deviceV2 = MagicMock()
|
||||
# Mock search tab selected (ExploreFeed)
|
||||
mock_device.dump_hierarchy.return_value = '<hierarchy><node package="com.instagram.android" resource-id="com.instagram.android:id/search_tab" selected="true" /></hierarchy>'
|
||||
mock_device.deviceV2.dump_hierarchy.return_value = '<hierarchy><node package="com.instagram.android" resource-id="com.instagram.android:id/search_tab" selected="true" /></hierarchy>'
|
||||
mock_device.dump_hierarchy.return_value = '<hierarchy><node package="com.instagram.android" resource-id="com.instagram.android:id/search_tab" selected="true" /></hierarchy>'
|
||||
|
||||
with patch('GramAddict.core.goap.GoalExecutor._instance', None), \
|
||||
patch('GramAddict.core.goap.ScreenIdentity._classify_screen', return_value=__import__('GramAddict.core.goap', fromlist=['ScreenType']).ScreenType.EXPLORE_GRID), \
|
||||
@@ -26,7 +26,7 @@ def test_qnavgraph_same_state_navigation_bug():
|
||||
graph = QNavGraph(mock_device)
|
||||
graph.current_state = "ExploreFeed"
|
||||
graph.navigate_to("ExploreFeed", zero_engine=None)
|
||||
mock_device.deviceV2.app_start.assert_not_called()
|
||||
mock_device.app_start.assert_not_called()
|
||||
|
||||
def test_qnavgraph_semantic_recovery_any_state():
|
||||
"""
|
||||
@@ -43,7 +43,7 @@ def test_qnavgraph_semantic_recovery_any_state():
|
||||
'<hierarchy><node package="com.instagram.android" resource-id="com.instagram.android:id/clips_tab" selected="true" /></hierarchy>'
|
||||
]
|
||||
mock_device.dump_hierarchy.side_effect = mock_hierarchy + [mock_hierarchy[-1]] * 10
|
||||
mock_device.deviceV2.dump_hierarchy.side_effect = mock_hierarchy + [mock_hierarchy[-1]] * 10
|
||||
mock_device.dump_hierarchy.side_effect = mock_hierarchy + [mock_hierarchy[-1]] * 10
|
||||
|
||||
graph = QNavGraph(mock_device)
|
||||
graph.current_state = "HomeFeed"
|
||||
@@ -79,7 +79,7 @@ def test_qnavgraph_telepathic_tagging(caplog):
|
||||
# 1. Test Keyword Fast Path (Score 1.0)
|
||||
mock_hierarchy_1 = ['<hierarchy><node package="com.instagram.android" class="before" /></hierarchy>', '<hierarchy><node package="com.instagram.android" class="after" /></hierarchy>']
|
||||
mock_device.dump_hierarchy.side_effect = mock_hierarchy_1 + [mock_hierarchy_1[-1]] * 10
|
||||
mock_device.deviceV2.dump_hierarchy.side_effect = mock_hierarchy_1 + [mock_hierarchy_1[-1]] * 10
|
||||
mock_device.dump_hierarchy.side_effect = mock_hierarchy_1 + [mock_hierarchy_1[-1]] * 10
|
||||
mock_telepathic = MagicMock()
|
||||
mock_telepathic.find_best_node.return_value = {
|
||||
"x": 100, "y": 100, "score": 1.0, "semantic": "test match", "source": "keyword", "skip": False
|
||||
@@ -93,7 +93,7 @@ def test_qnavgraph_telepathic_tagging(caplog):
|
||||
caplog.clear()
|
||||
mock_hierarchy_2 = ['<hierarchy><node package="com.instagram.android" class="before" /></hierarchy>', '<hierarchy><node package="com.instagram.android" class="after" /></hierarchy>']
|
||||
mock_device.dump_hierarchy.side_effect = mock_hierarchy_2 + [mock_hierarchy_2[-1]] * 10
|
||||
mock_device.deviceV2.dump_hierarchy.side_effect = mock_hierarchy_2 + [mock_hierarchy_2[-1]] * 10
|
||||
mock_device.dump_hierarchy.side_effect = mock_hierarchy_2 + [mock_hierarchy_2[-1]] * 10
|
||||
mock_telepathic.find_best_node.return_value = {
|
||||
"x": 100, "y": 100, "score": 0.85, "semantic": "test LLM", "source": "agentic_fallback", "skip": False
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ def test_full_mission_autopilot_sequence(fsd_fixtures):
|
||||
print(f"DEBUG: State advanced to {state['index']}")
|
||||
|
||||
device.dump_hierarchy.side_effect = get_ui
|
||||
device.deviceV2.dump_hierarchy.side_effect = get_ui
|
||||
device.dump_hierarchy.side_effect = get_ui
|
||||
device.click.side_effect = advance_state
|
||||
device.click.side_effect = advance_state
|
||||
device.deviceV2.click.side_effect = advance_state
|
||||
device.app_id = "com.instagram.android"
|
||||
device._get_current_app.return_value = "com.instagram.android"
|
||||
device.app_is_running.return_value = True
|
||||
@@ -262,8 +262,8 @@ def test_feed_loop_chaos_mode(fsd_fixtures):
|
||||
def advance_state(*args, **kwargs):
|
||||
state["index"] += 1
|
||||
|
||||
device.deviceV2.dump_hierarchy.side_effect = get_ui
|
||||
device.deviceV2.click.side_effect = advance_state
|
||||
device.dump_hierarchy.side_effect = get_ui
|
||||
device.click.side_effect = advance_state
|
||||
|
||||
from GramAddict.core.sensors.honeypot_radome import HoneypotRadome
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class TestSafetyGuard:
|
||||
mock_exists.return_value = False
|
||||
engine = TelepathicEngine()
|
||||
device = MagicMock()
|
||||
device.deviceV2.screenshot = MagicMock()
|
||||
device.screenshot = MagicMock()
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
|
||||
|
||||
nodes = self.explore_nodes
|
||||
@@ -197,7 +197,7 @@ class TestSafetyGuard:
|
||||
mock_exists.return_value = False
|
||||
engine = TelepathicEngine()
|
||||
device = MagicMock()
|
||||
device.deviceV2.screenshot = MagicMock()
|
||||
device.screenshot = MagicMock()
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
|
||||
|
||||
nodes = self.explore_nodes
|
||||
|
||||
@@ -17,7 +17,7 @@ from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
def mock_device(width=1080, height=2400):
|
||||
device = MagicMock()
|
||||
device.get_info.return_value = {"displayWidth": width, "displayHeight": height}
|
||||
device.deviceV2.screenshot = MagicMock()
|
||||
device.screenshot = MagicMock()
|
||||
device.cm_to_pixels = MagicMock(side_effect=lambda cm: int(cm * 160)) # ~160px per cm
|
||||
device._get_current_app.return_value = "com.instagram.android"
|
||||
device.app_id = "com.instagram.android"
|
||||
@@ -330,7 +330,7 @@ class TestTelepathicMemoryRecall:
|
||||
assert result["score"] == 1.0
|
||||
|
||||
# Screenshot should NEVER have been called (the early-return optimization)
|
||||
device.deviceV2.screenshot.assert_not_called()
|
||||
device.screenshot.assert_not_called()
|
||||
|
||||
@patch('GramAddict.core.telepathic_engine.TelepathicEngine._extract_semantic_nodes')
|
||||
@patch('builtins.open', new_callable=MagicMock)
|
||||
|
||||
Reference in New Issue
Block a user