fix(sae): stabilize navigation engine, fix container filtering, and negative reinforcement logic
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from GramAddict.core.bot_flow import (
|
||||
_align_active_post,
|
||||
_extract_post_content,
|
||||
_run_zero_latency_feed_loop,
|
||||
_run_zero_latency_stories_loop,
|
||||
_extract_post_content,
|
||||
is_ad,
|
||||
_align_active_post
|
||||
)
|
||||
from GramAddict.core.session_state import SessionState
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_device():
|
||||
@@ -20,12 +21,12 @@ def mock_device():
|
||||
return device
|
||||
|
||||
|
||||
@patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance')
|
||||
@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"}}
|
||||
{"original_attribs": {"desc": "test description of image with more than 10 chars"}},
|
||||
]
|
||||
mock_get_telepathic.return_value = mock_engine
|
||||
xml = "<xml/>"
|
||||
@@ -33,19 +34,21 @@ def test_extract_post_content(mock_get_telepathic):
|
||||
assert res["username"] == "test_user"
|
||||
assert "test description" in res["description"]
|
||||
|
||||
@patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance')
|
||||
|
||||
@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' ?>
|
||||
xml = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy>
|
||||
<node resource-id="" text="other_user this is a very long caption text that we want to extract as fallback" />
|
||||
</hierarchy>'''
|
||||
</hierarchy>"""
|
||||
res = _extract_post_content(xml)
|
||||
assert res["username"] == "other_user"
|
||||
assert "this is a very long caption" in res["caption"]
|
||||
|
||||
|
||||
def testis_ad():
|
||||
assert is_ad('<node resource-id="com.instagram.android:id/ad_cta_button" />') == True
|
||||
assert is_ad('<node resource-id="com.instagram.android:id/clips_single_image_ads_media_content" />') == True
|
||||
@@ -53,7 +56,8 @@ 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
|
||||
|
||||
@patch('GramAddict.core.telepathic_engine.TelepathicEngine.get_instance')
|
||||
|
||||
@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_engine = MagicMock()
|
||||
@@ -64,127 +68,176 @@ def test_align_active_post(mock_get_telepathic, mock_device):
|
||||
# The header is at 850px. Target is 250px. Diff is 600px. It should swipe.
|
||||
assert mock_device.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["growth_brain"].evaluate_governance.return_value = "SHIFT_CONTEXT"
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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.dump_hierarchy.return_value = "<hierarchy></hierarchy>" # Blind
|
||||
|
||||
|
||||
mock_device.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'):
|
||||
with (
|
||||
patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) 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
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
|
||||
with (
|
||||
patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) 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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
_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.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.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
|
||||
mock_device.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:
|
||||
</hierarchy>"""
|
||||
|
||||
with (
|
||||
patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) 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)
|
||||
|
||||
_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.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
|
||||
mock_device.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'):
|
||||
</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 == "FEED_EXHAUSTED"
|
||||
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'):
|
||||
|
||||
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.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()):
|
||||
|
||||
# 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()),
|
||||
):
|
||||
MockConfig.return_value.args.feed = True
|
||||
MockConfig.return_value.args.explore = False
|
||||
MockConfig.return_value.args.reels = False
|
||||
@@ -197,19 +250,20 @@ def test_start_bot_interrupt():
|
||||
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_user", device_id="123")
|
||||
|
||||
|
||||
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
|
||||
@@ -218,13 +272,15 @@ def test_feed_loop_deep_engagement(mock_device, mock_cognitive_stack):
|
||||
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
|
||||
|
||||
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.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
mock_device.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" />
|
||||
@@ -234,142 +290,182 @@ def test_feed_loop_deep_engagement(mock_device, mock_cognitive_stack):
|
||||
<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>'''
|
||||
|
||||
</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"].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), \
|
||||
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:
|
||||
|
||||
with (
|
||||
patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) 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),
|
||||
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_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._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}
|
||||
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)
|
||||
|
||||
|
||||
_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.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
session_state.check_limit.side_effect = (
|
||||
lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
|
||||
)
|
||||
|
||||
mock_device.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>'''
|
||||
|
||||
</hierarchy>"""
|
||||
|
||||
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.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:
|
||||
|
||||
with (
|
||||
patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) 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)
|
||||
|
||||
_run_zero_latency_feed_loop(
|
||||
mock_device,
|
||||
mock_cognitive_stack["zero_engine"],
|
||||
mock_cognitive_stack["nav_graph"],
|
||||
configs,
|
||||
session_state,
|
||||
"HomeFeed",
|
||||
mock_cognitive_stack,
|
||||
)
|
||||
|
||||
|
||||
def test_profile_learning_percentage_trigger(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 # Not high enough to trigger default
|
||||
|
||||
mock_cognitive_stack["resonance"].calculate_resonance.return_value = 0.50 # Not high enough to trigger default
|
||||
|
||||
configs = MagicMock()
|
||||
configs.args.profile_learning_percentage = 100 # Should force visit
|
||||
configs.args.profile_learning_percentage = 100 # Should force visit
|
||||
configs.args.likes_percentage = 0
|
||||
configs.args.comment_percentage = 0
|
||||
configs.args.follow_percentage = 0 # Won't trigger by follow chance either
|
||||
|
||||
configs.args.follow_percentage = 0 # Won't trigger by follow chance either
|
||||
|
||||
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.dump_hierarchy.return_value = '''<?xml version='1.0' ?>
|
||||
session_state.check_limit.side_effect = (
|
||||
lambda limit_type: (False, False, False, False) if getattr(limit_type, "name", "") == "ALL" else False
|
||||
)
|
||||
|
||||
mock_device.dump_hierarchy.return_value = """<?xml version='1.0' ?>
|
||||
<hierarchy>
|
||||
<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>'''
|
||||
|
||||
</hierarchy>"""
|
||||
|
||||
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:
|
||||
|
||||
with (
|
||||
patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) 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}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
_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.called
|
||||
|
||||
|
||||
def test_ai_learn_own_profile_triggers_goap():
|
||||
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.llm_provider.prewarm_ollama_models'), \
|
||||
patch('GramAddict.core.bot_flow.create_device') as mock_create_device, \
|
||||
patch('GramAddict.core.bot_flow.set_time_delta'), \
|
||||
patch('GramAddict.core.bot_flow.SessionState') as MockSession, \
|
||||
patch('GramAddict.core.bot_flow.open_instagram', return_value=True), \
|
||||
patch('GramAddict.core.bot_flow.verify_and_switch_account', return_value=True), \
|
||||
patch('GramAddict.core.bot_flow.get_instagram_version', return_value="1.0"), \
|
||||
patch('GramAddict.core.goap.GoalExecutor') as MockGoalExecutor, \
|
||||
patch('GramAddict.core.bot_flow.TelepathicEngine') as MockTelepathic, \
|
||||
patch('GramAddict.core.llm_provider.query_llm') as mock_query, \
|
||||
patch('GramAddict.core.bot_flow.DojoEngine'), \
|
||||
patch('GramAddict.core.bot_flow.sleep'):
|
||||
|
||||
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.llm_provider.prewarm_ollama_models"),
|
||||
patch("GramAddict.core.bot_flow.create_device") as mock_create_device,
|
||||
patch("GramAddict.core.bot_flow.set_time_delta"),
|
||||
patch("GramAddict.core.bot_flow.SessionState") as MockSession,
|
||||
patch("GramAddict.core.bot_flow.open_instagram", return_value=True),
|
||||
patch("GramAddict.core.bot_flow.verify_and_switch_account", return_value=True),
|
||||
patch("GramAddict.core.bot_flow.get_instagram_version", return_value="1.0"),
|
||||
patch("GramAddict.core.goap.GoalExecutor") as MockGoalExecutor,
|
||||
patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) as MockTelepathic,
|
||||
patch("GramAddict.core.llm_provider.query_llm") as mock_query,
|
||||
patch("GramAddict.core.bot_flow.DojoEngine"),
|
||||
patch("GramAddict.core.bot_flow.sleep"),
|
||||
):
|
||||
MockConfig.return_value.args.ai_learn_own_profile = True
|
||||
MockConfig.return_value.args.agent_strategy = "aggressive_growth"
|
||||
MockConfig.return_value.args.capture_e2e_dumps = False
|
||||
@@ -379,26 +475,25 @@ def test_ai_learn_own_profile_triggers_goap():
|
||||
MockConfig.return_value.args.stories = 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)
|
||||
|
||||
|
||||
mock_goap = MockGoalExecutor.get_instance.return_value
|
||||
mock_goap.achieve.return_value = True
|
||||
|
||||
mock_telepathic = MockTelepathic.return_value # the class constructor is called inside start_bot
|
||||
mock_telepathic._extract_semantic_nodes.return_value = [
|
||||
{"original_attribs": {"text": "my cool bio"}}
|
||||
]
|
||||
|
||||
|
||||
mock_telepathic = MockTelepathic.return_value # the class constructor is called inside start_bot
|
||||
mock_telepathic._extract_semantic_nodes.return_value = [{"original_attribs": {"text": "my cool bio"}}]
|
||||
|
||||
mock_query.return_value = {"persona": "cool dev", "vibe": "chill"}
|
||||
|
||||
|
||||
from GramAddict.core.bot_flow import start_bot
|
||||
|
||||
try:
|
||||
with patch('GramAddict.core.bot_flow.random_sleep', side_effect=KeyboardInterrupt()):
|
||||
with patch("GramAddict.core.bot_flow.random_sleep", side_effect=KeyboardInterrupt()):
|
||||
start_bot(username="testuser", device_id="123")
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
mock_goap.achieve.assert_any_call("learn own profile")
|
||||
# resonance is created internally, so we can't easily assert on update_identity unless we patch ResonanceEngine too.
|
||||
# It's sufficient to know the GOAP goal was triggered.
|
||||
@@ -408,58 +503,75 @@ 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
|
||||
|
||||
mock_cognitive_stack["resonance"].calculate_resonance.return_value = 0.50
|
||||
|
||||
configs = MagicMock()
|
||||
configs.args.profile_learning_percentage = 100 # Should force visit
|
||||
configs.args.profile_learning_percentage = 100 # Should force visit
|
||||
configs.args.likes_percentage = 0
|
||||
configs.args.comment_percentage = 0
|
||||
configs.args.follow_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' ?>
|
||||
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>"""
|
||||
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>'''
|
||||
</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:
|
||||
|
||||
with (
|
||||
patch("GramAddict.core.bot_flow.TelepathicEngine", autospec=True) 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
|
||||
[{"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]}"
|
||||
|
||||
_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]}"
|
||||
|
||||
Reference in New Issue
Block a user