test(e2e): eliminate all legacy mocks and establish real-world sim suite
This commit is contained in:
@@ -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
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user