Hardening autonomous navigation: Implemented Modal Guards, Transient Drift Protection (WhatsApp fix), and Aggressive Recovery paths. Cleaned up diagnostic artifacts.

This commit is contained in:
2026-04-17 12:57:12 +02:00
parent 89f14463c5
commit 0aeed11186
35 changed files with 1802 additions and 460 deletions

View File

@@ -0,0 +1,67 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.device_facade import DeviceFacade
from GramAddict.core.q_nav_graph import QNavGraph
from GramAddict.core.telepathic_engine import TelepathicEngine
@pytest.fixture
def mock_device():
device = MagicMock()
device.app_id = "com.instagram.android"
# Mock u2 app_current to simulate a notification flicker
# Hardened detection makes up to 3 calls in the 'still flicker' case
device.app_current.side_effect = [
{"package": "com.whatsapp", "activity": ".Main"},
{"package": "com.whatsapp", "activity": ".Main"},
{"package": "com.whatsapp", "activity": ".Main"}
]
return device
def test_drift_hardening_flicker_resolution(mock_device):
"""
Test that _get_current_app handles transient packages correctly.
"""
# DeviceFacade expects (device_id, app_id, args)
# We mock u2.connect to avoid actual connection attempts
with patch("GramAddict.core.device_facade.u2.connect") as mock_connect:
mock_connect.return_value = mock_device
facade = DeviceFacade("mock_serial", "com.instagram.android", MagicMock())
# We need to patch sleep to avoid waiting
with patch("GramAddict.core.device_facade.sleep"):
pkg = facade._get_current_app()
# It should return the app_id (inferred as still in Instagram)
# because it saw a transient package (whatsapp) twice but we
# hardened it to assume it's a notification overlay.
assert pkg == "com.instagram.android"
def test_structural_guard_prevention():
"""
Test that structural intents are NOT blacklisted even if drift is reported.
"""
# Reset singleton or use real instance
engine = TelepathicEngine.get_instance()
# Ensure it's not a mock from other tests
if hasattr(engine, "_blacklist"):
# Clear current blacklist for test
if "tap home tab" in engine._blacklist:
engine._blacklist["tap home tab"] = []
# Simulate a drift context
context = {
"intent": "tap home tab",
"semantic_string": "description: 'Home', id context: 'feed tab'",
"x": 100, "y": 2000
}
TelepathicEngine._last_click_context = context
# Trigger rejection
engine.reject_click("tap home tab")
# Verify it is NOT in the persistent blacklist
assert "description: 'Home', id context: 'feed tab'" not in engine._blacklist.get("tap home tab", [])

View File

@@ -0,0 +1,69 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.telepathic_engine import TelepathicEngine
import os
FAILED_XML_PATH = "/Volumes/Alpha SSD/Coding/bot/debug/xml_dumps/manual_interrupt__2026-04-17_12-35-23.xml"
def test_modal_guard_blocks_nav_intent_on_failed_xml():
"""
Test that the Modal Guard correctly identifies the bottom sheet in the failed XML
and prevents searching for the 'Home Tab'.
"""
if not os.path.exists(FAILED_XML_PATH):
pytest.skip("Failed XML dump not found for testing.")
with open(FAILED_XML_PATH, "r") as f:
xml_content = f.read()
engine = TelepathicEngine.get_instance()
# Intent that SHOULD be blocked because Home Tab is obscured by the comment sheet
intent = "tap home tab"
# We don't want to trigger actual LLM/VLM calls during the test
with patch("GramAddict.core.telepathic_engine.query_telepathic_llm") as mock_vlm:
result = engine.find_best_node(xml_content, intent)
# 1. Verify that no VLM call was even attempted because the Modal Guard should have caught it early
assert mock_vlm.called is False, "VLM should not be called when a modal obscures the target zone."
# 2. Result should be None (meaning 'Target blocked/missing')
assert result is None, "Modal Guard should return None for navigation intents when a sheet is open."
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()
# Minimal XML
xml = "<?xml version='1.0' ?><hierarchy><node index='0' text='Add comment' bounds='[42,2224][1038,2350]' visible-to-user='true' /></hierarchy>"
intent = "tap home tab"
# Mock VLM to return the 'Add comment' field which is at Y=2224 (0.917 of 2424)
# Our guard enforces Tabs MUST be in the bottom 10% (Y > 0.90 * Height).
# Wait, Y=2224 on H=2424 is 0.917. That IS in the bottom 10%.
# Let's mock a node at Y=1000 (middle of screen) to test the guard.
with patch("GramAddict.core.telepathic_engine.query_telepathic_llm") as mock_vlm:
# Mock VLM returning a middle-screen element (Index 0)
mock_vlm.return_value = '{"index": 0, "reason": "hallucination test"}'
# Injected node at middle screen
with patch.object(TelepathicEngine, "_extract_semantic_nodes") as mock_extract:
mock_extract.return_value = [{
"index": 0,
"x": 500, "y": 1000, "width": 100, "height": 100, "area": 10000,
"raw_bounds": "[450,950][550,1050]",
"semantic_string": "text: 'Fake Home', id context: 'fake_tab'"
}]
# We also need to patch _is_modal_active to False so it GETS to the VLM step
with patch.object(TelepathicEngine, "_is_modal_active", return_value=False):
result = engine.find_best_node(xml, intent)
# Should be rejected because navigation tabs should be in the nav bar zone
assert result is None, "Structural Guard should reject mid-screen navigation tab candidates."

View File

@@ -0,0 +1,135 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop
from GramAddict.core.session_state import SessionState
@pytest.fixture
def mock_device():
device = MagicMock()
device.deviceV2 = MagicMock()
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
device.app_id = "com.instagram.android"
return device
def test_reels_loop_repost_execution(mock_device):
"""
TDD Test: Verifies that the bot attempts to repost a Reel if resonance is high
and repost_percentage allows it.
"""
# 1. Setup Cognitive Stack & Configs
mock_cognitive_stack = {
"dopamine": MagicMock(),
"resonance": MagicMock(),
"zero_engine": MagicMock(),
"nav_graph": MagicMock(),
"telepathic": MagicMock()
}
# Simulate a single post interaction then exit
mock_cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
mock_cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
# High resonance to trigger repost
mock_cognitive_stack["resonance"].calculate_resonance.return_value = 0.95
configs = MagicMock()
configs.args.repost_percentage = 100
configs.args.interact_percentage = 100
configs.args.likes_percentage = 0
configs.args.comment_percentage = 0
configs.args.follow_percentage = 0
configs.args.visit_profiles = 0
session_state = MagicMock()
session_state.check_limit.return_value = (False, False, False, False)
# 2. Mock Reels UI
# Reels usually have 'clips_viewer' or similar in the hierarchy
reels_xml = '''<?xml version='1.0' ?>
<hierarchy>
<node resource-id="com.instagram.android:id/row_feed_profile_header" bounds="[0,200][1080,300]" />
<node resource-id="com.instagram.android:id/clips_author_username" text="test_user" />
<node resource-id="com.instagram.android:id/clips_media_component" content-desc="Check out this cool reel #repost #viral" />
<node resource-id="com.instagram.android:id/clips_video_container" />
<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
# Repost Sheet XML
repost_sheet_xml = '''<?xml version='1.0' ?>
<hierarchy>
<node resource-id="com.instagram.android:id/bottom_sheet_container">
<node text="Repost" content-desc="Repost interaction button with two arrows" />
</node>
</hierarchy>'''
# 3. Setup Telepathic Engine Mocks
mock_telepathic = mock_cognitive_stack["telepathic"]
# First call: find interaction buttons
mock_telepathic._extract_semantic_nodes.return_value = [{"x": 100, "y": 100, "semantic_string": "share button"}]
# Logic for finding the Repost button inside the share sheet
def mock_find_best_node(xml, intent, **kwargs):
if "Repost" in intent:
return {"x": 500, "y": 2000, "bounds": "[400,1950][600,2050]", "skip": False}
return {"x": 100, "y": 100, "bounds": "[90,90][110,110]", "skip": False}
mock_telepathic.find_best_node.side_effect = mock_find_best_node
# Simulate share button transition success
mock_cognitive_stack["nav_graph"]._execute_transition.return_value = True
# 4. Execute Feed Loop for Reels
with patch('GramAddict.core.bot_flow.TelepathicEngine') as MockEngine, \
patch('GramAddict.core.bot_flow._humanized_click') as mock_click, \
patch('GramAddict.core.bot_flow.sleep'):
MockEngine.get_instance.return_value = mock_telepathic
# Resilient state-based mock for dump_hierarchy
comment_sheet_xml = '''<?xml version='1.0' ?><hierarchy><node resource-id="com.instagram.android:id/layout_comment_thread" /><node resource-id="com.instagram.android:id/bottom_sheet_container" /></hierarchy>'''
state = {'current': reels_xml}
def side_effect_func(*args, **kwargs):
return state['current']
mock_device.dump_hierarchy.side_effect = side_effect_func
mock_device.deviceV2.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
def mocked_execute(transition_name):
if transition_name == "tap_comment_button":
state['current'] = comment_sheet_xml
elif transition_name == "tap_share_button":
state['current'] = repost_sheet_xml
return True
mock_cognitive_stack["nav_graph"]._execute_transition.side_effect = mocked_execute
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop
_run_zero_latency_feed_loop(
mock_device,
mock_cognitive_stack["zero_engine"],
mock_cognitive_stack["nav_graph"],
configs,
session_state,
"ReelsFeed",
mock_cognitive_stack,
is_reels=True
)
# 5. Assertions
# Should click the share button (via transition) and the repost button (direct click)
assert mock_cognitive_stack["nav_graph"]._execute_transition.called_with("tap_share_button")
# Check if _humanized_click was called for the Repost button (x=500, y=2000)
click_args = [call.args for call in mock_click.call_args_list]
repost_clicked = any(args[1] == 500 and args[2] == 2000 for args in click_args)
assert repost_clicked, "Repost button was not clicked on Reels share sheet"
assert mock_telepathic.confirm_click.called_with("Repost interaction button with two arrows")