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:
2026-04-21 02:00:01 +02:00
parent 2c6404f387
commit 0a02e901b6
37 changed files with 311 additions and 274 deletions

View File

@@ -42,9 +42,9 @@ class TestAnomalyInterruptions:
cleared = self.nav_graph._clear_anomaly_obstacles(xml_dump=obstacle_xml)
assert cleared is True, "Z-Depth Guard failed to clear the OS permission modal"
assert self.mock_device.deviceV2.click.call_count >= 1
assert self.mock_device.click.call_count >= 1
# Verify it clicked the 'Don't allow' button coordinates ([100,650][900,750] avg is [500, 700])
args, _ = self.mock_device.deviceV2.click.call_args
args, _ = self.mock_device.click.call_args
assert args[0] == 500
assert args[1] == 700
@@ -85,13 +85,13 @@ class TestAnomalyInterruptions:
# Secondary assertion: at least one dismissal action occurred.
# The SAE may press BACK (priority=-1 for OBSTACLE_MODAL) or click 'Not Now'.
pressed_back = (
self.mock_device.deviceV2.press.called and
self.mock_device.press.called and
any(
(a.args[0] if a.args else None) == "back"
for a in self.mock_device.deviceV2.press.call_args_list
for a in self.mock_device.press.call_args_list
)
)
did_click = self.mock_device.deviceV2.click.call_count >= 1
did_click = self.mock_device.click.call_count >= 1
assert pressed_back or did_click, (
"SAE did not take any dismissal action (expected BACK press or click on 'Not Now')"

View File

@@ -15,15 +15,15 @@ def test_app_perimeter_guard_after_click():
"com.android.vending", # POST-CLICK verification at line 361 (App drifted!)
"com.android.vending", # double check after BACK press in recovery
"com.android.vending" # fallback start check if needed
]
] + ["com.android.vending"] * 50
mock_device.app_id = "com.instagram.android"
# UI XML pre/post click
mock_device.deviceV2.dump_hierarchy.side_effect = [
mock_device.dump_hierarchy.side_effect = [
"<hierarchy><node resource-id='ad' /></hierarchy>", # initial context (line 293)
"<hierarchy><node resource-id='ad' /></hierarchy>", # anomaly guard check (line 191)
"<hierarchy><node resource-id='play_store_ui' /></hierarchy>" # post-click check (line 358)
]
] + ["<hierarchy />"] * 50
# Mock Telepathic Engine
mock_engine = MagicMock()

View File

@@ -30,9 +30,9 @@ def test_interact_with_profile_all_100_percent(mock_random, device, telepathic_m
# 2 scrolls (2 shell commands) via _humanized_scroll
# story, follow, grid, like all use QNavGraph click transitions because 'reel_viewer' is in MockDeviceV2 XML.
assert len(device.deviceV2.shells) == 2
assert device.shell.call_count == 2
for cmd in device.deviceV2.shells:
for cmd in [args[0][0] for args in device.shell.call_args_list]:
assert "input swipe" in cmd
@patch("random.random")
@@ -53,7 +53,7 @@ def test_interact_with_profile_zero_percent(mock_random, device, telepathic_mock
_interact_with_profile(device, configs, "testuser", session_state, 0.0, mock_logger)
# No interaction blocks run, so no shells.
assert len(device.deviceV2.shells) == 0
assert device.shell.call_count == 0
@patch("random.random")
def test_interact_with_profile_mixed_probability(mock_random, device, telepathic_mock, mock_logger):
@@ -77,7 +77,7 @@ def test_interact_with_profile_mixed_probability(mock_random, device, telepathic
_interact_with_profile(device, configs, "testuser", session_state, 0.0, mock_logger)
# Grid loop finishes with 1 scroll for 1 post.
assert len(device.deviceV2.shells) == 1
assert device.shell.call_count == 1
@patch("random.random")
def test_carousel_100_percent(mock_random, device, mock_logger):
@@ -91,8 +91,8 @@ def test_carousel_100_percent(mock_random, device, mock_logger):
_interact_with_carousel(device, configs, 0.0, mock_logger)
assert len(device.deviceV2.shells) == 4
for cmd in device.deviceV2.shells:
assert device.shell.call_count == 4
for cmd in [args[0][0] for args in device.shell.call_args_list]:
assert "swipe" in cmd
@patch("random.random")
@@ -107,7 +107,7 @@ def test_carousel_zero_percent(mock_random, device, mock_logger):
_interact_with_carousel(device, configs, 0.0, mock_logger)
assert len(device.deviceV2.shells) == 0
assert device.shell.call_count == 0
@patch("random.random")
def test_interact_with_profile_follow_limit_enforcement(mock_random, device, telepathic_mock, mock_logger):
@@ -131,7 +131,7 @@ def test_interact_with_profile_follow_limit_enforcement(mock_random, device, tel
_interact_with_profile(device, configs, "targetuser", session_state, 0.0, mock_logger)
# Assert shells is 0 (assuming stories and likes probability mathematically default to 0 due to MockArgs empty fallback)
assert len(device.deviceV2.shells) == 0
assert device.shell.call_count == 0
@patch("random.random")
def test_interact_with_profile_likes_limit_enforcement(mock_random, device, telepathic_mock, mock_logger):
@@ -157,7 +157,7 @@ def test_interact_with_profile_likes_limit_enforcement(mock_random, device, tele
_interact_with_profile(device, configs, "targetuser", session_state, 0.0, mock_logger)
# Limit restricts likes block.
assert len(device.deviceV2.shells) == 0
assert device.shell.call_count == 0
# NOTE: Repost is deeply integrated into `bot_flow._run_zero_latency_feed_loop`. We can't mock the

View File

@@ -41,7 +41,7 @@ class TestCriticalAnomalyGuards:
If a user account is private, _interact_with_profile must skip everything
and return without doing ANY interactions.
"""
self.mock_device.deviceV2.dump_hierarchy.return_value = '''
self.mock_device.dump_hierarchy.return_value = '''
<hierarchy>
<node text="marisaundmarc" />
<node text="This account is private" bounds="[100,500][900,600]" />
@@ -55,14 +55,14 @@ class TestCriticalAnomalyGuards:
_interact_with_profile(self.mock_device, configs, "test_private", session_state, sleep_mod=0.0, logger=self.logger)
# Verify it did not attempt to find stories, scrape, or anything
self.mock_device.deviceV2.click.assert_not_called()
self.mock_device.deviceV2.press.assert_not_called()
self.mock_device.click.assert_not_called()
self.mock_device.press.assert_not_called()
def test_interact_with_empty_profile_aborts(self):
"""
If a user account has 0 posts, we must skip.
"""
self.mock_device.deviceV2.dump_hierarchy.return_value = '''
self.mock_device.dump_hierarchy.return_value = '''
<hierarchy>
<node text="marisaundmarc" />
<node text="No posts yet" bounds="[100,500][900,600]" />
@@ -74,8 +74,8 @@ class TestCriticalAnomalyGuards:
_interact_with_profile(self.mock_device, configs, "test_empty", session_state, sleep_mod=0.0, logger=self.logger)
self.mock_device.deviceV2.click.assert_not_called()
self.mock_device.deviceV2.press.assert_not_called()
self.mock_device.click.assert_not_called()
self.mock_device.press.assert_not_called()
def test_comments_disabled_guard(self):
"""

View File

@@ -20,7 +20,7 @@ def test_profile_grid_sync_delay_after_follow():
"""
mock_device = MagicMock()
mock_device.app_id = "com.instagram.android"
mock_device.deviceV2.dump_hierarchy.return_value = "<hierarchy><node package='com.instagram.android' text='following' /></hierarchy>"
mock_device.dump_hierarchy.return_value = "<hierarchy><node package='com.instagram.android' text='following' /></hierarchy>"
mock_device.dump_hierarchy.return_value = "<hierarchy><node package='com.instagram.android' text='following' /></hierarchy>"
mock_configs = FakeConfig()

View File

@@ -104,6 +104,6 @@ class TestUnfollowEngine:
)
assert result == "BOREDOM_CHANGE_FEED"
self.mock_device.deviceV2.dump_hierarchy.assert_not_called()
self.mock_device.deviceV2.click.assert_not_called()
self.mock_device.dump_hierarchy.assert_not_called()
self.mock_device.click.assert_not_called()
self.mock_session_state.totalUnfollowed = 0