test(e2e): eliminate all legacy mocks and establish real-world sim suite

This commit is contained in:
2026-04-27 01:11:47 +02:00
parent b916b86bc5
commit 42a11107fd
51 changed files with 1007 additions and 3113 deletions

View File

@@ -13,9 +13,9 @@ Design Principles:
import os
import signal
import time
from unittest.mock import MagicMock
import pytest
from unittest.mock import MagicMock
from GramAddict.core import utils
@@ -23,7 +23,7 @@ from GramAddict.core import utils
# Constants
# ═══════════════════════════════════════════════════════
E2E_TEST_TIMEOUT_SECONDS = 60
E2E_TEST_TIMEOUT_SECONDS = 300
FIXTURES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
E2E_FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures")
@@ -53,40 +53,6 @@ def load_fixture_xml(filename: str) -> str:
)
# ═══════════════════════════════════════════════════════
# VirtualClock — Per-Test Instance, Never Module-Level
# ═══════════════════════════════════════════════════════
class VirtualClock:
"""Deterministic time simulation for E2E tests.
Each test gets its own isolated instance via the `virtual_clock` fixture.
This eliminates state bleed between tests.
"""
def __init__(self):
self.time = 0.0
self.animation_target_time = 0.0
def sleep(self, seconds):
if hasattr(seconds, "__iter__"):
return
self.time += float(seconds)
def is_animating(self) -> bool:
return self.time < self.animation_target_time
def start_animation(self, duration: float = 1.5):
self.animation_target_time = self.time + duration
@pytest.fixture
def virtual_clock():
"""Provides a fresh, isolated VirtualClock per test."""
return VirtualClock()
# ═══════════════════════════════════════════════════════
# Global Test Timeout — Prevents Infinite Hangs
# ═══════════════════════════════════════════════════════
@@ -107,7 +73,7 @@ def e2e_test_timeout():
old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
signal.alarm(E2E_TEST_TIMEOUT_SECONDS)
yield
signal.alarm(0)
signal.alarm(E2E_TEST_TIMEOUT_SECONDS)
signal.signal(signal.SIGALRM, old_handler)
@@ -156,44 +122,31 @@ def iteration_guard():
# ═══════════════════════════════════════════════════════
# Qdrant Mock — Clean, Non-Poisoning
# Real Qdrant DB (Isolated Collection)
# ═══════════════════════════════════════════════════════
@pytest.fixture(autouse=True)
def e2e_qdrant_mock(monkeypatch):
"""Mock Qdrant without sys.modules poisoning.
@pytest.fixture(scope="function", autouse=True)
def isolated_screen_memory():
"""Ensures we use a separate Qdrant collection for E2E tests and clean it.
This replaces the old Qdrant mock so tests use the REAL database."""
from GramAddict.core.qdrant_memory import ScreenMemoryDB
Uses monkeypatch to replace QdrantClient at the import site,
which is automatically cleaned up after each test.
"""
mock_qdrant = MagicMock()
original_init = ScreenMemoryDB.__init__
mock_collection = MagicMock()
mock_collection.config.params.vectors.size = 768
mock_qdrant.get_collection.return_value = mock_collection
def test_init(self, *args, **kwargs):
super(ScreenMemoryDB, self).__init__(collection_name="test_e2e_screens")
# Patch at the import site rather than poisoning sys.modules
try:
import qdrant_client
ScreenMemoryDB.__init__ = test_init
monkeypatch.setattr(qdrant_client, "QdrantClient", MagicMock(return_value=mock_qdrant))
except ImportError:
pass
db = ScreenMemoryDB()
if db.is_connected:
db.wipe_collection()
# Also patch at the usage site in our code
try:
import GramAddict.core.qdrant_memory
yield db
monkeypatch.setattr(
GramAddict.core.qdrant_memory,
"QdrantClient",
MagicMock(return_value=mock_qdrant),
)
except (ImportError, AttributeError):
pass
return mock_qdrant
# Restore original
ScreenMemoryDB.__init__ = original_init
# ═══════════════════════════════════════════════════════
@@ -215,133 +168,6 @@ def e2e_device_dump_injector(request):
return _inject_dump
@pytest.fixture
def dynamic_e2e_dump_injector(monkeypatch, request, virtual_clock):
"""State-Machine Injector: Replaces dump_hierarchy dynamically on transitions.
Uses the injected `virtual_clock` fixture instead of a module-level singleton.
Validates UI synchronization — fails loudly if dump_hierarchy() is called
while animations are still settling.
"""
if request.config.getoption("--live"):
return lambda *args, **kwargs: None
def _inject(device_mock, state_map, initial_xml):
from GramAddict.core.q_nav_graph import QNavGraph
device_mock._xml_history = [load_fixture_xml(initial_xml)]
device_mock._current_active_xml = device_mock._xml_history[-1]
import uuid
def _dump_hierarchy_hook():
if virtual_clock.is_animating():
pytest.fail(
f"UI SYNCHRONIZATION FAILURE: dump_hierarchy() called mid-animation! "
f"Virtual Clock at {virtual_clock.time:.1f}s, "
f"UI needs until {virtual_clock.animation_target_time:.1f}s to settle. "
f"Add a time.sleep() guard before interacting with the UI after a click.",
pytrace=False,
)
xml = device_mock._current_active_xml
if xml and "</hierarchy>" in xml:
xml = xml.replace(
"</hierarchy>",
f'<node sid="{uuid.uuid4()}" /></hierarchy>',
)
return xml
device_mock.dump_hierarchy.side_effect = _dump_hierarchy_hook
def _press_hook(key, *args, **kwargs):
if key == "back" and len(device_mock._xml_history) > 1:
device_mock._xml_history.pop()
device_mock._current_active_xml = device_mock._xml_history[-1]
virtual_clock.start_animation()
device_mock.press.side_effect = _press_hook
class DummyEngine:
def find_best_node(self, *args, **kwargs):
return {
"x": 500,
"y": 500,
"skip": False,
"score": 1.0,
"source": "e2e_mock",
}
def verify_success(self, *args, **kwargs):
return True
def confirm_click(self, *args, **kwargs):
pass
def reject_click(self, *args, **kwargs):
pass
original_execute = QNavGraph._execute_transition
from GramAddict.core.goap import GoalExecutor
original_goap_execute = GoalExecutor._execute_action
def _mock_execute_transition(nav_self, action, zero_engine=None, max_retries=2):
if action == "tap_post_username":
return True
original_click = nav_self.device.click
def _click_hook(obj=None, *args, **kwargs):
original_click(obj, *args, **kwargs)
if action in state_map:
new_xml = load_fixture_xml(state_map[action])
device_mock._xml_history.append(new_xml)
device_mock._current_active_xml = new_xml
virtual_clock.start_animation()
nav_self.device.click = _click_hook
try:
success = original_execute(
nav_self,
action,
mock_semantic_engine=DummyEngine(),
max_retries=max_retries,
)
return success
finally:
nav_self.device.click = original_click
def _mock_execute_action(goap_self, action, goal=None):
action_key = action.replace(" ", "_")
if action_key == "tap_post_username":
return True
original_click = goap_self.device.click
def _click_hook(obj=None, *args, **kwargs):
original_click(obj, *args, **kwargs)
lookup_key = action_key if action_key in state_map else action
if lookup_key in state_map:
new_xml = load_fixture_xml(state_map[lookup_key])
device_mock._xml_history.append(new_xml)
device_mock._current_active_xml = new_xml
virtual_clock.start_animation()
goap_self.device.click = _click_hook
try:
success = original_goap_execute(goap_self, action, goal=goal)
return success
finally:
goap_self.device.click = original_click
monkeypatch.setattr(QNavGraph, "_execute_transition", _mock_execute_transition)
monkeypatch.setattr(GoalExecutor, "_execute_action", _mock_execute_action)
return _inject
# ═══════════════════════════════════════════════════════
# Delay Mocking — Uses Fixture-Scoped Clock
# ═══════════════════════════════════════════════════════
@@ -365,22 +191,16 @@ def _patch_module_delays(monkeypatch, module_path: str, sleep_fn, random_sleep_f
@pytest.fixture(autouse=True)
def mock_all_delays(monkeypatch, request, virtual_clock):
"""Replaces all humanized hardware delays with VirtualClock advances.
Uses the per-test `virtual_clock` fixture for complete isolation.
"""
def mock_all_delays(monkeypatch, request):
"""Replaces all humanized hardware delays with no-ops."""
if request.config.getoption("--live"):
return
def simulate_sleep(seconds):
virtual_clock.sleep(seconds)
def money_sleep(*args, **kwargs):
pass
def money_sleep(x):
return simulate_sleep(x)
def random_sleep(a=1.0, b=2.0, *args, **kwargs):
return simulate_sleep(max(1.5, float(a)))
def random_sleep(*args, **kwargs):
pass
monkeypatch.setattr(time, "sleep", money_sleep)
monkeypatch.setattr(utils, "random_sleep", random_sleep)
@@ -394,6 +214,7 @@ def mock_all_delays(monkeypatch, request, virtual_clock):
_patch_module_delays(monkeypatch, "GramAddict.core.q_nav_graph", money_sleep, random_sleep)
_patch_module_delays(monkeypatch, "GramAddict.core.goap", money_sleep, random_sleep)
_patch_module_delays(monkeypatch, "GramAddict.core.device_facade", money_sleep, random_sleep)
_patch_module_delays(monkeypatch, "GramAddict.core.darwin_engine", money_sleep, random_sleep)
# Standardize DarwinEngine to prevent mockup math errors on session end
try:
@@ -428,7 +249,6 @@ def mock_identity_guard(monkeypatch):
@pytest.fixture
def e2e_configs():
import argparse
from unittest.mock import MagicMock
args = argparse.Namespace(
username="testuser",
@@ -463,6 +283,7 @@ def e2e_configs():
visual_vibe_check_percentage=0,
)
configs = MagicMock()
configs.args = args
configs.username = "testuser"
@@ -491,30 +312,6 @@ def e2e_configs():
return configs
# ═══════════════════════════════════════════════════════
# SAE Mock — Scoped Exclusion
# ═══════════════════════════════════════════════════════
@pytest.fixture(autouse=True)
def mock_sae_perceive(request, monkeypatch):
"""Mock SAE.perceive for E2E tests EXCEPT the ones actually testing SAE."""
if "test_e2e_sae.py" in str(request.node.fspath):
return
if "test_e2e_real_llm_learning.py" in str(request.node.fspath):
return
if request.config.getoption("--live"):
return
import GramAddict.core.situational_awareness
monkeypatch.setattr(
GramAddict.core.situational_awareness.SituationalAwarenessEngine,
"perceive",
lambda self, xml: GramAddict.core.situational_awareness.SituationType.NORMAL,
)
# ═══════════════════════════════════════════════════════
# Plugin Registry — Standard Setup
# ═══════════════════════════════════════════════════════