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

0
tests/__init__.py Normal file
View File

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)}")

105
tests/conftest.py Normal file
View File

@@ -0,0 +1,105 @@
import pytest
import logging
from unittest.mock import MagicMock
MagicMock.app_id = "com.instagram.android"
MagicMock._get_current_app = MagicMock(return_value="com.instagram.android")
class MockArgs:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
class MockConfigs:
def __init__(self, args):
self.args = args
class MockDeviceV2:
def __init__(self):
self.clicks = []
self.shells = []
self.double_clicks = []
self.presses = []
self.xml_dump = "<hierarchy><node resource-id='com.instagram.android:id/row_comment_imageview' bounds='[10,10][20,20]' content-desc='Story' text='following' /><node resource-id='com.instagram.android:id/button_like' bounds='[50,50][60,60]' /><node resource-id='com.instagram.android:id/reel_viewer' /></hierarchy>"
def click(self, x, y):
self.clicks.append((x, y))
def shell(self, cmd):
self.shells.append(cmd)
def double_click(self, x, y, duration=0):
self.double_clicks.append((x, y))
def press(self, key):
self.presses.append(key)
def dump_hierarchy(self):
return self.xml_dump
class MockDevice:
def __init__(self):
self.deviceV2 = MockDeviceV2()
def get_info(self):
return {"displayWidth": 1080, "displayHeight": 2400}
def cm_to_pixels(self, cm):
return cm * 10
class MockTelepathicEngine:
def find_best_node(self, xml, intent_description, device=None, **kwargs):
description = intent_description.lower()
if "story ring" in description:
return {"x": 100, "y": 100, "description": "Story Ring", "score": 1.0}
if "follow" in description or "folgen" in description:
return {"x": 200, "y": 200, "text": "Follow", "description": "Follow Button", "score": 1.0}
if "first image" in description or "profile grid" in description:
return {"x": 300, "y": 300, "description": "Grid Image", "score": 1.0}
return None
def _extract_semantic_nodes(self, xml):
return [{"x": 10, "y": 10}]
def confirm_click(self, *args, **kwargs):
pass
def reject_click(self, *args, **kwargs):
pass
@classmethod
def get_instance(cls):
return cls()
@pytest.fixture
def mock_logger():
return logging.getLogger("test")
@pytest.fixture
def device():
return MockDevice()
@pytest.fixture(autouse=True)
def telepathic_mock(monkeypatch):
import GramAddict.core.telepathic_engine
engine = MockTelepathicEngine()
monkeypatch.setattr(GramAddict.core.telepathic_engine.TelepathicEngine, "get_instance", lambda: engine)
return engine
@pytest.fixture
def mock_cognitive_stack():
stack = {
"dopamine": MagicMock(),
"darwin": MagicMock(),
"resonance": MagicMock(),
"active_inference": MagicMock(),
"growth_brain": MagicMock(),
"swarm": MagicMock(),
"radome": MagicMock(),
"nav_graph": MagicMock(),
"zero_engine": MagicMock(),
"crm": MagicMock(),
"telepathic": MockTelepathicEngine()
}
stack["radome"].sanitize_xml.side_effect = lambda x: x
return stack

0
tests/e2e/__init__.py Normal file
View File

126
tests/e2e/conftest.py Normal file
View File

@@ -0,0 +1,126 @@
import sys
import os
import pytest
import time
from unittest.mock import MagicMock
from GramAddict.core import utils
# Force Qdrant mocking globally across ALL E2E tests so we never
# block on connection refused trying to hit localhost:6344
mock_qdrant = MagicMock()
# Setup correct return types for dimension check warnings in qdrant_memory
mock_collection = MagicMock()
mock_collection.config.params.vectors.size = 768
mock_qdrant.get_collection.return_value = mock_collection
sys.modules["qdrant_client"].QdrantClient = MagicMock(return_value=mock_qdrant)
@pytest.fixture
def e2e_device_dump_injector():
"""
Provides a factory to mock device.deviceV2.dump_hierarchy using real XML files.
Will gracefully fail with a comprehensive assertion if the file is missing
(per 'ECHTE DUMPS fehlen' reporting requirement).
"""
def _inject_dump(device_mock, xml_filename):
fix_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
xml_path = os.path.join(fix_dir, xml_filename)
if not os.path.exists(xml_path):
pytest.fail(f"MISSING REAL DUMP: required XML fixture '{xml_filename}' for full E2E workflow testing could not be found at {xml_path}. FAKE_NOTHING policy implies dropping this test execution until it is captured.", pytrace=False)
with open(xml_path, "r") as f:
real_xml = f.read()
device_mock.deviceV2.dump_hierarchy.return_value = real_xml
return real_xml
return _inject_dump
@pytest.fixture
def dynamic_e2e_dump_injector(monkeypatch):
"""
State-Machine Injector: Replaces dump_hierarchy dynamically when transitions occur.
Validates that the Telepathic Engine's pathfinding truly worked.
"""
def _inject(device_mock, state_map, initial_xml):
fix_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
def load_xml(filename):
path = os.path.join(fix_dir, filename)
if not os.path.exists(path):
pytest.fail(f"MISSING REAL DUMP: {filename} not found.")
with open(path, "r") as f:
return f.read()
device_mock.deviceV2.dump_hierarchy.return_value = load_xml(initial_xml)
from GramAddict.core.q_nav_graph import QNavGraph
original_execute = QNavGraph._execute_transition
def _mock_execute_transition(nav_self, action, zero_engine):
if action == 'tap_post_username':
return True
# Evaluate using the real internal LLM/Keyword logic against the current mock XML!
success = original_execute(nav_self, action, zero_engine)
if success is True and action in state_map:
# The node was clicked successfully! Swap the XML to the target state.
device_mock.deviceV2.dump_hierarchy.return_value = load_xml(state_map[action])
return success
monkeypatch.setattr(QNavGraph, "_execute_transition", _mock_execute_transition)
return _inject
@pytest.fixture(autouse=True)
def mock_all_delays(monkeypatch):
"""
Strips out all humanized hardware delays specifically for the E2E test suite.
Ensures loops evaluate instantly using the injected dumps.
"""
monkeypatch.setattr(time, "sleep", lambda x: None)
monkeypatch.setattr(utils, "random_sleep", lambda *args, **kwargs: None)
monkeypatch.setattr(utils, "sleep", lambda x: None)
# Standardize DarwinEngine across tests to prevent mockup math errors on session end
try:
from GramAddict.core.darwin_engine import DarwinEngine
monkeypatch.setattr(DarwinEngine, "evaluate_session_end", lambda *args, **kwargs: None)
except ImportError:
pass
@pytest.fixture
def e2e_configs():
import argparse
configs = MagicMock()
configs.args = argparse.Namespace(
username="testuser",
device="emulator-5554",
app_id="com.instagram.android",
debug=True,
feed=None,
carousel_percentage=0,
carousel_count="1",
explore=None,
reels=None,
stories=None,
interact_percentage=0,
likes_percentage=0,
follow_percentage=0,
comment_percentage=0,
working_hours=[0.0, 24.0],
time_delta_session=0,
speed_multiplier=1.0,
disable_filters=False,
interaction_users_amount="1",
scrape_profiles=False,
disable_ai_messaging=True,
total_unfollows_limit=0,
ai_telepathic_url="http://localhost",
ai_telepathic_model="llama3",
ai_condenser_url="http://localhost",
ai_condenser_model="llama3"
)
return configs

View File

@@ -0,0 +1,50 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
@patch("GramAddict.core.bot_flow._humanized_horizontal_swipe")
def test_full_e2e_carousel_handling(
mock_swipe, mock_dopamine, mock_sess, mock_create_device, mock_rsleep, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector, e2e_configs
):
"""
Tests that the core feed loop successfully identifies native Carousel identifiers
in the XML and initiates organic swiping inputs.
"""
device = MagicMock()
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, False, True]
mock_d_inst.wants_to_change_feed.return_value = False
mock_d_inst.wants_to_doomscroll.return_value = False
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Carousel")]
e2e_configs.args.feed = "1-2"
e2e_configs.args.carousel_percentage = 100
e2e_configs.args.carousel_count = "3-3"
# Load the captured UI dump containing native carousel_page_indicator
dynamic_e2e_dump_injector(device, {}, "carousel_post_dump.xml")
try:
with patch("GramAddict.core.bot_flow.Config", return_value=e2e_configs):
with patch("GramAddict.core.bot_flow.QNavGraph.navigate_to", return_value=True):
with patch("secrets.choice", return_value="HomeFeed"):
with patch("random.random", return_value=0.0):
start_bot()
except Exception as e:
assert str(e) == "Clean Exit for Carousel"
# Verify that the bot accurately parsed the JSON/XML, detected the Carousel,
# and initiated exactly 3 horizontal right-to-left swipes as requested by args.
assert mock_swipe.call_count == 3

View File

@@ -0,0 +1,46 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_dm_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for DM")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
disable_ai_messaging = False
feed = None
reels = None
explore = None
stories = None
total_unfollows_limit = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_message_icon': 'dm_inbox_dump.xml'}, "home_feed_with_ad.xml")
# Let the core system hit its real execution loop with actual XMLs instead of circumventing it
try:
with patch("secrets.choice", return_value="MessageInbox"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for DM"
mock_open.assert_called()

View File

@@ -0,0 +1,44 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DojoEngine")
def test_dojo_lifecycle_integration(
mock_dojo, mock_sess, mock_create_device, mock_rsleep, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_dojo_inst = mock_dojo.get_instance.return_value
mock_dojo_inst.is_running = True
mock_sess.inside_working_hours.side_effect = [Exception("Lifecycle Exit")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
feed = "1"
working_hours = "00:00-23:59"
time_delta_session = "0"
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_profile_tab': 'scraping_profile_dump.xml'}, "home_feed_with_ad.xml")
try:
start_bot(configs=configs)
except Exception as e:
assert "Lifecycle Exit" in str(e)
mock_dojo.get_instance.assert_called()
mock_dojo_inst.start.assert_called()
mock_dojo_inst.stop.assert_called()

View File

@@ -0,0 +1,49 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_explore_feed_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Explore")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
explore = "5-8"
feed = None
reels = None
stories = None
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
# The actual dump we need for this workflow (available in fixtures/fixtures)
# The fixture will automatically hit pytest.fail if the dump vanishes.
dynamic_e2e_dump_injector(device, {'tap_explore_tab': 'explore_feed_dump.xml'}, "home_feed_with_ad.xml")
try:
with patch("secrets.choice", return_value="ExploreFeed"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for Explore"
mock_open.assert_called()

View File

@@ -0,0 +1,54 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_home_feed_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
"""
Test a full E2E sequence for Home Feed using actual real XML dumps.
"""
device = MagicMock()
mock_create_device.return_value = device
# Setup mock dopamine & session
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Home")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
feed = "5-8"
explore = None
reels = None
stories = None
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {}, "home_feed_with_ad.xml")
try:
# We must also mock secrets.choice to ensure HomeFeed is picked
with patch("secrets.choice", return_value="HomeFeed"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for Home"
mock_open.assert_called()

View File

@@ -0,0 +1,47 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_reels_feed_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Reels")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
reels = "10"
feed = None
explore = None
stories = None
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_reels_tab': 'reels_feed_dump.xml'}, "home_feed_with_ad.xml")
try:
with patch("secrets.choice", return_value="ReelsFeed"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for Reels"
mock_open.assert_called()

View File

@@ -0,0 +1,47 @@
import pytest
from unittest.mock import MagicMock, patch, PropertyMock
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
@patch("GramAddict.core.bot_flow.ResonanceEngine")
@patch("GramAddict.core.bot_flow._interact_with_profile")
def test_full_e2e_scraping_sequence(
mock_interact, mock_resonance, mock_dopamine, mock_sess, mock_create_device, mock_rsleep, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector, e2e_configs
):
device = MagicMock()
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.wants_to_change_feed.return_value = False
mock_d_inst.wants_to_doomscroll.return_value = False
type(mock_d_inst).boredom = PropertyMock(return_value=0.0)
mock_d_inst.is_app_session_over.side_effect = [False, False, False, False, False, True]
mock_res_inst = mock_resonance.return_value
mock_res_inst.calculate_resonance.return_value = 100.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit Scrape")]
e2e_configs.args.scrape_profiles = True
e2e_configs.args.interact_percentage = 100
e2e_configs.args.feed = "1"
dynamic_e2e_dump_injector(device, {'tap_profile_tab': 'scraping_profile_dump.xml'}, "carousel_post_dump.xml")
with patch("GramAddict.core.bot_flow.Config", return_value=e2e_configs):
with patch("GramAddict.core.bot_flow.QNavGraph.navigate_to", return_value=True):
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.find_best_node", return_value={"bounds": "[0,0][100,100]"}):
with patch("secrets.choice", return_value="HomeFeed"):
try:
start_bot()
except Exception as e:
if "Clean Exit Scrape" not in str(e):
raise e
mock_interact.assert_called()

View File

@@ -0,0 +1,52 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_search_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_rsleep, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Search")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
search = "coding"
feed = None
reels = None
explore = None
stories = None
working_hours = "00:00-23:59"
time_delta_session = "0"
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_explore_tab': 'explore_feed_dump.xml'}, "home_feed_with_ad.xml")
try:
with patch("secrets.choice", return_value="SearchFeed"):
start_bot(configs=configs)
except Exception as e:
assert "Clean Exit" in str(e)
mock_open.assert_called()

View File

@@ -0,0 +1,63 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
@patch("GramAddict.core.bot_flow.GrowthBrain")
def test_full_start_bot_e2e_working_hours_limits(
mock_brain, mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
"""
Test start_bot full loop with working hours limits.
Verifies that the bot correctly sleeps when outside working hours
and exits the loop when session limits are reached.
"""
device = MagicMock()
mock_create_device.return_value = device
# Setup mock dopamine
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
feed = "5-8"
explore = None
reels = None
stories = None
total_unfollows_limit = 0
working_hours = ["10.00-11.00", "15.00-16.00"]
time_delta_session = 10
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
# On iteration 1: valid working hours
# On iteration 2: Exception to jump out of loop
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit limits test")]
dynamic_e2e_dump_injector(device, {}, "home_feed_with_ad.xml")
try:
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit limits test"
# Verify key interactions
mock_sess.inside_working_hours.assert_called()
mock_open.assert_called()
mock_sleep.assert_called()

View File

@@ -0,0 +1,47 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_stories_feed_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Stories")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
stories = "5-8"
feed = None
reels = None
explore = None
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_home_tab': 'stories_feed_dump.xml'}, "home_feed_with_ad.xml")
try:
with patch("secrets.choice", return_value="StoriesFeed"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for Stories"
mock_open.assert_called()

View File

@@ -0,0 +1,48 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import start_bot
@patch("GramAddict.core.bot_flow.open_instagram", return_value=True)
@patch("GramAddict.core.bot_flow.close_instagram")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow.random_sleep")
@patch("GramAddict.core.bot_flow.create_device")
@patch("GramAddict.core.bot_flow.SessionState")
@patch("GramAddict.core.bot_flow.DopamineEngine")
def test_full_e2e_unfollow_sequence(
mock_dopamine, mock_sess, mock_create_device, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector
):
device = MagicMock()
mock_create_device.return_value = device
mock_d_inst = mock_dopamine.return_value
mock_d_inst.is_app_session_over.side_effect = [False, True]
mock_d_inst.boredom = 0.0
mock_sess.inside_working_hours.side_effect = [(True, 0), Exception("Clean Exit for Unfollow")]
class ConfigArgs:
username = "testuser"
device = "emulator-5554"
app_id = "com.instagram.android"
debug = True
total_unfollows_limit = 10
feed = None
reels = None
explore = None
stories = None
interact_percentage = 0
likes_percentage = 0
follow_percentage = 0
comment_percentage = 0
configs = MagicMock()
configs.args = ConfigArgs()
dynamic_e2e_dump_injector(device, {'tap_profile_tab': 'scraping_profile_dump.xml', 'tap_following_list': 'unfollow_list_dump.xml'}, "home_feed_with_ad.xml")
try:
with patch("secrets.choice", return_value="FollowingList"):
start_bot(configs=configs)
except Exception as e:
assert str(e) == "Clean Exit for Unfollow"
mock_open.assert_called()

View File

@@ -0,0 +1,39 @@
import pytest
import os
from GramAddict.core.bot_flow import _detect_ad_structural
FIX_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
def test_real_sponsored_reel_flexcode_is_detected():
"""
Test: The manual_interrupt dump is a sponsored Reel (flexcode_systems).
_detect_ad_structural MUST return True.
"""
xml_path = os.path.join(FIX_DIR, "sponsored_reel.xml")
with open(xml_path, "r") as f:
xml = f.read()
assert _detect_ad_structural(xml) is True, "Failed to detect Sponsored Reel ad in realistic dump!"
def test_normal_post_not_ad():
"""
Test: The manual_interrupt dump is a normal post.
_detect_ad_structural MUST return False to avoid false positives.
"""
xml_path = os.path.join(FIX_DIR, "organic_post.xml")
with open(xml_path, "r") as f:
xml = f.read()
assert _detect_ad_structural(xml) is False, "False positive! Detected normal post as ad!"
def test_peugeot_carousel_ad_is_detected():
"""
Test: The 'peugeot.deutschland' carousel ad from manual_interrupt dump.
_detect_ad_structural MUST return True.
"""
xml_path = os.path.join(FIX_DIR, "peugeot_ad.xml")
with open(xml_path, "r") as f:
xml = f.read()
assert _detect_ad_structural(xml) is True, "Failed to detect Peugeot Carousel ad from manual dump!"

View File

View File

@@ -0,0 +1,301 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import (
_run_zero_latency_feed_loop,
_run_zero_latency_stories_loop,
_extract_post_content,
_detect_ad_structural,
_align_active_post
)
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}
return device
def test_extract_post_content():
xml = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy>
<node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="test_user"/>
<node resource-id="com.instagram.android:id/row_feed_photo_imageview" content-desc="test description of image with more than 10 chars" />
</hierarchy>'''
res = _extract_post_content(xml)
assert res["username"] == "test_user"
assert "test description" in res["description"]
def test_extract_post_content_fallback_caption():
xml = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy>
<node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="other_user"/>
<node resource-id="" text="other_user this is a very long caption text that we want to extract as fallback" />
</hierarchy>'''
res = _extract_post_content(xml)
assert res["username"] == "other_user"
assert "this is a very long caption" in res["caption"]
def test_detect_ad_structural():
assert _detect_ad_structural('<node resource-id="com.instagram.android:id/ad_cta_button" />') == True
assert _detect_ad_structural('<node resource-id="com.instagram.android:id/clips_single_image_ads_media_content" />') == True
assert _detect_ad_structural('<node resource-id="com.instagram.android:id/secondary_label" text="Sponsored" />') == True
assert _detect_ad_structural('<node resource-id="com.instagram.android:id/secondary_label" text="regular post" />') == False
assert _detect_ad_structural('<node resource-id="com.instagram.android:id/normal_post" />') == False
def test_align_active_post(mock_device):
# Test snapping when post is far from ideal coordinates
mock_device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy>
<node resource-id="com.instagram.android:id/row_feed_profile_header" bounds="[0,800][1080,900]" />
</hierarchy>'''
res = _align_active_post(mock_device)
# The header is at 850px. Target is 250px. Diff is 600px. It should swipe.
assert mock_device.deviceV2.swipe.called
def test_feed_loop_boredom_change_feed(mock_device, mock_cognitive_stack):
mock_cognitive_stack["dopamine"].is_app_session_over.return_value = False
mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = True
configs = MagicMock()
session_state = MagicMock()
session_state.check_limit.return_value = (False, False, False, False)
res = _run_zero_latency_feed_loop(mock_device, mock_cognitive_stack["zero_engine"], mock_cognitive_stack["nav_graph"], configs, session_state, "HomeFeed", mock_cognitive_stack)
assert res == "BOREDOM_CHANGE_FEED"
def test_feed_loop_context_lost(mock_device, mock_cognitive_stack):
# Simulate not having any feed markers 3 times
mock_cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, False, False, True]
mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
mock_cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
mock_device.deviceV2.dump_hierarchy.return_value = "<hierarchy></hierarchy>" # Blind
# Needs telepathic engine mock
with patch('GramAddict.core.bot_flow.TelepathicEngine') as MockTelepathic, patch('GramAddict.core.bot_flow.dump_ui_state'):
mock_instance = MockTelepathic.get_instance.return_value
mock_instance._extract_semantic_nodes.return_value = [{"x": 1, "y": 2}] # Pretend we have nodes so it doesn't trigger zero-node immediately
configs = MagicMock()
session_state = MagicMock()
session_state.check_limit.return_value = (False, False, False, False)
res = _run_zero_latency_feed_loop(mock_device, mock_cognitive_stack["zero_engine"], mock_cognitive_stack["nav_graph"], configs, session_state, "HomeFeed", mock_cognitive_stack)
assert res == "CONTEXT_LOST"
def test_feed_loop_zero_nodes(mock_device, mock_cognitive_stack):
# Tests the Zero-Node recovery anomaly handler
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
with patch('GramAddict.core.bot_flow.TelepathicEngine') as MockTelepathic, \
patch('GramAddict.core.bot_flow._humanized_scroll') as mock_scroll:
mock_instance = MockTelepathic.get_instance.return_value
mock_instance._extract_semantic_nodes.return_value = [] # Zero interactive nodes
configs = MagicMock()
session_state = MagicMock()
session_state.check_limit.return_value = (False, False, False, False)
_run_zero_latency_feed_loop(mock_device, mock_cognitive_stack["zero_engine"], mock_cognitive_stack["nav_graph"], configs, session_state, "HomeFeed", mock_cognitive_stack)
assert mock_device.deviceV2.press.called_with("back")
assert mock_scroll.called
def test_feed_loop_ad_skip(mock_device, mock_cognitive_stack):
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
mock_device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
<hierarchy>
<node resource-id="com.instagram.android:id/row_feed_button_like" />
<node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="ad_account" />
<node resource-id="com.instagram.android:id/ad_cta_button" />
</hierarchy>'''
with patch('GramAddict.core.bot_flow.TelepathicEngine') as MockTelepathic, \
patch('GramAddict.core.bot_flow._humanized_scroll') as mock_scroll, \
patch('GramAddict.core.bot_flow._align_active_post') as mock_align:
mock_instance = MockTelepathic.get_instance.return_value
mock_instance._extract_semantic_nodes.return_value = [{"x": 1}]
mock_align.return_value = False
configs = MagicMock()
session_state = MagicMock()
session_state.check_limit.return_value = (False, False, False, False)
_run_zero_latency_feed_loop(mock_device, mock_cognitive_stack["zero_engine"], mock_cognitive_stack["nav_graph"], configs, session_state, "HomeFeed", mock_cognitive_stack)
assert mock_scroll.called
def test_stories_loop_success(mock_device, mock_cognitive_stack):
mock_cognitive_stack["dopamine"].is_app_session_over.return_value = False
mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
configs = MagicMock()
configs.args.stories = "1"
session_state = MagicMock()
mock_device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
<hierarchy>
<node resource-id="com.instagram.android:id/story_viewer" />
</hierarchy>'''
with patch('GramAddict.core.bot_flow._humanized_click') as mock_click, patch('GramAddict.core.bot_flow.sleep'):
res = _run_zero_latency_stories_loop(mock_device, configs, session_state, mock_cognitive_stack)
assert res == "SESSION_OVER"
assert mock_click.called
def test_stories_loop_boredom(mock_device, mock_cognitive_stack):
mock_cognitive_stack["dopamine"].is_app_session_over.return_value = False
mock_cognitive_stack["dopamine"].wants_to_change_feed.return_value = True
configs = MagicMock()
session_state = MagicMock()
with patch('GramAddict.core.bot_flow.sleep'):
res = _run_zero_latency_stories_loop(mock_device, configs, session_state, mock_cognitive_stack)
assert res == "BOREDOM_CHANGE_FEED"
assert mock_device.deviceV2.press.called_with("back")
def test_start_bot_interrupt():
from GramAddict.core.bot_flow import start_bot
# Mock all the heavy initialization
with patch('GramAddict.core.bot_flow.Config') as MockConfig, \
patch('GramAddict.core.bot_flow.configure_logger'), \
patch('GramAddict.core.bot_flow.check_if_updated'), \
patch('GramAddict.core.benchmark_guard.check_model_benchmarks'), \
patch('GramAddict.core.llm_provider.log_openrouter_burn'), \
patch('GramAddict.core.bot_flow.create_device') as mock_create_device, \
patch('GramAddict.core.bot_flow.set_time_delta') as mock_time_delta, \
patch('GramAddict.core.bot_flow.SessionState') as MockSession, \
patch('GramAddict.core.bot_flow.open_instagram', side_effect=KeyboardInterrupt()), \
patch('GramAddict.core.bot_flow.dump_ui_state') as mock_dump:
MockConfig.return_value.args.feed = True
MockConfig.return_value.args.explore = False
MockConfig.return_value.args.reels = False
MockConfig.return_value.args.stories = False
MockConfig.return_value.args.capture_e2e_dumps = False
MockConfig.return_value.args.working_hours = [10, 20]
MockConfig.return_value.args.time_delta_session = 30
MockSession.inside_working_hours.return_value = (True, 0)
with pytest.raises(KeyboardInterrupt):
start_bot(username="test", device_id="123")
assert mock_dump.called
def test_feed_loop_deep_engagement(mock_device, mock_cognitive_stack):
# This test hits the core interaction (Lines 900 - 1300)
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
mock_cognitive_stack["resonance"].calculate_resonance.return_value = 0.85
configs = MagicMock()
configs.args.likes_percentage = 100
configs.args.follow_percentage = 100
configs.args.comment_percentage = 100
configs.args.ai_vibe = "friendly"
configs.args.ai_condenser_model = "test-model"
configs.args.ai_condenser_url = "test-url"
configs.args.dry_run_comments = False
session_state = MagicMock()
# If checking ALL, return a tuple of Falses. If specific limit like LIKES/COMMENTS, return False bool.
session_state.check_limit.side_effect = lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
# Needs to report a structure that has NO ad, HAS content, and HAS feed markers.
mock_device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
<hierarchy>
<node resource-id="com.instagram.android:id/row_feed_button_like" />
<node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="legit_user" />
<node resource-id="com.instagram.android:id/row_feed_photo_imageview" content-desc="test image" />
<!-- Comment sheet structure for the sub-engagement logic -->
<node class="android.widget.LinearLayout">
<node resource-id="com.instagram.android:id/row_comment_textview_comment" text="This is a fantastic picture!" />
<node resource-id="com.instagram.android:id/row_comment_button_like" bounds="[10,10][20,20]" />
</node>
</hierarchy>'''
# Ensure radome doesn't destroy our XML string
mock_cognitive_stack["radome"].sanitize_xml.side_effect = lambda x: x
# Simulate that nav_graph transitions work
mock_cognitive_stack["nav_graph"]._execute_transition.return_value = True
with patch('GramAddict.core.bot_flow.TelepathicEngine') as MockTelepathic, \
patch('GramAddict.core.llm_provider.query_llm') as mock_llm, \
patch('GramAddict.core.bot_flow.random.random', return_value=0.11), \
patch('GramAddict.core.bot_flow.random.uniform', return_value=1.5), \
patch('GramAddict.core.bot_flow.random.randint', return_value=1), \
patch('GramAddict.core.bot_flow._align_active_post', return_value=False), \
patch('GramAddict.core.bot_flow._humanized_scroll') as mock_scroll, \
patch('GramAddict.core.bot_flow._humanized_click') as mock_click, \
patch('GramAddict.core.stealth_typing.ghost_type') as mock_type:
mock_instance = MockTelepathic.get_instance.return_value
mock_instance._extract_semantic_nodes.return_value = [{"x": 1, "y": 2}]
mock_instance.find_best_node.return_value = {"x": 50, "y": 50, "bounds": "[10,10][20,20]", "skip": False}
mock_llm.return_value = {"response": "Great shot!"}
mock_cognitive_stack["telepathic"] = mock_instance
# We need to ensure that the configs allow interacting!
configs.args.interact_percentage = 100
_run_zero_latency_feed_loop(mock_device, mock_cognitive_stack["zero_engine"], mock_cognitive_stack["nav_graph"], configs, session_state, "HomeFeed", mock_cognitive_stack)
assert mock_click.called
assert mock_type.called
def test_feed_loop_repost(mock_device, mock_cognitive_stack):
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
mock_cognitive_stack["resonance"].calculate_resonance.return_value = 0.85
configs = MagicMock()
configs.args.repost_percentage = 100
configs.args.likes_percentage = 0
configs.args.comment_percentage = 0
session_state = MagicMock()
session_state.check_limit.side_effect = lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
mock_device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
<hierarchy>
<node resource-id="com.instagram.android:id/row_feed_button_like" />
<node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="legit_user" />
<node resource-id="com.instagram.android:id/row_feed_photo_imageview" content-desc="test image" />
</hierarchy>'''
mock_cognitive_stack["radome"].sanitize_xml.side_effect = lambda x: x
mock_cognitive_stack["nav_graph"]._execute_transition.return_value = True
with patch('GramAddict.core.bot_flow.TelepathicEngine') as MockTelepathic, \
patch('GramAddict.core.bot_flow.random.random', return_value=0.11), \
patch('GramAddict.core.bot_flow._align_active_post', return_value=False), \
patch('GramAddict.core.bot_flow._humanized_scroll'), \
patch('GramAddict.core.bot_flow._humanized_click') as mock_click:
mock_instance = MockTelepathic.get_instance.return_value
mock_instance._extract_semantic_nodes.return_value = [{"x": 1, "y": 2}]
mock_instance.find_best_node.return_value = {"x": 50, "y": 50, "bounds": "[10,10][20,20]", "skip": False}
mock_cognitive_stack["telepathic"] = mock_instance
configs.args.interact_percentage = 100
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, "HomeFeed", mock_cognitive_stack)
assert mock_click.called

View File

@@ -0,0 +1,65 @@
import pytest
from unittest.mock import patch, MagicMock
from GramAddict.core.bot_flow import start_bot
@patch('GramAddict.core.persistent_list.PersistentList.persist')
@patch('secrets.choice', return_value="HomeFeed")
@patch('GramAddict.core.bot_flow._run_zero_latency_search_loop', return_value="SESSION_OVER")
@patch('GramAddict.core.bot_flow._run_zero_latency_dm_loop', return_value="SESSION_OVER")
@patch('GramAddict.core.bot_flow._run_zero_latency_unfollow_loop', return_value="SESSION_OVER")
@patch('GramAddict.core.bot_flow._run_zero_latency_stories_loop', return_value="SESSION_OVER")
@patch('GramAddict.core.bot_flow._run_zero_latency_feed_loop', return_value="SESSION_OVER")
@patch('GramAddict.core.bot_flow.DojoEngine')
@patch('GramAddict.core.bot_flow.HoneypotRadome')
@patch('GramAddict.core.bot_flow.ParasocialCRMDB')
@patch('GramAddict.core.bot_flow.GrowthBrain')
@patch('GramAddict.core.bot_flow.ResonanceEngine')
@patch('GramAddict.core.bot_flow.DopamineEngine')
@patch('GramAddict.core.bot_flow.ZeroLatencyEngine')
@patch('GramAddict.core.bot_flow.QNavGraph')
@patch('GramAddict.core.bot_flow.TelepathicEngine')
@patch('GramAddict.core.bot_flow.dump_ui_state')
@patch('GramAddict.core.bot_flow.random_sleep')
@patch('GramAddict.core.bot_flow.close_instagram')
@patch('GramAddict.core.bot_flow.get_instagram_version', return_value="1.0")
@patch('GramAddict.core.bot_flow.open_instagram', return_value=True)
@patch('GramAddict.core.bot_flow.SessionState')
@patch('GramAddict.core.bot_flow.set_time_delta')
@patch('GramAddict.core.bot_flow.create_device')
@patch('GramAddict.core.llm_provider.log_openrouter_burn')
@patch('GramAddict.core.benchmark_guard.check_model_benchmarks')
@patch('GramAddict.core.bot_flow.check_if_updated')
@patch('GramAddict.core.bot_flow.configure_logger')
@patch('GramAddict.core.bot_flow.Config')
def test_start_bot_normal_flow(MockConfig, mock_logger, mock_update, mock_benchmark, mock_burn,
mock_create_device, mock_time_delta, MockSession, mock_open_ig, mock_ig_version,
mock_close_ig, mock_sleep, mock_dump, mock_telepathic, mock_nav, mock_zero,
mock_dopamine_class, mock_resonance, mock_growth, mock_crm, mock_radome, mock_dojo,
mock_run_feed, mock_run_stories, mock_run_unfollow, mock_run_dm, mock_run_search,
mock_choice, mock_persist):
MockConfig.return_value.args.feed = True
MockConfig.return_value.args.explore = False
MockConfig.return_value.args.reels = True
MockConfig.return_value.args.stories = False
MockConfig.return_value.args.capture_e2e_dumps = False
MockConfig.return_value.args.working_hours = [10, 20]
MockConfig.return_value.args.time_delta_session = 30
MockSession.inside_working_hours.return_value = (True, 0)
# Simulate dopamine session over after one loop
mock_dopamine = mock_dopamine_class.return_value
mock_dopamine.is_app_session_over.side_effect = [False, True]
mock_dopamine.boredom = 10.0
# We need to intentionally throw an exception to break the "while True" loop
MockSession.side_effect = [MagicMock(), Exception("Break infinite loop")]
try:
start_bot(username="test", device_id="123")
except Exception as e:
if str(e) != "Break infinite loop":
raise e
assert mock_run_feed.called

View File

@@ -0,0 +1,103 @@
import pytest
import os
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import _extract_post_content
from GramAddict.core.resonance_engine import ResonanceEngine
from GramAddict.core.growth_brain import GrowthBrain
from datetime import datetime
# Path to the real XML dumps in the root directory
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DUMPS = {
"organic": os.path.join(ROOT_DIR, "fixtures", "organic_post.xml"),
"ad": os.path.join(ROOT_DIR, "fixtures", "peugeot_ad.xml"),
"explore": os.path.join(ROOT_DIR, "fixtures", "explore_feed_reel.xml"),
}
@pytest.fixture
def mock_engines():
"""Mock database connections but keep logic intact."""
with patch('GramAddict.core.resonance_engine.ContentMemoryDB') as mock_cm_cls, \
patch('GramAddict.core.resonance_engine.PersonaMemoryDB'), \
patch('GramAddict.core.growth_brain.PersonaMemoryDB'):
# Consistent mock instance
mock_cm = MagicMock()
mock_cm_cls.return_value = mock_cm
mock_cm.get_cached_evaluation.return_value = None
mock_cm._get_embedding.return_value = [0.1] * 1536
resonance = ResonanceEngine(my_username="test_bot", persona_interests=["fitness", "travel"])
growth = GrowthBrain(username="test_bot", persona_interests=["fitness", "travel"])
# Explicit inject
resonance._persona_vector = [0.1] * 1536
resonance.content_memory = mock_cm
# Reset mock after bootstrap
mock_cm._get_embedding.reset_mock()
mock_cm._get_embedding.return_value = [0.1] * 1536
return resonance, growth
def test_full_content_to_resonance_flow(mock_engines):
"""
REALITY CHECK: Tests the flow from RAW XML -> EXTRACED CONTENT -> RESONANCE SCORE.
Using 'dump.xml' which contains an organic post and an ad.
"""
resonance, _ = mock_engines
with open(DUMPS["organic"], "r") as f:
xml_content = f.read()
# 1. Extraction (The Bot's Eyes)
post_data = _extract_post_content(xml_content)
# Verify extraction from organic dump
assert post_data["username"] == "fiona.dawson"
assert "Sponsored Video" in post_data["description"]
# 2. Resonance (The Bot's Brain)
# Provide identical vectors to ensure 1.0 similarity math naturally
resonance.content_memory._get_embedding.return_value = [0.1] * 1536
score = resonance.calculate_resonance(post_data)
assert score == 1.0 # (1.0 - 0.15) / 0.30 -> capped to 1.0
assert resonance.judge_interaction(score) is True
def test_ad_detection_integration():
"""Verify that _detect_ad_structural works on the actual ad_dump.xml."""
from GramAddict.core.bot_flow import _detect_ad_structural
with open(DUMPS["ad"], "r") as f:
ad_xml = f.read()
# ad_dump.xml should contain nodes that trigger structural ad detection
is_ad = _detect_ad_structural(ad_xml)
assert is_ad is True or "secondary_label" in ad_xml
def test_circadian_pacing_logic(mock_engines):
"""Verify GrowthBrain adjusts pacing across artificial time shifts."""
_, growth = mock_engines
# Simulate Deep Sleep (03:00)
with patch('GramAddict.core.growth_brain.datetime') as mock_date:
mock_date.now.return_value = datetime(2026, 4, 13, 3, 0, 0)
pacing = growth.get_circadian_pacing()
assert pacing == 0.1
# Simulate Peak Hours (14:00)
with patch('GramAddict.core.growth_brain.datetime') as mock_date:
mock_date.now.return_value = datetime(2026, 4, 13, 14, 0, 0)
pacing = growth.get_circadian_pacing()
assert pacing == 1.0
def test_extract_explore_reel():
"""Verify extraction logic works on the Explore Grid/Reels dump."""
with open(DUMPS["explore"], "r") as f:
xml = f.read()
post_data = _extract_post_content(xml)
assert "steves_movies" in post_data["description"]
assert "Reel by" in post_data["description"]

View File

@@ -0,0 +1,145 @@
import sys
import time
from unittest.mock import MagicMock, patch
from qdrant_client.models import PointStruct
# Import under test
from GramAddict.core.qdrant_memory import (
ParasocialCRMDB, HeuristicMemoryDB, ContentMemoryDB,
NavigationMemoryDB, PersonaMemoryDB
)
from GramAddict.core.swarm_protocol import SwarmProtocol
from GramAddict.core.darwin_engine import DarwinEngine
from GramAddict.core.dojo_engine import DojoEngine
from GramAddict.core.growth_brain import GrowthBrain
from GramAddict.core.resonance_engine import ResonanceEngine
from GramAddict.core.q_nav_graph import QNavGraph
import pytest
@pytest.fixture
def mock_PointStruct():
with patch("GramAddict.core.qdrant_memory.PointStruct") as mock:
yield mock
@pytest.fixture
def mock_qdrant(mock_PointStruct):
with patch("GramAddict.core.qdrant_memory.QdrantClient") as MockClient:
client_instance = MockClient.return_value
client_instance.collection_exists.return_value = True
yield client_instance
# --- ORIGINAL CORE AUDIT ---
def test_parasocial_crm_logging(mock_qdrant, mock_PointStruct):
"""Verify that CRM interaction logging actually attempts to persist to Qdrant."""
crm = ParasocialCRMDB()
crm.client = mock_qdrant
with patch.object(ParasocialCRMDB, "_get_embedding", return_value=[0.1] * 1536):
crm.log_interaction("test_user_alpha", "like")
assert mock_qdrant.upsert.called
kwargs = mock_PointStruct.call_args[1]
assert kwargs["payload"]["username"] == "test_user_alpha"
assert kwargs["payload"]["interactions"][0]["type"] == "like"
def test_darwin_reward_signaling(mock_qdrant, mock_PointStruct):
"""Verify that Darwin Engine correctly records session rewards for reinforcement learning."""
engine = DarwinEngine("test_bot")
engine.client = mock_qdrant
engine.current_behavior = {"initial_dwell_sec": 4.5}
engine.emit_reward_signal(followers_gained=5, block_warnings_seen=0)
assert mock_qdrant.upsert.called
kwargs = mock_PointStruct.call_args[1]
assert kwargs["payload"]["reward"] == 5
def test_swarm_pheromone_emission(mock_qdrant, mock_PointStruct):
"""Verify that Swarm Protocol shares UI state outcomes with the fleet."""
swarm = SwarmProtocol("test_bot")
swarm.client = mock_qdrant
swarm.emit_pheromone("feed_scroll_A", "success")
assert mock_qdrant.upsert.called
kwargs = mock_PointStruct.call_args[1]
assert kwargs["payload"]["path_hash"] == "feed_scroll_A"
def test_dojo_background_learning(mock_qdrant, mock_PointStruct):
"""Verify that Dojo Engine processes snapshots and updates the heuristic memory."""
device = MagicMock()
dojo = DojoEngine(device)
dojo.db.client = mock_qdrant
with patch.object(dojo.compiler, "generate_heuristic", return_value={"rule_type": "regex", "pattern": ".*"}):
with patch.object(HeuristicMemoryDB, "_get_embedding", return_value=[0.1] * 1536):
dojo.start()
dojo.submit_snapshot("test_button_intent", "<xml/>", "Tap the like button")
start = time.time()
while mock_qdrant.upsert.call_count == 0 and time.time() - start < 3.0:
time.sleep(0.1)
dojo.stop()
assert mock_qdrant.upsert.called
kwargs = mock_PointStruct.call_args[1]
assert kwargs["payload"]["intent"] == "test_button_intent"
# --- EXTENDED ULTRA-AUDIT (PHASE 2) ---
def test_growth_brain_persona_learning(mock_qdrant, mock_PointStruct):
"""Verify that GrowthBrain persists persona insights derived from interactions."""
brain = GrowthBrain("test_user")
brain.persona_memory.client = mock_qdrant # Inject client into the wrapped DB
outcomes = [{"username": "niche_influencer", "action": "like", "resonance": 0.9}]
with patch.object(PersonaMemoryDB, "_get_embedding", return_value=[0.1] * 1536):
brain.refine_persona(outcomes)
assert mock_qdrant.upsert.called
kwargs = mock_PointStruct.call_args[1]
assert "High-resonance" in kwargs["payload"]["insight"]
def test_resonance_oracle_cross_talk(mock_qdrant, mock_PointStruct):
"""Verify that ResonanceEngine evaluation triggers both ContentMemory and ParasocialCRM updates."""
crm = ParasocialCRMDB()
crm.client = mock_qdrant
engine = ResonanceEngine("my_user", persona_interests=["cyberpunk", "tech"], crm=crm)
engine.content_memory.client = mock_qdrant
post = {"username": "cyber_artist", "description": "New neon artwork #cyberpunk", "caption": ""}
with patch.object(ContentMemoryDB, "_get_embedding", return_value=[0.1]*1536), \
patch.object(ParasocialCRMDB, "_get_embedding", return_value=[0.1]*1536), \
patch.object(NavigationMemoryDB, "_get_embedding", return_value=[0.1]*1536), \
patch.object(engine, "_cosine_similarity", return_value=0.9):
# We need to mock scroll for get_relationship_stage inside log_interaction
mock_qdrant.scroll.return_value = ([], None)
score = engine.calculate_resonance(post)
assert score > 0.7
# Should call upsert twice: 1 for content_memory, 1 for crm (inside ResonanceEngine)
assert mock_qdrant.upsert.call_count >= 2
# Verify ContentMemory storage
calls = [mock_PointStruct.call_args_list[i][1] for i in range(len(mock_PointStruct.call_args_list))]
content_storage = any("classification" in c["payload"] for c in calls)
crm_storage = any("stage" in c["payload"] for c in calls)
assert content_storage, "ResonanceEngine failed to cache evaluation in ContentMemory!"
assert crm_storage, "ResonanceEngine failed to update user profile in ParasocialCRM!"
def test_nav_graph_topological_persistence(mock_qdrant, mock_PointStruct):
"""Verify that QNavGraph shares learned navigation anchors with the fleet."""
device = MagicMock()
graph = QNavGraph(device)
graph.nav_memory.client = mock_qdrant
# Simulate discovering a transition
graph.nav_memory.store_transition("ExploreFeed", "tap_home_tab", "HomeFeed")
assert mock_qdrant.upsert.called
kwargs = mock_PointStruct.call_args[1]
assert kwargs["payload"]["from"] == "ExploreFeed"
assert kwargs["payload"]["action"] == "tap_home_tab"
assert kwargs["payload"]["to"] == "HomeFeed"

View File

@@ -0,0 +1,98 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.darwin_engine import DarwinEngine
class DummyArgs:
def __init__(self):
self.interact_percentage = 0
self.follow_percentage = 0
def test_darwin_engine_explore_exploit():
"""Test the Multi-Armed Bandit Epsilon-Greedy logic without a real Qdrant server."""
with patch("GramAddict.core.qdrant_memory.QdrantClient") as MockClient:
engine = DarwinEngine("test_user")
# Override epsilon to force exploitation (Greedy)
# Wait, inside synthesize_interaction_profile epsilon is hardcoded to 0.15
mock_record_1 = MagicMock()
mock_record_1.payload = {"params": {"initial_dwell_sec": 2.0}, "reward": 10.0}
mock_record_2 = MagicMock()
mock_record_2.payload = {"params": {"initial_dwell_sec": 10.0}, "reward": 50.0}
engine.client.scroll.return_value = ([mock_record_1, mock_record_2], None)
# We patch random.random to force Exploit
with patch("random.random", return_value=0.99):
profile = engine.synthesize_interaction_profile(0.5)
# Just ensure it generated something valid within bounds
assert 1.0 <= profile["initial_dwell_sec"] <= 20.0
# We patch random.random to force Explore
with patch("random.random", return_value=0.01):
profile_explore = engine.synthesize_interaction_profile(0.5)
# Just ensure it generated something valid
assert "initial_dwell_sec" in profile_explore
def test_evaluate_session_end_short_session():
"""Ensure short sessions are not recorded to avoid polluting RoI metrics."""
with patch("GramAddict.core.qdrant_memory.QdrantClient") as MockClient:
engine = DarwinEngine("test_user")
engine.current_behavior = {"initial_dwell_sec": 5.0} # Set behavior
# But wait, evaluate_session_end short sessions still emit, we didn't block it in the engine except by default math
engine.emit_reward_signal(followers_gained=10, block_warnings_seen=0)
# It should upsert
engine.client.upsert.assert_called_once()
def test_evaluate_session_end_upsert():
"""Ensure valid sessions are successfully logged to the database."""
with patch("GramAddict.core.qdrant_memory.QdrantClient") as MockClient:
engine = DarwinEngine("test_user")
engine.current_behavior = {"initial_dwell_sec": 5.0}
engine.evaluate_session_end(60.0, 100) # 60 minutes
engine.client.upsert.assert_called_once()
def test_execute_proof_of_resonance_close_comments():
"""Verify that Darwin correctly closes the comments section even if 'bottom_sheet_container' is missing."""
with patch("GramAddict.core.qdrant_memory.QdrantClient"):
engine = DarwinEngine("test_user")
device = MagicMock()
nav_graph = MagicMock()
zero_engine = MagicMock()
configs = MagicMock()
# Make the profile decide to read comments for 2s
fake_profile = {
"initial_dwell_sec": 1.0,
"scroll_velocity": 1.0,
"scroll_depth_clicks": 0,
"back_swipe_prob": 0.0,
"comment_read_dwell": 2.0
}
with patch.object(engine, 'synthesize_interaction_profile', return_value=fake_profile):
# Mock opening comments success
nav_graph._execute_transition.return_value = True
# Simulated UI Dump: No 'bottom_sheet_container', but neither 'row_feed' nor 'button_like'
# Which occurs when IG renames it to 'fragment_container_view' or similar wrapper
device.deviceV2.dump_hierarchy.return_value = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy>
<node class="android.widget.FrameLayout" bounds="[0,0][1080,2400]">
<!-- Random UI generic sheet classes the Bot doesn't track -->
<node class="androidx.appcompat.widget.LinearLayoutCompat" text="Reply" />
</node>
</hierarchy>
'''
# Act
with patch('random.random', return_value=0.0): # Force comment block entry
engine.execute_proof_of_resonance(device=device, resonance=0.9, nav_graph=nav_graph, zero_engine=zero_engine, configs=configs, resonance_oracle=None, username="test")
# Assert: Instead of checking string names for "bottom_sheet_container",
# it should verify the presence of 'row_feed' to confirm we are back in Home!
# If not in Home, it presses back twice.
assert device.deviceV2.press.call_count == 2

View File

@@ -0,0 +1,77 @@
import pytest
import os
from unittest.mock import MagicMock, patch
import xml.etree.ElementTree as ET
# Assuming bot_flow.py logic is modular enough or we test the extraction logic directly
# We want to prove our XML parser extracts comments and bounding boxes correctly.
FIX_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
def extract_comments_from_xml(sheet_xml):
"""
Duplicated extraction logic for validation of the parsing segment.
In real practice, this would ideally be a separate util function.
"""
existing_comments = []
comment_nodes = []
try:
root = ET.fromstring(sheet_xml)
for layout in root.findall(".//node[@class='android.widget.LinearLayout']"):
text_node = layout.find(".//node[@resource-id='com.instagram.android:id/row_comment_textview_comment']")
like_btn = layout.find(".//node[@resource-id='com.instagram.android:id/row_comment_button_like']")
reply_btn = layout.find(".//node[@resource-id='com.instagram.android:id/row_comment_textview_reply_button']")
if text_node is not None and text_node.get("text"):
text = text_node.get("text")
existing_comments.append(text)
comment_nodes.append({
"text": text,
"like_bounds": like_btn.get("bounds") if like_btn is not None else None,
"reply_bounds": reply_btn.get("bounds") if reply_btn is not None else None
})
except Exception:
pass
return existing_comments, comment_nodes
@pytest.mark.skip(reason="PENDING REAL DUMP: missing comment_sheet.xml")
def test_comment_sheet_extraction():
"""
Test: Ensures the XML parser correctly identifies comment text, like buttons, and reply buttons
from a real Instagram comment sheet XML dump.
"""
xml_path = os.path.join(FIX_DIR, "comment_sheet.xml")
with open(xml_path, "r") as f:
real_xml = f.read()
existing_comments, comment_nodes = extract_comments_from_xml(real_xml)
# These assertions will need to be aligned with the actual comments in comment_sheet.xml
assert len(existing_comments) > 0
assert len(comment_nodes) > 0
def test_ghost_typing_stealth_chunking():
"""
Test: Validates the ghost_typing module successfully calls the ADB input correctly
and handles spaces without failing.
"""
from GramAddict.core.stealth_typing import _adb_inject_text
mock_device = MagicMock()
_adb_inject_text(mock_device, "hello world")
# Assert space was correctly mapped to %s for native consumption
mock_device.deviceV2.shell.assert_called_with(["input", "text", "hello%sworld"])
def test_ghost_typing_special_character_escaping():
"""
Test: Validates we escape single quotes which break shell injection.
"""
from GramAddict.core.stealth_typing import _adb_inject_text
mock_device = MagicMock()
_adb_inject_text(mock_device, "it's cool")
# assert single quote was escaped
mock_device.deviceV2.shell.assert_called_with(["input", "text", "it\\'s%scool"])

View File

@@ -0,0 +1,144 @@
import pytest
from unittest.mock import patch, MagicMock
from GramAddict.core.device_facade import DeviceFacade, create_device, get_device_info
@pytest.fixture
def mock_u2():
with patch('uiautomator2.connect') as mock_connect:
mock_device = MagicMock()
mock_connect.return_value = mock_device
yield mock_connect, mock_device
def test_create_device_success(mock_u2):
mock_connect, mock_device = mock_u2
facade = create_device("fake_id", "com.instagram.android")
assert facade is not None
assert facade.device_id == "fake_id"
assert facade.app_id == "com.instagram.android"
def test_create_device_exception(mock_u2):
mock_connect, mock_device = mock_u2
mock_connect.side_effect = Exception("Fatal boot error")
with pytest.raises(Exception, match="Fatal boot error"):
create_device("fake_id", "com.instagram.android")
def test_get_device_info(mock_u2):
mock_connect, mock_device = mock_u2
mock_device.info = {"productName": "Galaxy", "sdkInt": 33}
facade = create_device("fake_id", "app")
assert facade.get_info() == {"productName": "Galaxy", "sdkInt": 33}
# Test global helper
get_device_info(facade) # Should not crash
get_device_info(None) # Should not crash
def test_cm_to_pixels(mock_u2):
mock_connect, mock_device = mock_u2
mock_device.info = {"displaySizeDpX": 400, "displayWidth": 1080}
facade = create_device("fake_id", "app")
pixels = facade.cm_to_pixels(5.0)
assert isinstance(pixels, int)
# The pure calculation logic ensures it returns an int > 0
assert pixels > 0
def test_wake_up(mock_u2):
mock_connect, mock_device = mock_u2
facade = create_device("fake_id", "app")
# Screen off
mock_device.info = {"screenOn": False}
with patch('GramAddict.core.device_facade.sleep'):
facade.wake_up()
mock_device.screen_on.assert_called_once()
mock_device.press.assert_called_with("home")
# Screen on (should do nothing)
mock_device.reset_mock()
mock_device.info = {"screenOn": True}
facade.wake_up()
mock_device.screen_on.assert_not_called()
def test_press(mock_u2):
mock_connect, mock_device = mock_u2
facade = create_device("fake_id", "app")
facade.press("back")
mock_device.press.assert_called_with("back")
def test_click_and_human_click(mock_u2):
mock_connect, mock_device = mock_u2
facade = create_device("fake_id", "app")
with patch('GramAddict.core.device_facade.sleep'):
# Click dict directly
facade.click(obj={"x": 10, "y": 20})
mock_device.touch.down.assert_called()
mock_device.touch.up.assert_called()
# Click obj with bounds
mock_device.reset_mock()
obj = MagicMock()
obj.bounds.return_value = (0, 0, 100, 100)
facade.click(obj=obj)
mock_device.touch.down.assert_called()
# Click bounds failure fallback
mock_device.reset_mock()
obj2 = MagicMock()
obj2.bounds.side_effect = Exception("No bounds")
facade.click(obj=obj2)
obj2.click.assert_called()
# Click x,y fallback
mock_device.reset_mock()
mock_device.touch.down.side_effect = Exception("Touch failure")
facade.human_click(50, 50)
mock_device.click.assert_called_with(50, 50)
def test_swipes(mock_u2):
mock_connect, mock_device = mock_u2
facade = create_device("fake_id", "app")
facade.swipe_points(0, 0, 100, 100, 0.5)
mock_device.swipe.assert_called_with(0, 0, 100, 100, 0.5)
mock_device.reset_mock()
facade.human_swipe(0, 0, 100, 100, 0.5)
mock_device.swipe.assert_called_with(0, 0, 100, 100, 0.5)
def test_get_current_app(mock_u2):
mock_connect, mock_device = mock_u2
mock_device.app_current.return_value = {"package": "com.target"}
facade = create_device("fake_id", "app")
assert facade._get_current_app() == "com.target"
def test_find_and_dump_and_screenshot(mock_u2):
mock_connect, mock_device = mock_u2
facade = create_device("fake_id", "app")
mock_device.return_value = "ui_node"
assert facade.find(text="hello") == "ui_node"
mock_device.dump_hierarchy.return_value = "<xml></xml>"
assert facade.dump_hierarchy() == "<xml></xml>"
mock_device.screenshot.return_value = "binary"
assert facade.screenshot() == "binary"
def test_find_semantic(mock_u2):
mock_connect, mock_device = mock_u2
facade = create_device("fake_id", "app")
with patch("GramAddict.core.telepathic_engine.TelepathicEngine._get_instance", create=True) as mock_get_engine:
# Instead of _get_instance, patch get_instance which is what the code calls
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance") as get_inst:
engine_mock = MagicMock()
get_inst.return_value = engine_mock
engine_mock.find_best_node.return_value = {"x": 1}
mock_device.dump_hierarchy.return_value = "<xml></xml>"
res = facade.find_semantic("Hello")
assert res == {"x": 1}
engine_mock.find_best_node.assert_called_once()

View File

@@ -0,0 +1,84 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.dm_engine import _run_zero_latency_dm_loop
@pytest.fixture
def dm_mock_dependencies():
device = MagicMock()
zero_engine = MagicMock()
nav_graph = MagicMock()
class ConfigArgs:
disable_ai_messaging = False
configs = MagicMock()
configs.args = ConfigArgs()
session_state = MagicMock()
session_state.check_limit.return_value = (False, False, False, False)
session_state.totalMessages = 0
telepathic = MagicMock()
dopamine = MagicMock()
dopamine.is_app_session_over.side_effect = [False, False, True]
dopamine.wants_to_change_feed.return_value = False
dopamine.boredom = 0.0
crm = MagicMock()
cognitive_stack = {
"telepathic": telepathic,
"dopamine": dopamine,
"crm": crm,
}
return device, zero_engine, nav_graph, configs, session_state, cognitive_stack
def test_dm_engine_basic_loop(dm_mock_dependencies):
device, zero_engine, nav_graph, configs, session_state, cognitive_stack = dm_mock_dependencies
telepathic = cognitive_stack["telepathic"]
crm = cognitive_stack["crm"]
# Simulate Telepathic node extraction for the DM flow
telepathic._extract_semantic_nodes.side_effect = [
[{"x": 100, "y": 200, "skip": False}], # Unread thread
[{"x": 10, "y": 20, "skip": False, "text": "hey how are you?"}], # Last message context
[{"x": 150, "y": 250, "skip": False}], # Input field
[{"x": 200, "y": 250, "skip": False}], # Send button
[], # Iteration 2: No more unread threads
]
with patch("GramAddict.core.bot_flow.sleep"), \
patch("GramAddict.core.bot_flow._humanized_click") as mock_click, \
patch("GramAddict.core.stealth_typing.ghost_type") as mock_ghost_type, \
patch("GramAddict.core.llm_provider.query_llm", return_value="I am good, thanks!") as mock_query_llm:
res = _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_state, "MessageInbox", cognitive_stack)
# Clicked thread -> Clicked input field -> Clicked send
assert mock_click.call_count == 3
mock_ghost_type.assert_called_with(device, "I am good, thanks!", speed="fast")
# Verify persistence memory is triggered
crm.log_sent_dm.assert_called_with("unknown_target", "I am good, thanks!", "", [])
assert session_state.totalMessages == 1
assert res == "SESSION_OVER" or res == "BOREDOM_CHANGE_FEED"
def test_dm_engine_no_unread(dm_mock_dependencies):
device, zero_engine, nav_graph, configs, session_state, cognitive_stack = dm_mock_dependencies
telepathic = cognitive_stack["telepathic"]
dopamine = cognitive_stack["dopamine"]
# Telepathic finds no unread threads
telepathic._extract_semantic_nodes.return_value = []
with patch("GramAddict.core.bot_flow.sleep"):
res = _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_state, "MessageInbox", cognitive_stack)
# Boredom should jump by 50.0 immediately because inbox is empty
assert dopamine.boredom >= 50.0
assert res == "BOREDOM_CHANGE_FEED"

View File

@@ -0,0 +1,15 @@
import pytest
import os
from GramAddict.core.bot_flow import _detect_ad_structural
FIX_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
def test_real_normal_post_is_not_ad():
"""
Test: Ensures the ad detector correctly ignores a standard organic post.
"""
xml_path = os.path.join(FIX_DIR, "organic_post.xml")
with open(xml_path, "r") as f:
real_xml = f.read()
assert _detect_ad_structural(real_xml) is False, "False positive! Normal post detected as ad!"

View File

@@ -0,0 +1,134 @@
import os
import pytest
from unittest.mock import patch, MagicMock
from GramAddict.core.llm_provider import extract_json, log_openrouter_burn, query_llm, query_telepathic_llm
def test_extract_json():
# 1. Normal JSON
assert extract_json('{"a": 1}') == '{"a": 1}'
# 2. Markdown JSOn Block
assert extract_json('```json\n{"a": 1}\n```') == '{"a": 1}'
# 3. Trailing text
assert extract_json('{"a": 1}\nSome extra talk') == '{"a": 1}'
# 4. Incomplete/invalid json (Returns None if no brace match, or garbage if mismatched)
assert extract_json('{"a": 1') is None
assert extract_json("") is None
# 5. Nested braces with prefix
assert extract_json('Here is your response:\n{"a": {"b": 2}}') == '{"a": {"b": 2}}'
# 6. Think blocks
assert extract_json('<think>thinking</think>\n{"a":1}') == '{"a":1}'
def test_log_openrouter_burn():
with patch.dict(os.environ, {"OPENROUTER_API_KEY": "123"}), \
patch('requests.get') as mock_get, \
patch('GramAddict.core.llm_provider.logger.info') as mock_log:
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"data": {"usage": 0.5, "usage_daily": 0.1, "limit": 1.0}}
mock_get.return_value = mock_resp
log_openrouter_burn()
mock_log.assert_called()
# Exception inside log_openrouter_burn
with patch.dict(os.environ, {"OPENROUTER_API_KEY": "123"}), \
patch('requests.get') as mock_get, \
patch('GramAddict.core.llm_provider.logger.debug') as mock_debug:
mock_get.side_effect = Exception("Network Down")
log_openrouter_burn()
mock_debug.assert_called()
# No API Key
with patch.dict(os.environ, clear=True), patch('requests.get') as mock_get:
log_openrouter_burn()
mock_get.assert_not_called()
def test_query_llm_success_openai():
with patch('requests.post') as mock_post:
resp = MagicMock()
resp.status_code = 200
resp.headers = {"x-openrouter-credits-spent": "0.001"}
resp.json.return_value = {"choices": [{"message": {"content": '{"test": 1}'}}]}
mock_post.return_value = resp
res = query_llm(
url="http://api.com/v1/chat/completions",
model="gpt-4",
prompt="Hello",
format_json=True
)
assert res.get("response") == '{"test": 1}'
def test_query_llm_success_ollama():
with patch('requests.post') as mock_post:
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {"response": '{"test": 2}'}
mock_post.return_value = resp
res = query_llm(
url="http://api.com", # no /v1/chat/completions
model="llama3",
prompt="Hello",
format_json=False
)
assert res.get("response") == '{"test": 2}'
def test_query_llm_failed_json_extraction():
# If formatting demands JSON, but the response is pure text
with patch('requests.post') as mock_post:
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {"response": 'Not a json'}
mock_post.return_value = resp
# Test the branch that raises ValueError inside `query_llm` and defaults to returning None
res = query_llm(
url="http://api.com",
model="llama3",
prompt="Hello",
format_json=True
)
assert res is None
def test_query_llm_http_error_no_fallback():
with patch('requests.post') as mock_post:
mock_post.side_effect = Exception("General Network Error")
res = query_llm(
url="http://api.com",
model="llama3",
prompt="Hello"
)
assert res is None
def test_query_telepathic_llm():
with patch('GramAddict.core.llm_provider.query_llm') as mock_llm:
mock_llm.return_value = {"response": "something"}
res = query_telepathic_llm("llama3", "http://fake.api", "system", "user")
assert res == "something"
# Edge Inference
with patch('GramAddict.core.config.Config') as MConfig, patch('GramAddict.core.llm_provider.query_llm') as mock_llm:
mock_llm.return_value = {"response": "edge_response"}
cfg = MConfig.return_value
cfg.args.ai_fallback_url = "http://edge.api"
cfg.args.ai_fallback_model = "edge_model"
res = query_telepathic_llm("llama3", "http://fake.api", "sys", "usr", use_local_edge=True)
assert res == "edge_response"
# Edge fallback config missing
with patch('GramAddict.core.config.Config', side_effect=Exception("No Config")):
with patch('GramAddict.core.llm_provider.query_llm') as mock_llm:
mock_llm.return_value = {"response": "fallback"}
res = query_telepathic_llm("m", "u", "s", "u", use_local_edge=True)
assert res == "fallback"
# Nothing returned
with patch('GramAddict.core.llm_provider.query_llm') as mock_llm:
mock_llm.return_value = None
assert query_telepathic_llm("m", "u", "s", "u") == "{}"

View File

@@ -0,0 +1,70 @@
import pytest
import logging
from unittest.mock import MagicMock, call, patch
from GramAddict.core.q_nav_graph import QNavGraph
def test_qnavgraph_same_state_navigation_bug():
"""
Test that reproducing the bug where `navigate_to` to the CURRENT state
triggers an unexpected `app_start()` restart due to `if not path:` treating `[]` as failure.
"""
mock_device = MagicMock()
mock_device.deviceV2 = MagicMock()
graph = QNavGraph(mock_device)
graph.current_state = "ExploreFeed"
graph.navigate_to("ExploreFeed", zero_engine=None)
mock_device.deviceV2.app_start.assert_not_called()
def test_qnavgraph_semantic_recovery_any_state():
"""
Ensures that semantic recovery (global nav bar mapping) triggers even if current_state is NOT 'UNKNOWN',
for example when transitioning from 'HomeFeed' to 'ReelsFeed' with an unknown graph path.
"""
mock_device = MagicMock()
mock_device.deviceV2.dump_hierarchy.return_value = "<hierarchy/>"
graph = QNavGraph(mock_device)
graph.current_state = "HomeFeed"
with patch.object(graph, '_execute_transition', return_value=True) as mock_exec:
# Patch the path finding: first it returns None (no known path), then it returns ["tap_reels_tab"] after recovery anchors it.
with patch.object(graph, '_find_path', side_effect=[None, ["tap_reels_tab"]]):
success = graph.navigate_to("ReelsFeed", zero_engine=None)
# _execute_transition should have been called with "tap_reels_tab" for semantic recovery mapping
mock_exec.assert_has_calls([call("tap_reels_tab", None)])
assert graph.current_state == "ReelsFeed"
def test_qnavgraph_telepathic_tagging(caplog):
"""
Verifies that the transition logs correctly output the 'source' of the interaction
(e.g. '[Keyword]' or '[Agentic Fallback]') instead of hardcoding '[Vision Cortex]' on a score of 1.0.
"""
caplog.set_level(logging.INFO)
mock_device = MagicMock()
graph = QNavGraph(mock_device)
# 1. Test Keyword Fast Path (Score 1.0)
mock_device.deviceV2.dump_hierarchy.side_effect = ["<before/>", "<after/>"]
mock_telepathic = MagicMock()
mock_telepathic.find_best_node.return_value = {
"x": 100, "y": 100, "score": 1.0, "semantic": "test match", "source": "keyword", "skip": False
}
with patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance', return_value=mock_telepathic):
graph._execute_transition("tap_home_tab", None)
assert "QNavGraph executing transition 'tap_home_tab' via [Keyword]" in caplog.text
# 2. Test Agentic Fallback (Score < 1.0)
caplog.clear()
mock_device.deviceV2.dump_hierarchy.side_effect = ["<before/>", "<after/>"]
mock_telepathic.find_best_node.return_value = {
"x": 100, "y": 100, "score": 0.85, "semantic": "test LLM", "source": "agentic_fallback", "skip": False
}
with patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance', return_value=mock_telepathic):
graph._execute_transition("tap_home_tab", None)
assert "QNavGraph executing transition 'tap_home_tab' via [Agentic Fallback]" in caplog.text

View File

@@ -0,0 +1,319 @@
import os
import sys
import time
import pytest
from unittest.mock import patch, MagicMock
# Inject mock qdrant_client
from GramAddict.core.qdrant_memory import (
QdrantBase, HeuristicMemoryDB, UIMemoryDB, CommentMemoryDB,
NavigationMemoryDB, PersonaMemoryDB, ContentMemoryDB, BannedPathsDB, ParasocialCRMDB, DMMemoryDB
)
@pytest.fixture(autouse=True)
def mock_qdrant():
with patch('GramAddict.core.qdrant_memory.QdrantClient') as mq:
yield mq
def test_qdrant_base(mock_qdrant):
mock_client = MagicMock()
mock_qdrant.return_value = mock_client
# Missing collection creation
mock_client.collection_exists.return_value = False
base = QdrantBase("test_collection", vector_size=4)
mock_client.create_collection.assert_called()
# Dimension mismatch
dim_mock = MagicMock()
dim_mock.config.params.vectors.size = 2
mock_client.get_collection.return_value = dim_mock
mock_client.collection_exists.return_value = True
base = QdrantBase("test_collection", vector_size=4)
# Should delete and recreate
mock_client.delete_collection.assert_called()
assert mock_client.create_collection.call_count == 2
# Upsert & Search
base.upsert_point("seed", {"a": 1})
mock_client.upsert.assert_called()
base.search_points([0.0]*4)
mock_client.search.assert_called()
def test_qdrant_base_embeddings(mock_qdrant):
base = QdrantBase("x", 4)
with patch('requests.post') as mock_post, patch('GramAddict.core.config.Config'):
# Ollama style
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {"embedding": [0.1, 0.2]}
mock_post.return_value = resp
assert base._get_embedding("hi") == [0.1, 0.2]
# OpenAI style
resp.json.return_value = {"data": [{"embedding": [0.3]}]}
assert base._get_embedding("hi") == [0.3]
# Failure
mock_post.side_effect = Exception("failed")
assert base._get_embedding("hi") is None
def test_heuristic_memory(mock_qdrant):
with patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding') as m_emb:
m_emb.return_value = [0.0] * 1536
db = HeuristicMemoryDB()
# learn heuristics
db.cache_heuristic("find_button", {"bounds": [0,0,10,10]})
# mock query_points
pt = MagicMock()
pt.payload = {"rule": "{'bounds': [0, 0, 10, 10]}", "rule_type": "regex"}
pt.score = 1.0
mock_result = MagicMock()
mock_result.points = [pt]
db.client.query_points.return_value = mock_result
res = db.fetch_heuristic("find_button")
assert res is not None
def test_ui_memory_db(mock_qdrant):
with patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding') as m_emb:
m_emb.return_value = [0.0] * 1536
db = UIMemoryDB()
db.store_memory("home", "<xml/>", {"res": 1})
pt = MagicMock()
pt.payload = {"solution": {"res": 1}, "structural_signature": db._create_structural_signature("<xml/>"), "confidence": 0.8}
pt.score = 1.0
mock_result = MagicMock()
mock_result.points = [pt]
db.client.query_points.return_value = mock_result
assert db.retrieve_memory("home", "<xml/>") == {'res': 1}
# confidence
db.client.query_points.return_value = mock_result
db.boost_confidence("home")
db.decay_confidence("home")
db.purge_stale_entries()
def test_content_and_comments(mock_qdrant):
with patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding') as m_emb:
m_emb.return_value = [0.0] * 1536
# Comments
cdb = CommentMemoryDB()
cdb.store_comment("nice", "positive", "user")
cdb.client.upsert.assert_called()
pt = MagicMock()
pt.payload = {"text": "nice"}
pt.score = 1.0
mock_result = MagicMock()
mock_result.points = [pt]
cdb.client.query_points.return_value = mock_result
res = cdb.get_relevant_comments("post")
assert len(res) == 1
# Content
cndb = ContentMemoryDB()
cndb.store_evaluation("nice pic", "POSITIVE", "good vibe")
# pt payload
pt.payload = {"classification": "POSITIVE", "reason": "ok"}
pt.score = 1.0
mock_result = MagicMock()
mock_result.points = [pt]
cndb.client.query_points.return_value = mock_result
assert cndb.get_cached_evaluation("nice pic") is not None
def test_banned_paths_db(mock_qdrant):
mock_client = MagicMock()
mock_qdrant.return_value = mock_client
# Mocking scroll to return some expired and some active
exp_pt = MagicMock()
exp_pt.id = "exp"
exp_pt.payload = {"banned_at": 100, "goal_hash": "a", "element_id": "e1"} # VERY OLD
act_pt = MagicMock()
act_pt.id = "act"
act_pt.payload = {"banned_at": time.time(), "goal_hash": "b", "element_id": "e2"}
mock_client.scroll.return_value = ([exp_pt, act_pt], None)
db = BannedPathsDB()
# Should have run clean up for exp_pt, and loaded act_pt
mock_client.delete.assert_called()
assert len(db._banned) == 1
# ban new
db.ban("My Goal", "ui_123", "Not working")
mock_client.upsert.assert_called()
# check
assert db.is_banned("My Goal", "ui_123") == True
def test_navigation_memory_db(mock_qdrant):
db = NavigationMemoryDB()
with patch('GramAddict.core.qdrant_memory.uuid.uuid4', return_value="1234"):
db.store_transition("Feed", "click_home", "Home")
db.client.upsert.assert_called()
pt = MagicMock()
pt.payload = {"from": "Feed", "action": "click_home", "to": "Home"}
db.client.scroll.return_value = ([pt], None)
res = db.get_all_transitions()
assert res.get("Feed") == {"transitions": {"click_home": "Home"}}
def test_persona_memory_db(mock_qdrant):
with patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding') as m_emb:
m_emb.return_value = [0.0] * 1536
db = PersonaMemoryDB()
db.store_persona_insight("likes", "Loves tech")
pt = MagicMock()
pt.payload = {"category": "likes", "insight": "Loves tech"}
db.client.scroll.return_value = ([pt], None)
assert "Loves tech" in db.get_persona_context("likes")
def test_crm_db(mock_qdrant):
with patch('GramAddict.core.qdrant_memory.ParasocialCRMDB.is_connected', new_callable=MagicMock, return_value=True), patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding') as m_emb:
m_emb.return_value = [0.0] * 1536
db = ParasocialCRMDB()
pt = MagicMock()
pt.payload = {"stage": 1, "intent_history": ["LIKE"], "last_interaction": 100}
# ParasocialCRMDB uses scroll
db.client.scroll.return_value = ([pt], None)
res = db.get_relationship_stage("user")
assert res["stage"] == 1
db.log_interaction("user", "COMMENT")
db.client.upsert.assert_called()
db.log_generated_comment("user", "hi")
db.log_profile_context("user", "Tech dev")
# Simulate DB state updated
pt.payload["bio"] = "Tech dev"
assert "Tech dev" in db.get_conversation_context("user")
def test_dm_history_db(mock_qdrant):
with patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding') as m_emb:
m_emb.return_value = [0.0] * 1536
db = DMMemoryDB()
db.log_sent_dm("user", "hi", "bio", [])
db.client.upsert.assert_called()
pt = MagicMock()
pt.payload = {"target_username": "user", "message": "hi", "score": 0.9}
mock_result = MagicMock()
mock_result.points = [pt]
db.client.query_points.return_value = mock_result
db.client.scroll.return_value = ([pt], None)
pending = db.get_pending_dms()
assert len(pending) == 1
db.update_dm_score("123", 1.0)
db.client.set_payload.assert_called()
best = db.get_best_performing_dms()
assert len(best) == 1
def test_unhappy_paths(mock_qdrant):
mock_client = MagicMock()
mock_qdrant.return_value = mock_client
with patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding') as m_emb:
m_emb.return_value = [0.0] * 1536
# Test 1: Exception on query_points
db = UIMemoryDB()
db.client.query_points.side_effect = Exception("failed")
assert db.retrieve_memory("home", "<xml/>") is None
# Test 2: Exception on upsert
db.client.upsert.side_effect = Exception("failed")
db.store_memory("home", "<xml/>", {"res": 1}) # shouldn't crash
# _adjust_confidence coverage
db.client.retrieve.return_value = []
db.boost_confidence("home") # handles empty retrieve
pt = MagicMock()
pt.payload = {"confidence": 0.5}
db.client.retrieve.return_value = [pt]
db.decay_confidence("home", amount=1.0) # falls below 0.1, calls delete
db.client.delete.assert_called()
# purge stale entries
stale_pt = MagicMock()
stale_pt.payload = {"confidence": 0.4, "stored_at": 100}
db.client.scroll.return_value = ([stale_pt], None)
db.purge_stale_entries()
db.client.delete.assert_called()
# fetch heuristic fail
db = HeuristicMemoryDB()
db.client.query_points.side_effect = Exception("failed")
assert db.fetch_heuristic("button") is None
cndb = ContentMemoryDB()
cndb.client.query_points.side_effect = Exception("failed")
assert cndb.get_cached_evaluation("pic") is None
# get_similar_examples
db.client.query_points.side_effect = None
pt.payload = {"description": "hello", "classification": "A", "reason": "B"}
mock_result = MagicMock()
mock_result.points = [pt]
cndb.client.query_points.return_value = mock_result
res = cndb.get_similar_examples("pic")
assert len(res) == 1
assert res[0]["classification"] == "A"
def test_disconnected_state(mock_qdrant):
with patch('GramAddict.core.qdrant_memory.QdrantBase.is_connected', new_callable=MagicMock, return_value=False), patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding') as m_emb:
m_emb.return_value = None
db = UIMemoryDB()
assert db.retrieve_memory("home", "<xml/>") is None
db.store_memory("home", "<xml/>", {})
db._adjust_confidence("home", 0.1)
cdb = CommentMemoryDB()
assert cdb.get_relevant_comments("post") == []
cdb.store_comment("p", "a", "u")
crm = ParasocialCRMDB()
assert crm.get_relationship_stage("user")["stage"] == 0
crm.log_profile_context("u", "b")
crm.log_interaction("u", "intent")
crm.log_generated_comment("u", "t")
dm = DMMemoryDB()
dm.update_dm_score("123", 1.0)
assert dm.get_pending_dms() == []
assert dm.get_best_performing_dms() == []
dm.log_sent_dm("a", "b", "c", [])
cndb = ContentMemoryDB()
assert cndb.get_similar_examples("hello") == []
assert cndb.get_cached_evaluation("hi") == None
cndb.store_evaluation("a", "b", "c")
ndb = NavigationMemoryDB()
assert ndb.get_all_transitions() == {}
ndb.store_transition("a", "b", "c")
pdb = PersonaMemoryDB()
assert pdb.get_persona_context("C") == ""
pdb.store_persona_insight("a", "b")
hdb = HeuristicMemoryDB()
assert hdb.fetch_heuristic("H") is None
hdb.cache_heuristic("a", {})

View File

@@ -0,0 +1,199 @@
import pytest
from unittest.mock import patch, MagicMock
from GramAddict.core.resonance_engine import ResonanceEngine
@pytest.fixture
def engine():
# Patch the databases at the source to prevent any real Qdrant connection
with patch('GramAddict.core.resonance_engine.ContentMemoryDB') as mock_cm_cls, \
patch('GramAddict.core.resonance_engine.PersonaMemoryDB'):
# Create a single consistent mock instance for ContentMemory
mock_cm = MagicMock()
mock_cm_cls.return_value = mock_cm
# KEY: Ensure cache lookups return None to avoid fake hits with MagicMocks
mock_cm.get_cached_evaluation.return_value = None
# Mock embedding return to ensure truthy checks pass
mock_cm._get_embedding.return_value = [0.1] * 1536
# Initialize
eng = ResonanceEngine(my_username="test_user", persona_interests=["fitness", "travel"])
# MANUALLY FORCE VALID STATE
eng._persona_vector = [0.1] * 1536
eng.content_memory = mock_cm # Re-enforce the mock
return eng
def test_resonance_calculation_happy_path(engine):
"""Verifies that resonance is calculated correctly for matching content."""
post_data = {
"username": "fitness_junkie",
"description": "Amazing morning workout session #fitness #gym",
"caption": "No pain no gain"
}
# 1. Provide Real Matching Vectors (exactly the same = 1.0 similarity)
# The real _persona_vector is [0.1]*1536 (from fixture).
# Returning the same vector for the content.
engine.content_memory._get_embedding.return_value = [0.1] * 1536
# 2. Real Math Logic
score = engine.calculate_resonance(post_data)
# Cosine Similarity 1.0 -> Normalization (1.0 - 0.15)/0.30 -> capped to 1.000
assert score == 1.0
assert engine.judge_interaction(score) is True
def test_resonance_calculation_low_match(engine):
"""Verifies low score for non-matching content."""
post_data = {
"username": "politics_daily",
"description": "New tax law discussed in parliament",
"caption": "Breaking news"
}
# Provide Orthogonal/Opposite Vectors (-0.1 to differ from 0.1)
engine.content_memory._get_embedding.return_value = [-0.1] * 1536
score = engine.calculate_resonance(post_data)
# Similarity will be low/negative -> Final score 0.0
assert score == 0.0
assert engine.judge_interaction(score) is False
def test_resonance_no_content(engine):
"""Empty content should return neutral score (0.5)."""
post_data = {"username": "ghost", "description": "", "caption": ""}
score = engine.calculate_resonance(post_data)
assert score == 0.5
def test_resonance_caching(engine):
"""Verify that ContentMemoryDB cache is checked first."""
post_data = {
"username": "test",
"description": "Some recycled content",
"caption": "Again"
}
# Reset mock to verify it's not called
engine.content_memory._get_embedding.reset_mock()
engine.content_memory._get_embedding.return_value = [0.1] * 1536
# Mock cache hit
engine.content_memory.get_cached_evaluation.return_value = {"classification": "high"}
score = engine.calculate_resonance(post_data)
assert score == 0.85 # 'high' classification from cache
# Should not have called embedding for the post
engine.content_memory._get_embedding.assert_not_called()
def test_extract_and_learn_comments_llm_kwargs(engine):
"""Verifies that query_llm is called with correct kwargs to prevent 'multiple values for argument' exception."""
configs = MagicMock()
configs.args = MagicMock()
configs.args.ai_condenser_model = "test-model"
configs.args.ai_condenser_url = "http://test-url"
# Mock XML dump containing some fake comments
xml_content = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy>
<node class="android.widget.TextView" text="Omg this is such a cool post! I love the lighting." />
<node class="android.widget.TextView" text="Reply" />
</hierarchy>
'''
with patch('builtins.open', return_value=MagicMock(__enter__=MagicMock(return_value=MagicMock(read=MagicMock(return_value=xml_content))))), \
patch('os.path.exists', return_value=True), \
patch('GramAddict.core.resonance_engine.query_llm', autospec=True) as mock_query, \
patch('GramAddict.core.resonance_engine.CommentMemoryDB') as mock_db:
mock_query.return_value = {"response": '["Omg this is such a cool post! I love the lighting."]'}
# This should naturally pass if kwargs are valid, or raise TypeError if it's the bug
configs.args.ai_learn_comments = True
configs.args.ai_vibe = "friendly"
configs.args.ai_blacklist_topics = "nsfw"
engine.extract_and_learn_comments(
xml_hierarchy=xml_content,
configs=configs,
author="test_author"
)
# We can also assert that query_llm was indeed called correctly
mock_query.assert_called_once()
args, kwargs = mock_query.call_args
# The prompt is the first positional argue
# We want to ensure that "url" and "model" are correctly mapped, and no duplicate positional argument is provided
# that overlaps with "url". If prompt is pos 0, 'url' parameter from query_llm is also pos 0.
# This assertion will fail if Python raises the TypeError first.
def test_resonance_math_normalization(engine):
"""Verifies that the normalization math for text-embedding-3-small allows natural matches to score HIGH."""
# text-embedding-3-small real matches are typically around 0.45-0.55 raw cosine.
# We want a raw cosine similarity of 0.45 to yield a normalized score >= 0.85 (High resonance)
# The current math returns around 0.25 (Low relevance), which effectively blocks all Autonomous likes/comments.
post_data = {
"username": "perfect_match",
"description": "This is a mathematically perfect match for the persona",
"caption": ""
}
# THE MATHEMATICAL TRICK:
# To get raw cosine 0.45 with a persona vector of [0.1]*1536:
# We need a content vector such that sum(a*b)/(norm(a)*norm(b)) = 0.45
persona_vec = [0.1] * 1536
# Create a vector that is partially aligned
content_vec = [0.1] * 691 + [0.0] * 845 # 691/1536 is approx 0.45
engine._persona_vector = persona_vec
engine.content_memory._get_embedding.return_value = content_vec
score = engine.calculate_resonance(post_data)
# (0.45 - 0.15) / 0.30 = 1.0 (Previously it was failing)
assert score >= 0.85
def test_extract_and_learn_comments_lenient_prompt():
"""
Test that the Condenser prompt is lenient enough to not return empty lists constantly.
We verify the prompt contains the lenient phrasing instead of 'perfectly match'.
"""
engine = ResonanceEngine(my_username="test_bot")
# Mock configs for comment learning
configs = MagicMock()
configs.args.ai_learn_comments = True
configs.args.ai_vibe = "friendly, authentic"
configs.args.ai_blacklist_topics = "crypto, spam"
# Minimal XML
xml = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" text="This lighting trick is insane!" content-desc=""/>
<node index="1" text="Like" content-desc=""/>
</hierarchy>
'''
with patch('GramAddict.core.resonance_engine.query_llm') as mock_llm:
mock_llm.return_value = {"response": "[\"This lighting trick is insane!\"]"}
# Act
with patch('GramAddict.core.resonance_engine.CommentMemoryDB') as MockDB:
engine.extract_and_learn_comments(xml_hierarchy=xml, configs=configs)
# Assert
assert mock_llm.call_count == 1
call_kwargs = mock_llm.call_args.kwargs
prompt = call_kwargs.get("prompt", "")
# Ensure we are using the lenient mapping theorem
assert "generally match this vibe" in prompt
assert "perfectly match the vibe" not in prompt
# Verify the parsed comments were still passed
assert "This lighting trick is insane!" in prompt

View File

@@ -0,0 +1,299 @@
import sys
from unittest.mock import MagicMock
# Force mock qdrant_client before importing any core modules that depend on it
import pytest
import os
from unittest.mock import patch
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop
FIX_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
class ArgsMock:
def __init__(self):
self.username = ["test_bot"]
self.interact_percentage = 100
self.likes_percentage = 100
self.total_likes_limit = 100
self.total_follows_limit = 100
self.total_unfollows_limit = 100
self.total_comments_limit = 100
self.total_pm_limit = 100
self.total_watches_limit = 100
self.total_successful_interactions_limit = 100
self.total_interactions_limit = 100
self.total_scraped_limit = 100
self.total_crashes_limit = 5
# Session state attributes expect these to exist
self.current_likes_limit = 100
self.current_follow_limit = 100
self.current_unfollow_limit = 100
self.current_comments_limit = 100
self.current_pm_limit = 100
self.current_watch_limit = 100
self.current_success_limit = 100
self.current_total_limit = 100
self.current_scraped_limit = 100
self.current_crashes_limit = 5
self.end_if_likes_limit_reached = False
self.end_if_follows_limit_reached = False
self.end_if_watches_limit_reached = False
self.end_if_comments_limit_reached = False
self.end_if_pm_limit_reached = False
class ConfigMock:
def __init__(self):
self.can_like = True
self.can_comment = False
self.can_follow = False
self.can_watch_stories = False
self.interaction_limit_reached = lambda: False
self.is_last_post = lambda x: False
self.args = ArgsMock()
@pytest.fixture
def fsd_fixtures():
def _load(name):
with open(os.path.join(FIX_DIR, name), "r") as f:
return f.read()
return {
"organic": _load("organic_post.xml"),
"ad": _load("sponsored_reel.xml"),
"modal": _load("survey_modal.xml")
}
def test_full_mission_autopilot_sequence(fsd_fixtures):
"""
MASTER SCENARIO:
1. Organic Post -> Action: LIKE
2. Ad -> Action: SKIP (SCROLL)
3. Survey Modal -> Action: DISMISS (TELEPATHIC)
4. Organic Post -> Action: LIKE
5. End Session (Boredom)
"""
device = MagicMock()
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
configs = ConfigMock()
# Sequence of UI states
ui_sequence = [
fsd_fixtures["organic"], # 0. First Organic Post
fsd_fixtures["ad"], # 1. Ad (Detected via resource-id)
fsd_fixtures["modal"], # 2. Modal (Miss 1)
fsd_fixtures["modal"], # 3. Modal (Miss 2 -> Telepathic Recovery)
fsd_fixtures["organic"], # 4. Second Organic Post
fsd_fixtures["organic"] # Buffer
]
state = {"index": 0}
def get_ui():
idx = min(state["index"], len(ui_sequence) - 1)
return ui_sequence[idx]
def advance_state(*args, **kwargs):
state["index"] += 1
print(f"DEBUG: State advanced to {state['index']}")
device.deviceV2.dump_hierarchy.side_effect = get_ui
device.deviceV2.click.side_effect = advance_state
# Trackers
class CRMTracker:
def __init__(self): self.interacted_users = []
def log_interaction(self, username, intent):
print(f"DEBUG: CRM log_interaction called for @{username} with {intent}")
self.interacted_users.append(username)
def log_profile_context(self, *args, **kwargs): pass
class DarwinTracker:
def __init__(self): self.called = False
def execute_micro_wobble(self, *args, **kwargs): pass
def execute_proof_of_resonance(self, *args, **kwargs): self.called = True
def synthesize_interaction_profile(self, *args, **kwargs): self.called = True
def evaluate_session_end(self, *args, **kwargs): pass
crm = CRMTracker()
darwin = DarwinTracker()
swarm = MagicMock()
resonance = MagicMock()
# Mock Resonance to always like organic posts
resonance.calculate_resonance.return_value = 0.9
# --- DETOX: Use REAL engines, mock only the BOUNDARY (LLM/DB) ---
from GramAddict.core.telepathic_engine import TelepathicEngine
from GramAddict.core.resonance_engine import ResonanceEngine
from GramAddict.core.swarm_protocol import SwarmProtocol
from GramAddict.core.session_state import SessionState
import hashlib
import builtins
# Capture original open BEFORE any patching to avoid recursion
original_open = builtins.open
def deterministic_embedding(text):
"""Generates a stable, unique 1536-dim vector for any string."""
# Use MD5 to get 16 bytes, then repeat to fill or just use first 16 floats
h = hashlib.md5(text.encode()).digest()
base = [float(b)/255.0 for b in h]
# Pad to 1536 with zeros or repeat
return (base * (1536 // 16 + 1))[:1536]
# We mock only the external API/Boundary calls inside the engines
with patch('GramAddict.core.qdrant_memory.QdrantClient') as MockClient, \
patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding', side_effect=deterministic_embedding), \
patch('GramAddict.core.telepathic_engine.query_telepathic_llm') as mock_vlm_api, \
patch('GramAddict.core.bot_flow.sleep'), \
patch('GramAddict.core.bot_flow._humanized_scroll', side_effect=advance_state), \
patch('builtins.open', new_callable=MagicMock) as mock_file_open, \
patch('random.random', return_value=0.99): # Pass interaction gates and bypass Resonance Skip
# Setup fake file reading for VLM screenshot
mock_file_open.return_value.__enter__.return_value.read.return_value = b"fake_screenshot_bytes"
# We need to selectively mock open for 'vlm_context.jpg' and allow real open for XML fixtures
def side_effect_open(path, *args, **kwargs):
if "vlm_context.jpg" in str(path):
return mock_file_open.return_value
return original_open(path, *args, **kwargs)
mock_file_open.side_effect = side_effect_open
# Harden Qdrant Config Mock to prevent dimension warnings
mock_client = MockClient.return_value
mock_client.collection_exists.return_value = False
# Force the REAL TelepathicEngine instead of conftest's MockTelepathicEngine
telepathic = TelepathicEngine()
# CLEAR MEMORY TO ENSURE VLM TRIGGER
if os.path.exists("telepathic_memory.json"):
os.remove("telepathic_memory.json")
with patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance', return_value=telepathic):
resonance = ResonanceEngine(my_username="test_bot", persona_interests=["travel", "nature"])
# Mock the specific method to always like organic posts, bypassing the deterministic embedding math
resonance.calculate_resonance = MagicMock(return_value=0.9)
swarm = SwarmProtocol(username="test_bot")
cognitive_stack = {
"active_inference": MagicMock(),
"dopamine": MagicMock(),
"growth_brain": MagicMock(),
"resonance": resonance,
"crm": crm,
"swarm": swarm,
"darwin": darwin,
"telepathic": telepathic
}
# Setup AI recovery (boundary mock result)
mock_vlm_api.return_value = '{"index": 2, "reason": "Dismiss Button"}'
# Setup Dopamine to run exactly long enough
cognitive_stack["dopamine"].is_app_session_over.side_effect = [False] * 12 + [True]
cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
# Run interaction loop - we patch swarm's emit_pheromone to verify it was called
with patch.object(swarm, 'emit_pheromone'):
session_state = SessionState(configs)
_run_zero_latency_feed_loop(device, MagicMock(), MagicMock(), configs, session_state, "HomeFeed", cognitive_stack)
# VERIFICATION
# 1. Sequence Progression
assert state["index"] >= 4, f"Bot sequence failed to progress. Final index: {state['index']}"
# 2. Interaction Accuracy (CRM)
# Real ResonanceEngine should have evaluated 'hoeltlfinanzgmbh' as high resonance
assert len(crm.interacted_users) >= 1, "CRM recorded ZERO interactions!"
assert "fiona.dawson" in crm.interacted_users or "hoeltlfinanzgmbh" in crm.interacted_users
# 3. Anomaly Handling
# Real TelepathicEngine should have called the Vision LLM (mock_vlm_api)
assert mock_vlm_api.called, "Anomaly recovery via REAL Vision Cortex was NEVER triggered!"
# 4. Resonance Proof
assert darwin.called, "Darwin Engine was NEVER called for resonance proof!"
assert swarm.emit_pheromone.called, "Swarm Protocol NEVER emitted success pheromones!"
print("\n🏆 TRUE INTEGRATION SCENARIO PASSED!")
print(f"Interacted with: {crm.interacted_users}")
def test_feed_loop_chaos_mode(fsd_fixtures):
"""
CHAOS MODE SCENARIO:
Simulate unpredictable UI behavior, random context loss, and unhandled exceptions
to ensure the zero-latency feed loop handles errors gracefully without crashing.
"""
device = MagicMock()
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
class ConfigMock:
def __init__(self):
self.args = MagicMock()
self.args.interact_percentage = 100
self.args.likes_percentage = 100
self.args.follow_percentage = 0
self.args.comment_percentage = 0
self.args.repost_percentage = 0
configs = ConfigMock()
# Sequence with invalid XML, completely empty hierarchy, then normal
ui_sequence = [
"INVALID XML {{",
"<hierarchy></hierarchy>",
fsd_fixtures["organic"]
]
state = {"index": 0}
def get_ui():
idx = min(state["index"], len(ui_sequence) - 1)
return ui_sequence[idx]
def advance_state(*args, **kwargs):
state["index"] += 1
device.deviceV2.dump_hierarchy.side_effect = get_ui
device.deviceV2.click.side_effect = advance_state
from GramAddict.core.sensors.honeypot_radome import HoneypotRadome
with patch('GramAddict.core.bot_flow.sleep'), \
patch('GramAddict.core.bot_flow._humanized_scroll', side_effect=advance_state):
telepathic = MagicMock()
# Have telepathic throw an error to simulate chaos/random failure
telepathic._extract_semantic_nodes.side_effect = Exception("CHAOS INJECTION")
cognitive_stack = {
"active_inference": MagicMock(),
"dopamine": MagicMock(),
"growth_brain": MagicMock(),
"resonance": MagicMock(),
"crm": MagicMock(),
"swarm": MagicMock(),
"darwin": MagicMock(),
"radome": HoneypotRadome(1080, 2400),
"telepathic": telepathic,
"nav_graph": MagicMock(),
"zero_engine": MagicMock()
}
cognitive_stack["resonance"].calculate_resonance.return_value = 0.9
cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, False, False, True]
cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
session_state = MagicMock()
session_state.check_limit.side_effect = lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop
# The loop should not crash despite exceptions (e.g. invalid XML or CHAOS exception from telepathic)
# Instead, it should catch exceptions and use _humanized_scroll or abort safely
_run_zero_latency_feed_loop(device, cognitive_stack["zero_engine"], cognitive_stack["nav_graph"], configs, session_state, "HomeFeed", cognitive_stack)
# Should have advanced through the states via fallback scroll mechanism
assert state["index"] >= 1

View File

@@ -0,0 +1,54 @@
import pytest
from unittest.mock import MagicMock, patch, PropertyMock
from GramAddict.core.swarm_protocol import SwarmProtocol
@pytest.fixture
def swarm():
with patch('GramAddict.core.qdrant_memory.QdrantClient'):
return SwarmProtocol(username="test_bot")
def test_emit_pheromone(swarm):
"""Verify that emitting a pheromone calls Qdrant upsert with correct payload."""
with patch("GramAddict.core.qdrant_memory.QdrantBase.is_connected", new_callable=PropertyMock, return_value=True):
path_hash = "some_ui_path_hash"
outcome = "success"
swarm.emit_pheromone(path_hash, outcome)
# Check if upsert was called with the expected payload
swarm.client.upsert.assert_called_once()
args, kwargs = swarm.client.upsert.call_args
points = kwargs.get('points')
assert points[0].payload['path_hash'] == path_hash
assert points[0].payload['outcome'] == outcome
assert points[0].payload['username'] == "test_bot"
def test_query_consensus_hit(swarm):
"""Verify consensus query returns the outcome from Qdrant scroll."""
with patch("GramAddict.core.qdrant_memory.QdrantBase.is_connected", new_callable=PropertyMock, return_value=True):
path_hash = "known_path"
# Mock scroll result
mock_point = MagicMock()
mock_point.payload = {"outcome": "banned"}
swarm.client.scroll.return_value = ([mock_point], None)
result = swarm.query_consensus(path_hash)
assert result == "banned"
swarm.client.scroll.assert_called_once()
def test_query_consensus_miss(swarm):
"""Verify None is returned when no pheromones found."""
with patch("GramAddict.core.qdrant_memory.QdrantBase.is_connected", new_callable=PropertyMock, return_value=True):
swarm.client.scroll.return_value = ([], None)
result = swarm.query_consensus("unknown_path")
assert result is None
def test_offline_mode(swarm):
"""Protocol should not crash if Qdrant is disconnected."""
with patch("GramAddict.core.qdrant_memory.QdrantBase.is_connected", new_callable=PropertyMock, return_value=False):
swarm.emit_pheromone("any", "thing")
swarm.client.upsert.assert_not_called()
assert swarm.query_consensus("any") is None

View File

@@ -0,0 +1,167 @@
import pytest
import math
import os
import tempfile
import json
from unittest.mock import patch, MagicMock
import sys
# Force mock qdrant_client before importing any core modules that depend on it
from GramAddict.core.telepathic_engine import TelepathicEngine
class TestTelepathicEngineEdgeCases:
@pytest.fixture(autouse=True)
def setup_engine(self):
self.engine = TelepathicEngine()
def test_cosine_similarity_edge_cases(self):
# 0 vectors
assert self.engine._cosine_similarity([0,0,0], [0,0,0]) == 0.0
assert self.engine._cosine_similarity([1,2,3], [0,0,0]) == 0.0
# Mismatched sizes
assert self.engine._cosine_similarity([1,2], [1,2,3]) == 0.0
# Empty lists
assert self.engine._cosine_similarity([], []) == 0.0
# Valid vectors
assert self.engine._cosine_similarity([1,0], [1,0]) == 1.0
assert self.engine._cosine_similarity([1,0], [0,1]) == 0.0
assert self.engine._cosine_similarity([1,1], [1,1]) > 0.99
def test_json_io_edge_cases(self):
# Try to load non-existent
with tempfile.TemporaryDirectory() as tmpdir:
file_path = os.path.join(tmpdir, "missing.json")
assert self.engine._load_json(file_path) == {}
# Save dict
self.engine._save_json(file_path, {"test": "ok"})
assert self.engine._load_json(file_path) == {"test": "ok"}
# Corrupted json
with open(file_path, "w") as f:
f.write("corrupted { string")
assert self.engine._load_json(file_path) == {}
def test_structural_sanity_check_edge_cases(self):
# Good node
good_node = {"y": 500, "area": 1000}
assert self.engine._structural_sanity_check(good_node, "tap button") == True
# Status bar zone (y < 4% of 2400 = 96)
status_bar_node = {"y": 50, "area": 1000}
assert self.engine._structural_sanity_check(status_bar_node, "tap button") == False
# Massive container without media intent
massive_node = {"y": 500, "area": 600000} # > MAX_CONTAINER_AREA (500000)
assert self.engine._structural_sanity_check(massive_node, "tap button") == False
# Massive container WITH media intent (allowed)
assert self.engine._structural_sanity_check(massive_node, "watch video post") == True
# 0 size
invisible_node = {"y": 500, "area": 0}
assert self.engine._structural_sanity_check(invisible_node, "tap button") == False
# Negative bounds/y, shouldn't crash, returns False
neg_node = {"y": -10, "area": 1000}
assert self.engine._structural_sanity_check(neg_node, "tap button") == False
def test_is_instagram_context_edge_cases(self):
# Set app ID
self.engine._cached_app_id = "com.instagram.android"
# No nodes
assert self.engine._is_instagram_context([]) == False
# Nodes from wrong app
wrong_app_nodes = [{"resource_id": "com.youtube.android:id/btn"}]
assert self.engine._is_instagram_context(wrong_app_nodes) == False
# Nodes from right app
right_app_nodes = [{"resource_id": "com.instagram.android:id/btn"}]
assert self.engine._is_instagram_context(right_app_nodes) == True
# Missing resource_id
missing_id_nodes = [{"y": 10}]
assert self.engine._is_instagram_context(missing_id_nodes) == False
def test_keyword_match_score_edge_cases(self):
# Empty intent (all filler words)
assert self.engine._keyword_match_score("tap the on a", [{"semantic_string": "button"}]) == None
# Empty nodes
assert self.engine._keyword_match_score("home", []) == None
# Valid nodes + Alias testing
nodes = [
{"semantic_string": "main view section", "x": 10, "y": 10, "area": 100},
{"semantic_string": "search bar", "x": 20, "y": 20, "area": 200}
]
# Alias: "home" expands to "main"
# The word 'home' is checked against 'main view section' and gets a hit
res = self.engine._keyword_match_score("tap home tab", nodes)
assert res is not None
assert res["semantic"] == "main view section"
assert res["score"] == 0.95
# No matches
assert self.engine._keyword_match_score("tap settings menu xyz", nodes) == None
# Like check (already liked)
liked_nodes = [
{"semantic_string": "heart button", "original_attribs": {"desc": "Liked by john"}}
]
res_like = self.engine._keyword_match_score("tap like button", liked_nodes)
assert res_like["skip"] == True
assert res_like["semantic"] == "already_liked"
def test_click_tracking_and_learning_edge_cases(self):
from GramAddict.core.telepathic_engine import TelepathicEngine as TE
# Clear tracker
TE._last_click_context = None
# confirming with no tracked click
self.engine.confirm_click("test") # Should not crash
# tracking
node = {"semantic_string": "my button", "x": 10, "y": 20}
self.engine._track_click("tap my button", node)
assert TE._last_click_context is not None
# Use a temporary dict for memory so we don't write to disk during test
self.engine._memory = {}
with patch.object(self.engine, '_save_json'):
self.engine.confirm_click()
# Check if stored
assert "tap my button" in self.engine._memory
assert "my button" in self.engine._memory["tap my button"]
# Confirming AGAIN should not duplicate
self.engine._track_click("tap my button", node)
self.engine.confirm_click()
assert len(self.engine._memory["tap my button"]) == 1
# Rejecting
self.engine._track_click("tap my button", node)
self.engine.reject_click()
# Should be removed from positive memory and added to blacklist
assert "my button" not in self.engine._memory.get("tap my button", [])
assert "my button" in self.engine._blacklist.get("tap my button", [])
# Confirming a blacklisted item should rehabilitate it
self.engine._track_click("tap my button", node)
self.engine.confirm_click()
assert "my button" in self.engine._memory.get("tap my button", [])
assert "my button" not in self.engine._blacklist.get("tap my button", [])

View File

@@ -0,0 +1,373 @@
"""
Test Suite: Real XML Fixture Validation
========================================
These tests use REAL UIAutomator XML dumps captured from a live Instagram
session on the device. No hand-crafted node arrays — the full XML goes
through _extract_semantic_nodes() exactly like in production.
This catches bugs that mock-based tests miss:
- Parser skipping nodes due to missing attribs
- Parser including non-interactive system UI elements
- Safety guard false positives on real Instagram layouts
- Ad detection on real ad XML structures
"""
import pytest
import re
import os
from unittest.mock import MagicMock, patch
import json
from GramAddict.core.telepathic_engine import TelepathicEngine
FIXTURE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
def load_fixture(name: str) -> str:
"""Load a real XML capture from tests/fixtures/"""
path = os.path.join(FIXTURE_DIR, name)
with open(path, "r") as f:
return f.read()
class TestNodeExtraction:
"""
Tests that _extract_semantic_nodes correctly parses REAL Instagram XML.
Uses captured XML dumps from the actual device.
"""
def test_home_feed_extracts_like_button(self):
"""
In a real Home Feed dump, the parser MUST find the Like button node
with resource-id 'row_feed_button_like'.
"""
engine = TelepathicEngine()
xml = load_fixture("home_feed_with_ad.xml")
nodes = engine._extract_semantic_nodes(xml)
like_nodes = [n for n in nodes if "row feed button like" in n["semantic_string"]]
# The real dump has an organic post AND an ad post, both with like buttons
assert len(like_nodes) > 0, (
"Like buttons must be extracted despite clickable=false, because our "
"new TelepathicEngine extraction heuristic includes highly semantic buttons."
)
# Instead, look for the parent Add to Saved button (which IS clickable)
save_nodes = [n for n in nodes if "row feed button save" in n["semantic_string"]]
assert len(save_nodes) >= 1, (
f"Expected to find 'Add to Saved' button in real home feed XML. "
f"Found nodes: {[n['semantic_string'][:60] for n in nodes]}"
)
def test_home_feed_extracts_tab_bar(self):
"""
The parser must find the bottom tab bar items (Home, Reels, Search, Profile).
These are critical for navigation.
"""
engine = TelepathicEngine()
xml = load_fixture("home_feed_with_ad.xml")
nodes = engine._extract_semantic_nodes(xml)
tab_descriptions = [n["semantic_string"] for n in nodes if "tab" in n["semantic_string"].lower()]
assert any("Home" in t for t in tab_descriptions), "Must find Home tab"
assert any("Search and explore" in t for t in tab_descriptions), "Must find Search tab"
assert any("Profile" in t for t in tab_descriptions), "Must find Profile tab"
def test_home_feed_node_count_is_realistic(self):
"""
A real Instagram home feed XML produces 20-40 interactive nodes.
If we get <10 or >100, the parser is broken.
"""
engine = TelepathicEngine()
xml = load_fixture("home_feed_with_ad.xml")
nodes = engine._extract_semantic_nodes(xml)
assert 10 <= len(nodes) <= 100, (
f"Expected 10-100 interactive nodes from real XML, got {len(nodes)}. "
f"Parser may be too strict or too lenient."
)
def test_explore_feed_extracts_like_button(self):
"""
In the real Explore/Reels feed, the Like button has id 'like_button'
and description 'Like'. The parser must find it.
"""
engine = TelepathicEngine()
xml = load_fixture("explore_feed_reel.xml")
nodes = engine._extract_semantic_nodes(xml)
like_nodes = [n for n in nodes
if "like" in n["semantic_string"].lower()
and "button" in n["semantic_string"].lower()]
assert len(like_nodes) >= 1, (
f"Expected to find Like button in explore feed. "
f"Found: {[n['semantic_string'][:60] for n in nodes]}"
)
def test_explore_feed_has_fullscreen_containers(self):
"""
Verify that the parser extracts the fullscreen containers
(swipeable_nav_view_pager_inner_recycler_view, clips_viewer_view_pager)
so that the Safety Guard has something to reject.
"""
engine = TelepathicEngine()
xml = load_fixture("explore_feed_reel.xml")
nodes = engine._extract_semantic_nodes(xml)
fullscreen = []
for n in nodes:
m = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', n["raw_bounds"])
if m:
l, t, r, b = map(int, m.groups())
if (r - l) > 900 and (b - t) > 1600:
fullscreen.append(n)
assert len(fullscreen) >= 2, (
f"Expected at least 2 fullscreen containers in explore XML "
f"(swipeable pager + recycler view). Got {len(fullscreen)}."
)
class TestSafetyGuard:
"""
Tests the VLM Safety Guard against nodes extracted from REAL XML.
Ensures that if a hallucinating VLM picks a fullscreen container,
the guard catches it.
"""
@pytest.fixture(autouse=True)
def setup_real_nodes(self):
"""Pre-parse real XML nodes BEFORE any mocking happens."""
engine = TelepathicEngine()
explore_xml = load_fixture("explore_feed_reel.xml")
self.explore_nodes = engine._extract_semantic_nodes(explore_xml)
@patch('builtins.open', new_callable=MagicMock)
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('os.path.exists')
def test_real_explore_fullscreen_container_rejected(self, mock_exists, mock_query, mock_open):
"""
Feed real explore XML nodes to the VLM fallback.
Force VLM to pick the fullscreen swipeable_nav_view_pager.
The safety guard MUST reject it.
"""
mock_exists.return_value = False
engine = TelepathicEngine()
device = MagicMock()
device.deviceV2.screenshot = MagicMock()
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
nodes = self.explore_nodes
# Find any fullscreen structural container
container_idx = None
for i, n in enumerate(nodes):
m = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', n["raw_bounds"])
if m:
l, t, r, b = map(int, m.groups())
if (r - l) > 900 and (b - t) > 1600:
# It's not a media container (no vid/clip keyword)
sem = n["semantic_string"].lower()
if not any(k in sem for k in ["vid", "img", "image", "clip", "reel"]):
container_idx = i
break
assert container_idx is not None, (
f"Test setup: couldn't find a non-media fullscreen container. "
f"Nodes: {[n['semantic_string'][:60] for n in nodes]}"
)
mock_query.return_value = json.dumps({"index": container_idx, "reason": "This looks like the like button"})
result = engine._vision_cortex_fallback("tap like button", nodes, device)
assert result is None, (
f"CRITICAL: Safety guard failed on REAL XML! "
f"Accepted node {container_idx}: {nodes[container_idx]['semantic_string']}"
)
@patch('builtins.open', new_callable=MagicMock)
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('os.path.exists')
def test_real_explore_like_button_accepted(self, mock_exists, mock_query, mock_open):
"""
Feed real explore XML nodes.
Force VLM to pick the actual like button.
The safety guard MUST allow it through.
"""
mock_exists.return_value = False
engine = TelepathicEngine()
device = MagicMock()
device.deviceV2.screenshot = MagicMock()
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
nodes = self.explore_nodes
# Find the like button
like_idx = None
for i, n in enumerate(nodes):
if "like" in n["semantic_string"].lower() and "button" in n["semantic_string"].lower():
# Ensure it's a small button, not a container
m = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', n["raw_bounds"])
if m:
l, t, r, b = map(int, m.groups())
if (r - l) < 200 and (b - t) < 200:
like_idx = i
break
assert like_idx is not None, (
f"Test setup: couldn't find Like button in real explore XML. "
f"Nodes: {[n['semantic_string'][:60] for n in nodes]}"
)
mock_query.return_value = json.dumps({"index": like_idx, "reason": "It literally says Like"})
result = engine._vision_cortex_fallback("tap like button", nodes, device)
assert result is not None, "The real Like button (116x116) must be accepted"
assert nodes[like_idx]["x"] == result["x"]
assert nodes[like_idx]["y"] == result["y"]
class TestAdDetection:
"""
Tests ad detection against REAL XML captured from the device.
The dump.xml actually contains a real Instagram ad (hoeltlfinanzgmbh).
"""
def test_real_explore_feed_is_not_ad(self):
"""
The explore_feed.xml is a real Reel without any ad markers.
It should NOT be flagged.
"""
from GramAddict.core.bot_flow import _detect_ad_structural
xml = load_fixture("explore_feed_reel.xml")
assert _detect_ad_structural(xml) is False, (
"Real explore/reel content was falsely flagged as an ad!"
)
class TestFeedMarkers:
"""
Tests feed marker detection against REAL XML.
"""
def test_real_home_feed_has_markers(self):
"""The real home feed XML must match our feed markers."""
from GramAddict.core.bot_flow import FEED_MARKERS
xml = load_fixture("home_feed_with_ad.xml")
has_markers = any(m in xml for m in FEED_MARKERS)
assert has_markers, (
"Real home feed XML did not match any feed markers! "
f"Markers: {FEED_MARKERS}"
)
def test_real_explore_feed_has_markers(self):
"""The real explore feed XML must match our feed markers."""
from GramAddict.core.bot_flow import FEED_MARKERS
xml = load_fixture("explore_feed_reel.xml")
has_markers = any(m in xml for m in FEED_MARKERS)
assert has_markers, (
"Real explore feed XML did not match any feed markers! "
f"Markers: {FEED_MARKERS}"
)
class TestTelepathicResolutionCascade:
"""
Tests that the Telepathic Engine's resolution cascade strictly enforces
cheapest-first processing (Fast Path -> Embeddings -> VLM) to avoid
token overkill and API spam. Uses real XML dumps.
"""
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding')
def test_keyword_fast_path_bypasses_ai(self, mock_get_embedding, mock_vlm):
"""
A direct keyword match (like 'tap like button') MUST be resolved by Stage 1.5.
It must never reach the Embedding (Stage 2) or VLM (Stage 3).
"""
from GramAddict.core.telepathic_engine import TelepathicEngine
engine = TelepathicEngine()
engine._embedding_cache.clear()
engine._intent_cache.clear()
# home_feed_with_ad.xml contains standard UI elements
xml_content = load_fixture("home_feed_with_ad.xml")
result = engine.find_best_node(xml_content, "tap comment button")
assert result is not None, "Failed to find the node."
assert "comment" in result["semantic"].lower(), "Did not find comment button."
assert result.get("score", 0) >= 0.4, "Fast path score ratio should be valid"
# PROOF: Zero AI tokens used!
mock_get_embedding.assert_not_called()
mock_vlm.assert_not_called()
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding')
def test_embedding_fallback_bypasses_vlm_if_confident(self, mock_get_embedding, mock_vlm):
"""
If we ask something without an exact keyword match, it should fail Stage 1.5,
hit Stage 2 (Embeddings), and if confident enough, avoid Stage 3 (VLM).
"""
from GramAddict.core.telepathic_engine import TelepathicEngine
engine = TelepathicEngine()
engine._embedding_cache.clear()
engine._intent_cache.clear()
xml_content = load_fixture("home_feed_with_ad.xml")
def fake_embed(text):
if "heart" in text.lower():
return [1.0, 0.0] # Intent vector
if "like" in text.lower() and "button" in text.lower():
return [0.99, 0.1] # Very similar to intent
return [0.0, 1.0] # Completely different for all other UI nodes
mock_get_embedding.side_effect = fake_embed
# "heart" is not mechanically in the keyword of the Like button,
# causing Keyword Path to fail. Vector Similarity will know Heart == Like.
result = engine.find_best_node(xml_content, "tap the heart symbol")
assert result is not None, "Failed to find the node through embeddings."
assert "like" in result["semantic"].lower(), "Embeddings didn't pick the like button."
assert result.get("score", 0) >= 0.82, "Confidence threshold should be met"
# PROOF: Embeddings matched it, VLM was NOT used
assert mock_get_embedding.call_count > 0, "Embeddings were not called"
mock_vlm.assert_not_called()
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('GramAddict.core.qdrant_memory.QdrantBase._get_embedding')
def test_vlm_fallback_triggered_on_low_confidence(self, mock_get_embedding, mock_vlm):
"""
If Embeddings fail to find a confident match (< 0.82), it must trigger
the Stage 3 VLM fallback.
"""
from GramAddict.core.telepathic_engine import TelepathicEngine
engine = TelepathicEngine()
engine._embedding_cache.clear()
engine._intent_cache.clear()
xml_content = load_fixture("home_feed_with_ad.xml")
def fake_embed(text):
if "mystical" in text:
return [1.0, 0.0]
return [0.0, 1.0]
mock_get_embedding.side_effect = fake_embed
mock_vlm.return_value = '{"index": 2, "reason": "Fallback chosen by VLM"}'
# A mockup device is needed for VLM fallback
import unittest.mock
device = unittest.mock.MagicMock()
result = engine.find_best_node(xml_content, "tap the mystical artifact", device=device)
# It might return a result (from VLM) or None depending on the XML structure,
# but we ONLY care that VLM was indeed queried!
mock_get_embedding.assert_called()
mock_vlm.assert_called_once()

View File

@@ -0,0 +1,616 @@
"""
Test Suite: VLM Bombproofing & Interaction Integrity
=====================================================
TDD validation that the Telepathic Engine and bot_flow interaction loop
are structurally safe against VLM hallucinations, cache poisoning,
and Darwin-induced context loss.
"""
import pytest
import re
from unittest.mock import MagicMock, patch, call
import json
from GramAddict.core.telepathic_engine import TelepathicEngine
# ── Shared Fixtures ──
def mock_device(width=1080, height=2400):
device = MagicMock()
device.get_info.return_value = {"displayWidth": width, "displayHeight": height}
device.deviceV2.screenshot = MagicMock()
device.cm_to_pixels = MagicMock(side_effect=lambda cm: int(cm * 160)) # ~160px per cm
device._get_current_app.return_value = "com.instagram.android"
device.app_id = "com.instagram.android"
return device
def make_node(x, y, bounds, semantic, text="", desc=""):
"""Helper to construct realistic UI nodes."""
node = {
"x": x, "y": y,
"raw_bounds": bounds,
"semantic_string": semantic,
"original_attribs": {"text": text, "desc": desc, "resource-id": "com.instagram.android:id/dummy"},
"resource_id": "com.instagram.android:id/dummy"
}
# Parse bounds to calculate area for VLM structural guards
m = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
if m:
l, t, r, b = map(int, m.groups())
width = r - l
height = b - t
node["width"] = width
node["height"] = height
node["area"] = width * height
else:
node["width"] = 0
node["height"] = 0
node["area"] = 0
return node
# Realistic node sets extracted from live Instagram XML dumps
EXPLORE_FEED_NODES = [
make_node(540, 1250, "[0,200][1080,2300]",
"id context: 'swipeable nav view pager inner recycler view'"),
make_node(100, 2200, "[50,2150][150,2250]",
"description: 'Search and explore', id context: 'search tab'"),
make_node(540, 2200, "[490,2150][590,2250]",
"description: 'Reels', id context: 'reels tab'"),
make_node(940, 2200, "[890,2150][990,2250]",
"description: 'Profile', id context: 'profile tab'"),
]
REEL_POST_NODES = [
make_node(540, 1200, "[0,0][1080,2200]",
"id context: 'clips video container'"),
make_node(1020, 800, "[980,760][1060,840]",
"description: 'Like', id context: 'row feed button like'"),
make_node(1020, 920, "[980,880][1060,960]",
"description: 'Comment', id context: 'row feed button comment'"),
make_node(1020, 1040, "[980,1000][1060,1080]",
"description: 'Share', id context: 'row feed button share'"),
make_node(180, 2100, "[50,2060][310,2140]",
"text: 'super_azores', description: 'super_azores'", text="super_azores"),
]
PHOTO_POST_NODES = [
make_node(540, 850, "[0,400][1080,1300]",
"id context: 'row feed photo imageview'"),
make_node(100, 1350, "[50,1310][150,1390]",
"description: 'Like', id context: 'row feed button like'"),
make_node(200, 1350, "[150,1310][250,1390]",
"description: 'Comment', id context: 'row feed button comment'"),
]
PROFILE_EDIT_NODES = [
make_node(540, 300, "[0,50][1080,550]",
"id context: 'profile header container'"),
make_node(540, 650, "[100,600][980,700]",
"text: 'Edit profile', id context: 'edit profile button'", text="Edit profile"),
make_node(540, 800, "[100,750][980,850]",
"text: 'Share profile', id context: 'share profile button'", text="Share profile"),
]
class TestVLMHallucinationRejection:
"""
Tests that the engine rejects VLM responses pointing to massive
structural containers instead of small interactive elements.
"""
@patch('builtins.open', new_callable=MagicMock)
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('os.path.exists')
def test_recycler_view_hallucination_is_rejected(self, mock_exists, mock_query, mock_open):
"""
Exact reproduction of the bug from log 2026-04-13 17:22:31:
VLM picked node 0 ('swipeable nav view pager inner recycler view')
for intent 'tap like button'. Must be rejected.
"""
mock_exists.return_value = False
engine = TelepathicEngine()
device = mock_device()
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
mock_query.return_value = '{"index": 0, "reason": "I think this is it"}'
result = engine._vision_cortex_fallback("tap like button", EXPLORE_FEED_NODES, device)
assert result is None, (
f"CRITICAL: Engine accepted a fullscreen recycler view as 'like button'! "
f"Got: {result}"
)
@patch('builtins.open', new_callable=MagicMock)
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('os.path.exists')
def test_frame_layout_hallucination_is_rejected(self, mock_exists, mock_query, mock_open):
"""
A different structural container variant: FrameLayout that spans
the full display. Must also be rejected.
"""
mock_exists.return_value = False
engine = TelepathicEngine()
device = mock_device()
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
nodes = [
make_node(540, 1200, "[0,0][1080,2400]",
"id context: 'action bar root'"),
make_node(100, 1350, "[50,1310][150,1390]",
"description: 'Like', id context: 'row feed button like'"),
]
mock_query.return_value = '{"index": 0, "reason": "action bar seems right"}'
result = engine._vision_cortex_fallback("tap like button", nodes, device)
assert result is None, (
f"CRITICAL: Engine accepted a fullscreen FrameLayout as 'like button'! "
f"Got: {result}"
)
@patch('builtins.open', new_callable=MagicMock)
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('os.path.exists')
def test_video_container_is_allowed_for_media_intent(self, mock_exists, mock_query, mock_open):
"""
Fullscreen video containers (clips_video_container) are legitimate
tap targets for intents like 'pause reel' or 'tap the reel video'.
"""
mock_exists.return_value = False
engine = TelepathicEngine()
device = mock_device()
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
mock_query.return_value = '{"index": 0, "reason": "Tapping the video to pause"}'
result = engine._vision_cortex_fallback("tap the reel video", REEL_POST_NODES, device)
assert result is not None, "Video container tap should be allowed for media intents"
assert result["x"] == 540
assert result["y"] == 1200
@patch('builtins.open', new_callable=MagicMock)
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('os.path.exists')
def test_correct_like_button_is_accepted(self, mock_exists, mock_query, mock_open):
"""
When VLM correctly identifies the small like button (80x80),
it must be accepted without interference from the safety guard.
"""
mock_exists.return_value = False
engine = TelepathicEngine()
device = mock_device()
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
# VLM returns index 1 — the correct, tiny Like button
mock_query.return_value = '{"index": 1, "reason": "It says Like and has the heart icon"}'
result = engine._vision_cortex_fallback("tap like button", REEL_POST_NODES, device)
assert result is not None, "Correct like button should be accepted"
assert result["x"] == 1020
assert result["y"] == 800
@patch('builtins.open', new_callable=MagicMock)
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('os.path.exists')
def test_comment_button_is_accepted(self, mock_exists, mock_query, mock_open):
"""Correct comment button selection on a Reel post."""
mock_exists.return_value = False
engine = TelepathicEngine()
device = mock_device()
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
mock_query.return_value = '{"index": 2, "reason": "Comment button"}'
result = engine._vision_cortex_fallback("tap comment button", REEL_POST_NODES, device)
assert result is not None
assert result["x"] == 1020
assert result["y"] == 920
@patch('builtins.open', new_callable=MagicMock)
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('os.path.exists')
def test_photo_imageview_container_not_blocked(self, mock_exists, mock_query, mock_open):
"""
A photo imageview is large (~900x900) but NOT fullscreen.
It should NOT trigger the safety guard.
"""
mock_exists.return_value = False
engine = TelepathicEngine()
device = mock_device()
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
mock_query.return_value = '{"index": 0, "reason": "Double tap to like the photo"}'
result = engine._vision_cortex_fallback("double tap photo to like", PHOTO_POST_NODES, device)
assert result is not None, (
"Photo imageview (1080x900) should NOT be blocked — it's a valid media target"
)
class TestTelepathicMemoryPoisoning:
"""
Tests that the cache (telepathic_memory.json) never stores
hallucinated or rejected VLM decisions.
"""
@patch('builtins.open', new_callable=MagicMock)
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('os.path.exists')
def test_rejected_hallucination_is_never_cached(self, mock_exists, mock_query, mock_open):
"""
When a VLM hallucination is rejected by the safety guard,
it must NOT be written to telepathic_memory.json.
"""
mock_exists.return_value = False
engine = TelepathicEngine()
device = mock_device()
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
mock_query.return_value = '{"index": 0, "reason": "I think this is it"}'
result = engine._vision_cortex_fallback("tap like button", EXPLORE_FEED_NODES, device)
assert result is None, "Hallucination should be rejected"
# Verify that open() was never called in WRITE mode ("w") for the cache file
write_calls = [
c for c in mock_open.call_args_list
if len(c[0]) >= 2 and c[0][1] == "w" and "telepathic_memory.json" in c[0][0]
]
assert len(write_calls) == 0, (
f"CRITICAL: A rejected hallucination was written to cache! Write calls: {write_calls}"
)
@patch('builtins.open', new_callable=MagicMock)
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('os.path.exists')
def test_valid_decision_is_tracked_but_not_cached(self, mock_exists, mock_query, mock_open):
"""
When a VLM correctly identifies a valid, small button,
it SHOULD be tracked, but NOT written to telepathic_memory.json
until explicit confirmation (bot_flow -> confirm_click).
"""
mock_exists.return_value = False
engine = TelepathicEngine()
device = mock_device()
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
# VLM correctly selects the tiny like button (node 1)
mock_query.return_value = '{"index": 1, "reason": "Like button"}'
result = engine._vision_cortex_fallback("tap like button", REEL_POST_NODES, device)
assert result is not None, "Valid like button should be accepted"
# Verify cache was NOT written immediately to prevent poisoning
write_calls = [
c for c in mock_open.call_args_list
if len(c[0]) >= 2 and c[0][1] == "w"
]
assert len(write_calls) == 0, (
"VLM decision should NOT be saved to cache immediately to prevent poisoning!"
)
# Verify it is tracked
assert TelepathicEngine._last_click_context is not None
assert TelepathicEngine._last_click_context["intent"] == "tap like button"
class TestTelepathicMemoryRecall:
"""
Tests that the cache short-circuits correctly and never
recalls a stale/wrong semantic match.
"""
@patch('GramAddict.core.telepathic_engine.TelepathicEngine._extract_semantic_nodes')
@patch('builtins.open', new_callable=MagicMock)
@patch('os.path.exists')
def test_cache_hit_returns_instantly(self, mock_exists, mock_open, mock_extract):
"""
If the telepathic memory already contains a hit for this intent,
it must return immediately WITHOUT taking a screenshot or calling VLM.
"""
mock_exists.return_value = True
engine = TelepathicEngine()
device = mock_device()
mock_extract.return_value = REEL_POST_NODES
# Simulate a cached memory file
cache_data = {
"tap like button": ["description: 'Like', id context: 'row feed button like'"]
}
mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(cache_data).encode()
# Provide nodes that match the cached semantic
result = engine.find_best_node("<fake_xml>", "tap like button", min_confidence=0.82, device=device)
assert result is not None, "Cache hit should return a result"
assert result["x"] == 1020
assert result["y"] == 800
assert result["score"] == 1.0
# Screenshot should NEVER have been called (the early-return optimization)
device.deviceV2.screenshot.assert_not_called()
@patch('GramAddict.core.telepathic_engine.TelepathicEngine._extract_semantic_nodes')
@patch('builtins.open', new_callable=MagicMock)
@patch('os.path.exists')
def test_cache_miss_does_not_match_wrong_intent(self, mock_exists, mock_open, mock_extract):
"""
Cached memory for 'tap like button' must NOT be used when the
intent is 'tap comment button'.
"""
mock_exists.return_value = True
engine = TelepathicEngine()
device = mock_device()
mock_extract.return_value = REEL_POST_NODES
cache_data = {
"tap like button": ["description: 'Like', id context: 'row feed button like'"]
}
mock_open.return_value.__enter__.return_value.read.return_value = json.dumps(cache_data).encode()
# We ask for "comment" but only "like" is cached — no early return
# Mock VLM fallback so it doesn't crash
with patch.object(engine, '_vision_cortex_fallback', return_value={"x": 999, "y": 999, "score": 1.0}):
result = engine.find_best_node("<fake>", "tap comment button", min_confidence=0.82, device=device)
# It should NOT return the like button coordinates
if result is not None:
assert result["y"] != 800, (
"CRITICAL: Cache returned like button coordinates for a comment intent!"
)
class TestDarwinScrollSafety:
"""
Tests that Darwin's 'cognitive wobble' and 'non-linear scroll'
don't produce scroll distances large enough to leave the current post.
"""
def test_back_swipe_distance_is_bounded(self):
"""
The back-swipe (cognitive wobble) must never exceed 1.5cm (~240px).
Anything larger risks scrolling past the current post entirely.
"""
from GramAddict.core.darwin_engine import DarwinEngine
# Run 200 Monte Carlo iterations to catch edge cases
max_observed_distance = 0
for _ in range(200):
# The back-swipe distance is: cm_to_pixels(uniform(0.8, 1.2))
# At 160px/cm ≈ 128 to 192 pixels. Must stay under 240px.
distance_cm = __import__('random').uniform(0.8, 1.2)
distance_px = int(distance_cm * 160) # approximate px/cm
max_observed_distance = max(max_observed_distance, distance_px)
assert max_observed_distance <= 240, (
f"Back-swipe distance reached {max_observed_distance}px! "
f"Max allowed is 240px to prevent scrolling off the current post."
)
def test_scroll_velocity_never_causes_multi_post_skip(self):
"""
The non-linear scroll in execute_proof_of_resonance must not
produce a swipe distance exceeding the screen height, which would
skip multiple posts at once.
"""
# scroll_velocity bounds: (0.1, 2.0)
# distance = cm_to_pixels(uniform(4.0, 7.0)) * velocity
# Worst case: 7cm * 2.0 velocity * 160px/cm = 2240px
# Screen height is 2400px — this is dangerously close!
max_distance_px = 0
for _ in range(500):
velocity = __import__('random').uniform(0.1, 2.0)
base_cm = __import__('random').uniform(4.0, 7.0)
distance_px = int(base_cm * 160 * velocity)
max_distance_px = max(max_distance_px, distance_px)
screen_height = 2400
assert max_distance_px < screen_height, (
f"Darwin scroll distance reached {max_distance_px}px which exceeds "
f"screen height {screen_height}px! This would skip multiple posts."
)
class TestFeedMarkerValidation:
"""
Tests that the feed marker self-check in bot_flow correctly
identifies when the bot is on a post vs. lost.
"""
def test_reel_feed_markers_detected(self):
"""
A Reel post contains 'clips_media_component' which is in FEED_MARKERS.
"""
from GramAddict.core.bot_flow import FEED_MARKERS
fake_xml = """
<node class="android.widget.FrameLayout">
<node resource-id="com.instagram.android:id/clips_media_component" />
<node resource-id="com.instagram.android:id/row_feed_button_like" />
</node>
"""
has_markers = any(m in fake_xml for m in FEED_MARKERS)
assert has_markers, "Reel XML should match feed markers via 'clips_media_component'"
def test_photo_feed_markers_detected(self):
"""
A photo post contains 'row_feed_photo_imageview' in FEED_MARKERS.
"""
from GramAddict.core.bot_flow import FEED_MARKERS
fake_xml = """
<node class="android.widget.FrameLayout">
<node resource-id="com.instagram.android:id/row_feed_photo_imageview" />
<node resource-id="com.instagram.android:id/row_feed_button_like" />
</node>
"""
has_markers = any(m in fake_xml for m in FEED_MARKERS)
assert has_markers, "Photo post XML should match feed markers via 'row_feed_photo_imageview'"
def test_profile_page_has_no_feed_markers(self):
"""
A profile page must NOT match any feed markers.
This is the bug scenario: the bot tapped a user tag, ended up on
a profile page, and then tried to interact with non-existent posts.
"""
from GramAddict.core.bot_flow import FEED_MARKERS
fake_profile_xml = """
<node class="android.widget.FrameLayout">
<node resource-id="com.instagram.android:id/profile_header_container" />
<node resource-id="com.instagram.android:id/action_bar_textview_title" text="marisaundmarc" />
<node resource-id="com.instagram.android:id/profile_tab_layout" />
<node resource-id="com.instagram.android:id/profile_tab_icon_view" />
</node>
"""
has_markers = any(m in fake_profile_xml for m in FEED_MARKERS)
assert not has_markers, (
"Profile page XML must NOT match feed markers! "
"If it does, the bot would try to like/interact on a profile page."
)
def test_edit_profile_page_has_no_feed_markers(self):
"""
The edit profile page (where the bot got stuck) must NOT match markers.
"""
from GramAddict.core.bot_flow import FEED_MARKERS
fake_edit_xml = """
<node class="android.widget.FrameLayout">
<node resource-id="com.instagram.android:id/action_bar_textview_title" text="Edit profile" />
<node resource-id="com.instagram.android:id/full_name_edit_text" />
<node resource-id="com.instagram.android:id/bio_edit_text" />
</node>
"""
has_markers = any(m in fake_edit_xml for m in FEED_MARKERS)
assert not has_markers, (
"Edit profile page must NOT match feed markers! "
"The bot was getting stuck here because it blindly tapped center-screen."
)
def test_explore_grid_has_no_feed_markers(self):
"""
The Explore grid (before opening a post) must NOT match feed markers.
The bot should only enter the interaction loop AFTER tapping into a post.
"""
from GramAddict.core.bot_flow import FEED_MARKERS
fake_explore_grid_xml = """
<node class="android.widget.FrameLayout">
<node resource-id="com.instagram.android:id/search_tab" />
<node resource-id="com.instagram.android:id/explore_grid" />
<node content-desc="Reel by user1 at row 1, column 1" clickable="true" />
<node content-desc="Photo by user2 at row 1, column 2" clickable="true" />
</node>
"""
has_markers = any(m in fake_explore_grid_xml for m in FEED_MARKERS)
assert not has_markers, (
"Explore grid must NOT match feed markers — the bot isn't on a post yet."
)
class TestAdDetection:
"""
Tests that ad detection is accurate and doesn't flag organic posts.
"""
def test_sponsored_post_detected(self):
"""A post with ad_cta_button is detected as an ad."""
from GramAddict.core.bot_flow import _detect_ad_structural
ad_xml = """
<node class="android.widget.FrameLayout">
<node resource-id="com.instagram.android:id/row_feed_photo_imageview" />
<node resource-id="com.instagram.android:id/ad_cta_button" text="Shop Now" />
</node>
"""
assert _detect_ad_structural(ad_xml) is True
def test_organic_post_not_flagged(self):
"""A normal post without ad markers is NOT flagged as adv."""
from GramAddict.core.bot_flow import _detect_ad_structural
organic_xml = """
<node class="android.widget.FrameLayout">
<node resource-id="com.instagram.android:id/row_feed_photo_imageview" />
<node resource-id="com.instagram.android:id/row_feed_button_like" />
<node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="super_azores" />
<node resource-id="com.instagram.android:id/secondary_label" text="Berlin, Germany" />
</node>
"""
assert _detect_ad_structural(organic_xml) is False, (
"Organic post with location secondary_label must NOT be marked as ad!"
)
def test_sponsored_secondary_label_detected(self):
"""An ad with a secondary_label containing 'Ad' should be flagged."""
from GramAddict.core.bot_flow import _detect_ad_structural
# Extracted directly from: manual_interrupt__2026-04-13_23-55-33.xml
ad_xml = """
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_profile_header" class="android.view.ViewGroup">
<node index="1" text="Ehuse Ferienhausvermietung" resource-id="com.instagram.android:id/row_feed_photo_profile_name" class="android.widget.Button" />
<node index="2" text="Ad" resource-id="com.instagram.android:id/secondary_label" class="android.widget.Button" package="com.instagram.android" content-desc="Ad" />
</node>
"""
assert _detect_ad_structural(ad_xml) is True, "Failed to detect an ad via the secondary_label 'Ad' badge!"
def test_organic_post_with_music_not_flagged(self):
"""A post with a music track attribution must NOT be flagged."""
from GramAddict.core.bot_flow import _detect_ad_structural
music_xml = """
<node class="android.widget.FrameLayout">
<node resource-id="com.instagram.android:id/clips_media_component" />
<node resource-id="com.instagram.android:id/secondary_label" text="Original Audio - Artist Name" />
<node resource-id="com.instagram.android:id/row_feed_button_like" />
</node>
"""
assert _detect_ad_structural(music_xml) is False, (
"Organic reel with music attribution must NOT be marked as ad!"
)
@patch('builtins.open', new_callable=MagicMock)
@patch('GramAddict.core.telepathic_engine.query_telepathic_llm')
@patch('os.path.exists')
def test_vlm_receives_semantically_relevant_nodes_first(self, mock_exists, mock_query, mock_open):
"""
If the target node is late in the XML hierarchy (e.g. node 40),
it MUST be passed to the VLM. The VLM should not be forced to
guess from the first 10 random XML nodes.
"""
mock_exists.return_value = False
engine = TelepathicEngine()
device = mock_device()
mock_open.return_value.__enter__.return_value.read.return_value = b'fakeimage'
# Create 15 garbage nodes
nodes = []
for i in range(15):
nodes.append(make_node(10, 10, "[0,0][20,20]", f"garbage node {i}"))
# Target node is at the end (index 15)
target_node = make_node(500, 500, "[400,400][600,600]", "description: 'Like', id context: 'row feed button like'")
nodes.append(target_node)
# We simulate that the embedding engine scores the target_node highest
with patch.object(engine, '_cosine_similarity', side_effect=lambda v1, v2: 1.0 if v1 == v2 else 0.0):
with patch.object(engine, '_get_cached_embedding', return_value=[0.1]*768) as mock_get_embed:
# Make target node have same embedding as intent
def fake_embed(text, is_intent=False):
if is_intent or "Like" in text: return [1.0]*768
return [0.0]*768
mock_get_embed.side_effect = fake_embed
# VLM should select index 0, because the target_node should be SORTED to the top!
mock_query.return_value = '{"index": 0, "reason": "Like button"}'
with patch.object(engine, '_extract_semantic_nodes', return_value=nodes):
# min_confidence=1.1 to force fallback
result = engine.find_best_node("<fake>", "tap like button", min_confidence=1.1, device=device)
assert result is not None, "VLM should have returned a result"
assert result["x"] == 500 and result["y"] == 500, "VLM did not receive the best semantic candidates!"

View File

@@ -0,0 +1,31 @@
import pytest
from GramAddict.core.telepathic_engine import TelepathicEngine
def test_keyword_fast_path_no_feed_pollution():
engine = TelepathicEngine()
# A generic Feed post that used to spoof the 'home' keyword due to 'row feed photo'
feed_post_node = {
"x": 100, "y": 200, "area": 500,
"semantic_string": "description: 'Profile picture of ericrubens', id context: 'row feed photo profile imageview'",
"resource_id": "row_feed_photo_profile_imageview",
"original_attribs": {"desc": "Profile picture of ericrubens", "text": ""}
}
# The actual Home Tab button
home_tab_node = {
"x": 100, "y": 2300, "area": 300,
"semantic_string": "description: 'Home', id context: 'tab bar'",
"resource_id": "tab_avatar",
"original_attribs": {"desc": "Home", "text": ""}
}
nodes = [feed_post_node, home_tab_node]
# Intention is to tap the home tab
result = engine._keyword_match_score("tap home tab", nodes)
assert result is not None
# Verify it matched the actual home tab and NOT the feed post
assert result["semantic"] == "description: 'Home', id context: 'tab bar'"

View File

@@ -0,0 +1,80 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.unfollow_engine import _run_zero_latency_unfollow_loop
@pytest.fixture
def unfollow_mock_dependencies():
device = MagicMock()
zero_engine = MagicMock()
nav_graph = MagicMock()
class ConfigArgs:
total_unfollows_limit = 5
configs = MagicMock()
configs.args = ConfigArgs()
session_state = MagicMock()
session_state.check_limit.return_value = (False, False, False, False)
session_state.totalUnfollowed = 0
telepathic = MagicMock()
dopamine = MagicMock()
dopamine.is_app_session_over.side_effect = [False, False, True]
dopamine.wants_to_change_feed.return_value = False
dopamine.boredom = 0.0
cognitive_stack = {
"telepathic": telepathic,
"dopamine": dopamine,
}
return device, zero_engine, nav_graph, configs, session_state, cognitive_stack
def test_unfollow_engine_basic_loop(unfollow_mock_dependencies):
device, zero_engine, nav_graph, configs, session_state, cognitive_stack = unfollow_mock_dependencies
telepathic = cognitive_stack["telepathic"]
# First node gives a "Following" button
telepathic._extract_semantic_nodes.side_effect = [
[{"x": 100, "y": 200, "skip": False, "bounds": "..."}], # following button
[{"x": 150, "y": 250, "skip": False}], # confirm dialog
[], # second iteration
[]
]
with patch("GramAddict.core.unfollow_engine._humanized_scroll_down") as mock_scroll, \
patch("GramAddict.core.bot_flow.sleep"), \
patch("GramAddict.core.bot_flow._humanized_click") as mock_click:
res = _run_zero_latency_unfollow_loop(device, zero_engine, nav_graph, configs, session_state, "FollowingList", cognitive_stack)
assert mock_click.call_count == 2 # Clicked following THEN clicked confirm
assert session_state.totalUnfollowed == 1
assert res == "SESSION_OVER"
def test_unfollow_engine_chaos_mode(unfollow_mock_dependencies):
device, zero_engine, nav_graph, configs, session_state, cognitive_stack = unfollow_mock_dependencies
telepathic = cognitive_stack["telepathic"]
# Simulate Telepathic failure / missing nodes
telepathic._extract_semantic_nodes.side_effect = Exception("UI DUMP CORRUPTED")
with patch("GramAddict.core.unfollow_engine._humanized_scroll_down") as mock_scroll, \
patch("GramAddict.core.bot_flow.sleep"), \
patch("GramAddict.core.bot_flow._humanized_click") as mock_click:
res = _run_zero_latency_unfollow_loop(device, zero_engine, nav_graph, configs, session_state, "FollowingList", cognitive_stack)
# It should catch the exception, scroll down, and increment failed scans until it realizes context is lost
assert mock_scroll.call_count > 0
assert res == "CONTEXT_LOST" or res == "SESSION_OVER"
def test_unfollow_engine_limits(unfollow_mock_dependencies):
device, zero_engine, nav_graph, configs, session_state, cognitive_stack = unfollow_mock_dependencies
session_state.check_limit.return_value = (True, False, False, False)
res = _run_zero_latency_unfollow_loop(device, zero_engine, nav_graph, configs, session_state, "FollowingList", cognitive_stack)
assert res == "BOREDOM_CHANGE_FEED"

5
tests/txt/txt_empty.txt Normal file
View File

@@ -0,0 +1,5 @@

6
tests/txt/txt_ok.txt Normal file
View File

@@ -0,0 +1,6 @@
Hello, test_user! How are you today?
Hello everyone!
Goodbye, test_user! Have a great day!

View File

@@ -0,0 +1,182 @@
import pytest
from unittest.mock import patch
from GramAddict.core.bot_flow import _interact_with_profile, _interact_with_carousel
from tests.conftest import MockArgs, MockConfigs
@pytest.fixture(autouse=True)
def silent_sleep(monkeypatch):
import GramAddict.core.bot_flow
monkeypatch.setattr(GramAddict.core.bot_flow, "sleep", lambda x: None)
@patch("random.random")
def test_interact_with_profile_all_100_percent(mock_random, device, telepathic_mock, mock_logger):
# Guaranteed to pass checks
mock_random.return_value = 0.0
args = MockArgs(
stories_percentage=100,
stories_count="3-3",
follow_percentage=100,
likes_percentage=100,
likes_count="2-2"
)
configs = MockConfigs(args)
from GramAddict.core.session_state import SessionState
session_state = SessionState(configs)
session_state.set_limits_session()
_interact_with_profile(device, configs, "testuser", session_state, 0.0, mock_logger)
# 1 target story click + 2 right-side skip clicks + 1 follow + 1 grid open + 2 post likes (double taps) + 2 scrolls
# 3 story (3 shell commands)
# 1 follow (1 shell command)
# 1 grid tap (1 shell config)
# 2 likes (Double tap = 2 shell commands each = 4 total)
# 2 scrolls (2 shell commands)
# Total shells expected: 3 + 1 + 1 + 4 + 2 = 11
# Check total shells
assert len(device.deviceV2.shells) == 11
# We no longer check explicit clicks/double_clicks array because we humanized them into shell commands.
for cmd in device.deviceV2.shells:
assert "input swipe" in cmd
@patch("random.random")
def test_interact_with_profile_zero_percent(mock_random, device, telepathic_mock, mock_logger):
# Guaranteed to fail chance logic
mock_random.return_value = 0.99
args = MockArgs(
stories_percentage=0,
follow_percentage=0,
likes_percentage=0
)
configs = MockConfigs(args)
from GramAddict.core.session_state import SessionState
session_state = SessionState(configs)
session_state.set_limits_session()
_interact_with_profile(device, configs, "testuser", session_state, 0.0, mock_logger)
assert len(device.deviceV2.shells) == 0
@patch("random.random")
def test_interact_with_profile_mixed_probability(mock_random, device, telepathic_mock, mock_logger):
# This simulates passing the Follow and Like percentage, but failing the Story percentage.
# It ensures there are no UnboundLocalErrors when certain blocks are skipped.
def mock_random_side_effect():
# Let's say random.random() returns a predictable sequence or just use a generator:
# 1st call: story probability (fail, e.g. 0.99 < 0.0)
# 2nd call: follow probability (pass, e.g. 0.0 < 1.0)
# 3rd call: likes probability (pass, e.g. 0.0 < 1.0)
return 0.5
mock_random.return_value = 0.5
args = MockArgs(
stories_percentage=0, # Fails (0.5 < 0)
follow_percentage=100, # Passes (0.5 < 1)
likes_percentage=100, # Passes (0.5 < 1)
likes_count="1-1"
)
configs = MockConfigs(args)
from GramAddict.core.session_state import SessionState
session_state = SessionState(configs)
session_state.set_limits_session()
# Should not throw any exception
_interact_with_profile(device, configs, "testuser", session_state, 0.0, mock_logger)
# 0 stories, 1 follow, 1 like block (1 grid open + 2 double tap shells + 1 scroll)
# total shells = 1 (follow) + 1 (grid click) + 2 (1 double tap) + 1 (scroll) = 5
assert len(device.deviceV2.shells) == 5
@patch("random.random")
def test_carousel_100_percent(mock_random, device, mock_logger):
mock_random.return_value = 0.0
args = MockArgs(
carousel_percentage=100,
carousel_count="4-4"
)
configs = MockConfigs(args)
_interact_with_carousel(device, configs, 0.0, mock_logger)
assert len(device.deviceV2.shells) == 4
for cmd in device.deviceV2.shells:
assert "swipe" in cmd
@patch("random.random")
def test_carousel_zero_percent(mock_random, device, mock_logger):
mock_random.return_value = 0.99
args = MockArgs(
carousel_percentage=0,
carousel_count="4-4"
)
configs = MockConfigs(args)
_interact_with_carousel(device, configs, 0.0, mock_logger)
assert len(device.deviceV2.shells) == 0
@patch("random.random")
def test_interact_with_profile_follow_limit_enforcement(mock_random, device, telepathic_mock, mock_logger):
# Guaranteed 100% probability
mock_random.return_value = 0.0
args = MockArgs(
follow_percentage=100,
total_follows_limit=0, # Set hard limit to 0
stories_percentage=0,
likes_percentage=0
)
configs = MockConfigs(args)
from GramAddict.core.session_state import SessionState
# Mocking that 1 follow was already made to exceed the 0 limit
session_state = SessionState(configs)
session_state.set_limits_session()
session_state.totalFollowed["targetuser"] = 1
_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
@patch("random.random")
def test_interact_with_profile_likes_limit_enforcement(mock_random, device, telepathic_mock, mock_logger):
# Guaranteed 100% probability
mock_random.return_value = 0.0
args = MockArgs(
likes_percentage=100,
likes_count="1-1",
total_likes_limit=2,
stories_percentage=0,
follow_percentage=0
)
configs = MockConfigs(args)
from GramAddict.core.session_state import SessionState
session_state = SessionState(configs)
session_state.set_limits_session()
# Faking exhaustion of total likes limit
session_state.totalLikes = 3
_interact_with_profile(device, configs, "targetuser", session_state, 0.0, mock_logger)
# Limit restricts likes block.
assert len(device.deviceV2.shells) == 0
# NOTE: Repost is deeply integrated into `bot_flow._run_zero_latency_feed_loop`. We can't mock the
# entire NavGraph loop here easily because it requires massive setup, but we verified the probability
# syntax by applying the same logic as Carousel/Profile interaction.
#
# Therefore we verify it via the manual `run.py` validation.
# However, this test suite guarantees the atomic config mapping syntax is correct.

View File

@@ -0,0 +1,31 @@
import pytest
import argparse
from unittest.mock import MagicMock, patch
from GramAddict.core.resonance_engine import ResonanceEngine
def test_config_persona_mapping():
"""Verify that bot_flow.py correctly extracts persona from config arguments."""
from GramAddict.core.config import Config
mock_args = argparse.Namespace()
mock_args.ai_target_audience = "travel, photography, coffee"
configs = MagicMock()
configs.args = mock_args
configs.username = "test_bot"
# Simulating bot_flow.py lines 61-62
persona_raw = getattr(configs.args, "ai_target_audience", getattr(configs.args, "persona_interests", ""))
persona_interests = [p.strip() for p in persona_raw.split(",") if p.strip()] if persona_raw else []
assert "coffee" in persona_interests
assert len(persona_interests) == 3
# Ensure ResonanceEngine accepts it without type tracebacks
with patch('GramAddict.core.resonance_engine.ContentMemoryDB'), \
patch('GramAddict.core.resonance_engine.ParasocialCRMDB'), \
patch('GramAddict.core.resonance_engine.PersonaMemoryDB'):
engine = ResonanceEngine(configs.username, persona_interests=persona_interests)
assert engine._persona_interests == ["travel", "photography", "coffee"]

View File

@@ -0,0 +1,67 @@
import pytest
from unittest.mock import patch, MagicMock
from GramAddict.core.llm_provider import get_model_pricing, query_llm
import GramAddict.core.llm_provider
def test_get_model_pricing_success():
# Reset cache
GramAddict.core.llm_provider._MODEL_PRICING_CACHE = None
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": [
{"id": "google/gemini-3.1-flash-lite-preview", "pricing": {"prompt": "0.00000025", "completion": "0.0000015"}},
{"id": "qwen3.5-32b", "pricing": {"prompt": "0.0000001", "completion": "0.0000002"}}
]
}
with patch("GramAddict.core.llm_provider.requests.get", return_value=mock_response):
pricing = get_model_pricing("google/gemini-3.1-flash-lite-preview")
assert pricing["prompt"] == "0.00000025"
assert pricing["completion"] == "0.0000015"
# Test caching
pricing2 = get_model_pricing("google/gemini-3.1-flash-lite-preview")
# Should NOT make another request
assert mock_response.json.call_count == 1
def test_get_model_pricing_partial_match():
# Reset cache
GramAddict.core.llm_provider._MODEL_PRICING_CACHE = {"google/gemini-pro": {"prompt": "0.1", "completion": "0.2"}}
# Should match via substring
pricing = get_model_pricing("gemini-pro")
assert pricing["prompt"] == "0.1"
def test_get_model_pricing_failure():
GramAddict.core.llm_provider._MODEL_PRICING_CACHE = None
with patch("GramAddict.core.llm_provider.requests.get", side_effect=Exception("Network error")):
pricing = get_model_pricing("some-model")
assert pricing == {}
def test_query_llm_cost_calculation():
# Set cache directly
GramAddict.core.llm_provider._MODEL_PRICING_CACHE = {
"test-model": {"prompt": "1.0", "completion": "2.0"}
}
mock_post_response = MagicMock()
mock_post_response.status_code = 200
mock_post_response.json.return_value = {
"choices": [{"message": {"content": "response text"}}],
"usage": {"prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15}
}
with patch("GramAddict.core.llm_provider.requests.post", return_value=mock_post_response) as mock_post:
with patch("GramAddict.core.llm_provider.logger.info") as mock_logger:
with patch.dict("os.environ", {"OPENROUTER_API_KEY": "test_key"}):
res = query_llm("https://openrouter.ai/api/v1/chat/completions", "test-model", "hello", format_json=False)
assert res["response"] == "response text"
# Check that logging included the calculated cost
# prompt = 5 * 1.0 = 5.0
# completion = 10 * 2.0 = 20.0
# total = 25.0
mock_logger.assert_called_with("🪙 [LLM Burn] test-model -> In: 5 | Out: 10 | Total: 15 | 💸 Cost: $25.000000", extra={"color": "\x1b[38;5;208m\x1b[1m"})

View File

@@ -0,0 +1,43 @@
import pytest
from unittest.mock import patch, MagicMock
# Attempt to load the module
from GramAddict.core.qdrant_memory import UIMemoryDB
class DummyMemory(UIMemoryDB):
def __init__(self):
# Prevent actual QdrantClient initialization for offline tests
self.client = None
self.collection_name = "test_collection"
def test_decay_confidence_signature():
"""Ensure decay_confidence doesn't crash from legacy plugins passing xml_context."""
mem = DummyMemory()
# Mock _adjust_confidence to just capture arguments
mem._adjust_confidence = MagicMock()
# Legacy caller pattern A (positional intent and amount)
mem.decay_confidence("my_intent", 0.40)
# Wait, passing positional "my_intent", 0.40 makes `args[0] = "my_intent"`, `args[1] = 0.40`.
# kwargs.get("amount") is 0.25 (default). But if passed positionally, args[1] was xml_context
# in legacy! Let's ensure it doesn't crash.
# Legacy caller pattern B (explicit kwargs)
mem.decay_confidence(intent="my_intent", amount=0.40)
mem._adjust_confidence.assert_called_with("my_intent", -0.40)
# Legacy caller pattern C (positional with string where amount should be)
mem.decay_confidence("my_intent", "xml_context_string")
mem._adjust_confidence.assert_called_with("my_intent", -0.50)
# Legacy caller pattern D (kwargs with xml_context)
mem.decay_confidence(intent="my_intent", xml_context="<node/>", amount=0.30)
mem._adjust_confidence.assert_called_with("my_intent", -0.30)
def test_boost_confidence_signature():
mem = DummyMemory()
mem._adjust_confidence = MagicMock()
mem.boost_confidence("intent", "xml_string", 0.5)
mem._adjust_confidence.assert_called_with("intent", 0.15) # Because "xml_string" fails float conversion, defaults to 0.15

View File

View File

@@ -0,0 +1,23 @@
def test_global_session_limit_evaluation(mock_logger):
from GramAddict.core.session_state import SessionState
from tests.conftest import MockArgs, MockConfigs
args = MockArgs(
total_likes_limit=100,
total_follows_limit=100,
total_interactions_limit=1000
)
configs = MockConfigs(args)
session_state = SessionState(configs)
session_state.set_limits_session()
# Simulate a fresh session - Limit should NOT be reached
limit_tuple_clean = session_state.check_limit(SessionState.Limit.ALL)
assert not any(limit_tuple_clean), "Fresh session should not evaluate to true for limits"
# Exhaust global limit
for _ in range(1001):
session_state.add_interaction("Feed", succeed=True, followed=False, scraped=False)
limit_tuple_exhausted = session_state.check_limit(SessionState.Limit.ALL)
assert any(limit_tuple_exhausted), "Exhausted session MUST evaluate to true for limits"