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:
2026-04-29 23:05:59 +02:00
parent 96fdbd7db7
commit 47f94b699c
3 changed files with 159 additions and 1 deletions

View File

@@ -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.")

View File

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

View 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