diff --git a/GramAddict/core/behaviors/obstacle_guard.py b/GramAddict/core/behaviors/obstacle_guard.py index da11b54..696b880 100644 --- a/GramAddict/core/behaviors/obstacle_guard.py +++ b/GramAddict/core/behaviors/obstacle_guard.py @@ -42,6 +42,21 @@ class ObstacleGuardPlugin(BehaviorPlugin): misses = ctx.shared_state.get("consecutive_marker_misses", 0) + # ── System Dialog / Permission Modal (e.g. "Allow Instagram to record audio?") ── + if situation == SituationType.OBSTACLE_SYSTEM: + logger.warning("⚠️ [ObstacleGuard] System permission dialog detected. Dismissing with BACK...") + ctx.device.press("back") + sleep(1.5 * ctx.sleep_mod) + return BehaviorResult(executed=True, should_skip=True) + + # ── Foreign App Takeover (e.g. browser opened, wrong app in foreground) ── + if situation == SituationType.OBSTACLE_FOREIGN_APP: + logger.warning("⚠️ [ObstacleGuard] Foreign app detected. Pressing BACK to recover...") + ctx.device.press("back") + sleep(1.5 * ctx.sleep_mod) + return BehaviorResult(executed=True, should_skip=True) + + # ── Instagram Modal / Overlay (survey, "Not Now" prompt, creation flow) ── if situation == SituationType.OBSTACLE_MODAL: if misses >= 2: logger.error("🛑 [ObstacleGuard] Failed to recover from OBSTACLE_MODAL after multiple attempts.") diff --git a/GramAddict/core/behaviors/resonance_evaluator.py b/GramAddict/core/behaviors/resonance_evaluator.py index f828348..c8b1dd0 100644 --- a/GramAddict/core/behaviors/resonance_evaluator.py +++ b/GramAddict/core/behaviors/resonance_evaluator.py @@ -72,7 +72,6 @@ class ResonanceEvaluatorPlugin(BehaviorPlugin): if marker and marker.strip(): from GramAddict.core.utils import learn_ad_marker learn_ad_marker(marker, ctx.context_xml) - from GramAddict.core.utils import humanized_scroll humanized_scroll(ctx.device) return BehaviorResult(executed=True, should_skip=True) diff --git a/tests/unit/test_obstacle_guard_system_dialog.py b/tests/unit/test_obstacle_guard_system_dialog.py new file mode 100644 index 0000000..fb23e4d --- /dev/null +++ b/tests/unit/test_obstacle_guard_system_dialog.py @@ -0,0 +1,144 @@ +""" +🔴 RED: Prove that ObstacleGuard currently FAILS to handle system permission dialogs. + +The obstacle_guard only checks for OBSTACLE_MODAL but ignores OBSTACLE_SYSTEM +and OBSTACLE_FOREIGN_APP. This test forces the bug to surface. + +Uses monkeypatch + real fixtures (no unittest.mock). +""" +import pytest +from dataclasses import dataclass, field +from typing import Any, Dict, Optional + +from GramAddict.core.behaviors import BehaviorContext, BehaviorResult +from GramAddict.core.behaviors.obstacle_guard import ObstacleGuardPlugin +from GramAddict.core.situational_awareness import SituationType + + +PERMISSION_DIALOG_XML = """ + + + + + + + + +""" + +NORMAL_INSTAGRAM_XML = '' + + +class FakeDevice: + """Minimal real-ish device stub for obstacle_guard tests.""" + + def __init__(self, xml_sequence=None): + self._xml_sequence = xml_sequence or [NORMAL_INSTAGRAM_XML] + self._dump_index = 0 + self._pressed = [] + self._clicks = [] + self.app_id = "com.instagram.android" + + class _V2: + info = {"screenOn": True} + self.deviceV2 = _V2() + + def dump_hierarchy(self): + if self._dump_index < len(self._xml_sequence): + xml = self._xml_sequence[self._dump_index] + self._dump_index += 1 + return xml + return self._xml_sequence[-1] + + def press(self, key): + self._pressed.append(key) + + def click(self, x, y): + self._clicks.append((x, y)) + + +class FakeConfig: + class args: + pass + + +class FakeSessionState: + job_target = "test" + + +def _make_ctx(xml: str, device: FakeDevice = None) -> BehaviorContext: + dev = device or FakeDevice() + return BehaviorContext( + device=dev, + configs=FakeConfig(), + session_state=FakeSessionState(), + cognitive_stack={}, + shared_state={}, + context_xml=xml, + ) + + +class TestObstacleGuardHandlesSystemDialogs: + """The guard MUST handle OBSTACLE_SYSTEM, not just OBSTACLE_MODAL.""" + + def test_system_permission_dialog_triggers_back_press(self, monkeypatch): + """ObstacleGuard must react to OBSTACLE_SYSTEM by pressing back.""" + plugin = ObstacleGuardPlugin() + device = FakeDevice(xml_sequence=[NORMAL_INSTAGRAM_XML]) + ctx = _make_ctx(PERMISSION_DIALOG_XML, device=device) + + # Monkeypatch SAE to return OBSTACLE_SYSTEM for the permission dialog + class FakeSAE: + @classmethod + def get_instance(cls, device=None): + return cls() + def perceive(self, xml): + return SituationType.OBSTACLE_SYSTEM + + monkeypatch.setattr( + "GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine", + FakeSAE, + ) + + result = plugin.execute(ctx) + + assert result.executed is True + assert result.should_skip is True + assert "back" in device._pressed + + def test_foreign_app_triggers_back_press(self, monkeypatch): + """ObstacleGuard must react to OBSTACLE_FOREIGN_APP by pressing back.""" + plugin = ObstacleGuardPlugin() + device = FakeDevice(xml_sequence=[NORMAL_INSTAGRAM_XML]) + ctx = _make_ctx(PERMISSION_DIALOG_XML, device=device) + + class FakeSAE: + @classmethod + def get_instance(cls, device=None): + return cls() + def perceive(self, xml): + return SituationType.OBSTACLE_FOREIGN_APP + + monkeypatch.setattr( + "GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine", + FakeSAE, + ) + + result = plugin.execute(ctx) + + assert result.executed is True + assert result.should_skip is True + assert "back" in device._pressed + + +class TestResonanceEvaluatorImportIntegrity: + """The humanized_scroll import must resolve correctly at module level.""" + + def test_humanized_scroll_is_importable_from_resonance_evaluator(self): + """Verify the resonance_evaluator module loads without ImportError.""" + # If the import on line 6 or 75 is broken, this will crash + from GramAddict.core.behaviors.resonance_evaluator import ResonanceEvaluatorPlugin + assert ResonanceEvaluatorPlugin is not None