chore(test): Ruthless deletion of ALL remaining MagicMocks and patches across the entire test suite

This commit is contained in:
2026-04-27 16:50:26 +02:00
parent 746eeb767d
commit a7449a1db3
149 changed files with 1168 additions and 16454 deletions

View File

@@ -1,198 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
# 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:
@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
def test_extract_post_content_edge_cases(self, mock_get_telepathic):
mock_engine = MagicMock()
mock_get_telepathic.return_value = mock_engine
# 1. Empty string / Invalid XML should not crash (mock finds nothing)
mock_engine.find_best_node.return_value = None
res = _extract_post_content("")
assert res.get("username") == ""
assert res.get("description") == ""
# 2. Extract when only username exists
# Side effect: first call (author) returns node, second (media) returns None
mock_engine.find_best_node.side_effect = [{"original_attribs": {"text": "just_user"}}, None]
res = _extract_post_content("<xml/>")
assert res.get("username") == "just_user"
assert res.get("description") == ""
# 3. Extract description
mock_engine.find_best_node.side_effect = [None, {"original_attribs": {"desc": "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"}}]
res = _extract_post_content("<xml/>")
assert res.get("description") == "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"
# 4. Another valid description tag
mock_engine.find_best_node.side_effect = [
None,
{"original_attribs": {"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.random.random", return_value=0.5)
@patch("GramAddict.core.bot_flow.random.uniform", return_value=1.5)
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow._humanized_scroll")
@patch("GramAddict.core.utils.is_ad")
@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_scroll, mock_sleep, mock_uniform, mock_random
):
# 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.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 _humanized_scroll
assert mock_scroll.call_count >= 1
@patch("GramAddict.core.bot_flow.random.random", return_value=0.5)
@patch("GramAddict.core.bot_flow.random.uniform", return_value=1.5)
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow._humanized_scroll")
@patch("GramAddict.core.bot_flow._extract_post_content")
@patch("GramAddict.core.utils.is_ad")
@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_scroll,
mock_sleep,
mock_uniform,
mock_random,
):
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.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()
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow._humanized_scroll")
@patch("GramAddict.core.utils.is_ad")
@patch("GramAddict.core.bot_flow._align_active_post")
@patch("GramAddict.core.bot_flow._extract_post_content")
@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
@patch("GramAddict.core.llm_provider.query_llm")
def test_llm_timeout_handled_smoothly(
self, mock_query_llm, mock_get_telepathic, mock_extract, mock_align, mock_ad, mock_scroll, mock_sleep
):
"""
TDD Test: Verifies that if qwen3.5:latest times out during comment generation
(simulated by query_llm returning None after circuit breaker), the bot_flow
catches the empty response and continues gracefully without crashing.
"""
device = MagicMock()
zero_engine = MagicMock()
nav_graph = MagicMock()
configs = MagicMock()
session_state = MagicMock()
mock_ad.return_value = False
mock_align.return_value = False
# Make the LLM generation completely timeout and return None
mock_query_llm.return_value = None
cognitive_stack = {"dopamine": MagicMock(), "darwin": MagicMock(), "resonance": 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
# Emulate that dopamine WANTS to comment
cognitive_stack["dopamine"].get_action_desires.return_value = {"comment": True, "like": False}
# Avoid MagicMock comparison errors in Resonance Engine
cognitive_stack["resonance"].calculate_resonance.return_value = 0.8
session_state.check_limit.return_value = [False] * 10
device.dump_hierarchy.return_value = "<xml>row_feed_photo_profile_name</xml>"
mock_engine = MagicMock()
mock_engine._extract_semantic_nodes.return_value = [{"x": 10}]
mock_get_telepathic.return_value = mock_engine
# Valid post content so it proceeds to comment generation
mock_extract.return_value = {"username": "test_user", "description": "a long enough description"}
# Run feed loop - MUST NOT CRASH
try:
_run_zero_latency_feed_loop(
device, zero_engine, nav_graph, configs, session_state, "HomeFeed", cognitive_stack
)
except Exception as e:
pytest.fail(f"Feed loop crashed on LLM timeout with {e}")

View File

@@ -1,69 +0,0 @@
import os
from unittest.mock import MagicMock, patch
# 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
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"}
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,
}
# 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.dump_hierarchy.return_value = alien_xml
with (
patch("GramAddict.core.bot_flow.sleep"),
patch("GramAddict.core.behaviors.obstacle_guard.sleep"),
patch("GramAddict.core.bot_flow._humanized_scroll"),
patch("GramAddict.core.behaviors.obstacle_guard.TelepathicEngine.get_instance", return_value=mock_telepathic),
patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance", return_value=mock_telepathic),
):
from GramAddict.core.behaviors import PluginRegistry
from GramAddict.core.behaviors.obstacle_guard import ObstacleGuardPlugin
PluginRegistry.get_instance().register(ObstacleGuardPlugin())
_run_zero_latency_feed_loop(device, None, MagicMock(), configs, MagicMock(), "HomeFeed", cognitive_stack)
# VERIFICATION:
assert mock_telepathic.find_best_node.called
assert device.click.called

View File

@@ -1,78 +0,0 @@
"""
TDD Tests for Zero-Hardcode Screen Classification and Situational Awareness
"""
import os
import sys
from unittest.mock import patch
import pytest
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
from GramAddict.core.goap import ScreenIdentity, ScreenType
@pytest.fixture
def mock_screen_memory():
with patch("GramAddict.core.qdrant_memory.ScreenMemoryDB") as mock_db:
instance = mock_db.return_value
instance.is_connected = True
yield instance
@pytest.fixture
def mock_query_llm():
with patch("GramAddict.core.llm_provider.query_llm") as mock_llm:
yield mock_llm
def test_classify_screen_uses_memory(mock_screen_memory, mock_query_llm):
"""
Test that _classify_screen FIRST tries to hit the ScreenMemoryDB.
"""
si = ScreenIdentity("testbot")
# Mock that memory ALREADY knows this screen
mock_screen_memory.get_screen_type.return_value = ScreenType.MODAL.name
# We pass random strings that would previously fail or hit hardcoded checks
res = si._classify_screen(
ids=set(),
descs=[],
texts=["totally ambiguous text"],
selected_tab=None,
desc_lower="",
text_lower="",
ids_str="random_id",
signature="MOCK_SIGNATURE",
)
assert res == ScreenType.MODAL
mock_screen_memory.get_screen_type.assert_called_once_with("MOCK_SIGNATURE", similarity_threshold=0.92)
# Should not fall back to LLM if memory hits
mock_query_llm.assert_not_called()
def test_classify_screen_uses_llm_fallback_and_learns(mock_screen_memory, mock_query_llm):
"""
Test that if memory misses, it uses LLM fallback and caches the result.
"""
si = ScreenIdentity("testbot")
mock_screen_memory.get_screen_type.return_value = None
mock_query_llm.return_value = {"response": "HOME_FEED"}
res = si._classify_screen(
ids={"random"},
descs=[],
texts=[],
selected_tab=None,
desc_lower="",
text_lower="",
ids_str="random",
signature="MOCK_SIGNATURE_2",
)
assert res == ScreenType.HOME_FEED
mock_query_llm.assert_called_once()
mock_screen_memory.store_screen.assert_called_once_with("MOCK_SIGNATURE_2", "HOME_FEED")

View File

@@ -1,190 +0,0 @@
import os
import time
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop, _wait_for_post_loaded
from GramAddict.core.perception.feed_analysis import FEED_MARKERS
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.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):
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.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.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("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.dump_hierarchy.return_value = broken_xml
from GramAddict.core.situational_awareness import SituationType
with (
patch("GramAddict.core.bot_flow._humanized_scroll") as mock_scroll,
patch("GramAddict.core.bot_flow.sleep"),
patch(
"GramAddict.core.situational_awareness.SituationalAwarenessEngine.perceive",
return_value=SituationType.NORMAL,
),
):
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 == "FEED_EXHAUSTED"
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.dump_hierarchy.return_value = alien_xml
with patch("GramAddict.core.bot_flow._humanized_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
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

@@ -1,50 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
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

@@ -1,96 +0,0 @@
import os
import sys
import unittest
# 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):
import unittest
self.deviceV2 = self.DeviceV2()
self.app_id = "com.instagram.android"
self.args = unittest.mock.MagicMock()
self.args.ai_telepathic_model = "qwen2.5:3b"
self.args.ai_telepathic_url = "http://localhost:11434/api/generate"
def _get_current_app(self):
return "com.instagram.android"
def get_info(self):
return {"displayHeight": 2400, "displayWidth": 1080}
def screenshot(self, path=None):
return "fake_screenshot"
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,
min_confidence=0.5,
)
# 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

@@ -1,52 +0,0 @@
from unittest.mock import MagicMock, patch
from GramAddict.core.llm_provider import query_llm
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

@@ -1,49 +0,0 @@
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.dump_hierarchy.return_value = "<hierarchy />"
mock_device.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.goap.PathMemory.learn_path"),
patch("GramAddict.core.goap.PathMemory.recall_path", return_value=None),
patch("GramAddict.core.qdrant_memory.ScreenMemoryDB._get_embedding", return_value=[0] * 1536),
patch(
"GramAddict.core.situational_awareness.SituationalAwarenessEngine.ensure_clear_screen", return_value=False
),
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.app_start.called, "Should have force-started the app when context was lost"

View File

@@ -1,123 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
# 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.info = {"screenOn": True}
self.device.dump_hierarchy.return_value = '<hierarchy><node package="com.instagram.android" /></hierarchy>'
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") is None
# 3. Unreachable states
self.graph.nodes = {
"HomeFeed": {"transitions": {"tap_explore": "ExploreFeed"}},
"IsolatedFeed": {"transitions": {}},
}
assert self.graph._find_path("HomeFeed", "IsolatedFeed") is 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") is 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") is 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.q_nav_graph.time.sleep", return_value=None)
@patch("GramAddict.core.q_nav_graph.random_sleep", return_value=None)
@patch("GramAddict.core.situational_awareness.random_sleep", return_value=None)
@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
def test_execute_transition_edge_cases(self, mock_get_telepathic, mock_sae_sleep, mock_q_rand_sleep, mock_q_sleep):
from GramAddict.core.telepathic_engine import TelepathicEngine
mock_engine = MagicMock(spec=TelepathicEngine)
mock_get_telepathic.return_value = mock_engine
# 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 not self.graph._execute_transition("unknown_action", mock_engine)
# 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", mock_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", mock_engine)
# 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}
same_xml = '<hierarchy><node package="com.instagram.android" class="same" /></hierarchy>'
self.device.dump_hierarchy.side_effect = None
self.device.dump_hierarchy.return_value = same_xml
assert not self.graph._execute_transition("click_action", mock_engine)
assert mock_engine.reject_click.call_count == 3
# 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}
before_xml = '<hierarchy><node package="com.instagram.android" class="before" /></hierarchy>'
after_xml = '<hierarchy><node package="com.instagram.android" class="after" /></hierarchy>'
initial_clicks = self.device.click.call_count
def dynamic_xml(*args, **kwargs):
return after_xml if self.device.click.call_count > initial_clicks else before_xml
self.device.dump_hierarchy.side_effect = dynamic_xml
# Explicitly ensure verify_success is truthy
mock_engine.verify_success.return_value = True
assert self.graph._execute_transition("click_action", mock_engine)
mock_engine.confirm_click.assert_called_once()
@patch("GramAddict.core.q_nav_graph.time.sleep", return_value=None)
@patch("GramAddict.core.q_nav_graph.random_sleep", return_value=None)
@patch("GramAddict.core.situational_awareness.random_sleep", return_value=None)
@patch("GramAddict.core.dojo_engine.DojoEngine.get_instance")
def test_navigate_to_recovery_edge_cases(self, mock_dojo, mock_sae_sleep, mock_q_rand_sleep, mock_q_sleep):
# We test the deepest recovery logic: when everything fails
zero_engine = MagicMock()
# Mock transitions completely failing
with patch.object(self.graph.goap, "navigate_to_screen", return_value=False):
# Recovery attempts maxed out
assert not self.graph.navigate_to("ExploreFeed", zero_engine, recovery_attempts=3)
# 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 `navigate_to_screen` always returns False
assert not self.graph.navigate_to("ExploreFeed", zero_engine, recovery_attempts=0)

View File

@@ -1,59 +0,0 @@
import os
import sys
from unittest.mock import MagicMock, patch
import pytest
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
from GramAddict.core.goap import GoalExecutor, ScreenType
@pytest.fixture
def mock_device():
device = MagicMock()
# Simulate XML changing but screen type not being the target
device.dump_hierarchy.side_effect = ["<xml1/>", "<xml2/>", "<xml2/>"]
device.app_id = "com.instagram.android"
return device
@pytest.fixture
def mock_telepathic():
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance") as mock:
engine = mock.return_value
engine.find_best_node.return_value = {"x": 100, "y": 200, "semantic_string": "mock_node"}
yield engine
def test_execution_rejects_wrong_screen(mock_device, mock_telepathic):
"""
TDD Case: If we intend to go to DMs but land on Reels,
TelepathicEngine.confirm_click should NOT be called.
"""
executor = GoalExecutor(mock_device, "testuser")
# We mock perceive to return ReelsFeed after the click
with patch.object(executor, "perceive") as mock_perceive:
# Before click
mock_perceive.side_effect = [
{"screen_type": ScreenType.HOME_FEED}, # Initial
{"screen_type": ScreenType.REELS_FEED}, # After click (WRONG!)
]
# Action that intends to go to DM_INBOX
action = "tap messages tab"
# We need to make sure _execute_action knows the goal is "open messages"
# Since _execute_action is usually called from achieve(), we mock that flow
success = executor._execute_action(action, goal="open messages")
# Success should be False because we didn't reach the goal
# (Or True if we only care about XML change, but that's what we're changing)
assert success is False
# CRITICAL: confirm_click should NOT have been called for 'messages tab'
# since we are on Reels.
mock_telepathic.confirm_click.assert_not_called()
mock_telepathic.reject_click.assert_called_once_with(action)

View File

@@ -1,68 +0,0 @@
from GramAddict.core.telepathic_engine import TelepathicEngine
class TestTelepathicGuards:
def setup_method(self):
self.engine = TelepathicEngine()
def test_strict_story_ring_guard(self):
"""
TDD: Story rings MUST be physically near the top of the screen (y < 30%).
Post profile headers that appear further down must be aggressively blocked
when the intent is 'tap story ring avatar'.
"""
intent = "tap story ring avatar"
screen_height = 2400
# Valid Story Ring (Top of screen, but below status bar)
valid_story = {"resource_id": "reel_ring", "y": 300, "area": 100}
assert self.engine._structural_sanity_check(valid_story, intent, screen_height) is True
# Invalid Story Ring (Hallucination: Post profile header in the feed)
invalid_story = {"resource_id": "row_feed_profile_header", "y": 800, "area": 100}
assert self.engine._structural_sanity_check(invalid_story, intent, screen_height) is False
def test_strict_button_guard(self):
"""
TDD: When explicitly looking for a 'button', nodes that declare themselves
as profiles (e.g. 'go to profile') must be blocked, to prevent accidental
profile visits when clicking 'like'.
"""
intent = "Heart like button for comment"
screen_height = 2400
# Valid Like Button
valid_btn = {"resource_id": "like_button", "semantic_string": "Like", "y": 1000, "area": 100}
assert self.engine._structural_sanity_check(valid_btn, intent, screen_height) is True
# Invalid Profile Link masquerading as a match due to string proximity
invalid_prof = {
"resource_id": "username",
"semantic_string": "Go to cayleighanddavid's profile",
"y": 1000,
"area": 100,
}
assert self.engine._structural_sanity_check(invalid_prof, intent, screen_height) is False
# However, if the intent *is* profile, it should pass
intent_prof = "go to profile"
assert self.engine._structural_sanity_check(invalid_prof, intent_prof, screen_height) is True
def test_like_semantic_verification(self):
"""
TDD: Verify that 'unlike' is treated as a successful 'Like' action,
because tapping 'Like' changes the state to 'Unlike' in English Instagram.
"""
# Testing the specific regex logic inside verify_success
import re
xml_dump_success = '<node class="android.widget.ImageView" content-desc="Unlike" />'
marker_found = re.search(r"\b(liked|unlike|gefällt mir nicht mehr|gefällt mir am)\b", xml_dump_success.lower())
assert marker_found is not None
xml_dump_fail = '<node class="android.widget.ImageView" content-desc="Like" />'
marker_found_fail = re.search(
r"\b(liked|unlike|gefällt mir nicht mehr|gefällt mir am)\b", xml_dump_fail.lower()
)
assert marker_found_fail is None

View File

@@ -1,67 +0,0 @@
import os
import sys
import unittest
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
from GramAddict.core.q_nav_graph import QNavGraph
from GramAddict.core.telepathic_engine import TelepathicEngine
class TestTrapEscape(unittest.TestCase):
@patch("GramAddict.core.q_nav_graph.time.sleep", return_value=None)
@patch("GramAddict.core.q_nav_graph.random_sleep", return_value=None)
@patch("GramAddict.core.situational_awareness.SituationalAwarenessEngine.ensure_clear_screen", return_value=False)
def test_trap_guard_autonomous_ai_escape(self, mock_sae_clear, mock_q_rand_sleep, mock_q_sleep):
print("Starting TDD: Testing autonomous Trap Escape with semantic bypass...")
# 1. Setup mocks
mock_device = MagicMock()
mock_device.app_id = "com.instagram.android"
mock_device._get_current_app.return_value = "com.instagram.android"
trap_xml = "<hierarchy><node resource-id='modal_trap' /></hierarchy>"
current_xml = [trap_xml]
# Dynamic dump that changes after click
def dynamic_dump():
return current_xml[0]
def dynamic_click(**kwargs):
if kwargs.get("obj") and kwargs["obj"].get("semantic") and "done" in kwargs["obj"].get("semantic").lower():
current_xml[0] = "<html><node text='Reels'/><node text='Home'/></html>"
mock_device.dump_hierarchy.side_effect = dynamic_dump
mock_device.click.side_effect = dynamic_click
nav_graph = QNavGraph(device=mock_device)
engine = TelepathicEngine.get_instance()
engine.confirm_click = MagicMock()
engine.reject_click = MagicMock()
original_find_best_node = engine.find_best_node
def spy_find_best_node(xml_hierarchy, intent_description, **kwargs):
if "tap home tab" in intent_description.lower():
return None
return original_find_best_node(xml_hierarchy, intent_description, **kwargs)
engine.find_best_node = spy_find_best_node
nav_graph.engine = engine # explicitly enforce
# 2. Execute transition
# Mock engine finds nothing, triggering the final fallback escape
nav_graph._execute_transition("tap_home_tab", max_retries=1, mock_semantic_engine=engine)
# 3. Assertions
# The new SAE/nav_graph behavior explicitly presses BACK when 'tap_home_tab' fails after all retries
self.assertTrue(mock_device.press.called, "Trap guard did not autonomously press BACK to escape the sub-view!")
called_key = mock_device.press.call_args_list[0][0][0]
self.assertEqual(called_key, "back")
print("TDD SUCCESS: Autonomous Backend fallback confirmed.")
if __name__ == "__main__":
unittest.main()