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:
@@ -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')"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user