This commit is contained in:
2026-04-17 00:44:41 +02:00
parent fa1d01527d
commit 89f14463c5
42 changed files with 2452 additions and 314 deletions

View File

@@ -38,11 +38,25 @@ def e2e_device_dump_injector():
return _inject_dump
class VirtualClock:
def __init__(self):
self.time = 0.0
self.animation_target_time = 0.0
def sleep(self, seconds):
if hasattr(seconds, '__iter__'):
return # For edge case where something weird is passed
self.time += float(seconds)
clock = VirtualClock()
@pytest.fixture
def dynamic_e2e_dump_injector(monkeypatch):
"""
State-Machine Injector: Replaces dump_hierarchy dynamically when transitions occur.
Validates that the Telepathic Engine's pathfinding truly worked.
It now inherently simulates UI animation delays. If a dump is requested
LESS than 1.5 virtual seconds after a transition, it returns a garbage animating UI.
"""
def _inject(device_mock, state_map, initial_xml):
fix_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
@@ -54,22 +68,54 @@ def dynamic_e2e_dump_injector(monkeypatch):
with open(path, "r") as f:
return f.read()
device_mock.deviceV2.dump_hierarchy.return_value = load_xml(initial_xml)
# The current active state XML
device_mock._current_active_xml = load_xml(initial_xml)
def _dump_hierarchy_hook():
# If the clock hasn't advanced past the UI animation time, return garbage
# Actually, explicitly fail the E2E test because the bot missed a sync guard!
if clock.time < clock.animation_target_time:
pytest.fail(f"UI SYNCHRONIZATION FAILURE: dump_hierarchy() called mid-animation! "
f"Virtual Clock is at {clock.time:.1f}s but UI needs until {clock.animation_target_time:.1f}s to settle. "
f"Add a time.sleep() guard before interacting with the UI after a click.", pytrace=False)
return device_mock._current_active_xml
device_mock.deviceV2.dump_hierarchy.side_effect = _dump_hierarchy_hook
from GramAddict.core.telepathic_engine import TelepathicEngine
def _mock_find_best_node(*args, **kwargs):
return {"x": 500, "y": 500, "skip": False, "score": 1.0, "source": "e2e_mock"}
monkeypatch.setattr(TelepathicEngine, "find_best_node", _mock_find_best_node)
monkeypatch.setattr(TelepathicEngine, "verify_success", lambda *args, **kwargs: True)
from GramAddict.core.q_nav_graph import QNavGraph
original_execute = QNavGraph._execute_transition
def _mock_execute_transition(nav_self, action, zero_engine):
def _mock_execute_transition(nav_self, action, zero_engine=None, max_retries=2):
if action == 'tap_post_username':
return True
# Evaluate using the real internal LLM/Keyword logic against the current mock XML!
success = original_execute(nav_self, action, zero_engine)
if success is True and action in state_map:
# The node was clicked successfully! Swap the XML to the target state.
device_mock.deviceV2.dump_hierarchy.return_value = load_xml(state_map[action])
return success
# We need to trigger the UI change exactly when the robot clicks physically
original_click = nav_self.device.click
def _click_hook(obj=None, *args, **kwargs):
original_click(obj, *args, **kwargs)
if action in state_map:
device_mock._current_active_xml = load_xml(state_map[action])
clock.animation_target_time = clock.time + 1.5
nav_self.device.click = _click_hook
try:
# Evaluate using the real internal LLM/Keyword logic against the current mock XML!
# Note: max_retries parameter needs to be passed through
success = original_execute(nav_self, action, zero_engine, max_retries=max_retries)
return success
finally:
nav_self.device.click = original_click
monkeypatch.setattr(QNavGraph, "_execute_transition", _mock_execute_transition)
return _inject
@@ -77,12 +123,34 @@ def dynamic_e2e_dump_injector(monkeypatch):
@pytest.fixture(autouse=True)
def mock_all_delays(monkeypatch):
"""
Strips out all humanized hardware delays specifically for the E2E test suite.
Ensures loops evaluate instantly using the injected dumps.
Replaces all humanized hardware delays specifically for the E2E test suite
with a Virtual Clock. Ensures loops evaluate instantly but preserves chronological
dependency for our Animation Simulator.
"""
monkeypatch.setattr(time, "sleep", lambda x: None)
monkeypatch.setattr(utils, "random_sleep", lambda *args, **kwargs: None)
monkeypatch.setattr(utils, "sleep", lambda x: None)
global clock
clock.time = 0.0 # reset for test
clock.animation_target_time = 0.0
def simulate_sleep(seconds):
clock.sleep(seconds)
money_sleep = lambda x: simulate_sleep(x)
random_sleep = lambda *args, **kwargs: simulate_sleep(1.0) # Assume 1.0 minimum for randoms
monkeypatch.setattr(time, "sleep", money_sleep)
monkeypatch.setattr(utils, "random_sleep", random_sleep)
monkeypatch.setattr(utils, "sleep", money_sleep)
# Needs to capture specific module sleeps depending on how they imported it
try:
from GramAddict.core import bot_flow
monkeypatch.setattr(bot_flow, "sleep", money_sleep)
monkeypatch.setattr(bot_flow.random, "uniform", lambda a, b: float(a)) # deterministic lower bound
from GramAddict.core import q_nav_graph
monkeypatch.setattr(q_nav_graph.random, "uniform", lambda a, b: float(a))
except Exception:
pass
# Standardize DarwinEngine across tests to prevent mockup math errors on session end
try:

View File

@@ -0,0 +1,26 @@
import pytest
import time
from unittest.mock import MagicMock
from GramAddict.core.q_nav_graph import QNavGraph
def test_animation_sync_guard_catches_missing_sleep(dynamic_e2e_dump_injector):
"""
Proves that the new Animation Simulator built into conftest.py
properly throws an error if we query the UI without waiting for animations.
"""
device = MagicMock()
# Inject dummy states
dynamic_e2e_dump_injector(device, {'tap_explore_tab': 'explore_feed_dump.xml'}, "home_feed_with_ad.xml")
# Simulate a raw bug where the developer clicked but didn't sleep
# We will simulate exactly what _execute_transition tries to do
nav = QNavGraph(device)
# We call transition. QNavGraph internally clicks and sleeps for 1.2s minimum.
# Our Animation target is 1.5s, so the dump inside _execute_transition will hit the fail guard!
from _pytest.outcomes import Failed
with pytest.raises(Failed) as exc_info:
nav._execute_transition("tap_explore_tab")
assert "UI SYNCHRONIZATION FAILURE" in str(exc_info.value), "The simulator failed to catch the missing sleep guard!"