From 0a73c3580904a17be8132b820ebf7cd602a0df5f Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 3 May 2026 10:21:23 +0200 Subject: [PATCH] purge: eliminate last MagicMock and all lambda dump_hierarchy overrides - device_emulator.py: Replace MagicMock(watcher) with explicit WatcherStub that implements exact u2 watcher chain (when/click/start). Any API drift now causes AttributeError instead of silent acceptance. - test_system_sae.py: Inject XML into emulator state instead of bypassing dump_hierarchy with lambda - test_behavior_ad_guard.py: Pass XML to fixture, remove lambda override - test_behavior_scrape_profile.py: Pass XML to fixture, remove lambda override Result: ZERO unittest.mock imports, ZERO MagicMock instances, ZERO dump_hierarchy lambdas across entire E2E suite (50 files). --- tests/e2e/device_emulator.py | 28 ++++++++-- tests/e2e/test_behavior_ad_guard.py | 3 +- tests/e2e/test_behavior_scrape_profile.py | 4 +- tests/e2e/test_system_sae.py | 65 +++++++++-------------- 4 files changed, 53 insertions(+), 47 deletions(-) diff --git a/tests/e2e/device_emulator.py b/tests/e2e/device_emulator.py index 519905e..674d464 100644 --- a/tests/e2e/device_emulator.py +++ b/tests/e2e/device_emulator.py @@ -255,15 +255,37 @@ def create_emulator_facade(initial_state, states, transitions, monkeypatch, imag emulator = InstagramEmulator(initial_state, states, transitions, images=images) # We must patch u2.connect to avoid ConnectError during DeviceFacade init - from unittest.mock import MagicMock - from GramAddict.core import device_facade + class WatcherStub: + """Mimics uiautomator2's watcher interface with zero magic. + + Production code at device_facade.py:111-114 calls: + self.deviceV2.watcher("crash_dialog").when(xpath='...').click() + self.deviceV2.watcher("system_dialog").when(xpath='...').click() + self.deviceV2.watcher.start() + + This stub implements EXACTLY that chain. Any other call will raise + AttributeError — unlike MagicMock which silently accepts anything. + """ + + def __call__(self, name): + return self + + def when(self, xpath=None, **kwargs): + return self + + def click(self): + return self + + def start(self): + pass + class MockU2Device: def __init__(self, *args, **kwargs): self.info = emulator._info self.settings = {} - self.watcher = MagicMock() + self.watcher = WatcherStub() def dump_hierarchy(self, *args, **kwargs): return emulator.dump_hierarchy() diff --git a/tests/e2e/test_behavior_ad_guard.py b/tests/e2e/test_behavior_ad_guard.py index 66c9b1c..3a998f3 100644 --- a/tests/e2e/test_behavior_ad_guard.py +++ b/tests/e2e/test_behavior_ad_guard.py @@ -22,8 +22,7 @@ def test_ad_guard_detects_sponsored_post(make_real_device_with_image): with open(xml_path, "r", encoding="utf-8") as f: xml = f.read() - device = make_real_device_with_image(jpg_path) - device.dump_hierarchy = lambda: xml + device = make_real_device_with_image(jpg_path, xml) import types diff --git a/tests/e2e/test_behavior_scrape_profile.py b/tests/e2e/test_behavior_scrape_profile.py index a3f9254..375bfd1 100644 --- a/tests/e2e/test_behavior_scrape_profile.py +++ b/tests/e2e/test_behavior_scrape_profile.py @@ -23,9 +23,7 @@ def test_scrape_profile_extracts_data_correctly(make_real_device_with_image): with open(xml_path, "r", encoding="utf-8") as f: xml = f.read() - device = make_real_device_with_image(jpg_path) - # mock dump_hierarchy so the plugin uses the static XML - device.dump_hierarchy = lambda: xml + device = make_real_device_with_image(jpg_path, xml) # Create dummy config and session state import types diff --git a/tests/e2e/test_system_sae.py b/tests/e2e/test_system_sae.py index 080b58b..840e792 100644 --- a/tests/e2e/test_system_sae.py +++ b/tests/e2e/test_system_sae.py @@ -9,37 +9,23 @@ without relying on brittle mock LLM responses. import pytest from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType - - -class DummyDeviceInfo: - def __init__(self, screen_on=True): - self.info = {"screenOn": screen_on} - - -class DummyDeviceV2: - def __init__(self, screen_on=True): - self.info = {"screenOn": screen_on} - - -class DummyDevice: - def __init__(self, screen_on=True): - self.app_id = "com.instagram.android" - self.deviceV2 = DummyDeviceV2(screen_on) +from tests.e2e.device_emulator import create_emulator_facade @pytest.fixture -def sae(): +def sae(monkeypatch): SituationalAwarenessEngine.reset() - device = DummyDevice() + device, emulator = create_emulator_facade("any", {"any": ""}, {}, monkeypatch) engine = SituationalAwarenessEngine.get_instance(device) # Clear the global ScreenMemoryDB so tests don't pollute each other from GramAddict.core.qdrant_memory import ScreenMemoryDB + db = ScreenMemoryDB() if db.is_connected: try: db.client.delete( - collection_name=db.collection_name, - points_selector={"filter": {}} # Delete all points + collection_name=db.collection_name, + points_selector={"filter": {}}, # Delete all points ) except Exception: pass @@ -54,30 +40,30 @@ class TestSAEPerception: assert sae.perceive("") == SituationType.OBSTACLE_FOREIGN_APP assert sae.perceive(None) == SituationType.OBSTACLE_FOREIGN_APP - def test_perceive_locked_screen(self, sae): + def test_perceive_locked_screen(self, sae, monkeypatch): """If the hardware reports the screen is off, it must be LOCKED_SCREEN.""" - sae.device.deviceV2.info["screenOn"] = False + sae.device.deviceV2.info = {"screenOn": False} xml_dump = '' assert sae.perceive(xml_dump) == SituationType.OBSTACLE_LOCKED_SCREEN def test_perceive_action_blocked(self, sae): """Critical account safety check: 'Action Blocked' must return DANGER_ACTION_BLOCKED.""" - xml_dump = ''' + xml_dump = """ - ''' + """ assert sae.perceive(xml_dump) == SituationType.DANGER_ACTION_BLOCKED def test_perceive_system_dialog(self, sae): """System permission dialogs must be identified structurally.""" - xml_dump = ''' + xml_dump = """ - ''' + """ assert sae.perceive(xml_dump) == SituationType.OBSTACLE_SYSTEM def test_perceive_creation_flow_modal(self, sae): @@ -85,20 +71,20 @@ class TestSAEPerception: Creation flows (camera, story gallery) are inside Instagram but block navigation. They must be caught by structural markers. """ - xml_dump = ''' + xml_dump = """ - ''' + """ assert sae.perceive(xml_dump) == SituationType.OBSTACLE_MODAL def test_perceive_story_gallery_cancel_modal(self, sae): """Story gallery cancel button blocks navigation.""" - xml_dump = ''' + xml_dump = """ - ''' + """ assert sae.perceive(xml_dump) == SituationType.OBSTACLE_MODAL @@ -106,7 +92,7 @@ class TestSAECompression: """Tests the XML compression logic to ensure signatures are compact but retain identity.""" def test_compress_strips_noise_keeps_important_data(self, sae): - xml_dump = ''' + xml_dump = """ @@ -114,9 +100,9 @@ class TestSAECompression: - ''' + """ compressed = sae._compress_xml(xml_dump) - + # Must extract packages assert "PACKAGES: com.instagram.android" in compressed # Must extract resource-ids without the package prefix @@ -141,22 +127,23 @@ class TestSAELoop: def test_ensure_clear_screen_returns_true_immediately_if_normal(self, sae): """If the screen is NORMAL, the SAE should return True without taking actions.""" - xml_dump = ''' + xml_dump = """ - ''' - # Bypass dump_hierarchy - sae.device.dump_hierarchy = lambda: xml_dump + """ + # Inject the XML into the emulator's state instead of bypassing dump_hierarchy + sae.device.deviceV2.info["screenOn"] = True + sae.device.deviceV2._parent.states[sae.device.deviceV2._parent.current_state] = xml_dump assert sae.ensure_clear_screen(max_attempts=3) is True assert sae._consecutive_failures == 0 def test_is_instagram_foreground(self, sae): - xml_dump = ''' + xml_dump = """ - ''' + """ assert sae.is_instagram_foreground(xml_dump=xml_dump) is True foreign_xml = ''