fix(guard): handle OBSTACLE_SYSTEM + OBSTACLE_FOREIGN_APP in obstacle_guard, fix broken humanized_scroll import
🔴 RED: Tests proved obstacle_guard silently ignored system permission dialogs and foreign app overlays, causing the bot to get stuck on Android modals. 🟢 GREEN: obstacle_guard now dismisses OBSTACLE_SYSTEM and OBSTACLE_FOREIGN_APP with immediate back-press, preventing the interaction chain from running against non-Instagram UI elements. 🔵 REFACTOR: Fixed broken import in resonance_evaluator (was importing humanized_scroll from utils instead of physics.humanized_input).
This commit is contained in:
@@ -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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
144
tests/unit/test_obstacle_guard_system_dialog.py
Normal file
144
tests/unit/test_obstacle_guard_system_dialog.py
Normal file
@@ -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 = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<hierarchy>
|
||||
<node package="com.android.permissioncontroller" class="android.widget.FrameLayout">
|
||||
<node text="Allow Instagram to record audio?" class="android.widget.TextView" />
|
||||
<node text="While using the app" class="android.widget.Button" clickable="true"
|
||||
resource-id="com.android.permissioncontroller:id/permission_allow_foreground_only_button" />
|
||||
<node text="Only this time" class="android.widget.Button" clickable="true"
|
||||
resource-id="com.android.permissioncontroller:id/permission_allow_one_time_button" />
|
||||
<node text="Don't allow" class="android.widget.Button" clickable="true"
|
||||
resource-id="com.android.permissioncontroller:id/permission_deny_button" />
|
||||
</node>
|
||||
</hierarchy>
|
||||
"""
|
||||
|
||||
NORMAL_INSTAGRAM_XML = '<hierarchy><node package="com.instagram.android" resource-id="row_feed_button_like" /></hierarchy>'
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user