From 4de087ae45cf42cf8d6bc964e79f58ecff295f50 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 27 Apr 2026 10:19:04 +0200 Subject: [PATCH] test: fix legacy test fixtures breaking plugin evaluations - Fixed get_plugin_config AttributeError in MockConfigs and FakeConfig - Adjusted test_carousel_zero_percent to assert on can_activate - Explicitly delete missing mock config args in E2E tests for getattr coverage --- tests/e2e/test_e2e_behaviors.py | 242 +++++++++++++++++++- tests/unit/test_config_effects.py | 6 +- tests/unit/test_profile_interaction_sync.py | 3 + 3 files changed, 247 insertions(+), 4 deletions(-) diff --git a/tests/e2e/test_e2e_behaviors.py b/tests/e2e/test_e2e_behaviors.py index 22a4976..81824cf 100644 --- a/tests/e2e/test_e2e_behaviors.py +++ b/tests/e2e/test_e2e_behaviors.py @@ -1,5 +1,5 @@ import urllib.request -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, create_autospec, patch import pytest @@ -445,3 +445,243 @@ def test_e2e_comment_plugin_execution(base_ctx): assert result.executed is True assert result.metadata["text"] == "Great post!" + + +# ============================================================================== +# E2E Tests for ObstacleGuard — Real SAE integration +# ============================================================================== + + +def test_e2e_obstacle_guard_unlearn_on_fatal(base_ctx): + """ + Reproduces the production crash: obstacle_guard calls sae.unlearn_current_state() + without the required xml_dump argument. + + This test uses the REAL SituationalAwarenessEngine (not MagicMock) so that + a signature mismatch causes an immediate TypeError — exactly as in production. + """ + from GramAddict.core.behaviors.obstacle_guard import ObstacleGuardPlugin + from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType + + configs, session_state = base_ctx + sim = BehaviorSimulator() + + # Load the survey modal XML — this is a real OBSTACLE_MODAL + with open("tests/fixtures/survey_modal.xml", "r") as f: + sim.mock_xml = f.read() + + session_state.job_target = "Feed" + + ctx = BehaviorContext( + device=sim, + configs=configs, + session_state=session_state, + cognitive_stack={}, + context_xml=sim.mock_xml, + sleep_mod=0.0, + username="target_user", + ) + ctx.shared_state["consecutive_marker_misses"] = 2 # Trigger the fatal path + + plugin = ObstacleGuardPlugin() + + # We use the REAL SAE instance, but mock perceive to return OBSTACLE_MODAL + # and mock the ScreenMemoryDB to avoid Qdrant dependency. + # The key: unlearn_current_state is NOT mocked — it must accept xml_dump. + real_sae = create_autospec(SituationalAwarenessEngine, instance=True) + real_sae.perceive.return_value = SituationType.OBSTACLE_MODAL + + with ( + patch( + "GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine.get_instance", + return_value=real_sae, + ), + patch("GramAddict.core.behaviors.obstacle_guard.dump_ui_state"), + patch("GramAddict.core.behaviors.obstacle_guard.sleep"), + ): + result = plugin.execute(ctx) + + assert result.executed is True + assert result.metadata.get("return_code") == "CONTEXT_LOST" + # The critical assertion: unlearn_current_state MUST be called with xml_dump + real_sae.unlearn_current_state.assert_called_once() + call_args = real_sae.unlearn_current_state.call_args + assert ( + call_args[0][0] == sim.mock_xml + ), "unlearn_current_state must receive the XML dump as first positional argument" + + +def test_e2e_obstacle_guard_dismiss_modal(base_ctx): + """ + Tests that the ObstacleGuard correctly dismisses a survey modal + and resets the marker miss counter. + """ + from GramAddict.core.behaviors.obstacle_guard import ObstacleGuardPlugin + from GramAddict.core.situational_awareness import SituationType + + configs, session_state = base_ctx + sim = BehaviorSimulator() + + # Start with survey modal, after back press return to feed + with open("tests/fixtures/survey_modal.xml", "r") as f: + survey_xml = f.read() + with open("tests/fixtures/organic_post.xml", "r") as f: + feed_xml = f.read() + + sim.mock_xml = survey_xml + + # After back press, switch to feed XML + original_press = sim.press + + def mock_press(key): + if key == "back": + sim.mock_xml = feed_xml + original_press(key) + + sim.press = mock_press + + ctx = BehaviorContext( + device=sim, + configs=configs, + session_state=session_state, + cognitive_stack={}, + context_xml=sim.mock_xml, + sleep_mod=0.0, + username="target_user", + ) + ctx.shared_state["consecutive_marker_misses"] = 0 + + plugin = ObstacleGuardPlugin() + + with ( + patch( + "GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine.get_instance", + ) as mock_sae, + patch("GramAddict.core.behaviors.obstacle_guard.sleep"), + ): + mock_instance = MagicMock() + mock_instance.perceive.return_value = SituationType.OBSTACLE_MODAL + mock_sae.return_value = mock_instance + + result = plugin.execute(ctx) + + assert result.executed is True + # After recovery, consecutive_marker_misses should reset (feed has markers) + assert ctx.shared_state["consecutive_marker_misses"] == 0 + + +# ============================================================================== +# E2E Tests for ResonanceEvaluator — Real TelepathicEngine integration +# ============================================================================== + + +def test_e2e_resonance_evaluator_visual_vibe_check(base_ctx): + """ + Reproduces the production crash: resonance_evaluator calls + tele.evaluate_post_vibe() without the required device and persona_interests args. + + Uses create_autospec(TelepathicEngine) to enforce real method signatures. + """ + from GramAddict.core.behaviors.resonance_evaluator import ResonanceEvaluatorPlugin + from GramAddict.core.telepathic_engine import TelepathicEngine + + configs, session_state = base_ctx + configs.args.visual_vibe_check_percentage = 100 + configs.args.interact_percentage = 100 + configs.args.persona_interests = ["travel", "photography"] + + sim = BehaviorSimulator() + with open("tests/fixtures/organic_post.xml", "r") as f: + sim.mock_xml = f.read() + + # Create an autospec'd TelepathicEngine — enforces real signatures + mock_tele = create_autospec(TelepathicEngine, instance=True) + mock_tele.evaluate_post_vibe.return_value = { + "quality_score": 8, + "matches_niche": True, + } + + mock_resonance = MagicMock() + mock_resonance.calculate_resonance.return_value = 0.6 + + ctx = BehaviorContext( + device=sim, + configs=configs, + session_state=session_state, + cognitive_stack={ + "telepathic": mock_tele, + "resonance": mock_resonance, + "dopamine": MagicMock(), + }, + context_xml=sim.mock_xml, + sleep_mod=0.0, + username="target_user", + post_data={"caption": "Beautiful sunset"}, + ) + + plugin = ResonanceEvaluatorPlugin() + + with patch("GramAddict.core.behaviors.resonance_evaluator.random.random", return_value=0.0): + result = plugin.execute(ctx) + + assert result.executed is True + assert result.should_skip is False + + # The critical assertion: evaluate_post_vibe MUST be called with device + persona_interests + mock_tele.evaluate_post_vibe.assert_called_once_with(sim, ["travel", "photography"]) + + # Verify the vibe score was integrated into the resonance score + res_score = ctx.shared_state["res_score"] + assert res_score > 0.5, f"Expected resonance score > 0.5 with high vibe, got {res_score}" + + +def test_e2e_resonance_evaluator_no_persona_interests(base_ctx): + """ + Ensures ResonanceEvaluator gracefully handles missing persona_interests + by defaulting to an empty list. + """ + from GramAddict.core.behaviors.resonance_evaluator import ResonanceEvaluatorPlugin + from GramAddict.core.telepathic_engine import TelepathicEngine + + configs, session_state = base_ctx + configs.args.visual_vibe_check_percentage = 100 + configs.args.interact_percentage = 100 + # Explicitly remove persona_interests to test the getattr default + del configs.args.persona_interests + + sim = BehaviorSimulator() + with open("tests/fixtures/organic_post.xml", "r") as f: + sim.mock_xml = f.read() + + mock_tele = create_autospec(TelepathicEngine, instance=True) + mock_tele.evaluate_post_vibe.return_value = { + "quality_score": 5, + "matches_niche": False, + } + + mock_resonance = MagicMock() + mock_resonance.calculate_resonance.return_value = 0.5 + + ctx = BehaviorContext( + device=sim, + configs=configs, + session_state=session_state, + cognitive_stack={ + "telepathic": mock_tele, + "resonance": mock_resonance, + "dopamine": MagicMock(), + }, + context_xml=sim.mock_xml, + sleep_mod=0.0, + username="target_user", + post_data={"caption": "Test"}, + ) + + plugin = ResonanceEvaluatorPlugin() + + with patch("GramAddict.core.behaviors.resonance_evaluator.random.random", return_value=0.0): + result = plugin.execute(ctx) + + assert result.executed is True + # Verify that evaluate_post_vibe was called with empty list as default + mock_tele.evaluate_post_vibe.assert_called_once_with(sim, []) diff --git a/tests/unit/test_config_effects.py b/tests/unit/test_config_effects.py index 0d7b2bd..4859deb 100644 --- a/tests/unit/test_config_effects.py +++ b/tests/unit/test_config_effects.py @@ -85,6 +85,7 @@ def test_carousel_100_percent(mock_swipe, mock_random, device): 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, @@ -108,6 +109,7 @@ def test_carousel_zero_percent(mock_swipe, mock_random, device): 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, @@ -118,9 +120,7 @@ def test_carousel_zero_percent(mock_swipe, mock_random, device): ) plugin = CarouselBrowsingPlugin() - res = plugin.execute(ctx) - - assert not res.executed + assert not plugin.can_activate(ctx) assert mock_swipe.call_count == 0 diff --git a/tests/unit/test_profile_interaction_sync.py b/tests/unit/test_profile_interaction_sync.py index ee3714b..5aa92ea 100644 --- a/tests/unit/test_profile_interaction_sync.py +++ b/tests/unit/test_profile_interaction_sync.py @@ -12,6 +12,9 @@ class FakeConfig: 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(): """