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

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

View File

@@ -1,91 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors import BehaviorContext
from GramAddict.core.behaviors.ad_guard import AdGuardPlugin
@pytest.fixture
def ad_guard():
plugin = AdGuardPlugin()
return plugin
@pytest.fixture
def mock_context():
device = MagicMock()
configs = MagicMock()
session_state = MagicMock()
# Mocking cognitive stack to have qdrant_memory
memory = MagicMock()
# Mocking radome for xml sanitization
radome = MagicMock()
radome.sanitize_xml.return_value = "<xml>dummy</xml>"
# Mocking nav_graph for escape
nav_graph = MagicMock()
# Mocking zero_latency_engine
zero_engine = MagicMock()
cognitive_stack = {
"qdrant_memory": memory,
"radome": radome,
"nav_graph": nav_graph,
"zero_latency_engine": zero_engine,
}
ctx = BehaviorContext(device=device, configs=configs, session_state=session_state, cognitive_stack=cognitive_stack)
return ctx
class TestAdGuardPlugin:
@patch("GramAddict.core.behaviors.ad_guard.is_ad")
def test_can_activate(self, mock_is_ad, ad_guard, mock_context):
mock_context.context_xml = "<xml>dummy</xml>"
mock_is_ad.return_value = True
assert ad_guard.can_activate(mock_context) is True
mock_is_ad.return_value = False
assert ad_guard.can_activate(mock_context) is False
ad_guard._enabled = False
assert ad_guard.can_activate(mock_context) is False
@patch("GramAddict.core.behaviors.ad_guard.sleep")
@patch("GramAddict.core.behaviors.ad_guard.humanized_scroll")
def test_execute_single_ad(self, mock_scroll, mock_sleep, ad_guard, mock_context):
result = ad_guard.execute(mock_context)
assert result.executed is True
assert ad_guard.consecutive_ads == 1
mock_scroll.assert_called_once_with(mock_context.device, is_skip=True)
mock_sleep.assert_called_once()
@patch("GramAddict.core.behaviors.ad_guard.sleep")
@patch("GramAddict.core.behaviors.ad_guard.humanized_scroll")
def test_execute_triple_ad(self, mock_scroll, mock_sleep, ad_guard, mock_context):
ad_guard.consecutive_ads = 2
result = ad_guard.execute(mock_context)
assert result.executed is True
assert ad_guard.consecutive_ads == 3
# Should do aggressive double skip
assert mock_scroll.call_count == 2
mock_sleep.assert_called()
@patch("GramAddict.core.behaviors.ad_guard.sleep")
@patch("GramAddict.core.behaviors.ad_guard.humanized_scroll")
def test_execute_deadlock_ad(self, mock_scroll, mock_sleep, ad_guard, mock_context):
ad_guard.consecutive_ads = 5
result = ad_guard.execute(mock_context)
assert result.executed is True
assert ad_guard.consecutive_ads == 0 # Resets after home feed escape
nav_graph = mock_context.cognitive_stack["nav_graph"]
zero_engine = mock_context.cognitive_stack["zero_latency_engine"]
nav_graph.navigate_to.assert_called_once_with("HomeFeed", zero_engine)

View File

@@ -1,59 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors import BehaviorContext
from GramAddict.core.behaviors.anomaly_handler import AnomalyHandlerPlugin
@pytest.fixture
def anomaly_handler():
plugin = AnomalyHandlerPlugin()
return plugin
@pytest.fixture
def mock_context():
device = MagicMock()
configs = MagicMock()
session_state = MagicMock()
ctx = BehaviorContext(device=device, configs=configs, session_state=session_state, cognitive_stack={})
return ctx
class TestAnomalyHandlerPlugin:
def test_can_activate(self, anomaly_handler, mock_context):
assert anomaly_handler.can_activate(mock_context) is True
@patch("GramAddict.core.behaviors.anomaly_handler.TelepathicEngine")
@patch("GramAddict.core.behaviors.anomaly_handler.sleep")
@patch("GramAddict.core.behaviors.anomaly_handler.humanized_scroll")
def test_execute_with_nodes(self, mock_scroll, mock_sleep, mock_telepathic, anomaly_handler, mock_context):
mock_instance = MagicMock()
mock_instance._extract_semantic_nodes.return_value = [{"node": "dummy"}]
mock_telepathic.get_instance.return_value = mock_instance
result = anomaly_handler.execute(mock_context)
# It shouldn't execute exclusive action, but it should populate shared_state
assert result.executed is False
assert mock_context.shared_state["interactive_nodes"] == [{"node": "dummy"}]
mock_scroll.assert_not_called()
@patch("GramAddict.core.behaviors.anomaly_handler.TelepathicEngine")
@patch("GramAddict.core.behaviors.anomaly_handler.sleep")
@patch("GramAddict.core.behaviors.anomaly_handler.humanized_scroll")
def test_execute_zero_nodes(self, mock_scroll, mock_sleep, mock_telepathic, anomaly_handler, mock_context):
mock_instance = MagicMock()
mock_instance._extract_semantic_nodes.return_value = []
mock_telepathic.get_instance.return_value = mock_instance
result = anomaly_handler.execute(mock_context)
# Should execute recovery
assert result.executed is True
assert mock_context.shared_state["interactive_nodes"] == []
mock_context.device.press.assert_called_once_with("back")
mock_scroll.assert_called_once()
assert mock_sleep.call_count == 2

View File

@@ -1,55 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin
@pytest.fixture
def carousel_plugin():
return CarouselBrowsingPlugin()
@pytest.fixture
def mock_context():
ctx = MagicMock()
ctx.configs = MagicMock()
ctx.configs.args.carousel_percentage = 100
ctx.configs.args.carousel_count = "2-2"
ctx.configs.get_plugin_config.return_value = {"percentage": 100, "count": "2-2"}
ctx.context_xml = '<xml><node content-desc="carousel_indicator"/></xml>'
ctx.device = MagicMock()
ctx.device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
ctx.sleep_mod = 1.0
return ctx
class TestCarouselBrowsingPlugin:
@patch("GramAddict.core.behaviors.carousel_browsing.has_carousel_in_view", return_value=True)
def test_can_activate_enabled(self, mock_has_carousel, carousel_plugin, mock_context):
assert carousel_plugin.can_activate(mock_context) is True
def test_can_activate_disabled_via_config(self, carousel_plugin, mock_context):
mock_context.configs.get_plugin_config.return_value = {"percentage": 0, "count": "2-2"}
assert carousel_plugin.can_activate(mock_context) is False
@patch("GramAddict.core.behaviors.carousel_browsing.has_carousel_in_view", return_value=False)
def test_can_activate_no_carousel(self, mock_has_carousel, carousel_plugin, mock_context):
assert carousel_plugin.can_activate(mock_context) is False
@patch("GramAddict.core.behaviors.carousel_browsing.random.random", return_value=0.1) # 0.1 < 1.0 (100%)
@patch("GramAddict.core.behaviors.carousel_browsing.humanized_horizontal_swipe")
@patch("GramAddict.core.behaviors.carousel_browsing.sleep")
def test_execute_success(self, mock_sleep, mock_swipe, mock_random, carousel_plugin, mock_context):
result = carousel_plugin.execute(mock_context)
assert result.executed is True
assert result.interactions == 2
# It should swipe 2 times based on "2-2" config
assert mock_swipe.call_count == 2
# Verify swipe arguments
mock_swipe.assert_any_call(
mock_context.device, start_x=1080 * 0.8, end_x=1080 * 0.2, y=2400 * 0.5, duration_ms=250
)

View File

@@ -1,45 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors import BehaviorContext
from GramAddict.core.behaviors.close_friends_guard import CloseFriendsGuardPlugin
@pytest.fixture
def cf_guard():
plugin = CloseFriendsGuardPlugin()
return plugin
@pytest.fixture
def mock_context():
device = MagicMock()
configs = MagicMock()
session_state = MagicMock()
ctx = BehaviorContext(device=device, configs=configs, session_state=session_state, cognitive_stack={})
return ctx
class TestCloseFriendsGuardPlugin:
def test_can_activate(self, cf_guard, mock_context):
mock_context.context_xml = "<xml>enge freunde</xml>"
assert cf_guard.can_activate(mock_context) is True
mock_context.context_xml = "<xml>regular post</xml>"
assert cf_guard.can_activate(mock_context) is False
cf_guard._enabled = False
assert cf_guard.can_activate(mock_context) is False
@patch("GramAddict.core.behaviors.close_friends_guard.sleep")
@patch("GramAddict.core.behaviors.close_friends_guard.humanized_scroll")
def test_execute_has_badge(self, mock_scroll, mock_sleep, cf_guard, mock_context):
mock_context.device.dump_hierarchy.return_value = "<xml>post enge freunde badge</xml>"
result = cf_guard.execute(mock_context)
assert result.executed is True
mock_scroll.assert_called_once_with(mock_context.device, is_skip=True)
mock_sleep.assert_called_once()

View File

@@ -1,66 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors.comment import CommentPlugin
@pytest.fixture
def comment_plugin():
return CommentPlugin()
@pytest.fixture
def mock_context():
ctx = MagicMock()
ctx.configs = MagicMock()
ctx.configs.get_plugin_config.return_value = {"percentage": 50}
ctx.configs.args.dry_run_comments = False
ctx.shared_state = {"res_score": 1.0}
ctx.context_xml = "<xml></xml>"
ctx.device = MagicMock()
ctx.post_data = {"description": "test desc"}
ctx.session_state = MagicMock()
ctx.session_state.check_limit.return_value = False
# Mock cognitive stack
writer = MagicMock()
writer.generate_comment.return_value = "Great post!"
mock_nav = MagicMock()
mock_nav.do.return_value = True
ctx.cognitive_stack = {"writer": writer, "nav_graph": mock_nav}
return ctx
class TestCommentPlugin:
@patch("GramAddict.core.behaviors.comment.random.random", return_value=0.1)
def test_can_activate_enabled(self, mock_random, comment_plugin, mock_context):
assert comment_plugin.can_activate(mock_context) is True
def test_can_activate_disabled_via_config(self, comment_plugin, mock_context):
mock_context.configs.get_plugin_config.return_value = {"percentage": 0}
assert comment_plugin.can_activate(mock_context) is False
def test_execute_success(self, comment_plugin, mock_context):
result = comment_plugin.execute(mock_context)
assert result.executed is True
assert result.interactions == 1
mock_context.cognitive_stack["nav_graph"].do.assert_any_call("open comments")
mock_context.cognitive_stack["nav_graph"].do.assert_any_call("type and post comment", text="Great post!")
def test_execute_fails_type_and_post(self, comment_plugin, mock_context):
def mock_do(intent, **kwargs):
if intent == "type and post comment":
return False
return True
mock_context.cognitive_stack["nav_graph"].do.side_effect = mock_do
result = comment_plugin.execute(mock_context)
assert result.executed is False
mock_context.cognitive_stack["nav_graph"].do.assert_any_call("open comments")

View File

@@ -1,50 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors import BehaviorContext
from GramAddict.core.behaviors.darwin_dwell import DarwinDwellPlugin
@pytest.fixture
def darwin_dwell():
return DarwinDwellPlugin()
@pytest.fixture
def mock_context():
ctx = BehaviorContext(
device=MagicMock(),
configs=MagicMock(),
session_state=MagicMock(),
cognitive_stack={
"darwin": MagicMock(),
"nav_graph": MagicMock(),
"zero_latency_engine": MagicMock(),
"resonance": MagicMock(),
},
context_xml="<xml></xml>",
post_data={"description": "test desc"},
username="test_user",
shared_state={"res_score": 0.8},
)
return ctx
class TestDarwinDwellPlugin:
def test_execute_with_darwin(self, darwin_dwell, mock_context):
result = darwin_dwell.execute(mock_context)
assert result.executed is True
mock_darwin = mock_context.cognitive_stack["darwin"]
mock_darwin.execute_micro_wobble.assert_called_once_with(mock_context.device)
mock_darwin.execute_proof_of_resonance.assert_called_once()
@patch("GramAddict.core.behaviors.darwin_dwell.sleep")
def test_execute_without_darwin(self, mock_sleep, darwin_dwell, mock_context):
mock_context.cognitive_stack.pop("darwin")
result = darwin_dwell.execute(mock_context)
assert result.executed is True
mock_sleep.assert_called_once()

View File

@@ -1,67 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors.follow import FollowPlugin
@pytest.fixture
def follow_plugin():
return FollowPlugin()
@pytest.fixture
def mock_context():
ctx = MagicMock()
ctx.configs = MagicMock()
ctx.configs.get_plugin_config.return_value = {"percentage": 100}
ctx.session_state = MagicMock()
ctx.session_state.check_limit.return_value = False
ctx.device = MagicMock()
ctx.username = "test_user"
ctx.sleep_mod = 1.0
ctx.cognitive_stack = {}
return ctx
class TestFollowPlugin:
@patch("random.random", return_value=0.1)
def test_can_activate_enabled(self, mock_random, follow_plugin, mock_context):
assert follow_plugin.can_activate(mock_context) is True
mock_context.session_state.check_limit.assert_called_once()
def test_can_activate_disabled_via_config(self, follow_plugin, mock_context):
mock_context.configs.get_plugin_config.return_value = {"percentage": 0}
assert follow_plugin.can_activate(mock_context) is False
def test_can_activate_limit_reached(self, follow_plugin, mock_context):
mock_context.session_state.check_limit.return_value = True
assert follow_plugin.can_activate(mock_context) is False
@patch("GramAddict.core.q_nav_graph.QNavGraph")
@patch("GramAddict.core.behaviors.follow.sleep")
def test_execute_success(self, mock_sleep, mock_qnavgraph, follow_plugin, mock_context):
mock_nav = MagicMock()
mock_nav.do.return_value = True
mock_qnavgraph.return_value = mock_nav
result = follow_plugin.execute(mock_context)
assert result.executed is True
assert result.interactions == 1
mock_nav.do.assert_called_once_with("tap follow button")
@patch("GramAddict.core.q_nav_graph.QNavGraph")
def test_execute_nav_failed(self, mock_qnavgraph, follow_plugin, mock_context):
mock_nav = MagicMock()
mock_nav.do.return_value = False
mock_qnavgraph.return_value = mock_nav
result = follow_plugin.execute(mock_context)
assert result.executed is False
assert result.metadata.get("reason") == "nav_failed"

View File

@@ -1,124 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors.grid_like import GridLikePlugin
@pytest.fixture
def grid_like_plugin():
return GridLikePlugin()
@pytest.fixture
def mock_context():
ctx = MagicMock()
ctx.configs = MagicMock()
ctx.configs.get_plugin_config.return_value = {"percentage": 100, "count": "2-2"}
ctx.session_state = MagicMock()
ctx.session_state.check_limit.return_value = False
ctx.session_state.totalLikes = 0
ctx.context_xml = '<xml><node content-desc="profile_header" text="followers"/></xml>'
ctx.device = MagicMock()
ctx.device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
ctx.device.dump_hierarchy.return_value = "<xml></xml>"
ctx.username = "test_user"
ctx.sleep_mod = 1.0
growth_brain = MagicMock()
growth_brain.wants_to_double_tap.return_value = False
ctx.cognitive_stack = {"growth_brain": growth_brain}
return ctx
class TestGridLikePlugin:
def test_can_activate_enabled(self, grid_like_plugin, mock_context):
assert grid_like_plugin.can_activate(mock_context) is True
def test_can_activate_disabled_via_config(self, grid_like_plugin, mock_context):
mock_context.configs.get_plugin_config.return_value = {"percentage": 0}
assert grid_like_plugin.can_activate(mock_context) is False
def test_can_activate_limit_reached(self, grid_like_plugin, mock_context):
mock_context.session_state.check_limit.return_value = True
assert grid_like_plugin.can_activate(mock_context) is False
def test_can_activate_wrong_screen(self, grid_like_plugin, mock_context):
mock_context.context_xml = '<xml><node content-desc="home_feed"/></xml>'
assert grid_like_plugin.can_activate(mock_context) is False
@patch("random.random", return_value=0.1)
@patch("GramAddict.core.q_nav_graph.QNavGraph")
@patch("GramAddict.core.behaviors.grid_like.wait_for_post_loaded", return_value=True)
@patch("GramAddict.core.behaviors.grid_like.humanized_scroll")
@patch("GramAddict.core.behaviors.grid_like.humanized_click")
@patch("GramAddict.core.behaviors.grid_like.sleep")
def test_execute_success_heart_button(
self,
mock_sleep,
mock_click,
mock_scroll,
mock_wait,
mock_qnavgraph,
mock_random,
grid_like_plugin,
mock_context,
):
mock_nav = MagicMock()
mock_nav.do.return_value = True # tap first image post AND tap like button
mock_qnavgraph.return_value = mock_nav
result = grid_like_plugin.execute(mock_context)
assert result.executed is True
assert result.interactions == 2
assert mock_context.session_state.totalLikes == 2
mock_nav.do.assert_any_call("tap first image post in profile grid")
mock_nav.do.assert_any_call("tap like button")
mock_context.device.press.assert_called_once_with("back")
@patch("random.random", return_value=0.1)
@patch("GramAddict.core.q_nav_graph.QNavGraph")
@patch("GramAddict.core.behaviors.grid_like.wait_for_post_loaded", return_value=True)
@patch("GramAddict.core.behaviors.grid_like.humanized_scroll")
@patch("GramAddict.core.behaviors.grid_like.humanized_click")
@patch("GramAddict.core.behaviors.grid_like.sleep")
def test_execute_success_double_tap(
self,
mock_sleep,
mock_click,
mock_scroll,
mock_wait,
mock_qnavgraph,
mock_random,
grid_like_plugin,
mock_context,
):
mock_nav = MagicMock()
mock_nav.do.return_value = True
mock_qnavgraph.return_value = mock_nav
mock_context.cognitive_stack["growth_brain"].wants_to_double_tap.return_value = True
result = grid_like_plugin.execute(mock_context)
assert result.executed is True
assert result.interactions == 2
assert mock_context.session_state.totalLikes == 2
mock_click.assert_called()
@patch("random.random", return_value=0.1)
@patch("GramAddict.core.q_nav_graph.QNavGraph")
def test_execute_nav_failed(self, mock_qnavgraph, mock_random, grid_like_plugin, mock_context):
mock_nav = MagicMock()
mock_nav.do.return_value = False
mock_qnavgraph.return_value = mock_nav
result = grid_like_plugin.execute(mock_context)
assert result.executed is False
assert result.metadata.get("reason") == "grid_nav_failed"

View File

@@ -1,55 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors.like import LikePlugin
@pytest.fixture
def like_plugin():
return LikePlugin()
@pytest.fixture
def mock_context():
ctx = MagicMock()
ctx.configs = MagicMock()
ctx.configs.get_plugin_config.return_value = {"percentage": 100}
ctx.shared_state = {"res_score": 1.0}
ctx.context_xml = "<xml></xml>"
ctx.device = MagicMock()
ctx.session_state = MagicMock()
ctx.session_state.check_limit.return_value = False
ctx.session_state.totalLikes = 0
mock_nav = MagicMock()
mock_nav.do.return_value = True
ctx.cognitive_stack = {"nav_graph": mock_nav}
return ctx
class TestLikePlugin:
@patch("GramAddict.core.behaviors.like.random.random", return_value=0.1)
def test_can_activate_enabled(self, mock_random, like_plugin, mock_context):
assert like_plugin.can_activate(mock_context) is True
def test_can_activate_disabled_via_config(self, like_plugin, mock_context):
mock_context.configs.get_plugin_config.return_value = {"percentage": 0}
assert like_plugin.can_activate(mock_context) is False
def test_execute_already_liked(self, like_plugin, mock_context):
mock_nav = mock_context.cognitive_stack["nav_graph"]
mock_nav.do.return_value = False
result = like_plugin.execute(mock_context)
assert result.executed is False
def test_execute_success(self, like_plugin, mock_context):
result = like_plugin.execute(mock_context)
assert result.executed is True
assert result.interactions == 1
mock_context.cognitive_stack["nav_graph"].do.assert_called_once_with("tap like button")

View File

@@ -1,122 +0,0 @@
from unittest.mock import MagicMock, create_autospec, patch
import pytest
from GramAddict.core.behaviors import BehaviorContext
from GramAddict.core.behaviors.obstacle_guard import ObstacleGuardPlugin
from GramAddict.core.situational_awareness import SituationType
@pytest.fixture
def obstacle_guard():
plugin = ObstacleGuardPlugin()
return plugin
@pytest.fixture
def mock_context():
device = MagicMock()
configs = MagicMock()
session_state = MagicMock()
ctx = BehaviorContext(device=device, configs=configs, session_state=session_state, cognitive_stack={})
return ctx
class TestObstacleGuardPlugin:
def test_can_activate(self, obstacle_guard, mock_context):
assert obstacle_guard.can_activate(mock_context) is True
@patch("GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine")
def test_execute_no_obstacle_has_markers(self, mock_sae, obstacle_guard, mock_context):
mock_instance = MagicMock()
mock_instance.perceive.return_value = SituationType.NORMAL
mock_sae.get_instance.return_value = mock_instance
# Give context xml that has feed markers
mock_context.device.dump_hierarchy.return_value = "<xml>dummy row_feed_button_like</xml>"
mock_context.shared_state["consecutive_marker_misses"] = 2
result = obstacle_guard.execute(mock_context)
assert result.executed is False
assert mock_context.shared_state["consecutive_marker_misses"] == 0
@patch("GramAddict.core.behaviors.obstacle_guard.humanized_scroll")
@patch("GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine")
def test_execute_no_obstacle_no_markers(self, mock_sae, mock_scroll, obstacle_guard, mock_context):
mock_instance = MagicMock()
mock_instance.perceive.return_value = SituationType.NORMAL
mock_sae.get_instance.return_value = mock_instance
mock_context.device.dump_hierarchy.return_value = "<xml>dummy</xml>"
mock_context.shared_state["consecutive_marker_misses"] = 1
result = obstacle_guard.execute(mock_context)
assert result.executed is True
assert mock_context.shared_state["consecutive_marker_misses"] == 2
mock_scroll.assert_called_once_with(mock_context.device)
@patch("GramAddict.core.behaviors.obstacle_guard.TelepathicEngine")
@patch("GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine")
@patch("GramAddict.core.behaviors.obstacle_guard.sleep")
def test_execute_obstacle_miss_1(self, mock_sleep, mock_sae, mock_telepathic, obstacle_guard, mock_context):
mock_instance = MagicMock()
mock_instance.perceive.return_value = SituationType.OBSTACLE_MODAL
mock_sae.get_instance.return_value = mock_instance
mock_context.device.dump_hierarchy.return_value = "<xml>dummy</xml>"
mock_context.shared_state["consecutive_marker_misses"] = 0
result = obstacle_guard.execute(mock_context)
assert result.executed is True
assert mock_context.shared_state["consecutive_marker_misses"] == 1
mock_context.device.press.assert_called_once_with("back")
mock_sleep.assert_called_once()
@patch("GramAddict.core.behaviors.obstacle_guard.TelepathicEngine")
@patch("GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine")
@patch("GramAddict.core.behaviors.obstacle_guard.sleep")
def test_execute_obstacle_miss_2_recovery(
self, mock_sleep, mock_sae, mock_telepathic, obstacle_guard, mock_context
):
mock_instance = MagicMock()
mock_instance.perceive.return_value = SituationType.OBSTACLE_MODAL
mock_sae.get_instance.return_value = mock_instance
mock_tele = MagicMock()
mock_tele.find_best_node.return_value = {"x": 10, "y": 20, "semantic": "Dismiss"}
mock_telepathic.get_instance.return_value = mock_tele
# Before recovery
mock_context.device.dump_hierarchy.side_effect = ["<xml>dummy</xml>", "<xml>row_feed_button_like</xml>"]
mock_context.shared_state["consecutive_marker_misses"] = 1
result = obstacle_guard.execute(mock_context)
assert result.executed is True
assert mock_context.shared_state["consecutive_marker_misses"] == 0
@patch("GramAddict.core.behaviors.obstacle_guard.dump_ui_state")
@patch("GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine")
def test_execute_obstacle_miss_3_abort(self, mock_sae, mock_dump, obstacle_guard, mock_context):
from GramAddict.core.situational_awareness import SituationalAwarenessEngine
mock_instance = create_autospec(SituationalAwarenessEngine, instance=True)
mock_instance.perceive.return_value = SituationType.OBSTACLE_MODAL
mock_sae.get_instance.return_value = mock_instance
xml_content = "<xml>dummy</xml>"
mock_context.device.dump_hierarchy.return_value = xml_content
mock_context.device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
mock_context.shared_state["consecutive_marker_misses"] = 2
mock_context.session_state.job_target = "Feed"
result = obstacle_guard.execute(mock_context)
assert result.executed is True
assert result.metadata.get("return_code") == "CONTEXT_LOST"
mock_instance.unlearn_current_state.assert_called_once_with(xml_content)
mock_dump.assert_called_once()

View File

@@ -1,61 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors import BehaviorContext
from GramAddict.core.behaviors.perfect_snapping import PerfectSnappingPlugin
@pytest.fixture
def perfect_snapping():
return PerfectSnappingPlugin()
@pytest.fixture
def mock_context():
ctx = BehaviorContext(
device=MagicMock(),
configs=MagicMock(),
session_state=MagicMock(),
cognitive_stack={},
context_xml="<xml>old</xml>",
)
return ctx
class TestPerfectSnappingPlugin:
@patch("GramAddict.core.behaviors.perfect_snapping._align_active_post")
def test_execute_no_alignment_needed(self, mock_align, perfect_snapping, mock_context):
mock_align.return_value = False
result = perfect_snapping.execute(mock_context)
assert result.executed is False
assert mock_context.context_xml == "<xml>old</xml>"
mock_align.assert_called_once_with(mock_context.device)
@patch("GramAddict.core.behaviors.perfect_snapping._align_active_post")
def test_execute_alignment_done(self, mock_align, perfect_snapping, mock_context):
mock_align.return_value = True
mock_context.device.dump_hierarchy.return_value = "<xml>new</xml>"
result = perfect_snapping.execute(mock_context)
assert result.executed is True
assert mock_context.context_xml == "<xml>new</xml>"
mock_align.assert_called_once_with(mock_context.device)
@patch("GramAddict.core.behaviors.perfect_snapping._align_active_post")
def test_execute_alignment_with_radome(self, mock_align, perfect_snapping, mock_context):
mock_align.return_value = True
mock_context.device.dump_hierarchy.return_value = "<xml>new</xml>"
mock_radome = MagicMock()
mock_radome.sanitize_xml.return_value = "<xml>sanitized</xml>"
mock_context.cognitive_stack["radome"] = mock_radome
result = perfect_snapping.execute(mock_context)
assert result.executed is True
assert mock_context.context_xml == "<xml>sanitized</xml>"
mock_radome.sanitize_xml.assert_called_once_with("<xml>new</xml>")

View File

@@ -1,47 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors import BehaviorContext
from GramAddict.core.behaviors.post_data_extraction import PostDataExtractionPlugin
@pytest.fixture
def post_data_extraction():
return PostDataExtractionPlugin()
@pytest.fixture
def mock_context():
session_state = MagicMock()
session_state.job_target = "Feed"
ctx = BehaviorContext(
device=MagicMock(),
configs=MagicMock(),
session_state=session_state,
cognitive_stack={},
context_xml="<xml></xml>",
)
return ctx
class TestPostDataExtractionPlugin:
@patch("GramAddict.core.behaviors.post_data_extraction.extract_post_content")
def test_execute_success(self, mock_extract, post_data_extraction, mock_context):
mock_extract.return_value = {"username": "test_user", "description": "hello"}
result = post_data_extraction.execute(mock_context)
assert result.executed is True
assert result.should_skip is False
assert mock_context.post_data == {"username": "test_user", "description": "hello"}
assert mock_context.username == "test_user"
@patch("GramAddict.core.behaviors.post_data_extraction.extract_post_content")
def test_execute_failure(self, mock_extract, post_data_extraction, mock_context):
mock_extract.return_value = None
result = post_data_extraction.execute(mock_context)
assert result.executed is False

View File

@@ -1,44 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors.post_interaction import PostInteractionPlugin
@pytest.fixture
def post_interaction_plugin():
return PostInteractionPlugin()
@pytest.fixture
def mock_context():
ctx = MagicMock()
ctx.shared_state = {"session_outcomes": ["like", "comment"]}
ctx.device = MagicMock()
ctx.post_data = {"id": "123"}
# Mock cognitive stack
telemetry = MagicMock()
ctx.cognitive_stack = {"telemetry": telemetry}
return ctx
class TestPostInteractionPlugin:
def test_can_activate_enabled(self, post_interaction_plugin, mock_context):
assert post_interaction_plugin.can_activate(mock_context) is True
@patch("GramAddict.core.behaviors.post_interaction.humanized_scroll")
@patch("GramAddict.core.behaviors.post_interaction.sleep")
def test_execute_success(self, mock_sleep, mock_scroll, post_interaction_plugin, mock_context):
result = post_interaction_plugin.execute(mock_context)
assert result.executed is True
assert result.should_skip is True # Signals the end of processing for this post
# Telemetry should be called
mock_context.cognitive_stack["telemetry"].log_post_interaction.assert_called_once_with(
{"id": "123"}, ["like", "comment"]
)
# Scroll should be called
mock_scroll.assert_called_once_with(mock_context.device)

View File

@@ -1,110 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors.profile_guard import ProfileGuardPlugin
@pytest.fixture
def profile_guard_plugin():
return ProfileGuardPlugin()
@pytest.fixture
def mock_context():
ctx = MagicMock()
ctx.configs = MagicMock()
ctx.configs.args.ignore_close_friends = True
ctx.configs.args.visual_vibe_check_percentage = 0
ctx.session_state = MagicMock()
ctx.session_state.my_username = "test_bot"
ctx.context_xml = '<xml><node content-desc="profile_header"/></xml>'
ctx.device = MagicMock()
ctx.username = "target_user"
nav_graph = MagicMock()
nav_graph.current_state = "ProfileView"
ctx.cognitive_stack = {"nav_graph": nav_graph}
return ctx
class TestProfileGuardPlugin:
def test_can_activate_enabled(self, profile_guard_plugin, mock_context):
assert profile_guard_plugin.can_activate(mock_context) is True
def test_can_activate_wrong_screen(self, profile_guard_plugin, mock_context):
mock_context.cognitive_stack["nav_graph"].current_state = "HomeFeed"
assert profile_guard_plugin.can_activate(mock_context) is False
def test_can_activate_no_username(self, profile_guard_plugin, mock_context):
mock_context.username = None
assert profile_guard_plugin.can_activate(mock_context) is False
def test_execute_guard_self_profile(self, profile_guard_plugin, mock_context):
mock_context.username = "test_bot"
result = profile_guard_plugin.execute(mock_context)
assert result.executed is True
assert result.should_skip is True
assert result.metadata["reason"] == "self_profile"
def test_execute_guard_private(self, profile_guard_plugin, mock_context):
mock_context.context_xml = "<xml>this account is private</xml>"
result = profile_guard_plugin.execute(mock_context)
assert result.executed is True
assert result.should_skip is True
assert result.metadata["reason"] == "private"
def test_execute_guard_empty(self, profile_guard_plugin, mock_context):
mock_context.context_xml = "<xml>no posts yet</xml>"
result = profile_guard_plugin.execute(mock_context)
assert result.executed is True
assert result.should_skip is True
assert result.metadata["reason"] == "empty"
def test_execute_guard_close_friend(self, profile_guard_plugin, mock_context):
mock_context.context_xml = "<xml>close friend</xml>"
result = profile_guard_plugin.execute(mock_context)
assert result.executed is True
assert result.should_skip is True
assert result.metadata["reason"] == "close_friend"
def test_execute_guard_pass(self, profile_guard_plugin, mock_context):
# Normal profile, no red flags
result = profile_guard_plugin.execute(mock_context)
assert result.executed is False
@patch("random.random", return_value=0.1) # 0.1 < 1.0
def test_execute_guard_vibe_check_fail(self, mock_random, profile_guard_plugin, mock_context):
mock_context.configs.args.visual_vibe_check_percentage = 100
mock_telepathic = MagicMock()
mock_telepathic.evaluate_profile_vibe.return_value = {
"quality_score": 3,
"matches_niche": False,
"reason": "Bad vibes",
}
mock_context.cognitive_stack["telepathic"] = mock_telepathic
result = profile_guard_plugin.execute(mock_context)
assert result.executed is True
assert result.should_skip is True
assert result.metadata["reason"] == "vibe_check_failed"
@patch("random.random", return_value=0.1)
def test_execute_guard_vibe_check_pass(self, mock_random, profile_guard_plugin, mock_context):
mock_context.configs.args.visual_vibe_check_percentage = 100
mock_telepathic = MagicMock()
mock_telepathic.evaluate_profile_vibe.return_value = {"quality_score": 8, "matches_niche": True}
mock_context.cognitive_stack["telepathic"] = mock_telepathic
result = profile_guard_plugin.execute(mock_context)
# Passes vibe check, continues execution
assert result.executed is False

View File

@@ -1,56 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors.profile_visit import ProfileVisitPlugin
@pytest.fixture
def profile_visit_plugin():
return ProfileVisitPlugin()
@pytest.fixture
def mock_context():
ctx = MagicMock()
ctx.configs = MagicMock()
ctx.configs.args.profile_visit_percentage = 30
ctx.configs.get_plugin_config.return_value = {"percentage": 30}
ctx.shared_state = {"res_score": 1.0}
ctx.context_xml = "<xml></xml>"
ctx.device = MagicMock()
ctx.username = "test_user"
mock_nav = MagicMock()
mock_nav.current_state = "HomeFeed"
ctx.cognitive_stack = {"nav_graph": mock_nav}
return ctx
class TestProfileVisitPlugin:
@patch("GramAddict.core.behaviors.profile_visit.random.random", return_value=0.1) # 0.1 < 0.3
def test_can_activate_enabled(self, mock_random, profile_visit_plugin, mock_context):
assert profile_visit_plugin.can_activate(mock_context) is True
def test_can_activate_disabled_via_config(self, profile_visit_plugin, mock_context):
mock_context.configs.get_plugin_config.return_value = {"percentage": 0}
assert profile_visit_plugin.can_activate(mock_context) is False
@patch("GramAddict.core.behaviors.profile_visit.sleep")
@patch("GramAddict.core.behaviors.PluginRegistry")
def test_execute_success(self, mock_registry, mock_sleep, profile_visit_plugin, mock_context):
mock_nav = mock_context.cognitive_stack["nav_graph"]
mock_nav.do.return_value = True
mock_registry_instance = MagicMock()
mock_registry.get_instance.return_value = mock_registry_instance
from GramAddict.core.behaviors import BehaviorResult
mock_registry_instance.execute_all.return_value = [BehaviorResult(executed=True)]
result = profile_visit_plugin.execute(mock_context)
assert result.executed is True
mock_nav.do.assert_any_call("tap post username")
mock_context.device.press.assert_called_with("back")

View File

@@ -1,54 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors import BehaviorContext
from GramAddict.core.behaviors.rabbit_hole import RabbitHolePlugin
@pytest.fixture
def rabbit_hole():
return RabbitHolePlugin()
@pytest.fixture
def mock_context():
ctx = BehaviorContext(
device=MagicMock(),
configs=MagicMock(),
session_state=MagicMock(),
cognitive_stack={"nav_graph": MagicMock()},
context_xml="<xml></xml>",
shared_state={"res_score": 0.95},
)
return ctx
class TestRabbitHolePlugin:
@patch("GramAddict.core.behaviors.rabbit_hole.sleep")
def test_execute_success(self, mock_sleep, rabbit_hole, mock_context):
mock_context.cognitive_stack["nav_graph"].do.return_value = True
result = rabbit_hole.execute(mock_context)
assert result.executed is True
mock_context.cognitive_stack["nav_graph"].do.assert_called_once_with("tap post username")
mock_context.device.press.assert_called_once_with("back")
assert mock_sleep.call_count == 2
def test_can_activate_low_resonance(self, rabbit_hole, mock_context):
mock_context.shared_state["res_score"] = 0.5
assert rabbit_hole.can_activate(mock_context) is False
def test_can_activate_success(self, rabbit_hole, mock_context):
mock_context.configs.get_plugin_config.return_value = {"percentage": 100}
with patch("GramAddict.core.behaviors.rabbit_hole.random.random", return_value=0.0):
assert rabbit_hole.can_activate(mock_context) is True
def test_execute_no_nav_graph(self, rabbit_hole, mock_context):
mock_context.cognitive_stack.pop("nav_graph")
result = rabbit_hole.execute(mock_context)
assert result.executed is False # Fails to execute if no nav_graph

View File

@@ -1,50 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors.repost import RepostPlugin
@pytest.fixture
def repost_plugin():
return RepostPlugin()
@pytest.fixture
def mock_context():
ctx = MagicMock()
ctx.configs = MagicMock()
ctx.configs.get_plugin_config.return_value = {"percentage": 100}
ctx.shared_state = {"res_score": 1.0}
ctx.context_xml = "<xml></xml>"
ctx.device = MagicMock()
mock_nav = MagicMock()
mock_nav.do.return_value = True
ctx.cognitive_stack = {"nav_graph": mock_nav}
return ctx
class TestRepostPlugin:
@patch("GramAddict.core.behaviors.repost.random.random", return_value=0.1)
def test_can_activate_enabled(self, mock_random, repost_plugin, mock_context):
assert repost_plugin.can_activate(mock_context) is True
def test_can_activate_disabled_via_config(self, repost_plugin, mock_context):
mock_context.configs.get_plugin_config.return_value = {"percentage": 0}
assert repost_plugin.can_activate(mock_context) is False
def test_execute_success(self, repost_plugin, mock_context):
result = repost_plugin.execute(mock_context)
assert result.executed is True
assert result.interactions == 1
mock_context.cognitive_stack["nav_graph"].do.assert_called_once_with("share to story")
def test_execute_fails_no_add_to_story(self, repost_plugin, mock_context):
mock_context.cognitive_stack["nav_graph"].do.return_value = False
result = repost_plugin.execute(mock_context)
assert result.executed is False

View File

@@ -1,83 +0,0 @@
from unittest import mock
from unittest.mock import MagicMock, create_autospec, patch
import pytest
from GramAddict.core.behaviors import BehaviorContext
from GramAddict.core.behaviors.resonance_evaluator import ResonanceEvaluatorPlugin
@pytest.fixture
def resonance_evaluator():
return ResonanceEvaluatorPlugin()
@pytest.fixture
def mock_context():
configs = MagicMock()
configs.args.visual_vibe_check_percentage = 0
configs.args.interact_percentage = 100 # Never skip
ctx = BehaviorContext(
device=MagicMock(),
configs=configs,
session_state=MagicMock(),
cognitive_stack={"resonance": MagicMock(), "dopamine": MagicMock()},
context_xml="<xml></xml>",
post_data={"username": "test_user"},
username="test_user",
shared_state={"session_outcomes": []},
)
return ctx
class TestResonanceEvaluatorPlugin:
def test_execute_high_resonance(self, resonance_evaluator, mock_context):
mock_context.cognitive_stack["resonance"].calculate_resonance.return_value = 0.9
result = resonance_evaluator.execute(mock_context)
assert result.executed is True
assert result.should_skip is False
assert mock_context.shared_state["res_score"] == 0.9
mock_context.cognitive_stack["dopamine"].process_content.assert_called_once_with(
{"score": 9.0, "quality": "high"}
)
@patch("GramAddict.core.behaviors.resonance_evaluator.humanized_scroll")
@patch("GramAddict.core.behaviors.resonance_evaluator.sleep")
def test_execute_low_resonance_skip(self, mock_sleep, mock_scroll, resonance_evaluator, mock_context):
mock_context.configs.args.interact_percentage = 0 # Force skip
mock_context.cognitive_stack["resonance"].calculate_resonance.return_value = 0.1
# Mock random so it always skips
with patch("GramAddict.core.behaviors.resonance_evaluator.random.random", return_value=0.0):
result = resonance_evaluator.execute(mock_context)
assert result.executed is True
assert result.should_skip is True
mock_scroll.assert_called_once_with(mock_context.device)
assert len(mock_context.shared_state["session_outcomes"]) == 1
assert mock_context.shared_state["session_outcomes"][0]["resonance"] == 0.1
assert mock_context.shared_state["session_outcomes"][0]["action"] == "skip"
@patch("GramAddict.core.behaviors.resonance_evaluator.humanized_scroll")
@patch("GramAddict.core.behaviors.resonance_evaluator.sleep")
def test_execute_visual_vibe_check(self, mock_sleep, mock_scroll, resonance_evaluator, mock_context):
from GramAddict.core.telepathic_engine import TelepathicEngine
mock_context.configs.args.visual_vibe_check_percentage = 100
mock_context.cognitive_stack["resonance"].calculate_resonance.return_value = 0.5
mock_tele = create_autospec(TelepathicEngine, instance=True)
mock_tele.evaluate_post_vibe.return_value = {"quality_score": 10, "matches_niche": True}
mock_context.cognitive_stack["telepathic"] = mock_tele
with patch("GramAddict.core.behaviors.resonance_evaluator.random.random", return_value=0.0):
result = resonance_evaluator.execute(mock_context)
assert result.executed is True
assert result.should_skip is False
# res_score = 0.5 * 0.3 + 1.0 * 0.7 = 0.15 + 0.70 = 0.85
assert round(mock_context.shared_state["res_score"], 2) == 0.85
mock_tele.evaluate_post_vibe.assert_called_once_with(mock_context.device, mock.ANY)

View File

@@ -1,92 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors.story_view import StoryViewPlugin
@pytest.fixture
def story_view_plugin():
return StoryViewPlugin()
@pytest.fixture
def mock_context():
ctx = MagicMock()
ctx.configs = MagicMock()
ctx.configs.get_plugin_config.return_value = {"percentage": 100, "count": "2-2"}
ctx.context_xml = '<xml><node content-desc="reel_ring"/></xml>'
ctx.device = MagicMock()
ctx.device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
ctx.device.dump_hierarchy.return_value = '<xml><node content-desc="reel_ring"/></xml>'
ctx.username = "test_user"
ctx.sleep_mod = 1.0
ctx.cognitive_stack = {}
return ctx
class TestStoryViewPlugin:
def test_can_activate_enabled(self, story_view_plugin, mock_context):
assert story_view_plugin.can_activate(mock_context) is True
def test_can_activate_disabled_via_config(self, story_view_plugin, mock_context):
mock_context.configs.get_plugin_config.return_value = {"percentage": 0}
assert story_view_plugin.can_activate(mock_context) is False
@patch("random.random", return_value=0.1)
def test_execute_no_story_in_xml(self, mock_random, story_view_plugin, mock_context):
mock_context.context_xml = "<xml></xml>"
mock_context.device.dump_hierarchy.return_value = "<xml></xml>"
result = story_view_plugin.execute(mock_context)
assert result.executed is False
assert result.metadata["reason"] == "no_story"
@patch("random.random", return_value=0.1)
@patch("GramAddict.core.q_nav_graph.QNavGraph")
def test_execute_nav_failed(self, mock_qnavgraph, mock_random, story_view_plugin, mock_context):
mock_nav = MagicMock()
mock_nav.do.return_value = False
mock_qnavgraph.return_value = mock_nav
result = story_view_plugin.execute(mock_context)
assert result.executed is False
assert result.metadata["reason"] == "nav_failed"
@patch("random.random", return_value=0.1)
@patch("GramAddict.core.q_nav_graph.QNavGraph")
@patch("GramAddict.core.behaviors.story_view.wait_for_story_loaded", return_value=False)
def test_execute_load_timeout(self, mock_wait, mock_qnavgraph, mock_random, story_view_plugin, mock_context):
mock_nav = MagicMock()
mock_nav.do.return_value = True
mock_qnavgraph.return_value = mock_nav
result = story_view_plugin.execute(mock_context)
assert result.executed is False
assert result.metadata["reason"] == "load_timeout"
@patch("random.random", return_value=0.1)
@patch("GramAddict.core.q_nav_graph.QNavGraph")
@patch("GramAddict.core.behaviors.story_view.wait_for_story_loaded", return_value=True)
@patch("GramAddict.core.behaviors.story_view.humanized_click")
@patch("GramAddict.core.behaviors.story_view.sleep")
def test_execute_success(
self, mock_sleep, mock_click, mock_wait, mock_qnavgraph, mock_random, story_view_plugin, mock_context
):
mock_nav = MagicMock()
mock_nav.do.return_value = True
mock_qnavgraph.return_value = mock_nav
result = story_view_plugin.execute(mock_context)
assert result.executed is True
assert result.interactions == 2
assert result.metadata["stories_viewed"] == 2
# 2 stories, so 1 click (to advance from 1st to 2nd)
mock_click.assert_called_once()
mock_context.device.press.assert_called_once_with("back")

View File

@@ -1,39 +0,0 @@
from unittest.mock import patch
import pytest
from GramAddict.core.perception.action_memory import ActionMemory
from GramAddict.core.perception.spatial_parser import SpatialNode
@pytest.fixture
def memory():
with patch("GramAddict.core.qdrant_memory.UIMemoryDB") as MockDB:
mock_db = MockDB.return_value
yield ActionMemory(ui_memory=mock_db)
def test_track_click_stores_memory(memory):
node = SpatialNode(bounds=(0, 0, 10, 10), content_desc="Test Node")
memory.track_click("tap test", node)
assert memory._last_click_context is not None
assert memory._last_click_context["intent"] == "tap test"
def test_confirm_click_boosts_confidence(memory):
node = SpatialNode(bounds=(0, 0, 10, 10), content_desc="Test Node")
memory.track_click("tap test", node)
memory.confirm_click()
memory.ui_memory.boost_confidence.assert_called_once()
assert memory._last_click_context is None
def test_reject_click_decays_confidence(memory):
node = SpatialNode(bounds=(0, 0, 10, 10), content_desc="Test Node")
memory.track_click("tap test", node)
memory.reject_click()
memory.ui_memory.decay_confidence.assert_called_once()
assert memory._last_click_context is None

View File

@@ -1,37 +0,0 @@
from GramAddict.core.perception.intent_resolver import IntentResolver
from GramAddict.core.perception.spatial_parser import SpatialNode
def test_intent_resolver_finds_bottom_tab():
resolver = IntentResolver()
# A tab at the top
top_tab = SpatialNode(bounds=(0, 0, 100, 100), content_desc="Explore Tab", clickable=True)
# A tab at the bottom
bottom_tab = SpatialNode(bounds=(0, 2200, 100, 2300), content_desc="Explore Tab", clickable=True)
# Intent resolver should prefer the one that geometrically matches the bottom navigation area
best_match = resolver.resolve("tap explore tab", [top_tab, bottom_tab])
assert best_match == bottom_tab
def test_intent_resolver_finds_button_by_text():
resolver = IntentResolver()
btn1 = SpatialNode(bounds=(0, 0, 100, 100), text="Follow", clickable=True)
btn2 = SpatialNode(bounds=(200, 200, 300, 300), text="Message", clickable=True)
best_match = resolver.resolve("tap follow button", [btn1, btn2])
assert best_match == btn1
def test_intent_resolver_returns_none_if_no_match():
resolver = IntentResolver()
btn = SpatialNode(bounds=(0, 0, 100, 100), text="Like", clickable=True)
best_match = resolver.resolve("tap follow button", [btn])
assert best_match is None

View File

@@ -1,199 +0,0 @@
from unittest.mock import MagicMock, patch
from GramAddict.core.q_nav_graph import QNavGraph
class TestAnomalyInterruptions:
def setup_method(self):
self.mock_device = MagicMock()
self.mock_device.deviceV2 = MagicMock()
self.mock_device.app_id = "com.instagram.android"
self.mock_device._get_current_app.return_value = "com.instagram.android"
self.nav_graph = QNavGraph(self.mock_device)
self.nav_graph = QNavGraph(self.mock_device)
# We test the LLM fallback planner logic here.
# Since the legacy structural planner was removed, we must mock the LLM
# to ensure deterministic test execution.
from GramAddict.core.situational_awareness import SituationalAwarenessEngine
sae = SituationalAwarenessEngine.get_instance(self.mock_device)
sae.episodes.recall = MagicMock(return_value=None)
sae.episodes.learn = MagicMock()
# We must also mock ScreenMemoryDB to prevent cached misclassifications
# from bypassing the LLM in perceive()
self.screen_memory_patcher = patch("GramAddict.core.qdrant_memory.ScreenMemoryDB")
self.mock_screen_memory_cls = self.screen_memory_patcher.start()
self.mock_screen_memory = self.mock_screen_memory_cls.return_value
self.mock_screen_memory.get_screen_type.return_value = None
def teardown_method(self):
self.screen_memory_patcher.stop()
def test_os_permission_dialog_denial(self):
"""
Z-Depth Guard must explicitly attempt to find a 'Deny' / 'Don't allow' button
when encountering the Android permission modal instead of just pressing BACK.
"""
obstacle_xml = """
<hierarchy>
<node package="com.android.permissioncontroller" resource-id="com.android.permissioncontroller:id/grant_dialog">
<node package="com.android.permissioncontroller" text="Allow Instagram to access your location?" />
<node package="com.android.permissioncontroller" text="While using the app" resource-id="com.android.permissioncontroller:id/permission_allow_button" bounds="[100,500][900,600]" clickable="true" />
<node package="com.android.permissioncontroller" text="Don't allow" resource-id="com.android.permissioncontroller:id/permission_deny_button" bounds="[100,650][900,750]" clickable="true" />
</node>
</hierarchy>
"""
normal_xml = '<hierarchy><node package="com.instagram.android" /></hierarchy>'
# 1. Obstacle Check (Attempt 1 Start) uses initial_xml (passed in _clear_anomaly_obstacles)
# 2. Re-dump in evaluate loop (next iterations)
self.mock_device.dump_hierarchy.side_effect = [normal_xml, normal_xml, normal_xml]
def mock_llm_side_effect(*args, **kwargs):
system_arg = kwargs.get("system")
if not system_arg and len(args) > 4:
system_arg = args[4]
prompt_arg = kwargs.get("prompt")
if not prompt_arg and len(args) > 2:
prompt_arg = args[2]
if system_arg == "Strict JSON classifier.":
if "How are we doing?" in prompt_arg or "Allow Instagram" in prompt_arg:
return {"response": '{"situation": "OBSTACLE_MODAL"}'}
return {"response": '{"situation": "NORMAL"}'}
return {"response": '{"action": "click", "x": 500, "y": 700, "reason": "Deny permission"}'}
with patch("GramAddict.core.llm_provider.query_llm") as mock_llm:
mock_llm.side_effect = mock_llm_side_effect
# When checking for obstacles, it should clear it by clicking deny
cleared = self.nav_graph._clear_anomaly_obstacles(xml_dump=obstacle_xml)
assert cleared is True, "Z-Depth Guard failed to clear the OS permission modal"
assert self.mock_device.click.call_count >= 1
# Verify it clicked the 'Don't allow' button coordinates ([100,650][900,750] avg is [500, 700])
args, _ = self.mock_device.click.call_args
assert args[0] == 500
assert args[1] == 700
def test_instagram_survey_dismissal(self):
"""
Instagram can prompt surveys mid-run. We must dismiss them gracefully.
The SAE prioritises BACK (priority=-1) for OBSTACLE_MODAL obstacles — this is
semantically correct because BACK dismisses Instagram modals reliably without
risk of accidentally tapping dangerous buttons. If BACK clears the screen
(i.e. post-action XML is NORMAL) the SAE considers the obstacle resolved and
returns True immediately without ever needing to click 'Not Now'.
This test verifies that the SAE takes at least one dismissal action
(either a BACK press OR a direct click on 'Not Now') and that the function
reports the obstacle as cleared.
"""
obstacle_xml = """
<hierarchy>
<node package="com.instagram.android" resource-id="com.instagram.android:id/survey_container">
<node package="com.instagram.android" text="How are we doing?" resource-id="com.instagram.android:id/survey_title" />
<node package="com.instagram.android" text="Take Survey" resource-id="com.instagram.android:id/take_survey_btn" bounds="[200,800][800,900]" clickable="true" />
<node package="com.instagram.android" text="Not Now" resource-id="com.instagram.android:id/button_negative" bounds="[200,950][800,1050]" clickable="true" />
</node>
</hierarchy>
"""
normal_xml = '<hierarchy><node package="com.instagram.android" /></hierarchy>'
# After any dismissal action the screen returns to normal
self.mock_device.dump_hierarchy.side_effect = [normal_xml, normal_xml, normal_xml]
def mock_llm_side_effect(*args, **kwargs):
system_arg = kwargs.get("system")
if not system_arg and len(args) > 4:
system_arg = args[4]
prompt_arg = kwargs.get("prompt")
if not prompt_arg and len(args) > 2:
prompt_arg = args[2]
if system_arg == "Strict JSON classifier.":
if "How are we doing?" in prompt_arg or "Allow Instagram" in prompt_arg:
return {"response": '{"situation": "OBSTACLE_MODAL"}'}
return {"response": '{"situation": "NORMAL"}'}
return {"response": '{"action": "back", "reason": "Safe dismissal of modal"}'}
with patch("GramAddict.core.llm_provider.query_llm") as mock_llm:
mock_llm.side_effect = mock_llm_side_effect
cleared = self.nav_graph._clear_anomaly_obstacles(xml_dump=obstacle_xml)
# Primary assertion: the SAE reported success
assert cleared is True, "Instagram survey was not dismissed"
# Secondary assertion: at least one dismissal action occurred.
# The SAE may press BACK (priority=-1 for OBSTACLE_MODAL) or click 'Not Now'.
pressed_back = self.mock_device.press.called and any(
(a.args[0] if a.args else None) == "back" for a in self.mock_device.press.call_args_list
)
did_click = self.mock_device.click.call_count >= 1
assert (
pressed_back or did_click
), "SAE did not take any dismissal action (expected BACK press or click on 'Not Now')"
def test_fake_creation_flow_in_bio_is_ignored(self):
"""
Ensures that a user bio containing 'quick_capture' or 'creation_flow'
does not falsely trigger the structural OBSTACLE_MODAL states.
"""
from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType
sae = SituationalAwarenessEngine.get_instance()
# XML containing the marker in text, not id
xml = """<hierarchy><node package="com.instagram.android" text="I love the post creation_flow" /></hierarchy>"""
with patch("GramAddict.core.llm_provider.query_telepathic_llm") as mock_llm:
mock_llm.return_value = {"response": '{"situation": "NORMAL"}'}
result = sae.perceive(xml)
assert result == SituationType.NORMAL
def test_real_creation_flow_is_caught(self):
"""
Ensures that a real structural marker in the resource-id triggers OBSTACLE_MODAL.
"""
from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType
sae = SituationalAwarenessEngine.get_instance()
xml = """<hierarchy><node package="com.instagram.android" resource-id="com.instagram.android:id/quick_capture_root_container" /></hierarchy>"""
with patch("GramAddict.core.llm_provider.query_telepathic_llm") as mock_llm:
result = sae.perceive(xml)
assert result == SituationType.OBSTACLE_MODAL
mock_llm.assert_not_called()
def test_fake_action_blocked_in_caption_is_ignored(self):
"""
Ensures that 'action blocked' in a text attribute without a dialog container
does NOT trigger DANGER_ACTION_BLOCKED.
"""
from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType
sae = SituationalAwarenessEngine.get_instance()
xml = """<hierarchy><node package="com.instagram.android" text="My account was action blocked yesterday!" /></hierarchy>"""
with patch("GramAddict.core.llm_provider.query_telepathic_llm") as mock_llm:
mock_llm.return_value = {"response": '{"situation": "NORMAL"}'}
result = sae.perceive(xml)
assert result != SituationType.DANGER_ACTION_BLOCKED
def test_real_action_blocked_is_caught(self):
"""
Ensures that 'action blocked' with a dialog container triggers DANGER_ACTION_BLOCKED.
"""
from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType
sae = SituationalAwarenessEngine.get_instance()
xml = """<hierarchy><node package="com.instagram.android" resource-id="com.instagram.android:id/dialog_container"><node package="com.instagram.android" text="Action Blocked" /></node></hierarchy>"""
result = sae.perceive(xml)
assert result == SituationType.DANGER_ACTION_BLOCKED

View File

@@ -1,70 +0,0 @@
from unittest.mock import MagicMock
from GramAddict.core.q_nav_graph import QNavGraph
from GramAddict.core.situational_awareness import SituationType
def test_app_perimeter_guard_after_click():
"""
Simulates a catastrophic VLM hallucination where clicking an element
(e.g., an ad) causes the OS to switch to another app (e.g., Google Play Store).
The QNavGraph MUST detect this post-click, reject the telemetry,
and return CONTEXT_LOST immediately to prevent memory poisoning.
"""
mock_device = MagicMock()
# Initial state is Instagram
mock_device._get_current_app.side_effect = [
"com.android.vending", # POST-CLICK verification at line 361 (App drifted!)
"com.android.vending", # double check after BACK press in recovery
"com.android.vending", # fallback start check if needed
] + ["com.android.vending"] * 50
mock_device.app_id = "com.instagram.android"
state_counter = {"calls": 0}
def dynamic_xml():
state_counter["calls"] += 1
if state_counter["calls"] <= 2:
return '<hierarchy><node resource-id="com.instagram.android:id/row_feed_photo_profile_name" /></hierarchy>'
return '<hierarchy><node resource-id="play_store_ui" /></hierarchy>'
mock_device.dump_hierarchy.side_effect = dynamic_xml
# Mock Telepathic Engine
mock_engine = MagicMock()
mock_engine.find_best_node.return_value = {
"x": 50,
"y": 50,
"semantic_string": "fake profile link",
"source": "vlm",
"score": 1.0,
}
mock_engine.verify_success.return_value = True
# Mock SAE to be hermetic (no real Ollama calls)
mock_sae = MagicMock()
# Before click: no obstacles (return False = nothing cleared)
# After click: detect foreign app
mock_sae.ensure_clear_screen.return_value = False
mock_sae.perceive.return_value = SituationType.OBSTACLE_FOREIGN_APP
nav_graph = QNavGraph.__new__(QNavGraph)
nav_graph.device = mock_device
nav_graph.nodes = {}
nav_graph.current_state = "UNKNOWN"
nav_graph.nav_memory = MagicMock()
nav_graph.sae = mock_sae
nav_graph.goap = MagicMock()
nav_graph.compiler = MagicMock()
# Execute the transition
result = nav_graph._execute_transition("tap_post_username", mock_semantic_engine=mock_engine)
# 1. It must return CONTEXT_LOST without saving to memory
assert result == "CONTEXT_LOST", f"Did not return CONTEXT_LOST after app drifted to Play Store! Got: {result}"
# 2. It MUST NOT confirm the click and poison telemetry!
mock_engine.confirm_click.assert_not_called()
# 3. It MUST reject the click to punish the VLM for hallucinating
mock_engine.reject_click.assert_called_once()

View File

@@ -1,58 +0,0 @@
from unittest.mock import MagicMock, patch
from GramAddict.core.q_nav_graph import QNavGraph
def test_autonomous_retry_on_ambiguity_failure():
"""
Verifies that _execute_transition uses an internal retry loop.
If the first attempt fails semantic verification (Ambiguity Guard),
it should press BACK, blacklist the node, and retry automatically.
"""
mock_device = MagicMock()
mock_device._get_current_app.side_effect = ["com.instagram.android"] * 100
mock_device.app_id = "com.instagram.android"
normal_xml = '<hierarchy><node package="com.instagram.android" /></hierarchy>'
wrong_context = '<hierarchy><node package="com.instagram.android" text="wrong" /></hierarchy>'
correct_context = '<hierarchy><node package="com.instagram.android" text="correct" /></hierarchy>'
# Each attempt consumes:
# 1. Initial context_xml dump
# 2. Re-acquire dump (triggered because _clear_anomaly_obstacles returns True when NORMAL)
# 3. Post-click verification dump
# Attempt 1 → normal, normal, wrong_context (verify=False → retry)
# Attempt 2 → normal, normal, correct_context (verify=True → return True)
mock_device.dump_hierarchy.side_effect = [
normal_xml, # Attempt 1: initial
normal_xml, # Attempt 1: re-acquire (cleared_something=True from SAE perceive=NORMAL)
wrong_context, # Attempt 1: post-click (verify_success=False)
normal_xml, # Attempt 2: initial
normal_xml, # Attempt 2: re-acquire
correct_context, # Attempt 2: post-click (verify_success=True)
] + [normal_xml] * 20
mock_engine = MagicMock()
# Return a different node on each find_best_node call (plus extras for safety)
mock_engine.find_best_node.side_effect = [
{"x": 10, "y": 10, "semantic_string": "Wrong Menu", "source": "vlm"},
{"x": 20, "y": 20, "semantic_string": "Correct Grid", "source": "vlm"},
{"x": 20, "y": 20, "semantic_string": "Correct Grid", "source": "vlm"}, # safety extra
]
# Mock verify_success: fail first (triggering Ambiguity Guard), succeed second
mock_engine.verify_success.side_effect = [False, True]
nav_graph = QNavGraph(mock_device)
with (
patch("time.sleep"),
patch("random.uniform"),
patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance", return_value=mock_engine),
):
result = nav_graph._execute_transition("tap_grid_first_post", mock_semantic_engine=mock_engine)
assert result is True, "Autonomous retry loop failed to complete successfully."
assert mock_device.click.call_count >= 2, "Should have attempted at least two different coordinates."
mock_engine.reject_click.assert_called() # Should have rejected the first mapping
mock_engine.confirm_click.assert_called() # Should have confirmed the final successful mapping

View File

@@ -1,28 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
def test_bot_flow_goal_executor_init_order():
# We want to prove that without explicit get_instance(device, username),
# QNavGraph(device) initializes GoalExecutor with an empty username.
# We have to reset GoalExecutor singleton first
from GramAddict.core.goap import GoalExecutor
from GramAddict.core.q_nav_graph import QNavGraph
if hasattr(GoalExecutor, "_instance"):
GoalExecutor._instance = None
device = MagicMock()
# Action: Instantiate QNavGraph
# Without the fix, this will initialize GoalExecutor with bot_username=""
# and PathMemory will get bound to "goap_paths_v1"
with patch("GramAddict.core.qdrant_memory.QdrantClient") as mock_qdrant:
# FIX: Pre-initialize with username (as done in bot_flow.py)
executor_pre = GoalExecutor.get_instance(device, "marisaundmarc")
nav_graph = QNavGraph(device)
executor = GoalExecutor.get_instance(device, "marisaundmarc")
# Now it should be bound correctly
assert executor.path_memory._db.collection_name == "goap_paths_v1_marisaundmarc", "PathMemory collection name does not contain the username suffix! Initialization leak occurred."

View File

@@ -1,53 +0,0 @@
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop
def test_bot_flow_unlearns_on_context_loss():
"""Prove that bot_flow calls unlearn_current_state when context is completely lost (3 misses)."""
device = MagicMock()
# Provide a dummy dump hierarchy
device.dump_hierarchy.return_value = "<hierarchy></hierarchy>"
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
session_state = MagicMock()
with (
patch("GramAddict.core.behaviors.PluginRegistry.get_instance") as MockRegistry,
patch("GramAddict.core.bot_flow.sleep"),
patch("GramAddict.core.physics.humanized_input.humanized_scroll"),
patch("GramAddict.core.bot_flow._humanized_scroll"),
):
from GramAddict.core.behaviors import BehaviorResult
mock_registry_instance = MockRegistry.return_value
mock_registry_instance.execute_all.return_value = [
BehaviorResult(executed=True, should_skip=True, metadata={"return_code": "CONTEXT_LOST"})
]
mock_cognitive_stack = MagicMock()
dopamine_mock = MagicMock()
dopamine_mock.is_app_session_over.return_value = False
dopamine_mock.wants_to_doomscroll.return_value = False
def stack_get(key):
if key == "radome":
return None
elif key == "dopamine":
return dopamine_mock
return MagicMock()
mock_cognitive_stack.get.side_effect = stack_get
result = _run_zero_latency_feed_loop(
device=device,
zero_engine=MagicMock(),
nav_graph=MagicMock(),
configs=MagicMock(),
session_state=session_state,
job_target="test_feed",
cognitive_stack=mock_cognitive_stack,
)
# Assert (RED)
assert result == "CONTEXT_LOST"

View File

@@ -1,53 +0,0 @@
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop
@patch("GramAddict.core.behaviors.PluginRegistry.get_instance")
@patch("GramAddict.core.bot_flow.sleep")
@patch("GramAddict.core.bot_flow._humanized_scroll")
@patch("GramAddict.core.bot_flow._extract_post_content")
@patch("GramAddict.core.bot_flow._align_active_post")
@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
def test_plugin_skip_breaks_feed_loop(
mock_telepathic, mock_align, mock_extract, mock_scroll, mock_sleep, mock_registry_get_instance
):
# Setup mocks
device = MagicMock()
zero_engine = MagicMock()
nav_graph = MagicMock()
configs = MagicMock()
session_state = MagicMock()
# Cognitive stack setup
cognitive_stack = {
"dopamine": MagicMock(),
"darwin": MagicMock(),
"resonance": MagicMock(),
"active_inference": MagicMock(),
}
# Dopamine should not abort the session on first run, but abort on second
cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
mock_align.return_value = False
device.dump_hierarchy.return_value = "<xml>row_feed_photo_profile_name</xml>"
mock_telepathic_instance = MagicMock()
mock_telepathic_instance._extract_semantic_nodes.return_value = [{"x": 10}]
mock_telepathic.return_value = mock_telepathic_instance
mock_extract.return_value = {"username": "test", "description": "", "caption": ""}
# Setup PluginRegistry to return a skip result
mock_registry_instance = MagicMock()
mock_plugin_result = MagicMock()
mock_plugin_result.executed = True
mock_plugin_result.should_skip = True
mock_registry_instance.execute_all.return_value = [mock_plugin_result]
mock_registry_get_instance.return_value = mock_registry_instance
_run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, "HomeFeed", cognitive_stack)
# Assert that because should_skip was True, active_inference.predict_state was NEVER called
# (Meaning the 'continue' correctly bypassed the rest of the feed loop)
cognitive_stack["active_inference"].predict_state.assert_not_called()

View File

@@ -1,145 +0,0 @@
"""
Camera Trap Escape Tests
Validates that ALL perception layers correctly identify the Instagram
camera/story creation overlay as a blocking obstacle, preventing the
softlock discovered in the 2026-04-22 bot run.
Uses the real-world XML fixture captured during the actual incident.
"""
import os
from unittest.mock import MagicMock
import pytest
FIXTURE_PATH = os.path.join(os.path.dirname(__file__), "..", "fixtures", "camera_trap.xml")
@pytest.fixture
def camera_xml():
with open(FIXTURE_PATH, "r") as f:
return f.read()
@pytest.fixture
def mock_device():
device = MagicMock()
device.deviceV2 = MagicMock()
device.deviceV2.info = {"screenOn": True}
device.app_id = "com.instagram.android"
device._get_current_app.return_value = "com.instagram.android"
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
return device
class TestSAEPerceivesCameraAsObstacle:
"""Layer 1: SituationalAwarenessEngine.perceive() must classify
the camera overlay as OBSTACLE_MODAL."""
def test_sae_perceives_camera_as_obstacle_modal(self, camera_xml, mock_device):
from GramAddict.core.situational_awareness import (
SituationalAwarenessEngine,
SituationType,
)
SituationalAwarenessEngine.reset()
sae = SituationalAwarenessEngine(mock_device)
# Bypass Qdrant to test pure structural logic
sae.episodes.recall = MagicMock(return_value=None)
sae.episodes.learn = MagicMock()
result = sae.perceive(camera_xml)
assert (
result == SituationType.OBSTACLE_MODAL
), f"SAE failed to detect camera overlay as OBSTACLE_MODAL (got {result})"
class TestScreenIdentityClassifiesCameraAsModal:
"""Layer 2: ScreenIdentity._classify_screen() must return
ScreenType.MODAL for the camera overlay."""
def test_screen_identity_classifies_camera_as_modal(self, camera_xml, mock_device):
from GramAddict.core.goap import ScreenIdentity, ScreenType
screen_id = ScreenIdentity("testuser")
result = screen_id.identify(camera_xml)
assert (
result["screen_type"] == ScreenType.MODAL
), f"ScreenIdentity classified camera as {result['screen_type']} instead of MODAL"
class TestGOAPTriggersSAEOnCameraDetection:
"""Layer 4: GoalExecutor.achieve() must invoke SAE.ensure_clear_screen()
when ScreenIdentity classifies the screen as MODAL."""
def test_goap_triggers_sae_on_camera_detection(self, camera_xml, mock_device):
from GramAddict.core.goap import GoalExecutor
GoalExecutor.reset()
executor = GoalExecutor(mock_device, "testuser")
# achieve() calls perceive() twice before the SAE branch:
# 1. Line 866: initial perceive for path recall → camera_xml (MODAL)
# 2. Line 883: loop perceive at step 0 → camera_xml (MODAL) → triggers SAE
# 3+: after SAE clears, next perceives return normal feed
normal_xml = '<hierarchy><node package="com.instagram.android" resource-id="com.instagram.android:id/feed_tab" selected="true" /><node package="com.instagram.android" /></hierarchy>'
mock_device.dump_hierarchy.side_effect = [
camera_xml,
camera_xml,
normal_xml,
normal_xml,
normal_xml,
normal_xml,
]
# Mock SAE to report successful clearance and track calls
mock_sae = MagicMock()
mock_sae.ensure_clear_screen.return_value = True
executor._sae = mock_sae
# Run a goal — should detect MODAL on first loop perceive and call SAE
executor.achieve("open home feed", max_steps=5)
assert (
mock_sae.ensure_clear_screen.called
), "GOAP did not invoke SAE.ensure_clear_screen() when camera overlay was detected"
class TestForbiddenGuardBlocksQuickCaptureNodes:
"""Layer 5: _is_forbidden_action() must refuse to click
any node with quick_capture in its resource-id."""
def test_forbidden_guard_blocks_quick_capture_nodes(self):
from GramAddict.core.telepathic_engine import TelepathicEngine
engine = TelepathicEngine()
camera_node = {
"text": "",
"description": "",
"resource_id": "com.instagram.android:id/quick_capture_root_container",
"semantic_string": "id context: 'quick capture root container'",
}
assert (
engine._is_forbidden_action(camera_node) is True
), "Forbidden Action Guard failed to block quick_capture node"
def test_forbidden_guard_allows_normal_nodes(self):
from GramAddict.core.telepathic_engine import TelepathicEngine
engine = TelepathicEngine()
normal_node = {
"text": "",
"description": "Like",
"resource_id": "com.instagram.android:id/row_feed_button_like",
"semantic_string": "description: 'Like', id context: 'row feed button like'",
}
assert (
engine._is_forbidden_action(normal_node) is False
), "Forbidden Action Guard incorrectly blocked a normal Like button"

View File

@@ -1,38 +0,0 @@
from unittest.mock import MagicMock, patch
from GramAddict.core.compiler_engine import VLMCompilerEngine
def test_compiler_intent_list_handling():
"""
Test that VLMCompilerEngine gracefully handles intents that are lists,
which can happen when Shadow Mode submits ['row_feed', 'button_like'].
"""
mock_device = MagicMock()
compiler = VLMCompilerEngine(mock_device)
# We submit a string representation of a list as intent, simulating Dojo's behavior:
# dojo.submit_snapshot(heuristic_name=str(expected_signature), ... intent_prompt=f"... {expected_signature}")
raw_list_intent = "['row_feed', 'button_like']"
# We want the prompt to be clean. Let's intercept the prompt going to the LLM.
with patch("GramAddict.core.llm_provider.query_telepathic_llm") as mock_query:
# Mock LLM to return a valid JSON so the rest of the flow can execute
mock_query.return_value = '{"rule_type": "regex", "target_attribute": "resource-id", "pattern": ".*row_feed.*", "confidence": 0.95, "reasoning": "Test"}'
result = compiler.generate_heuristic(raw_list_intent, "<xml/>")
# Verify the prompt is properly sanitized
called_args = mock_query.call_args
assert called_args is not None
user_prompt = called_args.kwargs["user_prompt"]
# User prompt should contain a readable description, NOT a python list string.
# e.g., if intent has "button_like", prompt should ask for it clearly.
assert "TARGET INTENT" in user_prompt
assert "row_feed" in user_prompt
assert "button_like" in user_prompt
assert "['" not in user_prompt # No python list syntax!
assert result is not None
assert result["pattern"] == ".*row_feed.*"

View File

@@ -1,180 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.behaviors import BehaviorContext
from GramAddict.core.behaviors.carousel_browsing import CarouselBrowsingPlugin
from GramAddict.core.bot_flow import _interact_with_profile
from tests.conftest import MockArgs, MockConfigs
@pytest.fixture(autouse=True)
def silent_sleep(monkeypatch):
import GramAddict.core.bot_flow
import GramAddict.core.physics.humanized_input
import GramAddict.core.physics.timing
monkeypatch.setattr(GramAddict.core.bot_flow, "sleep", lambda x: None)
monkeypatch.setattr(GramAddict.core.physics.timing, "sleep", lambda x: None)
monkeypatch.setattr(GramAddict.core.physics.humanized_input, "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)
# Grid loop finishes with scroll logic
assert device.shell.call_count >= 0
@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)
# No interaction blocks run, so no shells.
assert device.shell.call_count == 0
@patch("random.random")
def test_interact_with_profile_mixed_probability(mock_random, device, telepathic_mock, mock_logger):
def mock_random_side_effect():
return 0.5
mock_random.return_value = 0.5
args = MockArgs(stories_percentage=0, follow_percentage=100, likes_percentage=100, 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)
# Grid loop finishes with 1 scroll for 1 post.
assert device.shell.call_count >= 0
@patch("random.random")
@patch("GramAddict.core.behaviors.carousel_browsing.humanized_horizontal_swipe")
def test_carousel_100_percent(mock_swipe, mock_random, device):
mock_random.return_value = 0.0
args = MockArgs(carousel_percentage=100, carousel_count="4-4")
configs = MockConfigs(args)
configs.get_plugin_config = MagicMock(return_value={})
ctx = BehaviorContext(
device=device,
configs=configs,
session_state=MagicMock(),
cognitive_stack={},
context_xml="<carousel_page_indicator/>",
sleep_mod=1.0,
)
plugin = CarouselBrowsingPlugin()
res = plugin.execute(ctx)
assert res.executed
assert mock_swipe.call_count == 4
@patch("random.random")
@patch("GramAddict.core.behaviors.carousel_browsing.humanized_horizontal_swipe")
def test_carousel_zero_percent(mock_swipe, mock_random, device):
mock_random.return_value = 0.99
args = MockArgs(carousel_percentage=0, carousel_count="4-4")
configs = MockConfigs(args)
configs.get_plugin_config = MagicMock(return_value={})
ctx = BehaviorContext(
device=device,
configs=configs,
session_state=MagicMock(),
cognitive_stack={},
context_xml="<carousel_page_indicator/>",
sleep_mod=1.0,
)
plugin = CarouselBrowsingPlugin()
assert not plugin.can_activate(ctx)
assert mock_swipe.call_count == 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 device.shell.call_count == 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 device.shell.call_count == 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

@@ -1,31 +0,0 @@
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."""
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

@@ -1,107 +0,0 @@
import logging
from unittest.mock import MagicMock
import pytest
from GramAddict.core.bot_flow import _interact_with_profile
from GramAddict.core.exceptions import ActionBlockedError
from GramAddict.core.q_nav_graph import QNavGraph
class TestCriticalAnomalyGuards:
def setup_method(self):
self.mock_device = MagicMock()
self.mock_device.deviceV2 = MagicMock()
self.mock_device.app_id = "com.instagram.android"
self.mock_device._get_current_app.return_value = "com.instagram.android"
self.mock_device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
self.nav_graph = QNavGraph(self.mock_device)
self.logger = logging.getLogger("test")
def test_action_blocked_dialog_raises_exception(self):
"""
If the OS/App shows a 'Try Again Later' block, we must explicitly crash the bot
by throwing an ActionBlockedError to prevent spamming and risking permanent bans.
"""
self.mock_device.dump_hierarchy.return_value = """
<hierarchy>
<node package="com.instagram.android" resource-id="com.instagram.android:id/dialog_container">
<node package="com.instagram.android" text="Try Again Later" resource-id="com.instagram.android:id/dialog_title" />
<node package="com.instagram.android" text="We restrict certain activity to protect our community." />
<node package="com.instagram.android" text="OK" resource-id="com.instagram.android:id/button_positive" />
</node>
</hierarchy>
"""
with pytest.raises(ActionBlockedError, match="Instagram action block detected"):
self.nav_graph._clear_anomaly_obstacles()
def test_interact_with_private_profile_aborts(self):
"""
If a user account is private, _interact_with_profile must skip everything
and return without doing ANY interactions.
"""
self.mock_device.dump_hierarchy.return_value = """
<hierarchy>
<node text="marisaundmarc" />
<node text="This account is private" bounds="[100,500][900,600]" />
<node text="Follow to see their photos and videos." />
</hierarchy>
"""
configs = MagicMock()
session_state = MagicMock()
_interact_with_profile(
self.mock_device, configs, "test_private", session_state, sleep_mod=0.0, logger=self.logger
)
# Verify it did not attempt to find stories, scrape, or anything
self.mock_device.click.assert_not_called()
self.mock_device.press.assert_not_called()
def test_interact_with_empty_profile_aborts(self):
"""
If a user account has 0 posts, we must skip.
"""
self.mock_device.dump_hierarchy.return_value = """
<hierarchy>
<node text="marisaundmarc" />
<node text="No posts yet" bounds="[100,500][900,600]" />
</hierarchy>
"""
configs = MagicMock()
session_state = MagicMock()
_interact_with_profile(
self.mock_device, configs, "test_empty", session_state, sleep_mod=0.0, logger=self.logger
)
self.mock_device.click.assert_not_called()
self.mock_device.press.assert_not_called()
def test_comments_disabled_guard(self):
"""
If the user has disabled comments, 'tap_comment_button' must abort and return skip=True
instead of defaulting to the VLM fallback which hallucinates clicks on random UI elements.
"""
from GramAddict.core.telepathic_engine import TelepathicEngine
engine = TelepathicEngine() # Bypass singleton mock
xml_dump = """
<hierarchy>
<node text="marisaundmarc" />
<node text="comments are turned off." bounds="[100,500][900,600]" />
<node text="Following" />
</hierarchy>
"""
# TelepathicEngine should instantly return a "skip" object without invoking memory or vectors
result = engine.find_best_node(xml_dump, "tap comment button", device=self.mock_device)
assert result is not None, "Engine completely failed instead of returning skip"
assert result.get("skip") is True, "Engine did not return skip=True for disabled comments"
assert "disabled" in result.get("semantic", ""), "Semantic tag should reflect disabled comments"

View File

@@ -1,42 +0,0 @@
from unittest.mock import MagicMock, patch, PropertyMock
from GramAddict.core.qdrant_memory import UIMemoryDB
def test_delete_point_logs_only_if_exists(caplog):
# Setup
db = UIMemoryDB()
type(db).is_connected = PropertyMock(return_value=True)
db.client = MagicMock()
db.collection_name = "test_collection"
# RED: Force the mock to return an empty list (point does NOT exist)
db.client.retrieve.return_value = []
# Action
result = db.delete_point("fake_seed")
# Assertions
assert result is True # Should still return True as it didn't crash
# It should NOT call delete if it wasn't found
db.client.delete.assert_not_called()
# It should NOT log the "Purged poisoned memory" line
assert "Purged poisoned memory vector" not in caplog.text
def test_delete_point_logs_if_found(caplog):
# Setup
db = UIMemoryDB()
type(db).is_connected = PropertyMock(return_value=True)
db.client = MagicMock()
db.collection_name = "test_collection"
# RED: Force the mock to return a result (point DOES exist)
db.client.retrieve.return_value = [{"id": "some_uuid"}]
# Action
with patch("GramAddict.core.qdrant_memory.logger") as mock_logger:
result = db.delete_point("fake_seed")
# Assertions
assert result is True
db.client.delete.assert_called_once()
mock_logger.info.assert_called_once()
assert "Purged poisoned memory vector" in mock_logger.info.call_args[0][0]

View File

@@ -1,165 +0,0 @@
from unittest.mock import MagicMock
import pytest
from GramAddict.core.dm_engine import _run_zero_latency_dm_loop
from GramAddict.core.qdrant_memory import DMMemoryDB, ParasocialCRMDB
def test_parasocial_crm_missing_log_sent_dm():
"""
RED: This test proves that ParasocialCRMDB lacks log_sent_dm,
which caused the crash in the last production run.
"""
crm = ParasocialCRMDB()
with pytest.raises(AttributeError) as excinfo:
crm.log_sent_dm("test_user", "hello", "bio", [])
assert "'ParasocialCRMDB' object has no attribute 'log_sent_dm'" in str(excinfo.value)
def test_dm_engine_uses_correct_dm_memory_logging(monkeypatch):
"""
RED: This test checks if dm_engine correctly uses dm_memory from cognitive_stack.
Note: I am writing this to PROVE the fix is necessary.
"""
mock_device = MagicMock()
mock_device.dump_hierarchy.return_value = (
'<xml>resource-id="com.instagram.android:id/inbox_refreshable_thread_list_recyclerview"</xml>'
)
mock_telepathic = MagicMock()
# Mock unread thread found
mock_thread = {"x": 100, "y": 100, "text": "Mariischen"}
def side_effect_logging(*args, **kwargs):
res = next(iterator)
print(f"DEBUG: extract_semantic_nodes called with {args[1]}. Returning {res}")
return res
iterator = iter(
[
[mock_thread], # Step 1: unread threads
[{"text": "Hey"}], # Step 2: context
[{"x": 200, "y": 200}], # Step 3: input field
[{"x": 300, "y": 300}], # Step 4: send button
[], # Step 5: next iteration no unread
]
)
mock_telepathic._extract_semantic_nodes.side_effect = side_effect_logging
mock_dopamine = MagicMock()
mock_dopamine.is_app_session_over.return_value = False
mock_dopamine.wants_to_change_feed.return_value = True # exit after one
mock_dopamine.boredom = 0
mock_crm = ParasocialCRMDB()
mock_dm_memory = MagicMock(spec=DMMemoryDB)
mock_resonance = MagicMock()
mock_resonance.persona_prompt = "Persona prompt"
mock_resonance.args.ai_model = "test-model"
cognitive_stack = {
"telepathic": mock_telepathic,
"dopamine": mock_dopamine,
"crm": mock_crm,
"dm_memory": mock_dm_memory,
"resonance": mock_resonance,
}
mock_session = MagicMock()
mock_session.check_limit.return_value = False
mock_session.totalMessages = 0
# Mock LLM response
monkeypatch.setattr("GramAddict.core.llm_provider.query_llm", lambda **k: {"response": "hi"})
monkeypatch.setattr("GramAddict.core.bot_flow._humanized_click", lambda *a: None)
monkeypatch.setattr("GramAddict.core.bot_flow.sleep", lambda *a: None)
monkeypatch.setattr("GramAddict.core.stealth_typing.ghost_type", lambda *a, **k: None)
mock_configs = MagicMock()
mock_configs.args.disable_ai_messaging = False
mock_configs.args.ai_condenser_model = "test-model"
mock_configs.args.ai_condenser_url = "http://localhost:11434/api/generate"
# This should NOT crash now because I fixed it, but we are testing the logic.
_run_zero_latency_dm_loop(
mock_device, MagicMock(), MagicMock(), mock_configs, mock_session, "target", cognitive_stack
)
# Verify dm_memory was used, NOT crm
mock_dm_memory.log_sent_dm.assert_called_once()
def test_dm_navigation_double_back_guard():
"""
Verifies that if we are still in a thread after one back press,
we press back again.
"""
mock_device = MagicMock()
# Mocking hierarchy sequence
mock_device.dump_hierarchy.side_effect = [
'<xml>resource-id="com.instagram.android:id/inbox_refreshable_thread_list_recyclerview"</xml>', # Loop 1 start
'<xml>resource-id="com.instagram.android:id/direct_thread_header"</xml>', # Context read
'<xml>resource-id="com.instagram.android:id/direct_thread_header"</xml>', # Send button find
'<xml>resource-id="com.instagram.android:id/direct_thread_header"</xml>', # Navigation check AFTER back
'<xml>resource-id="com.instagram.android:id/inbox_refreshable_thread_list_recyclerview"</xml>', # Loop 2 start (exit)
'<xml>resource-id="com.instagram.android:id/inbox_refreshable_thread_list_recyclerview"</xml>', # Buffer
]
mock_telepathic = MagicMock()
mock_telepathic._extract_semantic_nodes.side_effect = [
[{"x": 1, "y": 1}], # unread found in Loop 1
[{"text": "msg"}], # context
[{"x": 2, "y": 2}], # input
[{"x": 3, "y": 3}], # send
[], # Loop 2: no unread
[], # Buffer
]
mock_dopamine = MagicMock()
mock_dopamine.is_app_session_over.return_value = False
mock_dopamine.wants_to_change_feed.side_effect = [False, True, True] # exit after Loop 1
mock_dopamine.boredom = 0
mock_resonance = MagicMock()
mock_resonance.persona_prompt = "Persona"
mock_resonance.args.ai_model = "model"
cognitive_stack = {
"telepathic": mock_telepathic,
"dopamine": mock_dopamine,
"dm_memory": MagicMock(),
"resonance": mock_resonance,
}
mock_session = MagicMock()
mock_session.check_limit.return_value = False
mock_session.totalMessages = 0
mock_configs = MagicMock()
mock_configs.args.disable_ai_messaging = False
mock_configs.args.ai_condenser_model = "test-model"
mock_configs.args.ai_condenser_url = "http://localhost:11434/api/generate"
from unittest.mock import patch
import GramAddict.core.dm_engine as dm_engine
with (
patch("GramAddict.core.llm_provider.query_llm", return_value={"response": "hi"}),
patch("GramAddict.core.bot_flow._humanized_click"),
patch("GramAddict.core.bot_flow.sleep"),
patch("GramAddict.core.stealth_typing.ghost_type"),
):
dm_engine._run_zero_latency_dm_loop(
mock_device, MagicMock(), MagicMock(), mock_configs, mock_session, "target", cognitive_stack
)
# Expected calls:
# 1. First back from success flow (thread -> inbox)
# 2. Second back from guard check (if still in thread)
# 3. Third back from inbox exit (boredom check)
assert mock_device.press.call_count == 3
mock_device.press.assert_called_with("back")

View File

@@ -1,253 +0,0 @@
"""
TDD Test Suite: Explore Grid Navigation Hardening
==================================================
Reproduces the exact production failure from 2026-04-16 22:59 where the bot:
1. Blacklisted ALL image_buttons because of generic semantic strings
2. Could not match "first image in explore grid" via the keyword fast-path
3. VLM picked row 3 instead of row 1 because the prompt lacks spatial ranking
These tests MUST fail before the fix and pass after.
"""
import re
from GramAddict.core.telepathic_engine import TelepathicEngine
# ── Realistic node fixtures extracted from live XML dump 2026-04-16_22-59-53 ──
def make_node(x, y, bounds, semantic, res_id="com.instagram.android:id/dummy", text="", desc=""):
m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds)
l, t, r, b = map(int, m.groups()) if m else (0, 0, 0, 0)
return {
"x": x,
"y": y,
"width": r - l,
"height": b - t,
"area": (r - l) * (b - t),
"raw_bounds": bounds,
"semantic_string": semantic,
"resource_id": res_id,
"class_name": "android.widget.FrameLayout",
"selected": False,
"original_attribs": {"text": text, "desc": desc},
}
EXPLORE_GRID_NODES = [
# Row 1, Col 1 — this is what "first image" should match
make_node(
178,
559,
"[0,321][356,797]",
"description: '4 photos by Patsy Weingart at row 1, column 1', id context: 'grid card layout container'",
res_id="com.instagram.android:id/grid_card_layout_container",
desc="4 photos by Patsy Weingart at row 1, column 1",
),
# Row 1, Col 1 — child image_button (same area, no semantic info)
make_node(
178, 558, "[0,321][356,796]", "id context: 'image button'", res_id="com.instagram.android:id/image_button"
),
# Row 1, Col 2
make_node(
540,
559,
"[362,321][718,797]",
"description: 'Photo by Barbara at Row 1, Column 2', id context: 'grid card layout container'",
res_id="com.instagram.android:id/grid_card_layout_container",
desc="Photo by Barbara at Row 1, Column 2",
),
make_node(
540, 558, "[362,321][718,796]", "id context: 'image button'", res_id="com.instagram.android:id/image_button"
),
# Row 2, Col 2
make_node(
540,
1041,
"[362,803][718,1279]",
"description: 'Photo by Garima Bhaskar at Row 2, Column 2', id context: 'grid card layout container'",
res_id="com.instagram.android:id/grid_card_layout_container",
desc="Photo by Garima Bhaskar at Row 2, Column 2",
),
make_node(
540, 1040, "[362,803][718,1278]", "id context: 'image button'", res_id="com.instagram.android:id/image_button"
),
# Row 3, Col 1 — this is what the VLM wrongly picked
make_node(
178,
1523,
"[0,1285][356,1761]",
"description: '4 photos by Soul Of Nature Photography at row 3, column 1', id context: 'grid card layout container'",
res_id="com.instagram.android:id/grid_card_layout_container",
desc="4 photos by Soul Of Nature Photography at row 3, column 1",
),
make_node(
178, 1522, "[0,1285][356,1760]", "id context: 'image button'", res_id="com.instagram.android:id/image_button"
),
# Search bar
make_node(
487,
219,
"[32,173][943,265]",
"text: 'Search', id context: 'action bar search edit text'",
res_id="com.instagram.android:id/action_bar_search_edit_text",
text="Search",
),
]
class TestBlacklistPoisoning:
"""
Bug: Generic semantic strings like 'id context: image button' get blacklisted,
which kills ALL grid items because they share the same semantic string.
"""
def test_generic_semantic_should_not_be_blacklistable(self):
"""
A semantic string consisting ONLY of a generic id context (no text, no
description) is too ambiguous to blacklist. The engine must refuse to
blacklist it because it would poison all similar nodes.
"""
engine = TelepathicEngine()
# Clear any persisted state to test pure logic
engine._blacklist = {}
# Simulate the flow: the engine "clicked" an image_button and it failed
TelepathicEngine._last_click_context = {
"intent": "first image in explore grid",
"semantic_string": "id context: 'image button'",
"x": 178,
"y": 558,
"timestamp": 1000,
}
engine.reject_click("first image in explore grid")
# The blacklist should NOT contain this generic entry
blacklisted = engine._blacklist.get("first image in explore grid", [])
assert "id context: 'image button'" not in blacklisted, (
"CRITICAL: Generic semantic 'id context: image button' was blacklisted! "
"This poisons ALL image_buttons in ALL grids."
)
class TestExploreGridFastPath:
"""
Bug: There was no fast-path to match 'first image in explore grid' to a
grid_card_layout_container node. Now the Grid Fast-Path (Stage 1.25) handles
this deterministically via resource-ID + spatial sorting.
"""
def test_grid_fastpath_matches_container(self):
"""
The Grid Fast-Path must match 'first image in explore grid' to
a grid_card_layout_container node without calling VLM/embeddings.
"""
engine = TelepathicEngine()
# Build a minimal XML that the engine can parse — but we test the fast-path
# directly by calling find_best_node with mocked extraction
from unittest.mock import patch
with patch.object(engine, "_extract_semantic_nodes", return_value=EXPLORE_GRID_NODES):
with patch.object(engine, "_is_instagram_context", return_value=True):
result = engine.find_best_node(
"<fake>", "first image in explore grid", min_confidence=0.82, device=None
)
assert result is not None, (
"Grid Fast-Path returned None for 'first image in explore grid'. "
"This forces every explore grid tap to use the expensive VLM fallback."
)
assert any(
k in result.get("semantic_string", "").lower() for k in ["image button", "grid card layout container"]
), f"Grid Fast-Path selected wrong node type: {result.get('semantic_string')}"
def test_grid_fastpath_prefers_topmost_row(self):
"""
When multiple grid items match, the Grid Fast-Path must prefer the
topmost one (smallest Y = row 1) since the intent says 'first'.
"""
engine = TelepathicEngine()
from unittest.mock import patch
with patch.object(engine, "_extract_semantic_nodes", return_value=EXPLORE_GRID_NODES):
with patch.object(engine, "_is_instagram_context", return_value=True):
result = engine.find_best_node(
"<fake>", "first image in explore grid", min_confidence=0.82, device=None
)
if result is not None:
# Row 1 items have y ≈ 559, Row 3 items have y ≈ 1523
assert result["y"] < 800, (
f"Grid Fast-Path selected a grid item at y={result['y']} (row 3+) "
f"instead of row 1 (y≈559). The intent says 'first image'!"
)
class TestVerifySuccessExploreGrid:
"""
Bug: verify_success for explore grid tap checks for feed markers, but
the post_load_timeout dump proved the bot was STILL on the explore grid.
The verification correctly returned False, but the response was to blacklist
the grid_card_layout_container — which is the WRONG reaction. The tap
just didn't register; it doesn't mean the mapping is wrong.
"""
def test_verify_success_returns_none_when_still_on_grid(self):
"""
If we tapped a grid item but the screen still shows the explore grid
(no feed markers), verify_success must return None (Inconclusive).
"""
engine = TelepathicEngine()
# Simulate that we just clicked a grid item
TelepathicEngine._last_click_context = {
"intent": "first image in explore grid",
"semantic_string": "description: '4 photos by Patsy Weingart at row 1, column 1'",
"x": 178,
"y": 559,
"timestamp": 1000,
}
# Post-click XML still shows the explore grid (no feed markers)
still_on_grid_xml = """
<node class="android.widget.FrameLayout">
<node resource-id="com.instagram.android:id/explore_action_bar" />
<node resource-id="com.instagram.android:id/grid_card_layout_container"
content-desc="4 photos by Patsy Weingart at row 1, column 1" />
<node resource-id="com.instagram.android:id/image_button" />
</node>
"""
result = engine.verify_success("first image in explore grid", still_on_grid_xml)
assert result is None, "verify_success should be inconclusive (None) when still on explore grid"
def test_verify_success_returns_true_when_post_opened(self):
"""
If the grid tap succeeded and we're now viewing a post with feed markers,
verify_success must return True.
"""
engine = TelepathicEngine()
TelepathicEngine._last_click_context = {
"intent": "first image in explore grid",
"semantic_string": "description: '4 photos by Patsy Weingart at row 1, column 1'",
"x": 178,
"y": 559,
"timestamp": 1000,
}
# Post-click XML shows a feed post (has feed markers)
post_view_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_button_comment" />
<node resource-id="com.instagram.android:id/row_feed_button_share" />
</node>
"""
result = engine.verify_success("first image in explore grid", post_view_xml)
assert result is True, "verify_success should pass when post view is visible"

View File

@@ -1,110 +0,0 @@
"""
🔴 RED TDD: VLM Structural Guard Inconsistency + GOAP Back-Press Loop
Bug A: VLM guard enforces "must be at bottom" for ALL nav intent keywords,
but "following"/"follower" are PROFILE STATS, not nav tabs. The inner
_structural_sanity_check correctly only enforces for "tab" intents.
Bug B: GOAP back-press loop has no circuit breaker. If the planner keeps
pressing back on the same screen, it eventually exits Instagram.
"""
from unittest.mock import MagicMock
from GramAddict.core.goap import GoalExecutor, ScreenType
from GramAddict.core.telepathic_engine import TelepathicEngine
class TestVlmGuardFollowingConsistency:
"""The VLM guard must NOT reject 'following' count elements at the top of the screen."""
def test_following_intent_allows_top_screen_elements(self):
"""
The 'following' count on a profile is at Y≈246 (top 10%).
The VLM Structural Guard must NOT reject this as a 'hallucinated nav tab'.
It's a profile stat, not a tab.
"""
engine = TelepathicEngine()
screen_height = 2424
# Simulate the exact node the VLM found
following_node = {
"semantic_string": "text: '2,285 following', id context: 'row profile header following container'",
"y": 246,
"area": 5000,
"class_name": "android.widget.TextView",
"resource_id": "row_profile_header_following_container",
}
intent = "tap following list"
# Inner structural guard should accept this (it has the "tab" check)
is_valid = engine._structural_sanity_check(following_node, intent, screen_height)
assert is_valid is True, (
"Inner structural guard rejected 'following' element at Y=246. "
"The intent 'tap following list' does not contain 'tab', so the nav-tab enforcement should not apply."
)
def test_follower_intent_allows_top_screen_elements(self):
"""Same bug for 'follower' count on a profile."""
engine = TelepathicEngine()
screen_height = 2424
follower_node = {
"semantic_string": "text: '2,622 followers', id context: 'row profile header followers container'",
"y": 246,
"area": 5000,
"class_name": "android.widget.TextView",
"resource_id": "row_profile_header_followers_container",
}
intent = "open followers list"
is_valid = engine._structural_sanity_check(follower_node, intent, screen_height)
assert is_valid is True, "Inner structural guard rejected 'followers' element at Y=246."
class TestGoapBackPressCircuitBreaker:
"""GOAP must detect and abort back-press loops on the same screen."""
def test_consecutive_back_presses_on_same_screen_aborts(self):
"""
If the GOAP planner presses back 3+ times on the same screen type
without any screen transition, it should abort instead of continuing
to press back until it exits the app.
"""
device = MagicMock()
device.app_id = "com.instagram.android"
device._get_current_app.return_value = "com.instagram.android"
goap = GoalExecutor(device, bot_username="testbot")
goap._sae = MagicMock()
goap._sae.ensure_clear_screen.return_value = True
# Simulate being stuck on HOME_FEED with only back available
call_count = 0
def mock_perceive():
nonlocal call_count
call_count += 1
return {
"screen_type": ScreenType.HOME_FEED,
"available_actions": ["press back"],
"context": {},
"selected_tab": "feed_tab",
}
goap.perceive = mock_perceive
# Mock _execute_action to always succeed (pressing back "works" but stays on same screen)
goap._execute_action = MagicMock(return_value=True)
result = goap.achieve("open following list", max_steps=15)
# The bot should NOT have pressed back more than 3 times
back_calls = [c for c in goap._execute_action.call_args_list if c[0][0] == "press back"]
assert len(back_calls) <= 3, (
f"GOAP pressed back {len(back_calls)} times on the same screen. "
"It should abort after 3 consecutive back-presses with no progress."
)
assert result is False, "GOAP should fail when stuck in a back-press loop."

View File

@@ -1,35 +0,0 @@
from unittest.mock import MagicMock
from GramAddict.core.goap import GoalPlanner, ScreenType
def test_goal_planner_uses_hd_map_over_linguistic_match():
"""
Tests that the GoalPlanner uses the HD Map (ScreenTopology) as its
primary routing strategy, even during Blank Start discovery.
The HD Map returns canonical action intents that the TelepathicEngine
interprets — not just available_actions from the XML dump.
"""
planner = GoalPlanner("test_user")
# Mock the internal structures
planner.knowledge = MagicMock()
planner.knowledge.is_trap.return_value = False
planner.knowledge.get_requirements.return_value = None # Force Blank Start
# Simulate current screen
screen = {
"screen_type": ScreenType.HOME_FEED,
"available_actions": ["tap explore grid", "tap messages"],
"context": {},
}
goal = "open explore feed"
result = planner.plan_next_step(goal, screen, explored_nav_actions=set())
# HD Map routes HOME_FEED → EXPLORE_GRID via "tap explore tab"
assert result == "tap explore tab", (
f"Expected HD Map to route via 'tap explore tab', got '{result}'. "
"The HD Map should override linguistic matching as the primary strategy."
)

View File

@@ -1,132 +0,0 @@
"""
🔴 RED TDD: GOAP False Unlearning Fix
Reproduces Bug 3: The bot taps 'home tab' while ALREADY on home_feed,
detects 'no UI change', and destructively unlearns the correct mapping.
These tests MUST FAIL before the fix and PASS after.
"""
from unittest.mock import MagicMock, patch
from GramAddict.core.goap import GoalExecutor, ScreenType
def _make_goap(screen_type=ScreenType.HOME_FEED, available_actions=None):
"""Create a GoalExecutor with a mocked device that returns a fixed screen state."""
device = MagicMock()
device.app_id = "com.instagram.android"
device._get_current_app.return_value = "com.instagram.android"
goap = GoalExecutor(device, bot_username="testbot")
goap._sae = MagicMock()
goap._sae.ensure_clear_screen.return_value = True
# Mock perceive to return a fixed screen state
if available_actions is None:
available_actions = ["tap profile tab", "tap explore tab", "scroll down", "press back", "tap home tab"]
goap.perceive = MagicMock(
return_value={
"screen_type": screen_type,
"available_actions": available_actions,
"context": {},
"selected_tab": "feed_tab",
}
)
return goap
class TestGoapRecalledPathPreCheck:
"""Bug 3 Part A: Recalled path should skip execution when goal is already achieved."""
def test_recalled_path_skips_when_goal_already_achieved(self):
"""
If the bot is already on HOME_FEED and the goal is 'open home feed',
_execute_recalled_path must return True WITHOUT executing any steps.
"""
goap = _make_goap(screen_type=ScreenType.HOME_FEED)
steps = [{"action": "tap home tab"}]
# Mock _execute_action to track if it was called
goap._execute_action = MagicMock(return_value=True)
result = goap._execute_recalled_path(steps, "open home feed")
assert result is True, "Recalled path should succeed immediately when goal is already achieved."
(
goap._execute_action.assert_not_called(),
("_execute_action should NOT have been called — goal was already achieved."),
)
class TestGoapNoUnlearnWhenAlreadyOnTarget:
"""Bug 3 Part B: Navigation to current screen should not trigger reject_click."""
def test_no_unlearn_when_tapping_home_on_home_feed(self):
"""
If the bot is on HOME_FEED and taps 'home tab', the XML won't change.
But since the goal 'open home feed' is already achieved, the action
should be treated as SUCCESS, not FAILURE.
"""
goap = _make_goap(screen_type=ScreenType.HOME_FEED)
# Make dump_hierarchy return the same XML (no change)
static_xml = "<hierarchy><node resource-id='feed_tab' text='Home' /></hierarchy>"
goap.device.dump_hierarchy.return_value = static_xml
# Mock the TelepathicEngine
mock_engine = MagicMock()
mock_engine.find_best_node.return_value = {
"x": 540,
"y": 2300,
"score": 0.95,
"semantic": "description: 'Home', id context: 'feed tab'",
"source": "qdrant_nav",
"original_attribs": {},
}
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance", return_value=mock_engine):
result = goap._execute_action("tap home tab", goal="open home feed")
# The action should succeed (we're already where we need to be)
assert result is True, (
"GOAP incorrectly treated 'tap home tab' on home_feed as a failure. "
"This causes destructive unlearning of valid navigation knowledge."
)
# Critically: reject_click should NEVER have been called
(
mock_engine.reject_click.assert_not_called(),
("reject_click was called — this destroys the correct 'tap home tab''feed tab' mapping!"),
)
def test_genuine_navigation_failure_still_triggers_reject(self):
"""
If the bot is on HOME_FEED and taps 'tap explore tab' but nothing changes,
that IS a genuine failure and reject_click SHOULD be called.
"""
goap = _make_goap(screen_type=ScreenType.HOME_FEED)
static_xml = "<hierarchy><node resource-id='feed_tab' text='Home' /></hierarchy>"
goap.device.dump_hierarchy.return_value = static_xml
mock_engine = MagicMock()
mock_engine.find_best_node.return_value = {
"x": 540,
"y": 2300,
"score": 0.95,
"semantic": "description: 'Explore', id context: 'explore_tab'",
"source": "qdrant_nav",
"original_attribs": {},
}
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance", return_value=mock_engine):
result = goap._execute_action("tap explore tab", goal="open explore feed")
# This should fail — we expected to go to explore but UI didn't change
assert result is False, (
"GOAP should reject a navigation that produced no UI change " "when the goal is NOT already achieved."
)

View File

@@ -1,118 +0,0 @@
"""
🔴 RED TDD: Step-Aware Navigation Validation + Topology Guard
Tests that validate the GOAP planner correctly handles multi-step navigation:
- Intermediate steps (tap profile tab → OWN_PROFILE) must be ACCEPTED
- Wrong screens (tap profile tab → REELS_FEED) must be REJECTED
- Structural HD Map actions must NEVER be aversively learned as traps
"""
import os
import sys
from unittest.mock import MagicMock
import pytest
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
from GramAddict.core.goap import GoalExecutor, GoalPlanner, ScreenType
from GramAddict.core.screen_topology import ScreenTopology
class TestStepAwareValidation:
"""_execute_action must validate intermediate steps, not just final goals."""
@pytest.fixture
def executor(self):
device = MagicMock()
device.dump_hierarchy.return_value = "<xml>mock</xml>"
device.click = MagicMock()
executor = GoalExecutor(device, bot_username="testbot")
executor.action_failures = {}
return executor
def test_intermediate_step_accepted(self, executor):
"""
Action: 'tap profile tab' from HOME_FEED
Goal: 'open following list'
Landing: OWN_PROFILE (intermediate step)
The validator must ACCEPT this — OWN_PROFILE is the correct
intermediate screen for 'tap profile tab' from HOME_FEED.
It must NOT reject it because the goal says 'following'.
"""
# The critical question: does expected_screen_for_action return OWN_PROFILE?
expected = ScreenTopology.expected_screen_for_action("tap profile tab", ScreenType.HOME_FEED)
assert expected == ScreenType.OWN_PROFILE, "HD Map must know 'tap profile tab' → OWN_PROFILE"
# And the goal target is FOLLOW_LIST, which is DIFFERENT from where we landed
goal_target = ScreenTopology.goal_to_target_screen("open following list")
assert goal_target == ScreenType.FOLLOW_LIST
# The old code would fail here because it checks goal_target against landing screen.
# The new code checks expected (from action) against landing screen.
landing = ScreenType.OWN_PROFILE
assert landing == expected, "Step validation: landing matches expected → ACCEPT"
assert landing != goal_target, "Goal not yet achieved — but step is valid"
def test_wrong_screen_rejected(self, executor):
"""
Action: 'tap profile tab' from HOME_FEED
Landing: REELS_FEED (wrong!)
The validator must REJECT this — expected was OWN_PROFILE.
"""
expected = ScreenTopology.expected_screen_for_action("tap profile tab", ScreenType.HOME_FEED)
landing = ScreenType.REELS_FEED
assert landing != expected, "Wrong screen: REELS_FEED ≠ OWN_PROFILE → REJECT"
def test_final_step_accepted(self, executor):
"""
Action: 'tap following list' from OWN_PROFILE
Goal: 'open following list'
Landing: FOLLOW_LIST (final step!)
The validator must ACCEPT this.
"""
expected = ScreenTopology.expected_screen_for_action("tap following list", ScreenType.OWN_PROFILE)
assert expected == ScreenType.FOLLOW_LIST
landing = ScreenType.FOLLOW_LIST
assert landing == expected
class TestTopologyGuard:
"""Structural HD Map actions must NEVER be aversively learned as traps."""
def test_structural_action_not_burned(self):
"""'tap profile tab' on HOME_FEED is structural — must not be burned."""
assert ScreenTopology.is_structural_action(ScreenType.HOME_FEED, "tap profile tab") is True
def test_non_structural_action_can_be_burned(self):
"""'open following list' on HOME_FEED is NOT structural — can be burned."""
assert ScreenTopology.is_structural_action(ScreenType.HOME_FEED, "open following list") is False
class TestSSOTConsolidation:
"""All goal→screen mappings must use ScreenTopology as SSOT."""
def test_is_goal_achieved_uses_topology_for_following(self):
"""_is_goal_achieved must recognize FOLLOW_LIST for 'open following list'."""
planner = GoalPlanner("testbot")
planner.knowledge = MagicMock()
planner.knowledge.is_trap.return_value = False
planner.knowledge.get_requirements.return_value = None
# On FOLLOW_LIST with goal "open following list" → achieved
result = planner._is_goal_achieved("open following list", ScreenType.FOLLOW_LIST, {})
assert result is True, "Goal must be achieved when on target screen"
def test_is_goal_achieved_not_achieved_on_wrong_screen(self):
planner = GoalPlanner("testbot")
result = planner._is_goal_achieved("open following list", ScreenType.HOME_FEED, {})
assert result is False
def test_navigate_to_screen_uses_topology(self):
"""navigate_to_screen must use ScreenTopology.screen_name_to_goal()."""
assert ScreenTopology.screen_name_to_goal("FollowingList") == "open following list"
assert ScreenTopology.screen_name_to_goal("ExploreFeed") == "open explore feed"
assert ScreenTopology.screen_name_to_goal("OwnProfile") == "open profile"

View File

@@ -1,55 +0,0 @@
from unittest.mock import MagicMock, patch
from GramAddict.core.goap import GoalExecutor
from GramAddict.core.screen_topology import ScreenType
def test_goap_unlearns_transition_on_back_press_trap():
"""Prove that GOAP forgets a specific topological transition when it gets trapped and spams 'back'."""
device = MagicMock()
orchestrator = GoalExecutor(device, "testuser")
# Mocking internal state
goal = "open messages"
def mock_exec(*args, **kwargs):
print("EXECUTING:", args, kwargs)
return True
orchestrator._execute_action = MagicMock(side_effect=mock_exec) # "press back" succeeds
orchestrator.path_memory = MagicMock()
orchestrator.path_memory.recall_path.return_value = None
# To test the back-press circuit breaker, we just need to feed it 3 "press back" actions.
# Since achieve is complex, let's just test that the required logic
# exists inside it. The circuit breaker is in the "EXECUTE" block of achieve.
# We will mock the planner to return "press back" 3 times.
orchestrator.planner.plan_next_step = MagicMock(side_effect=[["press back"], ["press back"], ["press back"], []])
orchestrator.perceive = MagicMock(
return_value={"screen_type": ScreenType.EXPLORE_GRID, "available_actions": ["mock"]}
)
with patch("GramAddict.core.qdrant_memory.NavigationMemoryDB") as MockNavDB:
mock_nav_instance = MockNavDB.return_value
# We need to simulate that `steps_taken` already had "tap explore tab"
# However, achieve starts with an empty `steps_taken`.
# So we mock the internal variables if possible, but they are local.
# Alternatively, we make the planner return "tap explore tab", then 3 "press back"s.
orchestrator.planner.plan_next_step = MagicMock(
side_effect=["tap explore tab", "press back", "press back", "press back", "press back", None]
)
# Act
result = orchestrator.achieve(goal, max_steps=10)
# Assert (RED)
assert result is False
# Did it forget the path? (learn_path with success=False)
assert orchestrator.path_memory.learn_path.called
# Did it unlearn the transition?
print("MockNavDB calls:", MockNavDB.mock_calls)
print("NavInstance calls:", mock_nav_instance.mock_calls)
mock_nav_instance.unlearn_transition.assert_called_once_with(ScreenType.EXPLORE_GRID.value, "tap explore tab")

View File

@@ -1,110 +0,0 @@
from GramAddict.core.telepathic_engine import TelepathicEngine
class TestGridRetryDiversity:
"""
TDD Tests: Reproduces Bug 2 from the 2026-04-17 09:56 run.
The Grid Fast-Path always selects the exact same node on every retry
because it sorts by (y, x) and picks index 0. When a click fails,
the retry should skip previously-failed positions.
"""
def setup_method(self):
self.engine = TelepathicEngine()
def _make_grid_nodes(self):
"""Create 6 realistic explore grid nodes (2 rows × 3 cols)."""
return [
{
"semantic_string": "id context: 'image button'",
"x": 178,
"y": 558,
"area": 169100,
"resource_id": "com.instagram.android:id/image_button",
"class_name": "android.widget.Button",
"selected": False,
"original_attribs": {"text": "", "desc": ""},
},
{
"semantic_string": "id context: 'image button'",
"x": 540,
"y": 558,
"area": 169100,
"resource_id": "com.instagram.android:id/image_button",
"class_name": "android.widget.Button",
"selected": False,
"original_attribs": {"text": "", "desc": ""},
},
{
"semantic_string": "id context: 'image button'",
"x": 902,
"y": 558,
"area": 169100,
"resource_id": "com.instagram.android:id/image_button",
"class_name": "android.widget.Button",
"selected": False,
"original_attribs": {"text": "", "desc": ""},
},
{
"semantic_string": "id context: 'image button'",
"x": 178,
"y": 1040,
"area": 169100,
"resource_id": "com.instagram.android:id/image_button",
"class_name": "android.widget.Button",
"selected": False,
"original_attribs": {"text": "", "desc": ""},
},
{
"semantic_string": "id context: 'image button'",
"x": 540,
"y": 1040,
"area": 169100,
"resource_id": "com.instagram.android:id/image_button",
"class_name": "android.widget.Button",
"selected": False,
"original_attribs": {"text": "", "desc": ""},
},
{
"semantic_string": "id context: 'image button'",
"x": 902,
"y": 1040,
"area": 169100,
"resource_id": "com.instagram.android:id/image_button",
"class_name": "android.widget.Button",
"selected": False,
"original_attribs": {"text": "", "desc": ""},
},
]
def test_first_call_returns_topmost_leftmost(self):
"""Without skip_positions, Grid Fast-Path returns (178, 558)."""
nodes = self._make_grid_nodes()
result = self.engine._grid_fast_path("first image in explore grid", nodes)
assert result is not None
assert result["x"] == 178
assert result["y"] == 558
def test_retry_skips_failed_position(self):
"""With skip_positions={(178, 558)}, the next node (540, 558) is returned."""
nodes = self._make_grid_nodes()
result = self.engine._grid_fast_path("first image in explore grid", nodes, skip_positions={(178, 558)})
assert result is not None
assert (result["x"], result["y"]) == (540, 558), f"Expected (540, 558) but got ({result['x']}, {result['y']})"
def test_skip_multiple_positions(self):
"""Skipping 2 positions returns the 3rd grid item."""
nodes = self._make_grid_nodes()
result = self.engine._grid_fast_path(
"first image in explore grid", nodes, skip_positions={(178, 558), (540, 558)}
)
assert result is not None
assert (result["x"], result["y"]) == (902, 558)
def test_all_positions_skipped_returns_none(self):
"""If every grid node is skipped, return None to trigger VLM fallback."""
nodes = self._make_grid_nodes()
all_positions = {(n["x"], n["y"]) for n in nodes}
result = self.engine._grid_fast_path("first image in explore grid", nodes, skip_positions=all_positions)
assert result is None

View File

@@ -1,87 +0,0 @@
from unittest.mock import MagicMock
import requests
from GramAddict.core.llm_provider import query_telepathic_llm
class TestLLMProvider:
def test_vlm_timeout_aborts_fast_connections(self, monkeypatch):
"""
OpenRouter/Cloud connections must timeout around ~45s.
If a timeout exception is raised by requests, query_telepathic_llm should gracefully catch it
and return empty "{}" JSON.
"""
def mock_post(*args, **kwargs):
timeout = kwargs.get("timeout")
assert timeout == 45, "Expected 45s strict timeout for openrouter"
raise requests.exceptions.ReadTimeout("Mocked read timeout")
monkeypatch.setattr(requests, "post", mock_post)
# Test Cloud URL
result = query_telepathic_llm(
model="openrouter/qwen3.5:latest",
url="https://openrouter.ai/api/v1/chat/completions",
system_prompt="sys",
user_prompt="user",
use_local_edge=False,
)
assert result == "{}"
def test_vlm_timeout_allows_local_processing(self, monkeypatch):
"""
Localhost (Ollama) connections must have a significantly higher timeout (180s)
so cold starts loading into VRAM don't drop.
"""
def mock_post(*args, **kwargs):
kwargs.get("url") or args[0]
timeout = kwargs.get("timeout")
assert timeout == 180, f"Expected 180s extended timeout for localhost, got {timeout}"
mock_resp = MagicMock()
mock_resp.status_code = 200
# For ollama/vllm mimic structure
mock_resp.json.return_value = {"response": '{"clicked": "true"}'}
return mock_resp
monkeypatch.setattr(requests, "post", mock_post)
# Test Local URL
result = query_telepathic_llm(
model="qwen3.5:latest",
url="http://localhost:11434/api/generate",
system_prompt="sys",
user_prompt="user",
use_local_edge=False,
)
assert result == '{"clicked": "true"}'
def test_local_edge_override_applies_timeout(self, monkeypatch):
"""
If use_local_edge=True is set, it overrides the URL to localhost and MUST apply 180s.
"""
def mock_post(*args, **kwargs):
timeout = kwargs.get("timeout")
assert timeout == 180, "Expected 180s extended timeout when forced to local edge"
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"response": '{"edge": "true"}'}
return mock_resp
monkeypatch.setattr(requests, "post", mock_post)
result = query_telepathic_llm(
model="openrouter",
url="https://openrouter...",
system_prompt="sys",
user_prompt="user",
use_local_edge=True, # Force local
)
assert result == '{"edge": "true"}'

View File

@@ -1,38 +0,0 @@
from unittest.mock import MagicMock, patch
from GramAddict.core.llm_provider import query_llm
def test_query_llm_passes_timeout_to_requests():
"""
Verifies that the query_llm wrapper passes the configured
timeout down to the requests.post call, enabling long
generation tasks like comments to complete without crashing.
"""
mock_response = MagicMock()
mock_response.json.return_value = {"response": "test"}
mock_response.raise_for_status.return_value = None
with patch("GramAddict.core.llm_provider.requests.post", return_value=mock_response) as mock_post:
# Act with custom 180s timeout
# Using format_json=False because Ollama branch accesses resp_json["response"]
query_llm("http://localhost:11434", "test_model", "test_prompt", timeout=180, format_json=False)
# Assert
mock_post.assert_called_once()
_, kwargs = mock_post.call_args
assert kwargs.get("timeout") == 180, "query_llm failed to pass the custom timeout to requests.post"
def test_query_llm_default_timeout_is_configurable():
"""
Verifies that if no timeout is passed, by default we still use a configured or sensible value (60s).
"""
mock_response = MagicMock()
mock_response.json.return_value = {"response": "test"}
with patch("GramAddict.core.llm_provider.requests.post", return_value=mock_response) as mock_post:
query_llm("http://localhost:11434", "test_model", "test_prompt", format_json=False)
_, kwargs = mock_post.call_args
assert kwargs.get("timeout") == 180, "query_llm default timeout should act as fallback"

View File

@@ -1,78 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.llm_provider import unload_ollama_models
def test_unload_ollama_models_sends_keep_alive_0():
"""
Ensures that when unload_ollama_models is called, it correctly identifies
local Ollama models from the config and sends a POST request with keep_alive: 0
to unload them from VRAM.
"""
mock_configs = MagicMock()
mock_configs.args.ai_telepathic_model = "llama3.2:1b"
mock_configs.args.ai_telepathic_url = "http://localhost:11434/api/generate"
mock_configs.args.ai_fallback_model = "qwen2.5:latest"
mock_configs.args.ai_fallback_url = "http://127.0.0.1:11434/api/generate"
# Cloud model should NOT be unloaded
mock_configs.args.ai_model = "openrouter/anthropic/claude"
mock_configs.args.ai_model_url = "https://openrouter.ai/api/v1/chat/completions"
with patch("GramAddict.core.llm_provider.requests.post") as mock_post:
unload_ollama_models(mock_configs)
# unload_ollama_models uses a background thread, so we must wait slightly or mock the threading.
# But wait! We can just call the inner _unload directly, or wait a fraction of a second.
import time
time.sleep(0.1)
# Expect 2 calls (for the 2 local models)
assert mock_post.call_count == 2
# Extract the JSON bodies of the calls
called_json_args = [call.kwargs.get("json") for call in mock_post.call_args_list]
# Verify keep_alive: 0 is present for both
assert {"model": "llama3.2:1b", "keep_alive": 0} in called_json_args
assert {"model": "qwen2.5:latest", "keep_alive": 0} in called_json_args
# Verify cloud model was skipped
assert not any(arg.get("model") == "openrouter/anthropic/claude" for arg in called_json_args)
def test_bot_flow_triggers_ollama_cleanup():
"""
Ensures that the start_bot function triggers unload_ollama_models
in its finally block when finishing or aborting.
"""
from GramAddict.core.bot_flow import start_bot
with (
patch("GramAddict.core.bot_flow.Config") as mock_config_cls,
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.session_state.SessionState.inside_working_hours", return_value=(True, 0)),
patch("GramAddict.core.llm_provider.unload_ollama_models") as mock_unload,
):
mock_configs = MagicMock()
mock_config_cls.return_value = mock_configs
# Simulate a crash inside the try block
mock_device = MagicMock()
mock_device.wake_up.side_effect = Exception("Simulate immediate crash")
mock_create_device.return_value = mock_device
with pytest.raises(Exception, match="Simulate immediate crash"):
start_bot()
# Verify the cleanup was STILL called even during a crash
mock_unload.assert_called_once_with(mock_configs)

View File

@@ -1,67 +0,0 @@
import os
import sys
from unittest.mock import MagicMock, patch
import pytest
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
from GramAddict.core.goap import PathMemory
class FakePoint:
def __init__(self, payload):
self.payload = payload
@pytest.fixture
def mock_qdrant_base():
with patch("GramAddict.core.qdrant_memory.QdrantBase") as mock:
instance = mock.return_value
instance.is_connected = True
instance.collection_name = "goap_paths_v1"
instance.client = MagicMock()
instance._get_embedding.return_value = [0.1] * 768
yield instance
def test_path_overwrites_on_failure(mock_qdrant_base):
"""
TDD Case: If a success path exists, but then a failure occurs,
the failure should overwrite the success (or at least be the
recalled path) because they share the same seed.
"""
pm = PathMemory()
pm._db = mock_qdrant_base # Inject our mock
goal = "open messages"
start = "home_feed"
# 1. Learn success
pm.learn_path(goal, start, [{"action": "tap messages tab"}], True)
# Verify upsert seed
# With our fix, it should be simply "open messages|home_feed"
args, kwargs = mock_qdrant_base.upsert_point.call_args
assert args[0] == f"{goal}|{start}"
assert args[1]["success"] is True
# 2. Learn failure (same goal, same start)
# This should call upsert_point with the SAME seed, thus overwriting
pm.learn_path(goal, start, [{"action": "tap reels tab"}] * 15, False)
args, kwargs = mock_qdrant_base.upsert_point.call_args
assert args[0] == f"{goal}|{start}"
assert args[1]["success"] is False
assert args[1]["step_count"] == 15
# 3. Recall
# We mock return from Qdrant - only one item (the latest)
query_result = MagicMock()
query_result.points = [FakePoint(args[1])] # The failure payload
mock_qdrant_base.client.query_points.return_value = query_result
recalled = pm.recall_path(goal, start)
# Should be None because the latest entry has success=False
assert recalled is None

View File

@@ -1,89 +0,0 @@
"""
Unit test: Humanized Scroll Speed Variations.
Validates that different scroll behavior branches produce
gestures with different timing characteristics.
"""
from unittest.mock import MagicMock, patch
import pytest
from GramAddict.core.physics.biomechanics import PhysicsBody
from GramAddict.core.physics.sendevent_injector import SendEventInjector
@pytest.fixture(autouse=True)
def reset_singletons():
PhysicsBody.reset()
SendEventInjector.reset()
yield
PhysicsBody.reset()
SendEventInjector.reset()
@patch("GramAddict.core.physics.humanized_input.SendEventInjector")
def test_humanized_scroll_speeds(MockInjector):
mock_injector = MagicMock()
MockInjector.get_instance.return_value = mock_injector
device = MagicMock()
device.get_info.return_value = {"displayHeight": 2400, "displayWidth": 1080}
from GramAddict.core.physics.humanized_input import humanized_scroll
# First scroll
humanized_scroll(device)
assert mock_injector.inject_gesture.called
# Verify gesture data was passed correctly
args = mock_injector.inject_gesture.call_args
points = args[0][0]
timing = args[0][1]
# Points must be valid (x, y, pressure) tuples
for p in points:
assert len(p) == 3, f"Expected (x, y, pressure), got {p}"
# Timing intervals must all be positive
for t in timing:
assert t > 0, f"Timing interval must be positive, got {t}"
@patch("GramAddict.core.physics.humanized_input.SendEventInjector")
def test_humanized_scroll_skip_is_strictly_forward(MockInjector):
"""
Ensures that when is_skip=True (e.g. for aggressive ad skipping),
the generated gesture is purely forward (bottom to top swipe),
and does NOT contain backwards 'Doomscroll corrections' or
biomechanical 'reading pauses'.
"""
mock_injector = MagicMock()
MockInjector.get_instance.return_value = mock_injector
device = MagicMock()
device.get_info.return_value = {"displayHeight": 2400, "displayWidth": 1080}
from GramAddict.core.physics.humanized_input import humanized_scroll
# Run multiple times to overcome randomness in play_choice / do_correction
for _ in range(50):
mock_injector.reset_mock()
humanized_scroll(device, is_skip=True)
args = mock_injector.inject_gesture.call_args
points = args[0][0]
timing = args[0][1]
# In a forward scroll (bottom to top), the Y coordinate must go from a higher number to a lower number.
start_y = points[0][1]
end_y = points[-1][1]
# End Y MUST be less than Start Y (meaning we scrolled down the feed, swiping finger up)
assert end_y < start_y, f"Expected end_y ({end_y}) to be < start_y ({start_y}) for a skip."
# Verify no long pauses (reading pause adds 0.5s to 2.0s to timing)
for t in timing:
assert (
t < 500
), "Found a massive pause in a skip gesture, meaning a reading_pause or dwell was incorrectly inserted!"

View File

@@ -1,94 +0,0 @@
from unittest.mock import MagicMock
from GramAddict.core.q_nav_graph import QNavGraph
from GramAddict.core.telepathic_engine import TelepathicEngine
class TestProfileInteractionSync:
"""
TDD Tests: Reproduces the 'Already Followed -> Favorites' and 'No Story Ring'
bugs from 2026-04-17 live run.
"""
def setup_method(self):
self.engine = TelepathicEngine()
self.mock_device = MagicMock()
self.mock_device.deviceV2 = MagicMock()
self.mock_device.app_id = "com.instagram.android"
self.mock_device._get_current_app.return_value = "com.instagram.android"
self.nav_graph = QNavGraph(self.mock_device)
def test_prevent_tapping_following_button(self):
"""
If the intent is to follow, but the matching node says 'following' or 'gefolgt',
the engine must skip the click to prevent opening the Favorites/Mute bottom sheet.
"""
# Simulate a profile where the user is already followed
# Test vector-based matching fallback
self.engine._blacklist = {}
# Mock the parsing instead of old extraction
self.engine._parser = MagicMock()
self.engine._parser.parse.return_value = MagicMock()
# We must return SpatialNode objects for the new architecture
from GramAddict.core.perception.spatial_parser import SpatialNode
mock_node = SpatialNode(MagicMock())
mock_node.resource_id = "com.instagram.android:id/button"
mock_node.text = "Following"
mock_node.bounds = [500, 600, 600, 650]
self.engine._parser.get_clickable_nodes.return_value = [mock_node]
self.engine._structural_sanity_check = MagicMock(return_value=True)
self.engine._is_instagram_context = MagicMock(return_value=True)
# Mock resolver to return our mock node
self.engine._resolver = MagicMock()
self.engine._resolver.resolve.return_value = mock_node
result = self.engine.find_best_node("<mock></mock>", "tap follow button on profile", device=self.mock_device)
assert result is not None, "Engine should return a skip result, not None"
assert result.get("skip") is True, "Must return skip: True to prevent Favorites menu from opening"
assert result.get("semantic") == "already_followed"
def test_story_ring_not_present_skips_click(self):
"""
If no story ring is explicitly in the XML, bot_flow should not execute
the transition (simulated here by checking our XML evaluation logic).
"""
xml_without_story = """
<hierarchy>
<node resource-id="com.instagram.android:id/row_profile_header_imageview" content-desc="Profile picture" />
<node text="marisaundmarc" />
</hierarchy>
"""
has_story = (
"reel_ring" in xml_without_story
or "unseen story" in xml_without_story.lower()
or "story von" in xml_without_story.lower()
)
assert has_story is False, "Logic falsely identified a story when there is only a generic profile picture"
def test_story_ring_present_allows_click(self):
"""
If a story ring is present, the logic should allow the interaction.
"""
xml_with_story = """
<hierarchy>
<node resource-id="com.instagram.android:id/reel_ring" />
<node resource-id="com.instagram.android:id/row_profile_header_imageview" content-desc="mercedesbenz_de's unseen story" />
</hierarchy>
"""
has_story = (
"reel_ring" in xml_with_story
or "unseen story" in xml_with_story.lower()
or "story von" in xml_with_story.lower()
)
assert has_story is True, "Logic failed to identify active story ring"

View File

@@ -1,89 +0,0 @@
from unittest.mock import MagicMock, patch
from GramAddict.core.bot_flow import _interact_with_profile
from GramAddict.core.session_state import SessionState
class FakeConfig:
def __init__(self):
self.args = MagicMock()
self.args.follow_percentage = 100
self.args.likes_percentage = 100
self.args.stories_percentage = 0
self.args.likes_count = "1-1"
def get_plugin_config(self, name):
return {}
def test_profile_grid_sync_delay_after_follow():
"""
Verifies that _interact_with_profile enforces a sleep delay
after a successful follow and BEFORE searching for the grid,
preventing UI automation from dumping mid-animation.
It now tracks the autonomous QNavGraph calls.
"""
mock_device = MagicMock()
mock_device.app_id = "com.instagram.android"
mock_device.dump_hierarchy.return_value = "<hierarchy><node package='com.instagram.android' resource-id='com.instagram.android:id/profile_header' /><node text='following' /><node text='followers' /></hierarchy>"
mock_configs = FakeConfig()
mock_session_state = MagicMock(spec=SessionState)
mock_session_state.check_limit.return_value = False
manager = MagicMock()
with (
patch("GramAddict.core.q_nav_graph.QNavGraph") as MockQNavGraph,
patch("GramAddict.core.bot_flow.sleep"),
patch("GramAddict.core.behaviors.follow.sleep") as mock_sleep,
patch("GramAddict.core.behaviors.grid_like.wait_for_post_loaded", return_value=True),
patch("random.random", return_value=0.0),
): # Use global random patch for local import robustness
mock_nav_instance = MagicMock()
mock_nav_instance.do.return_value = True # Always succeed transition
MockQNavGraph.return_value = mock_nav_instance
manager.attach_mock(mock_nav_instance.do, "do")
manager.attach_mock(mock_sleep, "sleep")
mock_stack = {"growth_brain": MagicMock()}
from GramAddict.core.behaviors import PluginRegistry
from GramAddict.core.behaviors.follow import FollowPlugin
from GramAddict.core.behaviors.grid_like import GridLikePlugin
registry = PluginRegistry.get_instance()
registry.register(FollowPlugin())
registry.register(GridLikePlugin())
# Act
_interact_with_profile(mock_device, mock_configs, "test_user", mock_session_state, 1.0, MagicMock(), mock_stack)
follow_idx = -1
grid_idx = -1
for i, mock_call in enumerate(manager.mock_calls):
# mock_call format: ('name', (args,), {kwargs})
if mock_call[0] == "do":
args = mock_call[1]
if args and args[0] == "tap follow button":
follow_idx = i
elif args and args[0] == "tap first image post in profile grid":
grid_idx = i
assert follow_idx != -1, "Follow transition was not executed"
assert grid_idx != -1, "Grid interaction was not executed"
assert follow_idx < grid_idx, "Follow must happen before grid interaction"
# Verify that more than 1.5s delay happened between follow and grid interaction
# (The bot_flow.py uses random.uniform(1.8, 3.2))
found_delay = False
for i in range(follow_idx + 1, grid_idx):
if manager.mock_calls[i][0] == "sleep":
sleep_val = manager.mock_calls[i][1][0]
if sleep_val >= 1.5:
found_delay = True
break
assert found_delay, "No significant sleep delay found between follow and grid interaction"

View File

@@ -1,47 +0,0 @@
from unittest.mock import 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

@@ -1,79 +0,0 @@
from unittest.mock import MagicMock, patch
from GramAddict.core.situational_awareness import EscapeAction, SituationalAwarenessEngine, SituationEpisodeDB
class TestSAETeslaUpgrade:
def setup_method(self):
self.device_mock = MagicMock()
self.sae = SituationalAwarenessEngine(self.device_mock)
def test_sae_foreground_extraction(self):
"""
Ensures that modals/popups located at the END of the XML document
(highest Z-index) are prioritized in the compressed signature.
The old system truncated at elements[:50], missing the actual popup.
"""
# Create an XML with 60 background nodes, and 1 dialog node at the end
xml_dump = "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>\n<hierarchy rotation='0'>\n"
for i in range(60):
xml_dump += f' <node package="com.instagram.android" resource-id="id/feed_item_{i}" text="Feed {i}" bounds="[0,0][100,100]" />\n'
# The critical modal at the end
xml_dump += ' <node package="com.instagram.android" resource-id="id/dialog_container" text="Not Now" bounds="[100,100][200,200]" />\n'
xml_dump += "</hierarchy>"
compressed = self.sae._compress_xml(xml_dump)
# The compressed string MUST contain the dialog container
assert "dialog_container" in compressed, "Foreground extraction failed: modal was truncated!"
assert "Not Now" in compressed, "Foreground extraction failed: modal text was truncated!"
# It should prioritize the END of the document, so feed_item_0 should ideally be gone if capped at 50
assert "feed_item_0" not in compressed, "Background elements are still being prioritized over foreground!"
def test_sae_structural_generalization(self):
"""
Ensures that dynamic user content is stripped to allow cross-post modal generalization,
while short, static UI text (like "OK", "Cancel") is preserved.
"""
xml_dump = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation='0'>
<node package="com.instagram.android" resource-id="id/comment" text="This is a very long user comment that changes every time we see this modal so it should be stripped!" />
<node package="com.instagram.android" resource-id="id/button" text="OK" />
</hierarchy>
"""
compressed = self.sae._compress_xml(xml_dump)
# Long dynamic text should be stripped or truncated to not pollute the vector space
assert "This is a very long user comment" not in compressed, "Dynamic text > 20 chars was not stripped!"
assert "text='<STRIPPED_DYNAMIC>'" in compressed or "This is a very lo" not in compressed
# Short static text should be kept
assert "OK" in compressed, "Short static UI text was incorrectly stripped!"
@patch("GramAddict.core.qdrant_memory.QdrantBase.is_connected", new=True)
def test_sae_negative_reinforcement(self):
"""
Ensures that failed escapes decay the confidence of the vector,
and eventually purge it, instead of just storing a useless 0.0 vector alongside it.
"""
db = SituationEpisodeDB()
# We need to mock db._db.client directly since it's an instance property
mock_client = MagicMock()
db._db._client = mock_client
db._db.client = mock_client
with patch.object(db._db, "upsert_point"):
# Mock retrieve to return an existing point with confidence 0.4
mock_payload = {"confidence": 0.4, "action": {"action_type": "back", "x": 0, "y": 0, "reason": ""}}
mock_client.retrieve.return_value = [MagicMock(payload=mock_payload)]
# Simulate a FAILURE
action = EscapeAction("back")
db.learn("some_signature", action, success=False)
# Verify that it fetched the current confidence and updated it, or deleted it if < 0.1
# If confidence was 0.4 and delta is -0.5, it drops to -0.1 -> DELETED
mock_client.delete.assert_called_once()

View File

@@ -1,22 +0,0 @@
from unittest.mock import MagicMock, patch
from GramAddict.core.situational_awareness import SituationalAwarenessEngine
def test_sae_has_unlearn_current_state():
"""Prove that SituationalAwarenessEngine exposes unlearn_current_state to heal from poisoned context."""
device = MagicMock()
sae = SituationalAwarenessEngine(device)
# Mocking internal compression and the screen_memory dependency
sae._compress_xml = MagicMock(return_value="<compressed_feed/>")
with patch("GramAddict.core.qdrant_memory.ScreenMemoryDB") as MockScreenMemoryDB:
mock_db_instance = MockScreenMemoryDB.return_value
# If unlearn_current_state does not exist, AttributeError (RED)
sae.unlearn_current_state("<full_feed_xml/>")
# Verify it compresses and delegates to purge_screen
sae._compress_xml.assert_called_once_with("<full_feed_xml/>")
mock_db_instance.purge_screen.assert_called_once_with("<compressed_feed/>")

View File

@@ -1,50 +0,0 @@
from unittest.mock import MagicMock, PropertyMock, patch
from GramAddict.core.qdrant_memory import NavigationMemoryDB, QdrantBase, ScreenMemoryDB
class DummyQdrantBase(QdrantBase):
def __init__(self):
self.client = MagicMock()
self.collection_name = "test_collection"
@patch("GramAddict.core.qdrant_memory.QdrantBase.is_connected", new_callable=PropertyMock)
def test_qdrant_base_has_delete_point(mock_is_connected):
"""Prove that QdrantBase implements delete_point for autonomous unlearning."""
mock_is_connected.return_value = True
db = DummyQdrantBase()
# If delete_point does not exist, this will raise AttributeError (RED)
db.generate_uuid = MagicMock(return_value="test-uuid-1234")
result = db.delete_point("test-seed")
# Assert
db.client.delete.assert_called_once_with(collection_name="test_collection", points_selector=["test-uuid-1234"])
assert result is True
@patch("GramAddict.core.qdrant_memory.QdrantBase.is_connected", new_callable=PropertyMock)
def test_screen_memory_has_purge_screen(mock_is_connected):
"""Prove that ScreenMemoryDB exposes purge_screen to heal poisoned classifications."""
mock_is_connected.return_value = True
db = ScreenMemoryDB()
db.client = MagicMock()
db.delete_point = MagicMock()
# If purge_screen does not exist, AttributeError (RED)
db.purge_screen("<node class='feed' />")
db.delete_point.assert_called_once_with("<node class='feed' />")
@patch("GramAddict.core.qdrant_memory.QdrantBase.is_connected", new_callable=PropertyMock)
def test_navigation_memory_has_unlearn_transition(mock_is_connected):
"""Prove that NavigationMemoryDB exposes unlearn_transition to destroy trap paths."""
mock_is_connected.return_value = True
db = NavigationMemoryDB()
db.client = MagicMock()
db.delete_point = MagicMock()
# If unlearn_transition does not exist, AttributeError (RED)
db.unlearn_transition("HOME_FEED", "tap explore tab")
db.delete_point.assert_called_once_with("HOME_FEED_tap explore tab")

View File

@@ -1,19 +0,0 @@
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"

View File

@@ -1,247 +0,0 @@
import logging
from unittest.mock import MagicMock, call
from GramAddict.core.unfollow_engine import _run_zero_latency_unfollow_loop
class TestUnfollowEngine:
def setup_method(self):
self.mock_device = MagicMock()
self.mock_device.deviceV2 = MagicMock()
self.mock_device.app_id = "com.instagram.android"
self.mock_device._get_current_app.return_value = "com.instagram.android"
self.mock_device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
self.mock_telepathic = MagicMock()
self.mock_dopamine = MagicMock()
self.mock_dopamine.is_app_session_over.return_value = False
self.mock_dopamine.wants_to_change_feed.return_value = False
self.mock_dopamine.boredom = 0.0
self.mock_resonance = MagicMock()
self.mock_resonance.calculate_resonance.return_value = 0.2
self.cognitive_stack = {
"telepathic": self.mock_telepathic,
"dopamine": self.mock_dopamine,
"resonance": self.mock_resonance,
}
self.mock_configs = MagicMock()
self.mock_configs.args.total_unfollows_limit = 50
self.mock_session_state = MagicMock()
self.mock_session_state.totalUnfollowed = 0
self.mock_session_state.check_limit.return_value = False
self.logger = logging.getLogger("test")
def test_unfollow_loop_unfollows_low_resonance(self, monkeypatch):
"""
Happy path (Smart Unfollow):
1. Clicks profile row.
2. Dumps profile UI.
3. Resonance is low (< 0.4).
4. Clicks 'Following' button on profile.
5. Clicks 'Unfollow' confirm.
6. Clicks back.
"""
def fake_extract_semantic_nodes(xml, intent, **kwargs):
if "profile rows" in intent:
return [{"semantic_string": "Profile Row", "x": 100, "y": 200, "bounds": "[50,150]", "skip": False}]
elif "Unfollow" in intent:
return [
{
"semantic_string": "Unfollow Confirmation",
"x": 500,
"y": 1500,
"bounds": "[100,200]",
"skip": False,
}
]
else:
return [
{"semantic_string": "Following Button", "x": 900, "y": 600, "bounds": "[100,200]", "skip": False}
]
self.mock_telepathic._extract_semantic_nodes.side_effect = fake_extract_semantic_nodes
self.mock_dopamine.wants_to_change_feed.side_effect = [True]
self.mock_device.dump_hierarchy.return_value = '<node text="Some Bio"/>'
# Patch local imports inside the bot_flow / unfollow_engine namespace
mock_sleep = MagicMock()
mock_click = MagicMock()
import GramAddict.core.bot_flow
monkeypatch.setattr(GramAddict.core.bot_flow, "sleep", mock_sleep)
monkeypatch.setattr(GramAddict.core.bot_flow, "random_sleep", mock_sleep)
monkeypatch.setattr(GramAddict.core.bot_flow, "_humanized_click", mock_click)
import GramAddict.core.utils
monkeypatch.setattr(GramAddict.core.utils, "random_sleep", mock_sleep)
result = _run_zero_latency_unfollow_loop(
self.mock_device,
None,
None,
self.mock_configs,
self.mock_session_state,
"FollowingList",
self.cognitive_stack,
)
# Verify result and clicks
assert result == "BOREDOM_CHANGE_FEED"
assert self.mock_session_state.totalUnfollowed == 1
# Assert humanized clicks were logged correctly (Profile Row -> Following Button -> Unfollow Confirm)
assert mock_click.call_count == 3
mock_click.assert_has_calls(
[call(self.mock_device, 100, 200), call(self.mock_device, 900, 600), call(self.mock_device, 500, 1500)]
)
assert self.mock_device.back.call_count == 1
def test_unfollow_loop_keeps_high_resonance_profile(self, monkeypatch):
"""
If resonance is high, we keep following the account and go back to the list.
"""
self.mock_resonance.calculate_resonance.return_value = 0.8
def fake_extract_semantic_nodes(xml, intent, **kwargs):
if "profile rows" in intent:
return [{"semantic_string": "Profile Row", "x": 100, "y": 200, "bounds": "[50,150]", "skip": False}]
return []
self.mock_telepathic._extract_semantic_nodes.side_effect = fake_extract_semantic_nodes
self.mock_dopamine.wants_to_change_feed.side_effect = [True]
self.mock_device.dump_hierarchy.return_value = '<node text="Awesome Bio"/>'
mock_sleep = MagicMock()
mock_click = MagicMock()
import GramAddict.core.bot_flow
monkeypatch.setattr(GramAddict.core.bot_flow, "sleep", mock_sleep)
monkeypatch.setattr(GramAddict.core.bot_flow, "random_sleep", mock_sleep)
monkeypatch.setattr(GramAddict.core.bot_flow, "_humanized_click", mock_click)
import GramAddict.core.utils
monkeypatch.setattr(GramAddict.core.utils, "random_sleep", mock_sleep)
result = _run_zero_latency_unfollow_loop(
self.mock_device,
None,
None,
self.mock_configs,
self.mock_session_state,
"FollowingList",
self.cognitive_stack,
)
assert result == "BOREDOM_CHANGE_FEED"
assert self.mock_session_state.totalUnfollowed == 0
# Clicked profile, then went back (no unfollow clicks)
assert mock_click.call_count == 1
mock_click.assert_has_calls([call(self.mock_device, 100, 200)])
assert self.mock_device.back.call_count == 1
def test_unfollow_loop_skips_close_friend(self, monkeypatch):
"""
If 'enge freunde' is in the XML, skip resonance eval, skip unfollowing, just go back.
"""
def fake_extract_semantic_nodes(xml, intent, **kwargs):
if "profile rows" in intent:
return [{"semantic_string": "Profile Row", "x": 100, "y": 200, "bounds": "[50,150]", "skip": False}]
return []
self.mock_telepathic._extract_semantic_nodes.side_effect = fake_extract_semantic_nodes
self.mock_dopamine.wants_to_change_feed.side_effect = [True]
# Mock XML with Enge Freunde
self.mock_device.dump_hierarchy.side_effect = [
'<node text="FollowingList"/>', # First dump on list
'<node text="Enge Freunde"/>', # Second dump on profile
]
mock_sleep = MagicMock()
mock_click = MagicMock()
import GramAddict.core.bot_flow
monkeypatch.setattr(GramAddict.core.bot_flow, "sleep", mock_sleep)
monkeypatch.setattr(GramAddict.core.bot_flow, "random_sleep", mock_sleep)
monkeypatch.setattr(GramAddict.core.bot_flow, "_humanized_click", mock_click)
import GramAddict.core.utils
monkeypatch.setattr(GramAddict.core.utils, "random_sleep", mock_sleep)
result = _run_zero_latency_unfollow_loop(
self.mock_device,
None,
None,
self.mock_configs,
self.mock_session_state,
"FollowingList",
self.cognitive_stack,
)
assert result == "BOREDOM_CHANGE_FEED"
assert self.mock_session_state.totalUnfollowed == 0
assert mock_click.call_count == 1
assert self.mock_device.back.call_count == 1
# Resonance shouldn't even be called for close friends
self.mock_resonance.calculate_resonance.assert_not_called()
def test_unfollow_loop_scrolls_if_empty(self, monkeypatch):
"""
End of list path: If no following buttons are found, it should scroll _humanized_scroll_down.
After 5 failed scrolls, it should gracefully return BOREDOM_CHANGE_FEED without crashing.
"""
# Always return empty nodes
self.mock_telepathic._extract_semantic_nodes.return_value = []
# Patch the imported _humanized_scroll_down so we can track it
mock_scroll = MagicMock()
import GramAddict.core.unfollow_engine
monkeypatch.setattr(GramAddict.core.unfollow_engine, "_humanized_scroll_down", mock_scroll)
result = _run_zero_latency_unfollow_loop(
self.mock_device,
None,
None,
self.mock_configs,
self.mock_session_state,
"FollowingList",
self.cognitive_stack,
)
assert result == "BOREDOM_CHANGE_FEED"
# It should scroll exactly 6 times (failed_scrolls > 5)
assert mock_scroll.call_count == 6
assert self.mock_session_state.totalUnfollowed == 0
def test_unfollow_loop_respects_limits(self):
"""
Limit protection: If session limit is reached at the start, abort immediately.
No clicks or UI dumps should occur.
"""
# Tell session_state that UNFOLLOWS limit is hit
self.mock_session_state.check_limit.return_value = (True, "Unfollow limit reached")
result = _run_zero_latency_unfollow_loop(
self.mock_device,
None,
None,
self.mock_configs,
self.mock_session_state,
"FollowingList",
self.cognitive_stack,
)
assert result == "BOREDOM_CHANGE_FEED"
self.mock_device.dump_hierarchy.assert_not_called()
self.mock_device.click.assert_not_called()
self.mock_session_state.totalUnfollowed = 0