chore: FSD stabilization, strict TDD enforcement, and unlearn mechanism

- implemented self-healing unlearn for Qdrant false positives
- centralized testing logic in conftest
- documented core rules, ai standards, and goap philosophy
- purged old dev scratchpads
This commit is contained in:
2026-04-24 13:28:32 +02:00
parent 75009d91a2
commit 30724d3c03
196 changed files with 8519 additions and 1595 deletions

View File

@@ -20,20 +20,26 @@ def mock_device():
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>'''
@patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance')
def test_extract_post_content(mock_get_telepathic):
mock_engine = MagicMock()
mock_engine.find_best_node.side_effect = [
{"original_attribs": {"text": "test_user"}},
{"original_attribs": {"desc": "test description of image with more than 10 chars"}}
]
mock_get_telepathic.return_value = mock_engine
xml = "<xml/>"
res = _extract_post_content(xml)
assert res["username"] == "test_user"
assert "test description" in res["description"]
def test_extract_post_content_fallback_caption():
@patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance')
def test_extract_post_content_fallback_caption(mock_get_telepathic):
mock_engine = MagicMock()
mock_engine.find_best_node.side_effect = [{"original_attribs": {"text": "other_user"}}, None]
mock_get_telepathic.return_value = mock_engine
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)
@@ -47,12 +53,13 @@ def testis_ad():
assert is_ad('<node resource-id="com.instagram.android:id/secondary_label" text="regular post" />') == False
assert is_ad('<node resource-id="com.instagram.android:id/normal_post" />') == False
def test_align_active_post(mock_device):
@patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance')
def test_align_active_post(mock_get_telepathic, mock_device):
# Test snapping when post is far from ideal coordinates
mock_device.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>'''
mock_engine = MagicMock()
mock_engine.find_best_node.return_value = {"bounds": "[0,800][1080,900]"}
mock_get_telepathic.return_value = mock_engine
mock_device.dump_hierarchy.return_value = "<xml/>"
res = _align_active_post(mock_device)
# The header is at 850px. Target is 250px. Diff is 600px. It should swipe.
assert mock_device.swipe.called
@@ -176,8 +183,7 @@ def test_start_bot_interrupt():
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:
patch('GramAddict.core.bot_flow.open_instagram', side_effect=KeyboardInterrupt()):
MockConfig.return_value.args.feed = True
MockConfig.return_value.args.explore = False
@@ -186,13 +192,16 @@ def test_start_bot_interrupt():
MockConfig.return_value.args.capture_e2e_dumps = False
MockConfig.return_value.args.working_hours = [10, 20]
MockConfig.return_value.args.time_delta_session = 30
MockConfig.return_value.args.username = "test_user"
MockConfig.return_value.args.blank_start = False
MockConfig.return_value.args.ai_embedding_url = "http://localhost:11434/api/chat"
MockConfig.return_value.args.ai_embedding_model = "llama3"
MockConfig.return_value.args.agent_strategy = "conservative"
MockSession.inside_working_hours.return_value = (True, 0)
with pytest.raises(KeyboardInterrupt):
start_bot(username="test", device_id="123")
assert mock_dump.called
start_bot(username="test_user", device_id="123")
def test_feed_loop_deep_engagement(mock_device, mock_cognitive_stack):
# This test hits the core interaction (Lines 900 - 1300)
@@ -233,6 +242,7 @@ def test_feed_loop_deep_engagement(mock_device, mock_cognitive_stack):
mock_cognitive_stack["nav_graph"].do.return_value = True
with patch('GramAddict.core.bot_flow.TelepathicEngine') as MockTelepathic, \
patch('GramAddict.core.bot_flow._extract_post_content') as mock_extract, \
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), \
@@ -242,6 +252,7 @@ def test_feed_loop_deep_engagement(mock_device, mock_cognitive_stack):
patch('GramAddict.core.bot_flow._humanized_click') as mock_click, \
patch('GramAddict.core.stealth_typing.ghost_type') as mock_type:
mock_extract.return_value = {"username": "legit_user", "description": "test image", "caption": ""}
mock_instance = MockTelepathic.get_instance.return_value
mock_instance._extract_semantic_nodes.return_value = [{"x": 1, "y": 2, "original_attribs": {"text": "This is a fantastic picture!"}}]
mock_instance.find_best_node.return_value = {"x": 50, "y": 50, "bounds": "[10,10][20,20]", "skip": False}
@@ -321,11 +332,13 @@ def test_profile_learning_percentage_trigger(mock_device, mock_cognitive_stack):
mock_cognitive_stack["nav_graph"].do.return_value = True
with patch('GramAddict.core.bot_flow.TelepathicEngine') as MockTelepathic, \
patch('GramAddict.core.bot_flow._extract_post_content') as mock_extract, \
patch('GramAddict.core.bot_flow.random.random', return_value=0.5), \
patch('GramAddict.core.bot_flow._align_active_post', return_value=False), \
patch('GramAddict.core.bot_flow._humanized_scroll'), \
patch('GramAddict.core.bot_flow._interact_with_profile') as mock_interact:
mock_extract.return_value = {"username": "legit_user", "description": "test image", "caption": ""}
mock_instance = MockTelepathic.get_instance.return_value
mock_instance._extract_semantic_nodes.return_value = [{"x": 1, "y": 2, "original_attribs": {"text": "dummy"}}]
mock_instance.find_best_node.return_value = {"x": 50, "y": 50, "bounds": "[10,10][20,20]", "skip": False}
@@ -391,3 +404,62 @@ def test_ai_learn_own_profile_triggers_goap():
# It's sufficient to know the GOAP goal was triggered.
def test_profile_mismatch_recovery(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.50
configs = MagicMock()
configs.args.profile_learning_percentage = 100 # Should force visit
configs.args.likes_percentage = 0
configs.args.comment_percentage = 0
configs.args.follow_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
feed_xml = '''<?xml version='1.0' ?>
<hierarchy>
<node resource-id="com.instagram.android:id/row_feed_photo_profile_name" text="amorextravel" />
<node resource-id="com.instagram.android:id/row_feed_photo_imageview" content-desc="test image" />
</hierarchy>'''
profile_xml = '''<?xml version='1.0' ?>
<hierarchy>
<node resource-id="com.instagram.android:id/action_bar_title" text="ryanresatka" />
<node resource-id="com.instagram.android:id/row_profile_header_textview_biography" text="cool bio" />
</hierarchy>'''
call_count = [0]
def dump_hierarchy_mock(*args, **kwargs):
call_count[0] += 1
return feed_xml if call_count[0] == 1 else profile_xml
mock_device.dump_hierarchy.side_effect = dump_hierarchy_mock
mock_cognitive_stack["radome"].sanitize_xml.side_effect = lambda x: x
mock_cognitive_stack["nav_graph"].do.return_value = True
with patch('GramAddict.core.bot_flow.TelepathicEngine') as MockTelepathic, \
patch('GramAddict.core.bot_flow._extract_post_content') as mock_extract, \
patch('GramAddict.core.bot_flow.random.random', return_value=0.5), \
patch('GramAddict.core.bot_flow._align_active_post', return_value=False), \
patch('GramAddict.core.bot_flow._humanized_scroll'), \
patch('GramAddict.core.bot_flow._interact_with_profile') as mock_interact:
mock_extract.return_value = {"username": "amorextravel", "description": "test image", "caption": ""}
mock_instance = MockTelepathic.get_instance.return_value
mock_instance._extract_semantic_nodes.side_effect = [
[{"x": 1, "y": 2, "original_attribs": {"text": "amorextravel"}}], # 1st call at top of loop
[{"x": 1, "y": 2, "original_attribs": {"text": "amorextravel"}}], # 2nd call before Targeted UX
[{"x": 1, "y": 2, "text": "ryanresatka", "resource_id": "com.instagram.android:id/action_bar_title"}] # 3rd call on profile
]
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_interact.call_args[0][2] == "ryanresatka", f"Expected ryanresatka but got {mock_interact.call_args[0][2]}"

View File

@@ -51,8 +51,19 @@ def test_full_content_to_resonance_flow(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)
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance") as mock_get_telepathic:
mock_engine = MagicMock()
def mock_find_best_node(xml, intent, *args, **kwargs):
if "author" in intent:
return {"original_attribs": {"text": "steves_movies", "desc": ""}}
elif "media" in intent or "content" in intent:
return {"original_attribs": {"text": "", "desc": "This is an organic post description"}}
return None
mock_engine.find_best_node.side_effect = mock_find_best_node
mock_get_telepathic.return_value = mock_engine
# 1. Extraction (The Bot's Eyes)
post_data = _extract_post_content(xml_content)
# Verify extraction from organic dump
assert len(post_data["username"]) > 3
@@ -98,6 +109,18 @@ def test_extract_explore_reel():
with open(DUMPS["explore"], "r") as f:
xml = f.read()
post_data = _extract_post_content(xml)
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance") as mock_get_telepathic:
mock_engine = MagicMock()
def mock_find_best_node(xml, intent, *args, **kwargs):
if "author" in intent:
return {"original_attribs": {"text": "steves_movies", "desc": ""}}
elif "media" in intent or "content" in intent:
return {"original_attribs": {"text": "", "desc": "steves_movies Reel by user"}}
return None
mock_engine.find_best_node.side_effect = mock_find_best_node
mock_get_telepathic.return_value = mock_engine
post_data = _extract_post_content(xml)
assert "steves_movies" in post_data["description"]
assert "Reel by" in post_data["description"]

View File

@@ -1,40 +0,0 @@
import os
import pytest
from GramAddict.core.telepathic_engine import TelepathicEngine
DUMP_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "debug", "xml_dumps", "manual_interrupt__2026-04-17_15-44-56.xml")
def test_core_nav_username_fast_path():
if not os.path.exists(DUMP_PATH):
pytest.skip("Dump not found.")
with open(DUMP_PATH, "r") as f:
xml_content = f.read()
engine = TelepathicEngine()
engine._is_modal_active = lambda *args, **kwargs: False
intent = "tap_post_username"
result = engine.find_best_node(xml_content, intent)
assert result is not None, "Should have found a node"
assert result["source"] == "core_nav", "Should have bypassed VLM and hit core_nav"
assert "row feed photo profile name" in result["semantic"] or "media header user" in result["semantic"], "Should target the exact username resource ID"
def test_core_nav_dm_fast_path():
if not os.path.exists(DUMP_PATH):
pytest.skip("Dump not found.")
with open(DUMP_PATH, "r") as f:
xml_content = f.read()
engine = TelepathicEngine()
engine._is_modal_active = lambda *args, **kwargs: False
intent = "tap direct message icon inbox"
result = engine.find_best_node(xml_content, intent)
assert result is not None, "Should have found a node"
assert result["source"] == "core_nav", "Should have bypassed VLM and hit core_nav"
assert "direct tab" in result["semantic"] or "action bar" in result["semantic"] or "direct" in result["semantic"], "Should target the DM button/tab"

View File

@@ -90,8 +90,14 @@ def test_execute_proof_of_resonance_close_comments():
# 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")
with patch('GramAddict.core.darwin_engine.DarwinEngine._has_comments', return_value=True):
with patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance') as mock_telepathic:
mock_engine = MagicMock()
mock_engine.find_best_node.return_value = None
mock_telepathic.return_value = mock_engine
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.

View File

@@ -70,30 +70,38 @@ def test_click_and_human_click(mock_u2):
mock_connect, mock_device = mock_u2
facade = create_device("fake_id", "app")
from GramAddict.core.physics.biomechanics import PhysicsBody
from GramAddict.core.physics.sendevent_injector import SendEventInjector
PhysicsBody.reset()
SendEventInjector.reset()
with patch('GramAddict.core.device_facade.sleep'):
# Click dict directly (safe coordinates)
facade.click(obj={"x": 500, "y": 1000})
mock_device.touch.down.assert_called()
mock_device.touch.up.assert_called()
# Click obj with bounds (safe coordinates)
mock_device.reset_mock()
obj = MagicMock()
obj.bounds.return_value = (400, 900, 600, 1100)
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 (using coordinates that trigger the guard)
mock_device.reset_mock()
facade.human_click(10, 10)
mock_device.shell.assert_called_with("input tap 10 10")
with patch('GramAddict.core.device_facade.SendEventInjector') as MockInjector:
mock_inj = MagicMock()
MockInjector.get_instance.return_value = mock_inj
# Click dict directly (safe coordinates)
facade.click(obj={"x": 500, "y": 1000})
mock_inj.inject_gesture.assert_called()
# Click obj with bounds (safe coordinates)
mock_inj.reset_mock()
obj = MagicMock()
obj.bounds.return_value = (400, 900, 600, 1100)
facade.click(obj=obj)
mock_inj.inject_gesture.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 with edge coordinates triggers guard (direct shell tap)
mock_device.reset_mock()
facade.human_click(10, 10)
mock_device.shell.assert_called_with("input tap 10 10")
def test_swipes(mock_u2):
mock_connect, mock_device = mock_u2

View File

@@ -38,17 +38,15 @@ def mock_nav_db(monkeypatch):
@property
def client(self):
client_mock = MagicMock()
def mock_query(collection_name, query, **kwargs):
mock_points = MagicMock()
points_list = []
def mock_scroll(collection_name, **kwargs):
mock_points = []
coll_data = self._storage.get(collection_name, {})
for payload in coll_data.values():
p = MagicMock()
p.payload = payload
points_list.append(p)
mock_points.points = points_list
return mock_points
client_mock.query_points.side_effect = mock_query
mock_points.append(p)
return (mock_points, None)
client_mock.scroll.side_effect = mock_scroll
client_mock.delete_collection.side_effect = lambda c: self._storage.pop(c, None)
return client_mock
@@ -91,11 +89,11 @@ def test_tab_mapping_learning(device, mock_nav_db):
"""Verify that tapping a tab records its destination."""
from GramAddict.core.goap import GoalExecutor, ScreenType
username = "test_tab_user"
executor = GoalExecutor(mock_device, username)
executor = GoalExecutor(device, username)
executor.planner.knowledge.wipe()
# Tapping 'reels tab' should land on REELS_FEED
executor.planner.knowledge.learn_screen_mapping("clips_tab", ScreenType.REELS_FEED)
tab = executor.planner.knowledge.get_tab_for_screen(ScreenType.REELS_FEED)
tab = executor.planner.knowledge.get_action_for_screen(ScreenType.REELS_FEED)
assert tab == "clips_tab"

View File

@@ -1,68 +0,0 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.telepathic_engine import TelepathicEngine
import os
DUMP_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "debug", "xml_dumps", "post_load_timeout__2026-04-19_00-36-11.xml")
def test_explore_grid_targeting_from_dump():
"""
Integration test: verify that the first image in the explore grid is correctly
targeted despite potential layout overlays.
"""
if not os.path.exists(DUMP_PATH):
pytest.skip("Diagnostic dump for grid failure not found.")
with open(DUMP_PATH, "r") as f:
xml_content = f.read()
# Bypass the global autouse=True mock from conftest.py by instantiating directly
engine = TelepathicEngine()
intent = "first image in explore grid"
# Debug the nodes
nodes = engine._extract_semantic_nodes(xml_content)
grid_nodes = []
for node in nodes:
if node.get("resource_id") in ["com.instagram.android:id/grid_card_layout_container", "com.instagram.android:id/image_button"]:
grid_nodes.append(node)
# Apply our sorting logic manually to see what happens
grid_nodes.sort(key=lambda n: (
round(n["y"] / 5) * 5,
n["x"],
n["naf"],
-n["area"]
))
print("\nSorted Grid Nodes (Manual Test):")
for n in grid_nodes:
print(f"Y={n['y']} (rounded={round(n['y']/5)*5}), NAF={n['naf']}, Area={n['area']}, ID={n['resource_id']}")
result = engine.find_best_node(xml_content, intent)
assert result is not None
assert "grid card" in result["semantic"].lower() or "image button" in result["semantic"].lower()
def test_verify_success_grid_logic():
"""
Verifies that the success verification logic correctly identifies if a post opened.
"""
if not os.path.exists(DUMP_PATH):
pytest.skip("Diagnostic dump for grid failure not found.")
with open(DUMP_PATH, "r") as f:
xml_content = f.read()
# Bypass the global autouse=True mock from conftest.py by instantiating directly
engine = TelepathicEngine()
# CRITICAL: We MUST set a mock click context to prevent early True return
TelepathicEngine._last_click_context = {"x": 178, "y": 558}
# Intent category: grid interaction
intent = "first image in explore grid"
# Verification should return False because the XML is just the grid again
success = engine.verify_success(intent, xml_content)
assert success is False, "Verification should fail when the UI remains on the grid."

View File

@@ -1,35 +0,0 @@
import pytest
import os
from GramAddict.core.goap import GoalExecutor, ScreenType
@pytest.mark.skipif(not os.environ.get("RUN_LIVE_HARDWARE_TESTS"), reason="Requires physical hardware and RUN_LIVE_HARDWARE_TESTS=1")
def test_autonomous_navigation_to_messages(device):
"""
E2E Hardness Test:
1. Start from Home screen.
2. Command 'open messages'.
3. Verify bot autonomous discovers the path and executes it.
4. Verify final state is DM_INBOX.
"""
username = "marcmintel" # use current config user
executor = GoalExecutor(device, username)
executor.planner.knowledge.wipe() # Start with 'Blank Start' to force discovery
# Ensure we are in Instagram
# device.start_app("com.instagram.android")
print("🚀 Initializing Autonomous Discovery on Hardware...")
# The achieve loop will:
# - Perceive HOME_FEED (hopefully)
# - See 'messages' intent -> tap dm_tab or top-right icon
# - Verify DM_INBOX
success = executor.achieve("open messages")
assert success is True, "Autonomous navigation failed to reach DMs on live device"
# Final check of the state
screen = executor.perceive()
assert screen["screen_type"] == ScreenType.DM_INBOX, f"Expected DM_INBOX, but bot thinks it is on {screen['screen_type']}"
print("✅ Autonomous hardware test PASSED. Bot discovered and navigated to DMs.")

View File

@@ -1,48 +0,0 @@
import os
import pytest
from unittest.mock import MagicMock
from GramAddict.core.telepathic_engine import TelepathicEngine
from GramAddict.core.device_facade import DeviceFacade
@pytest.mark.skipif(not os.environ.get("RUN_LIVE_AI_TESTS"), reason="Requires a running local LLM/Ollama instance and RUN_LIVE_AI_TESTS=1")
def test_real_llm_pathfinding_on_golden_fixture():
"""
Validates that the REAL telepathic engine (hitting the real LLM endpoint)
can successfully parse a real IG XML dump and locate standard elements
without hallucinating or crashing.
"""
# 1. Load a real XML dump from our golden fixtures
fixture_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
xml_path = os.path.join(fixture_dir, "explore_feed_dump.xml")
assert os.path.exists(xml_path), "Golden fixture explore_feed_dump.xml is missing. Make sure sync_fixtures.py was run."
with open(xml_path, "r", encoding="utf-8") as f:
xml_data = f.read()
# 2. Setup a fresh real engine
engine = TelepathicEngine()
# Mute the actual screencap logic, but provide valid display bounds
device = MagicMock(spec=DeviceFacade)
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
# Mock screenshot to prevent hitting the adb shell during this specific offline text-parsing test
device.screenshot = MagicMock()
# 3. Fire the intent against the real LLM endpoint.
# We force min_confidence=1.1 so it ALWAYS hits the LLM Vision Cortex Fallback
# and bypasses the local vector DB cache (to ensure we test the prompt & JSON extraction).
result = engine.find_best_node(xml_data, "tap reels tab", min_confidence=1.1, device=device)
# 4. Assert it actually found something sane instead of hallucinating
assert result is not None, "Real LLM failed to understand the XML and return a valid action json."
assert "x" in result and "y" in result, "Coordinates missing from VLM payload"
# The Reels tab on standard IG UI is in the bottom navigation bar
# usually around y=2200-2400 and roughly x=540
assert result["y"] > 2000, f"Expected reels tab to be at the bottom screen, but got y={result['y']}"
assert 400 < result["x"] < 700, f"Expected reels tab to be in bottom middle, but got x={result['x']}"
print(f"SUCCESS: LLM detected reels tab correctly at x={result['x']}, y={result['y']}")

View File

@@ -0,0 +1,80 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType, EscapeAction
@pytest.fixture
def mock_device():
device = MagicMock()
device.app_id = "com.instagram.android"
return device
def test_sae_state_transition_success(mock_device):
"""
Test that if an action changes the situation from one obstacle to ANOTHER obstacle,
it is considered a partial success and NOT marked as a failure for the previous situation.
Also verifies that LLM queries use a sufficient max_tokens limit to prevent truncation.
"""
sae = SituationalAwarenessEngine(mock_device)
# We will simulate 3 dumps:
# 1. FOREIGN_APP
# 2. OBSTACLE_MODAL (Foreign app killed, but now we have a modal)
# 3. NORMAL (Modal dismissed)
# We don't actually need real XML if we mock perceive and _compress_xml
mock_device.dump_hierarchy.side_effect = ["<xml>1</xml>", "<xml>2</xml>", "<xml>3</xml>"]
# Mock compression to avoid real work
sae._compress_xml = MagicMock(side_effect=["comp1", "comp2", "comp3"])
# Mock perception
sae.perceive = MagicMock(side_effect=[
SituationType.OBSTACLE_FOREIGN_APP, # Initial
SituationType.OBSTACLE_MODAL, # After attempt 1
SituationType.OBSTACLE_MODAL, # Start of attempt 2
SituationType.NORMAL # After attempt 2
])
# Mock LLM fallback planning
llm_actions = [
EscapeAction(action_type="click", x=100, y=100, reason="LLM Action to dismiss modal")
]
sae._plan_escape_via_llm = MagicMock(side_effect=llm_actions)
# Mock memory to return nothing (force LLM/heuristic)
sae.episodes.recall = MagicMock(return_value=None)
sae.episodes.learn = MagicMock()
# Mock execution
sae._kill_foreign_apps = MagicMock()
sae._execute_escape = MagicMock()
# Let's use the REAL _plan_escape_via_llm but mock `query_llm`
sae._plan_escape_via_llm = SituationalAwarenessEngine._plan_escape_via_llm.__get__(sae, SituationalAwarenessEngine)
with patch("GramAddict.core.llm_provider.query_llm") as mock_query_llm:
mock_query_llm.return_value = {"response": '{"action": "click", "x": 100, "y": 100, "reason": "test"}'}
result = sae.ensure_clear_screen(max_attempts=5, initial_xml="<xml>0</xml>")
assert result is True, "SAE should eventually clear the screen"
# Check that query_llm was called with max_tokens >= 300
assert mock_query_llm.called
kwargs = mock_query_llm.call_args[1]
assert kwargs.get("max_tokens", 0) >= 300, f"max_tokens is too low: {kwargs.get('max_tokens')}"
# Check that the first action (killing foreign apps) was NOT marked as a failure,
# because it successfully transitioned from FOREIGN_APP to OBSTACLE_MODAL.
# Wait, the failure is tracked in `failed_this_session`. We can't easily inspect it directly
# since it's a local variable. But we can check `sae.episodes.learn` calls!
# The first learn call should be success=True because the state changed!
learn_calls = sae.episodes.learn.call_args_list
assert len(learn_calls) >= 2
# First action (kill_foreign_apps)
assert learn_calls[0][0][2] is True, "kill_foreign_apps should be marked as success because situation changed"
# Second action (click from LLM)
assert learn_calls[1][0][2] is True, "click should be marked as success because we reached NORMAL"

View File

@@ -81,8 +81,8 @@ def test_full_mission_autopilot_sequence(fsd_fixtures):
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["modal"].replace("not_now_btn", "skip_survey_btn").replace("Maybe Later", "Ignore"), # 2. Modal (Miss 1)
fsd_fixtures["modal"].replace("not_now_btn", "skip_survey_btn").replace("Maybe Later", "Ignore"), # 3. Modal (Miss 2 -> Telepathic Recovery)
fsd_fixtures["organic"], # 4. Second Organic Post
fsd_fixtures["organic"] # Buffer
]
@@ -151,6 +151,7 @@ def test_full_mission_autopilot_sequence(fsd_fixtures):
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.telepathic_engine.TelepathicEngine._cosine_similarity', return_value=0.1), \
patch('GramAddict.core.bot_flow._extract_post_content', return_value={"username": "fiona.dawson", "description": "Organic post", "caption": ""}), \
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, \

View File

@@ -100,16 +100,16 @@ class TestTelepathicEngineEdgeCases:
# Valid nodes + Alias testing
nodes = [
{"semantic_string": "main view section", "x": 10, "y": 10, "area": 100},
{"semantic_string": "main tab 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
# Threshold: 0.45 for short intents (2 words)
# The word 'home' matches 'main' via alias, 'tab' matches literally
# Navigation intents require 100% keyword match threshold
res = self.engine._keyword_match_score("tap home tab", nodes)
assert res is not None
assert res["semantic"] == "main view section"
assert res["semantic"] == "main tab section"
# No matches
assert self.engine._keyword_match_score("tap settings menu xyz", nodes) == None

View File

@@ -87,6 +87,27 @@ class TestNodeExtraction:
f"Parser may be too strict or too lenient."
)
def test_home_feed_extracts_post_author_and_content(self):
"""
In a real Home Feed dump, we MUST confidently extract the author node
and the content node without hitting VLM, using our struct-aliases.
"""
engine = TelepathicEngine()
xml = load_fixture("home_feed_with_ad.xml")
# Test exact strings from feed_analysis.py & timing.py
author_node1 = engine.find_best_node(xml, "post author username header", min_confidence=0.35)
author_node2 = engine.find_best_node(xml, "post author header profile", min_confidence=0.35)
content_node = engine.find_best_node(xml, "post media content", min_confidence=0.35)
assert author_node1 is not None, "Failed to find 'post author username header'"
assert author_node2 is not None, "Failed to find 'post author header profile'"
assert content_node is not None, "Failed to find 'post media content'"
# Should be resolved by fast path -> score >= 0.75
assert author_node1.get("score", 0) >= 0.75, "Author extraction fell out of Fast Path!"
assert content_node.get("score", 0) >= 0.75, "Content extraction fell out of Fast Path!"
def test_explore_feed_extracts_like_button(self):
"""
In the real Explore/Reels feed, the Like button has id 'like_button'

View File

@@ -40,7 +40,9 @@ def test_keyword_nav_threshold(engine):
def test_direct_tab_fast_path(engine):
"""
Verify that "tap messages tab" now hits the core_nav_fast_path.
Verify that _core_navigation_fast_path returns None without Qdrant data
(Blank Start architecture). Navigation discovery is Qdrant-only.
The keyword_match_score fallback handles it with resource-id matching.
"""
direct_node = {
"x": 800, "y": 2300, "area": 100,
@@ -51,7 +53,9 @@ def test_direct_tab_fast_path(engine):
}
}
# Without Qdrant data, fast path returns None (Blank Start)
res = engine._core_navigation_fast_path("tap messages tab", [direct_node])
assert res is not None
assert res["source"] == "core_nav"
assert res["x"] == 800
# In Blank Start, if Qdrant has no learned data, this MUST return None
# to force the agent into telepathic discovery mode
assert res is None, "Fast path should return None without learned Qdrant data"

View File

@@ -0,0 +1,78 @@
import pytest
from unittest.mock import MagicMock, patch
from GramAddict.core.telepathic_engine import TelepathicEngine
@pytest.fixture
def mock_device():
device = MagicMock()
device.get_screenshot_b64.return_value = "fake_base64_image_data"
# Mock args
class Args:
ai_telepathic_model = "test-model"
ai_telepathic_url = "http://test-url"
device.args = Args()
return device
@patch("GramAddict.core.llm_provider.query_llm")
def test_evaluate_post_vibe_rejects_poor_quality(mock_query_llm, mock_device):
engine = TelepathicEngine()
persona_interests = ["aesthetic architecture", "minimalism"]
# Mock VLM response to reject the post
mock_query_llm.return_value = {
"response": '{"quality_score": 3, "matches_niche": false, "reason": "Generic text meme, no architectural elements."}'
}
result = engine.evaluate_post_vibe(device=mock_device, persona_interests=persona_interests)
# Verify screenshot was evaluated
assert mock_device.get_screenshot_b64.called
assert mock_query_llm.called
# Verify the structured response parsing
assert result is not None
assert result["quality_score"] == 3
assert result["matches_niche"] is False
assert "Generic text meme" in result["reason"]
@patch("GramAddict.core.llm_provider.query_llm")
def test_evaluate_post_vibe_accepts_high_quality(mock_query_llm, mock_device):
engine = TelepathicEngine()
persona_interests = ["aesthetic architecture", "minimalism"]
# Mock VLM response to accept the post
mock_query_llm.return_value = {
"response": '{"quality_score": 9, "matches_niche": true, "reason": "Beautiful cohesive architectural shot."}'
}
result = engine.evaluate_post_vibe(device=mock_device, persona_interests=persona_interests)
# Verify screenshot was evaluated
assert mock_device.get_screenshot_b64.called
assert mock_query_llm.called
# Verify the structured response parsing
assert result is not None
assert result["quality_score"] == 9
assert result["matches_niche"] is True
assert "Beautiful cohesive" in result["reason"]
@patch("GramAddict.core.llm_provider.query_llm")
def test_evaluate_post_vibe_handles_invalid_json(mock_query_llm, mock_device):
engine = TelepathicEngine()
persona_interests = ["aesthetic architecture", "minimalism"]
# Mock VLM response with garbage output
mock_query_llm.return_value = {
"response": 'I think this is a nice picture but I forgot to output JSON.'
}
result = engine.evaluate_post_vibe(device=mock_device, persona_interests=persona_interests)
# Verify fallback to None on error
assert result is None

View File

@@ -95,7 +95,14 @@ def test_visual_vibe_check_accepts_high_quality(mock_query_llm, mock_get_instanc
}
# We also have to prevent the nav_graph.do from throwing if we reach it
with patch("GramAddict.core.q_nav_graph.QNavGraph.do", return_value=True) as mock_do:
with patch("GramAddict.core.q_nav_graph.QNavGraph.do", return_value=True) as mock_do, \
patch("GramAddict.core.behaviors.follow.sleep"):
from GramAddict.core.behaviors import PluginRegistry
from GramAddict.core.behaviors.follow import FollowPlugin
registry = PluginRegistry.get_instance()
registry.register(FollowPlugin())
_interact_with_profile(
device=mock_device,
configs=mock_configs,