This commit is contained in:
2026-04-16 18:58:18 +02:00
commit fa1d01527d
124 changed files with 13989 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
import pytest
from unittest.mock import patch, MagicMock
import sys
# Force mock qdrant_client before importing any core modules that depend on it
from GramAddict.core.bot_flow import _extract_post_content, _run_zero_latency_feed_loop
class TestBotFlowEdgeCases:
def test_extract_post_content_edge_cases(self):
# 1. Empty string / Invalid XML should not crash
res = _extract_post_content("")
assert res.get("username") == ""
assert res.get("description") == ""
# 2. Extract when only username exists
xml = "<node resource-id='com.instagram.android:id/row_feed_photo_profile_name' text='just_user'/>"
res = _extract_post_content(xml)
assert res.get("username") == "just_user"
assert res.get("description") == ""
# 3. Extract when emoji only in description
xml = "<node resource-id='com.instagram.android:id/row_feed_photo_imageview' content-desc='🔥🔥🔥'/>"
res = _extract_post_content(xml)
# However, bot_flow requires len(desc) > 10!
# So "🔥🔥🔥" will NOT be extracted if it's too short. Let's provide a long text.
xml = "<node resource-id='com.instagram.android:id/row_feed_photo_imageview' content-desc='🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥'/>"
res = _extract_post_content(xml)
assert res.get("description") == "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"
# 4. Another valid description tag
xml = "<node resource-id='com.instagram.android:id/clips_media_component' content-desc='some desc with more than 10 chars limits'/>"
res = _extract_post_content(xml)
assert res.get("description") == "some desc with more than 10 chars limits"
@patch('GramAddict.core.bot_flow.sleep')
@patch('GramAddict.core.bot_flow._humanized_scroll')
@patch('GramAddict.core.bot_flow.dump_ui_state')
@patch('GramAddict.core.bot_flow._detect_ad_structural')
@patch('GramAddict.core.bot_flow._align_active_post')
@patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance')
def test_zero_node_recovery(self, mock_get_telepathic, mock_align, mock_ad, mock_dump, mock_scroll, mock_sleep):
# Tests the explicit Zero-Node Recovery added previously
device = MagicMock()
zero_engine = MagicMock()
nav_graph = MagicMock()
configs = MagicMock()
session_state = MagicMock()
mock_ad.return_value = False
mock_align.return_value = False
cognitive_stack = {
"dopamine": MagicMock(),
"darwin": MagicMock(),
"resonance": MagicMock(),
"active_inference": MagicMock(),
"growth_brain": MagicMock(),
"swarm": MagicMock()
}
# Dopamine breaks loop after 1st iteration
cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
# Fake extreme limits => doesn't break limits
session_state.check_limit.return_value = [False]*10
# Telepathic Engine returns ZERO nodes on extract
mock_engine = MagicMock()
mock_engine._extract_semantic_nodes.return_value = []
mock_get_telepathic.return_value = mock_engine
device.deviceV2.dump_hierarchy.return_value = "<xml></xml>"
# Execute the main loop
_run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, "HomeFeed", cognitive_stack)
# It should trigger device.press("back") and then _humanized_scroll
device.deviceV2.press.assert_called_with("back")
assert mock_scroll.call_count >= 1
@patch('GramAddict.core.bot_flow.sleep')
@patch('GramAddict.core.bot_flow._humanized_scroll')
@patch('GramAddict.core.bot_flow.dump_ui_state')
@patch('GramAddict.core.bot_flow._extract_post_content')
@patch('GramAddict.core.bot_flow._detect_ad_structural')
@patch('GramAddict.core.bot_flow._align_active_post')
@patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance')
def test_content_extraction_failed_recovery(self, mock_get_telepathic, mock_align, mock_ad, mock_extract, mock_dump, mock_scroll, mock_sleep):
device = MagicMock()
zero_engine = MagicMock()
nav_graph = MagicMock()
configs = MagicMock()
session_state = MagicMock()
mock_ad.return_value = False
mock_align.return_value = False
cognitive_stack = {
"dopamine": MagicMock(),
"darwin": MagicMock()
}
# break after 1 loop
cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
session_state.check_limit.return_value = [False]*10
# Ensure it HAS feed markers
device.deviceV2.dump_hierarchy.return_value = "<xml>row_feed_photo_profile_name</xml>"
# Ensure interactive_nodes is NOT zero
mock_engine = MagicMock()
mock_engine._extract_semantic_nodes.return_value = [{"x": 10}]
mock_get_telepathic.return_value = mock_engine
# Make the extraction fail
mock_extract.return_value = {"username": "", "description": ""}
_run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, "HomeFeed", cognitive_stack)
# Should call mock_scroll (Graceful degradation)
mock_scroll.assert_called_once()
mock_dump.assert_called_with(device, "content_extraction_failed", {"feed": "HomeFeed"})

View File

@@ -0,0 +1,57 @@
import pytest
from GramAddict.core.resonance_engine import ResonanceEngine
from GramAddict.core.darwin_engine import DarwinEngine
from GramAddict.core.growth_brain import GrowthBrain
class TestCognitiveEdgeCases:
# Resonance Engine
def test_resonance_edge_cases(self):
engine = ResonanceEngine(my_username="test_user")
# 1. Empty strings shouldn't crash
assert engine.calculate_resonance({"description": "", "username": ""}) == 0.5
# 2. Very long descriptions
long_str = "word " * 10000
res = engine.calculate_resonance({"description": long_str})
assert isinstance(res, float)
# 3. None values
score = engine.calculate_resonance({"description": None})
assert score == 0.5
# Darwin Engine
def test_darwin_edge_cases(self):
engine = DarwinEngine("test_user")
# 1. synthesize interaction with 0.0
prof = engine.synthesize_interaction_profile(0.0)
assert prof["initial_dwell_sec"] > 0
# 2. Negative resonance (should default upwards or bound)
prof_neg = engine.synthesize_interaction_profile(-10.0)
assert prof_neg["initial_dwell_sec"] > 0
# 3. Extreme resonance
prof_max = engine.synthesize_interaction_profile(10.0) # > 1.0
assert prof_max["initial_dwell_sec"] > 0
def test_growth_brain_edge_cases(self):
engine = GrowthBrain(username="test")
# 1. Call circadian without history
engine.session_history = []
pacing = engine.get_circadian_pacing()
assert 0.4 <= pacing <= 1.2
# 2. Call with extreme limits
engine.session_history = [{"boredom_peak": 100.0, "time": "unknown"}] * 100
pacing2 = engine.get_circadian_pacing()
assert pacing2 > 0.0
# 3. Evaluate persona drift with empty outcomes
engine.refine_persona([])
# Shouldn't crash

View File

@@ -0,0 +1,60 @@
import os
import hashlib
from unittest.mock import MagicMock, patch
import pytest
# Mock directory setup
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FIXTURE_DIR = os.path.join(ROOT_DIR, "fixtures")
class ConfigMock:
def __init__(self):
self.args = MagicMock()
self.args.app_id = "com.instagram.android"
def test_fsd_handles_persistent_survey_modal():
"""
Simulates a case where the bot gets stuck on a survey modal.
The FSD (Full Self Driving) anomaly handler should trigger,
detect that 'Back' didn't work, and engage TelepathicEngine
to find and tap the 'Not Now' or 'Dismiss' button.
"""
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop
from GramAddict.core.telepathic_engine import TelepathicEngine
device = MagicMock()
device.app_id = "com.instagram.android"
device._get_current_app.return_value = "com.instagram.android"
configs = ConfigMock()
# Mock the TelepathicEngine singleton behavior entirely
mock_telepathic = MagicMock()
mock_telepathic.find_best_node.return_value = {"x": 500, "y": 1400, "semantic": "Not Now"}
mock_telepathic._extract_semantic_nodes.return_value = [{"x": 10}]
dopamine = MagicMock()
dopamine.is_app_session_over.side_effect = [False, False, True] # Run twice, then exit
dopamine.wants_to_change_feed.return_value = False
dopamine.wants_to_doomscroll.return_value = False
ai = MagicMock()
ai.get_sleep_modifier.return_value = 1.0
cognitive_stack = {"dopamine": dopamine, "growth_brain": None, "active_inference": ai, "telepathic": mock_telepathic}
# Load the mock survey modal UI
xml_path = os.path.join(FIXTURE_DIR, "survey_modal.xml")
with open(xml_path, "r") as f:
alien_xml = f.read()
device.deviceV2.dump_hierarchy.return_value = alien_xml
with patch('GramAddict.core.bot_flow.sleep'), \
patch('GramAddict.core.bot_flow._humanized_scroll'), \
patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance', return_value=mock_telepathic):
result = _run_zero_latency_feed_loop(device, None, MagicMock(), configs, MagicMock(), "HomeFeed", cognitive_stack)
# VERIFICATION:
# Handler should have called Telepathic after 2 misses
assert mock_telepathic.find_best_node.called
assert device.deviceV2.click.called
assert result != "CONTEXT_LOST"

View File

@@ -0,0 +1,166 @@
import pytest
import os
import time
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import _wait_for_post_loaded, _run_zero_latency_feed_loop, FEED_MARKERS
from GramAddict.core.device_facade import DeviceFacade
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DUMPS = {
"organic": os.path.join(ROOT_DIR, "fixtures", "organic_post.xml"),
"explore": os.path.join(ROOT_DIR, "fixtures", "explore_feed_dump.xml"),
}
FIXTURE_DIR = os.path.join(ROOT_DIR, "fixtures")
def mutate_xml_to_foreign(xml_content: str) -> str:
"""Removes meaningful text content to simulate a language failure or empty state."""
import re
# Strip text and content-desc
xml = re.sub(r'text="[^"]*"', 'text=""', xml_content)
xml = re.sub(r'content-desc="[^"]*"', 'content-desc=""', xml)
return xml
def mutate_xml_remove_feed_markers(xml_content: str) -> str:
"""Removes all feed markers to simulate a grid view or random popup."""
xml = xml_content
for marker in FEED_MARKERS:
xml = xml.replace(marker, "some_random_id")
return xml
class ConfigMock:
def __init__(self):
self.args = MagicMock()
self.args.interact_percentage = 0
self.args.comment_percentage = 0
@pytest.fixture
def test_dumps():
dumps = {}
with open(DUMPS["organic"], "r") as f:
dumps["post"] = f.read()
# Fake explore grid that lacks ALL feed markers
dumps["grid"] = '<?xml version="1.0"?><hierarchy><node resource-id="com.instagram.android:id/explore_grid_container" /></hierarchy>'
return dumps
def test_slow_loading_post_recovery(test_dumps):
"""
Test that _wait_for_post_loaded correctly handles a delay where the
first few dumps are grids, and only later it becomes a post.
"""
device = MagicMock()
# Simulate: Grid -> Grid -> Error -> Post
device.deviceV2.dump_hierarchy.side_effect = [
test_dumps["grid"],
test_dumps["grid"],
Exception("uiautomator2 temp failure"),
test_dumps["post"]
]
# We patch sleep to make the test super fast
with patch('GramAddict.core.bot_flow.sleep', return_value=None):
start = time.time()
success = _wait_for_post_loaded(device, timeout=5)
# Should return true when it hits the 4th element
assert success is True
assert device.deviceV2.dump_hierarchy.call_count == 4
def test_wait_timeout_aborts_gracefully(test_dumps):
"""Test what happens if the network is so slow it times out entirely."""
device = MagicMock()
# Always return grid
device.deviceV2.dump_hierarchy.return_value = test_dumps["grid"]
# Patch time.time to simulate 6 seconds passing immediately
# We add sequence padding because python's logger internally uses time.time()
with patch('GramAddict.core.bot_flow.time.time', side_effect=[0, 1, 6, 6, 6, 6, 6, 6, 6, 6]):
with patch('GramAddict.core.bot_flow.sleep', return_value=None):
success = _wait_for_post_loaded(device, timeout=5)
assert success is False
def test_empty_content_extraction_guard(test_dumps):
"""
Test that if a post is loaded, but it has strange empty text (foreign language or bug),
the bot aborts interaction and scrolls instead of judging empty content.
"""
device = MagicMock()
nav_graph = MagicMock()
configs = ConfigMock()
# We create a fake active inference engine to just break the loop after 1 iteration
ai = MagicMock()
# Dopamine engine controls loop exit
dopamine = MagicMock()
dopamine.is_app_session_over.side_effect = [False, True] # Run once, then exit
dopamine.wants_to_change_feed.return_value = False
dopamine.wants_to_doomscroll.return_value = False
cognitive_stack = {
"dopamine": dopamine,
"active_inference": ai,
"resonance": None, "growth_brain": None, "swarm": None, "darwin": None
}
# Mutate the post so it has NO text or description
broken_xml = mutate_xml_to_foreign(test_dumps["post"])
device.deviceV2.dump_hierarchy.return_value = broken_xml
with patch('GramAddict.core.bot_flow._humanized_scroll') as mock_scroll, \
patch('GramAddict.core.bot_flow.sleep'):
result = _run_zero_latency_feed_loop(device, None, nav_graph, configs, MagicMock(), "HomeFeed", cognitive_stack)
# Ensure scroll was called (the recovery mechanism)
assert mock_scroll.called
# Check that we never called resonance evaluation because we broke early
assert not ai.predict_state.called
assert result == "SESSION_OVER"
def test_missing_feed_markers_guard(test_dumps):
"""
Test that if the UI is completely foreign (e.g., a system popup),
the bot detects missing feed markers and scrolls to recover.
"""
device = MagicMock()
configs = ConfigMock()
dopamine = MagicMock()
dopamine.is_app_session_over.side_effect = [False, True]
dopamine.wants_to_change_feed.return_value = False
dopamine.wants_to_doomscroll.return_value = False
cognitive_stack = {"dopamine": dopamine, "growth_brain": None, "active_inference": None}
# Mutate XML to remove all FEED MARKERS
alien_xml = mutate_xml_remove_feed_markers(test_dumps["post"])
device.deviceV2.dump_hierarchy.return_value = alien_xml
with patch('GramAddict.core.bot_flow._humanized_scroll') as mock_scroll, \
patch('GramAddict.core.bot_flow.sleep'):
_run_zero_latency_feed_loop(device, None, MagicMock(), configs, MagicMock(), "HomeFeed", cognitive_stack)
@patch('GramAddict.core.device_facade.u2')
def test_xpath_watcher_initialization(mock_u2):
"""
Test fixing the critical watcher API bug.
Ensures that device facade uses .watcher("name").when(xpath=...)
"""
mock_d = MagicMock()
mock_u2.connect.return_value = mock_d
# Setup mock chain: deviceV2.watcher("crash_dialog").when(...)
mock_watcher = MagicMock()
mock_d.watcher.return_value = mock_watcher
mock_when = MagicMock()
mock_watcher.when.return_value = mock_when
# Just init the facade
from GramAddict.core.device_facade import create_device
device = create_device("fake_serial", "com.fake.app", MagicMock())
# Verify exact API call structure for XPath
mock_d.watcher.assert_any_call("crash_dialog")
mock_d.watcher.assert_any_call("system_dialog")
# We can't perfectly assert the chained arguments natively without a bit of inspection,
# but we can verify it didn't crash and called start
assert mock_d.watcher.start.called

View File

@@ -0,0 +1,47 @@
import pytest
import os
from unittest.mock import MagicMock, patch
from GramAddict.core.device_facade import DeviceFacade
def test_adb_retry_recovers_from_transient_error():
# Attempt simulated disconnect on dump_hierarchy
device_id = "test"
app_id = "test"
with patch('uiautomator2.connect') as mock_connect:
mock_device = MagicMock()
mock_connect.return_value = mock_device
facade = DeviceFacade(device_id, app_id, None)
# Make the first 2 calls fail, the 3rd one pass
mock_device.dump_hierarchy.side_effect = [
Exception("ConnectError uiautomator2"),
Exception("RPC Error"),
"<hierarchy></hierarchy>"
]
# Patch sleep to speed up test
with patch('GramAddict.core.device_facade.sleep'):
res = facade.dump_hierarchy()
assert res == "<hierarchy></hierarchy>"
assert mock_device.dump_hierarchy.call_count == 3
def test_adb_retry_crashes_gracefully_after_all_retries():
# Attempt simulated disconnect on dump_hierarchy
device_id = "test"
app_id = "test"
with patch('uiautomator2.connect') as mock_connect:
mock_device = MagicMock()
mock_connect.return_value = mock_device
facade = DeviceFacade(device_id, app_id, None)
# Always fail
mock_device.dump_hierarchy.side_effect = Exception("Permanent ConnectError")
with patch('GramAddict.core.device_facade.sleep'):
with pytest.raises(Exception, match="Permanent ConnectError"):
facade.dump_hierarchy()
assert mock_device.dump_hierarchy.call_count == 3

View File

@@ -0,0 +1,81 @@
import unittest
import sys
import os
# Add parent dir to path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from GramAddict.core.telepathic_engine import TelepathicEngine
class DummyDevice:
class DeviceV2:
def __init__(self):
self.last_click = None
def click(self, x, y):
self.last_click = (x, y)
def screenshot(self, path=None):
return "fake_screenshot"
def __init__(self):
self.deviceV2 = self.DeviceV2()
self.app_id = "com.instagram.android"
def _get_current_app(self):
return "com.instagram.android"
class TestHumanHesitation(unittest.TestCase):
def setUp(self):
self.telepathic = TelepathicEngine()
self.device = DummyDevice()
def test_discard_dialog_extraction(self):
"""
Prove that the Telepathic Engine can correctly identify the 'Discard'
button inside a synthetic XML dump, ensuring the 'Umentscheidung'
abort logic works in the wild.
"""
# Synthetic Discard Dialog XML
synthetic_dump = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" bounds="[0,0][1080,2400]" package="com.instagram.android">
<node index="1" class="android.widget.TextView" text="Discard Comment?" bounds="[200,1000][800,1100]" />
<node index="2" class="android.widget.Button" text="IGNORE" content-desc="IGNORE" bounds="[200,1200][400,1300]" />
<node index="3" class="android.widget.Button" text="Verwerfen" content-desc="Discard or Verwerfen popup button" bounds="[600,1200][800,1300]" resource-id="com.instagram.android:id/button_discard" />
</node>
</hierarchy>
'''
# Act
result = self.telepathic.find_best_node(
synthetic_dump,
"Discard or Verwerfen popup button to cancel comment",
device=self.device
)
# Assert (Should hit the [600,1200][800,1300] box, which centers to (700, 1250))
self.assertIsNotNone(result, "Telepathic engine failed to find 'Verwerfen'.")
self.assertEqual(result["x"], 700)
self.assertEqual(result["y"], 1250)
def test_dm_inbox_tab_resolution(self):
"""
Verify that teleporting specifically to the Inbox tab (DM button)
succeeds if 'Message' describes it.
"""
synthetic_dump = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="2" text="" id="direct_tab" package="com.instagram.android" content-desc="Direct messages tab button" bounds="[432,2235][648,2361]" resource-id="com.instagram.android:id/direct_tab">
<node content-desc=""/>
</node>
</hierarchy>'''
# If ID didn't match perfectly, we fall back to description as programmed.
# Direct simulation of UI Automator check isn't in scope for this telepathic test,
# but we can ensure Telepathic Engine CAN find it if we rely on it.
result = self.telepathic.find_best_node(synthetic_dump, "Direct messages tab button", device=self.device)
self.assertIsNotNone(result, "Should find the Message tab")
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,56 @@
import pytest
from unittest.mock import patch, MagicMock
from GramAddict.core.llm_provider import query_llm
from GramAddict.core.resonance_engine import ResonanceEngine
def test_query_llm_hallucination_recovery():
# Test that when the primary model hallucinates non-JSON, it triggers fallback
with patch('requests.post') as mock_post:
# 1st call: Primary fails entirely (e.g., Timeout or strange error)
mock_response_1 = MagicMock()
mock_response_1.status_code = 500
mock_response_1.raise_for_status.side_effect = Exception("500 Server Error")
# 2nd call: Fallback works and returns valid JSON
mock_response_2 = MagicMock()
mock_response_2.status_code = 200
mock_response_2.raise_for_status.return_value = None
mock_response_2.json.return_value = {
"choices": [{"message": {"content": '{"test": "success"}'}}]
}
mock_post.side_effect = [mock_response_1, mock_response_2]
# Attempt a query with a primary model
res = query_llm(
url="http://fake.api/v1/chat/completions",
model="primary-model",
prompt="Hello",
format_json=True,
fallback_model="fallback-model",
fallback_url="http://fake.api/v1/chat/completions"
)
assert res is not None
assert "response" in res
assert res["response"] == '{"test": "success"}'
assert mock_post.call_count == 2
def test_query_llm_double_hallucination_safe_return():
# Test that when both models hallucinate, we return None gracefully
with patch('requests.post') as mock_post:
# Both models fail
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.raise_for_status.side_effect = Exception("500 Server Error")
mock_post.side_effect = [mock_response, mock_response]
res = query_llm(
url="http://fake.api/v1/chat/completions",
model="primary-model",
prompt="Hello",
format_json=True
)
assert res is None

View File

@@ -0,0 +1,41 @@
import pytest
import os
from unittest.mock import MagicMock, patch
from GramAddict.core.q_nav_graph import QNavGraph
def test_tap_home_tab_recovery_from_homescreen():
"""
TDD: Reproduce the failure where tap_home_tab fails because the bot is on
the Android Homescreen (app.lawnchair), and verify that it recovers
via app_start instead of enterring an auto-repair loop.
"""
# 1. Setup Mock Device
mock_device = MagicMock()
mock_device.app_id = "com.instagram.android"
# Return homescreen package to simulate context loss
mock_device._get_current_app.return_value = "app.lawnchair"
# 2. Mock DeviceV2 responses
mock_device.deviceV2.dump_hierarchy.return_value = "<hierarchy />"
mock_device.deviceV2.app_start.return_value = True
# 3. Initialize NavGraph
graph = QNavGraph(mock_device)
graph.current_state = "ProfileFeed" # Assume stale state
# 4. Patch TelepathicEngine.get_instance to return a mock engine
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance") as mock_get_instance, \
patch("GramAddict.core.q_nav_graph.time.sleep"):
mock_engine = MagicMock()
mock_get_instance.return_value = mock_engine
# Simulate Context Guard hitting: return None forever
mock_engine.find_best_node.return_value = None
# 5. Execute
# We expect this to return False gracefully after 3 attempts, without infinitely looping
success = graph.navigate_to("ExploreFeed", zero_engine=None)
# 6. Assertion
assert not success, "Navigation should fail gracefully when context cannot be recovered"
assert mock_device.deviceV2.app_start.called, "Should have force-started the app when context was lost"

View File

@@ -0,0 +1,106 @@
import pytest
from unittest.mock import patch, MagicMock
import sys
# Force mock qdrant_client before importing any core modules that depend on it
from GramAddict.core.q_nav_graph import QNavGraph
class TestQNavGraphEdgeCases:
@pytest.fixture(autouse=True)
def setup_graph(self):
self.device = MagicMock()
self.device.app_id = "com.instagram.android"
self.device._get_current_app = MagicMock(return_value="com.instagram.android")
# Prevent Dojo engine instantiation during tests
with patch('GramAddict.core.compiler_engine.VLMCompilerEngine'):
self.graph = QNavGraph(self.device)
def test_find_path_edge_cases(self):
# 1. Start == End
assert self.graph._find_path("HomeFeed", "HomeFeed") == []
# 2. Start not in nodes
assert self.graph._find_path("UnknownState", "HomeFeed") == None
# 3. Unreachable states
self.graph.nodes = {
"HomeFeed": {"transitions": {"tap_explore": "ExploreFeed"}},
"IsolatedFeed": {"transitions": {}}
}
assert self.graph._find_path("HomeFeed", "IsolatedFeed") == None
# 4. Infinite loop protection (A -> B -> A)
self.graph.nodes = {
"A": {"transitions": {"to_b": "B"}},
"B": {"transitions": {"to_a": "A"}}
}
assert self.graph._find_path("A", "C") == None # Should safely return None without exceeding recursion/loop depth
# 5. Longest path possible before unreachability is confirmed
assert self.graph._find_path("B", "D") == None
# 6. Diamond shape path
self.graph.nodes = {
"Start": {"transitions": {"top": "Top", "bottom": "Bottom"}},
"Top": {"transitions": {"top_to_end": "End"}},
"Bottom": {"transitions": {"bottom_to_end": "End"}},
"End": {}
}
# BFS should find shortest path (len 2)
assert len(self.graph._find_path("Start", "End")) == 2
@patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance')
def test_execute_transition_edge_cases(self, mock_get_telepathic):
from GramAddict.core.telepathic_engine import TelepathicEngine
mock_engine = MagicMock(spec=TelepathicEngine)
mock_get_telepathic.return_value = mock_engine
zero_engine = MagicMock()
# Case 1: Telepathic engine finds nothing
mock_engine.find_best_node.return_value = None
# If still in Instagram, it returns False
self.device._get_current_app.return_value = "com.instagram.android"
assert self.graph._execute_transition("unknown_action", zero_engine) == False
# If app is different, it returns "CONTEXT_LOST"
self.device._get_current_app.return_value = "com.android.launcher3"
assert self.graph._execute_transition("unknown_action", zero_engine) == "CONTEXT_LOST"
# Case 2: Best node has skip flag
mock_engine.find_best_node.return_value = {"skip": True}
assert self.graph._execute_transition("already_done_action", zero_engine) == True
# Case 3: Proper interaction, but XML doesn't change (verification fail)
mock_engine.find_best_node.return_value = {"x": 10, "y": 10, "score": 0.9}
self.device.deviceV2.dump_hierarchy.side_effect = ["<xml>same</xml>", "<xml>same</xml>"]
assert self.graph._execute_transition("click_action", zero_engine) == False
mock_engine.reject_click.assert_called_once()
# Case 4: Proper interaction, XML changes (verification pass)
mock_engine.reset_mock()
mock_engine.find_best_node.return_value = {"x": 10, "y": 10, "score": 0.9}
self.device.deviceV2.dump_hierarchy.side_effect = ["<xml>before</xml>", "<xml>after</xml>"]
assert self.graph._execute_transition("click_action", zero_engine) == True
mock_engine.confirm_click.assert_called_once()
@patch('GramAddict.core.dojo_engine.DojoEngine.get_instance')
def test_navigate_to_recovery_edge_cases(self, mock_dojo):
# We test the deepest recovery logic: when everything fails
zero_engine = MagicMock()
# Mock transitions completely failing
with patch.object(self.graph, '_execute_transition', return_value=False):
# Recovery attempts maxed out
assert self.graph.navigate_to("ExploreFeed", zero_engine, recovery_attempts=3) == False
# Start logic where path is None and direct fallback also fails
self.graph.current_state = "IsolatedNode"
# It should trigger fallback and then return False because `_execute_transition` always returns False
assert self.graph.navigate_to("ExploreFeed", zero_engine, recovery_attempts=0) == False

View File

@@ -0,0 +1,64 @@
import os
import glob
import pytest
from unittest.mock import patch, MagicMock
# Removed sys.modules poison that mock qdrant_client globally
from GramAddict.core.telepathic_engine import TelepathicEngine
# Path to real xml dumps
DUMPS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug", "xml_dumps")
# Gather all XML files
xml_files = glob.glob(os.path.join(DUMPS_DIR, "*.xml"))
if not xml_files:
print(f"WARNING: No xml dumps found in {DUMPS_DIR}. Fuzzer cannot run.")
xml_files = ["dummy_path_to_prevent_pytest_crash"]
@pytest.fixture(autouse=True)
def mock_ai_services():
"""Ensure that the fuzzer never makes real LLM API or Qdrant DB calls."""
with patch('GramAddict.core.qdrant_memory.QdrantClient'), \
patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding', return_value=[0.0]*1536), \
patch('GramAddict.core.telepathic_engine.query_telepathic_llm', return_value='{"index": 0, "reason": "Fuzz Mock"}'):
yield
@pytest.mark.parametrize("xml_path", xml_files, ids=lambda x: os.path.basename(x))
def test_xml_parser_does_not_crash(xml_path):
"""
Reads an arbitrary XML dump from the physical device during crash events
and guarantees that the core TelepathicEngine parser handles it gracefully.
"""
if xml_path == "dummy_path_to_prevent_pytest_crash":
pytest.skip("No XML dumps found. Skipping fuzzer.")
if not os.path.exists(xml_path):
pytest.skip(f"XML dump missing: {xml_path}")
with open(xml_path, "r", encoding="utf-8") as f:
xml_content = f.read()
engine = TelepathicEngine()
try:
# Phase 1: Pure parsing stability
nodes = engine._extract_semantic_nodes(xml_content)
# Verify node structure if nodes exist
for n in nodes:
assert "raw_bounds" in n, f"Extracted node is missing raw_bounds. Content: {n}"
assert "semantic_string" in n, f"Extracted node missing semantic_string. Content: {n}"
if len(nodes) == 0:
print(f"WARN: {os.path.basename(xml_path)} parsed perfectly, but yielded ZERO readable nodes.")
# Phase 2: Query resolution stability (Keyword + Vector + VLM Fallbacks)
device_mock = MagicMock()
# Find completely arbitrary intent, just to trigger full resolution path
best_node = engine.find_best_node(xml_content, "dismiss this modal immediately or try clicking like", device=device_mock)
# It's totally fine if `best_node` is None (e.g. 0 nodes). We just verify NO Crash.
except Exception as e:
pytest.fail(f"Fuzz test crashed on {os.path.basename(xml_path)} with error: {str(e)}")