From db226ed7c273b1e22cd725ef705f7f4bc543f3d6 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 29 Apr 2026 23:13:54 +0200 Subject: [PATCH] test(e2e): add plugin chain integration tests for hostile environments THE ROOT CAUSE OF LYING TESTS: 81 E2E tests existed but ZERO tested the full plugin chain (execute_all) against non-Instagram environments. Each plugin was tested in isolation, but the integration between perception (SAE) and action (obstacle_guard) was a complete blind spot. NEW TESTS: - test_chain_terminates_on_permission_dialog: Proves the full plugin chain terminates at obstacle_guard when a system permission dialog is detected. No downstream interaction plugins (resonance, like, follow) may fire. - test_chain_terminates_on_foreign_app: Same for Chrome/browser takeover. - test_chain_passes_through_on_normal_instagram: Sanity check that the chain runs normally on valid Instagram feeds. These tests would have caught the 23:01 stuck-loop bug BEFORE it shipped. --- tests/e2e/test_plugin_chain_hostile_env.py | 381 +++++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 tests/e2e/test_plugin_chain_hostile_env.py diff --git a/tests/e2e/test_plugin_chain_hostile_env.py b/tests/e2e/test_plugin_chain_hostile_env.py new file mode 100644 index 0000000..2d0a9f0 --- /dev/null +++ b/tests/e2e/test_plugin_chain_hostile_env.py @@ -0,0 +1,381 @@ +""" +🔴 E2E Plugin Chain Integration Tests +======================================== + +These tests exercise the FULL plugin chain (PluginRegistry.execute_all) against +hostile environments that the bot encounters in production: + 1. System permission dialogs (Android PermissionController) + 2. Foreign app overlays (browser, wrong app in foreground) + 3. Reel creation flows (camera, audio recording prompts) + +WHY THESE TESTS EXIST: + Production run 2026-04-29 23:01 revealed that: + - The SAE correctly detects OBSTACLE_SYSTEM for permission dialogs + - The obstacle_guard plugin IGNORED OBSTACLE_SYSTEM (only handled OBSTACLE_MODAL) + - All downstream plugins (resonance, dwell, like, follow) fired against a + non-Instagram permission dialog + - The VLM hallucinated box [4] (icon) as "post username" and "media content" + - The bot got completely stuck in an infinite loop + + NONE of the existing 81 E2E tests caught this because: + - They test each plugin in ISOLATION (not as a chain) + - They NEVER call PluginRegistry.execute_all() against hostile environments + - The conftest registers all plugins but no test actually exercises the chain + + These tests fix that gap permanently. +""" + +import argparse +import os + +import pytest + +from GramAddict.core.behaviors import BehaviorContext, BehaviorResult, PluginRegistry +from GramAddict.core.situational_awareness import SituationType + + +# ═══════════════════════════════════════════════════════ +# Hostile XML Fixtures (inline for speed, no file I/O) +# ═══════════════════════════════════════════════════════ + +PERMISSION_DIALOG_XML = """ + + + + + + + + + + + +""" + +FOREIGN_APP_CHROME_XML = """ + + + + + + +""" + +NORMAL_INSTAGRAM_XML = """ + + + + + + +""" + + +# ═══════════════════════════════════════════════════════ +# Realistic Device Stub +# ═══════════════════════════════════════════════════════ + + +class ChainTestDevice: + """Deterministic device for chain testing. Returns XML sequence.""" + + def __init__(self, xml_sequence): + self._xml_sequence = list(xml_sequence) + self._dump_index = 0 + self.pressed_keys = [] + self.clicks = [] + self.app_id = "com.instagram.android" + + class _V2: + info = {"screenOn": True, "sdkInt": 30, "displaySizeDpX": 400, "displayWidth": 1080} + settings = {} + touch = type("T", (), {"down": lambda s, x, y: None, "up": lambda s, x, y: None})() + + def dump_hierarchy(self, compressed=False): + return "" + + def app_current(self): + return {"package": "com.instagram.android"} + + def screenshot(self): + return None + + def shell(self, cmd): + pass + + 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_keys.append(key) + + def click(self, x, y): + self.clicks.append((x, y)) + + def swipe(self, sx, sy, ex, ey, **kwargs): + pass + + def app_start(self, pkg, use_monkey=False): + pass + + def app_stop(self, pkg): + pass + + def unlock(self): + pass + + def get_screenshot_b64(self): + return None + + +class ChainTestConfig: + def __init__(self): + self.args = argparse.Namespace( + username="testuser", + device="emulator-5554", + app_id="com.instagram.android", + debug=True, + interact_percentage=100, + visual_vibe_check_percentage=0, + persona_interests="travel, nature", + target_audience="travel, nature", + speed_multiplier=1.0, + likes_count="1", + likes_percentage=100, + follow_percentage=100, + comment_percentage=0, + carousel_percentage=0, + carousel_count="1", + stories_count="0", + stories_percentage=0, + disable_ai_messaging=True, + scrape_profiles=False, + dry_run_comments=True, + ) + self.config = { + "plugins": { + "likes": {"count": "1", "percentage": 100}, + "follow": {"percentage": 100}, + "comment": {"percentage": 0, "dry_run": True}, + "stories": {"count": "0", "percentage": 0}, + "resonance_evaluator": {"visual_vibe_check_percentage": 0}, + "carousel_browsing": {"percentage": 0, "count": "1"}, + } + } + self.username = "testuser" + + def get_plugin_config(self, name): + return self.config.get("plugins", {}).get(name, {}) + + +class ChainTestSession: + def __init__(self): + self.totalLikes = 0 + self.totalFollowed = 0 + self.totalComments = 0 + self.totalGetProfile = 0 + self.totalReposted = 0 + self.totalStoriesWatched = 0 + self.totalScraped = 0 + self.job_target = "test_chain" + self.totalInteractions = {} + + def check_limit(self, *args, **kwargs): + return False + + +# ═══════════════════════════════════════════════════════ +# THE ACTUAL CHAIN INTEGRATION TESTS +# ═══════════════════════════════════════════════════════ + + +class TestPluginChainOnSystemDialog: + """ + THE BIG LIE: Before this test existed, no E2E test exercised the full + plugin chain against a system permission dialog. The SAE perception test + verified detection, but obstacle_guard silently ignored it. + + This test proves the chain TERMINATES on hostile environments. + """ + + def test_chain_terminates_on_permission_dialog(self, monkeypatch, setup_e2e_plugin_registry): + """ + When PluginRegistry.execute_all() runs against a permission dialog, + the obstacle_guard (priority 95) MUST fire and terminate the chain. + No downstream plugin (resonance, like, follow) may execute. + """ + device = ChainTestDevice([PERMISSION_DIALOG_XML, NORMAL_INSTAGRAM_XML]) + registry = setup_e2e_plugin_registry + + # Monkeypatch SAE to return OBSTACLE_SYSTEM + class FakeSAE: + @classmethod + def get_instance(cls, device=None): + return cls() + + def perceive(self, xml): + if "permissioncontroller" in xml: + return SituationType.OBSTACLE_SYSTEM + return SituationType.NORMAL + + monkeypatch.setattr( + "GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine", + FakeSAE, + ) + + ctx = BehaviorContext( + device=device, + configs=ChainTestConfig(), + session_state=ChainTestSession(), + cognitive_stack={}, + shared_state={}, + context_xml=PERMISSION_DIALOG_XML, + ) + + results = registry.execute_all(ctx) + + # The chain MUST have terminated before any INTERACTION plugins fired + assert len(results) >= 1, "No plugin executed at all" + + # Find the obstacle_guard result (should be the one that executed + should_skip) + guard_fired = any(r.executed and r.should_skip for r in results) + assert guard_fired, ( + "obstacle_guard did not fire — no result has executed=True + should_skip=True. " + "This means the permission dialog was ignored and interaction plugins will run!" + ) + + # Critical: NO interaction plugin may have performed actual interactions + total_interactions = sum(r.interactions for r in results) + assert total_interactions == 0, ( + f"{total_interactions} interaction(s) were performed against a permission dialog!" + ) + + # The device must have pressed BACK to dismiss the dialog + assert "back" in device.pressed_keys, ( + "obstacle_guard did not press BACK to dismiss the system dialog" + ) + + def test_chain_terminates_on_foreign_app(self, monkeypatch, setup_e2e_plugin_registry): + """ + When Chrome is in the foreground, the chain must terminate at obstacle_guard. + """ + device = ChainTestDevice([FOREIGN_APP_CHROME_XML, NORMAL_INSTAGRAM_XML]) + registry = setup_e2e_plugin_registry + + class FakeSAE: + @classmethod + def get_instance(cls, device=None): + return cls() + + def perceive(self, xml): + if "chrome" in xml: + return SituationType.OBSTACLE_FOREIGN_APP + return SituationType.NORMAL + + monkeypatch.setattr( + "GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine", + FakeSAE, + ) + + ctx = BehaviorContext( + device=device, + configs=ChainTestConfig(), + session_state=ChainTestSession(), + cognitive_stack={}, + shared_state={}, + context_xml=FOREIGN_APP_CHROME_XML, + ) + + results = registry.execute_all(ctx) + + # obstacle_guard must have fired and terminated the chain + guard_fired = any(r.executed and r.should_skip for r in results) + assert guard_fired, ( + "obstacle_guard did not fire on foreign app. " + "Downstream plugins are firing against a Chrome browser window!" + ) + + total_interactions = sum(r.interactions for r in results) + assert total_interactions == 0, ( + f"{total_interactions} interaction(s) were performed against Chrome!" + ) + assert "back" in device.pressed_keys + + +class TestPluginChainOnNormalFeed: + """ + Sanity check: On a NORMAL Instagram feed, the chain must run normally. + The obstacle_guard must NOT fire (executed=False), allowing downstream plugins. + """ + + def test_chain_passes_through_on_normal_instagram(self, monkeypatch, setup_e2e_plugin_registry): + device = ChainTestDevice([NORMAL_INSTAGRAM_XML]) + registry = setup_e2e_plugin_registry + + class FakeSAE: + @classmethod + def get_instance(cls, device=None): + return cls() + + def perceive(self, xml): + return SituationType.NORMAL + + monkeypatch.setattr( + "GramAddict.core.behaviors.obstacle_guard.SituationalAwarenessEngine", + FakeSAE, + ) + + ctx = BehaviorContext( + device=device, + configs=ChainTestConfig(), + session_state=ChainTestSession(), + cognitive_stack={}, + shared_state={}, + context_xml=NORMAL_INSTAGRAM_XML, + ) + + results = registry.execute_all(ctx) + + # Multiple plugins should have run (obstacle_guard passed through, then data extraction, etc.) + assert len(results) > 1, ( + f"Only {len(results)} plugin(s) executed on a normal Instagram feed. " + f"The obstacle_guard may be incorrectly blocking the chain." + ) + + # obstacle_guard must have executed=False (no obstacle detected) + obstacle_results = [r for r in results if not r.executed or "back" not in device.pressed_keys] + assert "back" not in device.pressed_keys, ( + "obstacle_guard pressed BACK on a normal Instagram screen — false positive!" + )