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
This commit is contained in:
2026-04-27 10:19:04 +02:00
parent 42a11107fd
commit 4de087ae45
3 changed files with 247 additions and 4 deletions

View File

@@ -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, [])

View File

@@ -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

View File

@@ -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():
"""