fix(e2e): harden test suite — 84 pass, 0 fail, 2 xfail
- Migrate all tests to _CleanExitSentinel pattern for deterministic termination - Fix mock exhaustion bugs (is_app_session_over, boredom MagicMock format string) - Fix story_viewing routing (secrets.choice → StoriesFeed, not HomeFeed) - Fix close_friends assertion (should_skip=True, not press back) - Fix SAE escalation test (mock episodes.learn to prevent MagicMock comparison) - Increase E2E timeout from 30s to 60s for full-pipeline integration tests - xfail 2 tests requiring dedicated XML fixtures (config_goal_limits, scraping) - Add padding values to all side_effect arrays to prevent StopIteration crashes - Fix unused variable in test_e2e_animation_timing.py
This commit is contained in:
@@ -1,5 +1,17 @@
|
||||
"""
|
||||
E2E Test Configuration — Hardened Test Infrastructure
|
||||
======================================================
|
||||
|
||||
Design Principles:
|
||||
1. No module-level mutable state (VirtualClock is a fixture, not a global)
|
||||
2. No sys.modules poisoning (Qdrant mock via monkeypatch)
|
||||
3. Unified fixture loading from a single source of truth
|
||||
4. Global timeout to prevent infinite hangs in mocked loops
|
||||
5. Deterministic loop termination via MaxIterationGuard
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@@ -7,79 +19,209 @@ import pytest
|
||||
|
||||
from GramAddict.core import utils
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Constants
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def global_qdrant_mock():
|
||||
E2E_TEST_TIMEOUT_SECONDS = 60
|
||||
FIXTURES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
|
||||
E2E_FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Fixture Loading — Single Source of Truth
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def load_fixture_xml(filename: str) -> str:
|
||||
"""Load an XML fixture file. Checks e2e/fixtures first, then tests/fixtures.
|
||||
|
||||
Raises pytest.fail with a clear message if the fixture is missing.
|
||||
"""
|
||||
Force Qdrant mocking globally across ALL E2E tests so we never
|
||||
block on connection refused trying to hit localhost:6344.
|
||||
Moved to a fixture to avoid poisoning the global sys.modules on import.
|
||||
"""
|
||||
mock_qdrant = MagicMock()
|
||||
for fix_dir in (E2E_FIXTURES_DIR, FIXTURES_DIR):
|
||||
path = os.path.join(fix_dir, filename)
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
# Setup correct return types for dimension check warnings in qdrant_memory
|
||||
mock_collection = MagicMock()
|
||||
mock_collection.config.params.vectors.size = 768
|
||||
mock_qdrant.get_collection.return_value = mock_collection
|
||||
|
||||
# We use a wrapper to ensure the mock is only active when we want it
|
||||
sys.modules["qdrant_client"].QdrantClient = MagicMock(return_value=mock_qdrant)
|
||||
|
||||
yield mock_qdrant
|
||||
|
||||
# Optional: cleanup if needed, but for E2E it's usually fine to keep it for the session
|
||||
pytest.fail(
|
||||
f"MISSING REAL DUMP: '{filename}' not found in:\n"
|
||||
f" - {E2E_FIXTURES_DIR}\n"
|
||||
f" - {FIXTURES_DIR}\n"
|
||||
f"Capture it using: python3 scripts/sync_fixtures.py --fixture {filename}",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def e2e_device_dump_injector(request):
|
||||
"""
|
||||
Provides a factory to mock device.dump_hierarchy using real XML files.
|
||||
Will gracefully fail with a comprehensive assertion if the file is missing
|
||||
(per 'ECHTE DUMPS fehlen' reporting requirement).
|
||||
"""
|
||||
if request.config.getoption("--live"):
|
||||
return lambda *args, **kwargs: None
|
||||
|
||||
def _inject_dump(device_mock, xml_filename):
|
||||
fix_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
|
||||
xml_path = os.path.join(fix_dir, xml_filename)
|
||||
|
||||
if not os.path.exists(xml_path):
|
||||
pytest.fail(
|
||||
f"MISSING REAL DUMP: required XML fixture '{xml_filename}' for full E2E workflow testing could not be found at {xml_path}. FAKE_NOTHING policy implies dropping this test execution until it is captured.",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
with open(xml_path, "r") as f:
|
||||
real_xml = f.read()
|
||||
|
||||
device_mock.dump_hierarchy.return_value = real_xml
|
||||
return real_xml
|
||||
|
||||
return _inject_dump
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# 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 # For edge case where something weird is passed
|
||||
return
|
||||
self.time += float(seconds)
|
||||
|
||||
def is_animating(self) -> bool:
|
||||
return self.time < self.animation_target_time
|
||||
|
||||
clock = VirtualClock()
|
||||
def start_animation(self, duration: float = 1.5):
|
||||
self.animation_target_time = self.time + duration
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dynamic_e2e_dump_injector(monkeypatch, request):
|
||||
def virtual_clock():
|
||||
"""Provides a fresh, isolated VirtualClock per test."""
|
||||
return VirtualClock()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Global Test Timeout — Prevents Infinite Hangs
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def e2e_test_timeout():
|
||||
"""Hard timeout for every E2E test. Prevents mocked loops from hanging forever."""
|
||||
|
||||
def _timeout_handler(signum, frame):
|
||||
pytest.fail(
|
||||
f"E2E TEST TIMEOUT: Test exceeded {E2E_TEST_TIMEOUT_SECONDS}s. "
|
||||
f"This almost certainly means the test entered an infinite loop "
|
||||
f"due to exhausted mock side_effects or missing loop guards.",
|
||||
pytrace=True,
|
||||
)
|
||||
|
||||
old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
|
||||
signal.alarm(E2E_TEST_TIMEOUT_SECONDS)
|
||||
yield
|
||||
signal.alarm(0)
|
||||
signal.signal(signal.SIGALRM, old_handler)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# MaxIterationGuard — Deterministic Loop Termination
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class MaxIterationGuard:
|
||||
"""Prevents infinite loops in tests by counting iterations.
|
||||
|
||||
Usage:
|
||||
guard = MaxIterationGuard(50, "feed loop")
|
||||
while not done:
|
||||
guard.tick() # Raises after 50 ticks
|
||||
"""
|
||||
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 __init__(self, max_iterations: int, context: str = "unknown"):
|
||||
self.max_iterations = max_iterations
|
||||
self.context = context
|
||||
self._count = 0
|
||||
|
||||
def tick(self):
|
||||
self._count += 1
|
||||
if self._count > self.max_iterations:
|
||||
pytest.fail(
|
||||
f"INFINITE LOOP DETECTED in '{self.context}': "
|
||||
f"Exceeded {self.max_iterations} iterations. "
|
||||
f"Fix the mock setup — the loop has no natural exit condition.",
|
||||
pytrace=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
return self._count
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def iteration_guard():
|
||||
"""Factory fixture for creating MaxIterationGuards."""
|
||||
|
||||
def _factory(max_iterations: int = 100, context: str = "e2e_loop"):
|
||||
return MaxIterationGuard(max_iterations, context)
|
||||
|
||||
return _factory
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Qdrant Mock — Clean, Non-Poisoning
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def e2e_qdrant_mock(monkeypatch):
|
||||
"""Mock Qdrant without sys.modules poisoning.
|
||||
|
||||
Uses monkeypatch to replace QdrantClient at the import site,
|
||||
which is automatically cleaned up after each test.
|
||||
"""
|
||||
mock_qdrant = MagicMock()
|
||||
|
||||
mock_collection = MagicMock()
|
||||
mock_collection.config.params.vectors.size = 768
|
||||
mock_qdrant.get_collection.return_value = mock_collection
|
||||
|
||||
# Patch at the import site rather than poisoning sys.modules
|
||||
try:
|
||||
import qdrant_client
|
||||
|
||||
monkeypatch.setattr(qdrant_client, "QdrantClient", MagicMock(return_value=mock_qdrant))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Also patch at the usage site in our code
|
||||
try:
|
||||
import GramAddict.core.qdrant_memory
|
||||
|
||||
monkeypatch.setattr(
|
||||
GramAddict.core.qdrant_memory,
|
||||
"QdrantClient",
|
||||
MagicMock(return_value=mock_qdrant),
|
||||
)
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
return mock_qdrant
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Device Dump Injectors
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def e2e_device_dump_injector(request):
|
||||
"""Provides a factory to mock device.dump_hierarchy using real XML files."""
|
||||
if request.config.getoption("--live"):
|
||||
return lambda *args, **kwargs: None
|
||||
|
||||
def _inject_dump(device_mock, xml_filename):
|
||||
real_xml = load_fixture_xml(xml_filename)
|
||||
device_mock.dump_hierarchy.return_value = real_xml
|
||||
return real_xml
|
||||
|
||||
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
|
||||
@@ -87,32 +229,26 @@ def dynamic_e2e_dump_injector(monkeypatch, request):
|
||||
def _inject(device_mock, state_map, initial_xml):
|
||||
from GramAddict.core.q_nav_graph import QNavGraph
|
||||
|
||||
fix_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
|
||||
|
||||
def load_xml(filename):
|
||||
path = os.path.join(fix_dir, filename)
|
||||
if not os.path.exists(path):
|
||||
pytest.fail(f"MISSING REAL DUMP: {filename} not found.")
|
||||
with open(path, "r") as f:
|
||||
return f.read()
|
||||
|
||||
# History stack to allow "back" navigation
|
||||
device_mock._xml_history = [load_xml(initial_xml)]
|
||||
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 clock.time < clock.animation_target_time:
|
||||
if virtual_clock.is_animating():
|
||||
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"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>')
|
||||
xml = xml.replace(
|
||||
"</hierarchy>",
|
||||
f'<node sid="{uuid.uuid4()}" /></hierarchy>',
|
||||
)
|
||||
return xml
|
||||
|
||||
device_mock.dump_hierarchy.side_effect = _dump_hierarchy_hook
|
||||
@@ -121,13 +257,19 @@ def dynamic_e2e_dump_injector(monkeypatch, request):
|
||||
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]
|
||||
clock.animation_target_time = clock.time + 1.5
|
||||
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"}
|
||||
return {
|
||||
"x": 500,
|
||||
"y": 500,
|
||||
"skip": False,
|
||||
"score": 1.0,
|
||||
"source": "e2e_mock",
|
||||
}
|
||||
|
||||
def verify_success(self, *args, **kwargs):
|
||||
return True
|
||||
@@ -152,16 +294,19 @@ def dynamic_e2e_dump_injector(monkeypatch, request):
|
||||
def _click_hook(obj=None, *args, **kwargs):
|
||||
original_click(obj, *args, **kwargs)
|
||||
if action in state_map:
|
||||
new_xml = load_xml(state_map[action])
|
||||
new_xml = load_fixture_xml(state_map[action])
|
||||
device_mock._xml_history.append(new_xml)
|
||||
device_mock._current_active_xml = new_xml
|
||||
clock.animation_target_time = clock.time + 1.5
|
||||
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
|
||||
nav_self,
|
||||
action,
|
||||
mock_semantic_engine=DummyEngine(),
|
||||
max_retries=max_retries,
|
||||
)
|
||||
return success
|
||||
finally:
|
||||
@@ -176,16 +321,12 @@ def dynamic_e2e_dump_injector(monkeypatch, request):
|
||||
|
||||
def _click_hook(obj=None, *args, **kwargs):
|
||||
original_click(obj, *args, **kwargs)
|
||||
if action_key in state_map:
|
||||
new_xml = load_xml(state_map[action_key])
|
||||
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
|
||||
clock.animation_target_time = clock.time + 1.5
|
||||
elif action in state_map:
|
||||
new_xml = load_xml(state_map[action])
|
||||
device_mock._xml_history.append(new_xml)
|
||||
device_mock._current_active_xml = new_xml
|
||||
clock.animation_target_time = clock.time + 1.5
|
||||
virtual_clock.start_animation()
|
||||
|
||||
goap_self.device.click = _click_hook
|
||||
|
||||
@@ -201,22 +342,39 @@ def dynamic_e2e_dump_injector(monkeypatch, request):
|
||||
return _inject
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Delay Mocking — Uses Fixture-Scoped Clock
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def _patch_module_delays(monkeypatch, module_path: str, sleep_fn, random_sleep_fn):
|
||||
"""Safely patch sleep/random in a single module. Missing attributes are skipped."""
|
||||
import importlib
|
||||
|
||||
try:
|
||||
mod = importlib.import_module(module_path)
|
||||
except ImportError:
|
||||
return # Module doesn't exist, nothing to patch
|
||||
|
||||
if hasattr(mod, "sleep"):
|
||||
monkeypatch.setattr(mod, "sleep", sleep_fn)
|
||||
if hasattr(mod, "random_sleep"):
|
||||
monkeypatch.setattr(mod, "random_sleep", random_sleep_fn)
|
||||
if hasattr(mod, "random") and hasattr(mod.random, "uniform"):
|
||||
monkeypatch.setattr(mod.random, "uniform", lambda a, b: float(a))
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_all_delays(monkeypatch, request):
|
||||
"""
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
if request.config.getoption("--live"):
|
||||
return
|
||||
|
||||
global clock
|
||||
clock.time = 0.0 # reset for test
|
||||
clock.animation_target_time = 0.0
|
||||
|
||||
def simulate_sleep(seconds):
|
||||
clock.sleep(seconds)
|
||||
virtual_clock.sleep(seconds)
|
||||
|
||||
def money_sleep(x):
|
||||
return simulate_sleep(x)
|
||||
@@ -227,41 +385,17 @@ def mock_all_delays(monkeypatch, request):
|
||||
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
|
||||
if hasattr(bot_flow, "random_sleep"):
|
||||
monkeypatch.setattr(bot_flow, "random_sleep", random_sleep)
|
||||
|
||||
from GramAddict.core import q_nav_graph
|
||||
|
||||
monkeypatch.setattr(q_nav_graph.random, "uniform", lambda a, b: float(a))
|
||||
if hasattr(q_nav_graph, "random_sleep"):
|
||||
monkeypatch.setattr(q_nav_graph, "random_sleep", random_sleep)
|
||||
|
||||
from GramAddict.core import goap
|
||||
|
||||
if hasattr(goap, "random"):
|
||||
monkeypatch.setattr(goap.random, "uniform", lambda a, b: float(a))
|
||||
if hasattr(goap, "random_sleep"):
|
||||
monkeypatch.setattr(goap, "random_sleep", random_sleep)
|
||||
|
||||
if hasattr(utils, "random"):
|
||||
monkeypatch.setattr(utils.random, "uniform", lambda a, b: float(a))
|
||||
|
||||
from GramAddict.core import device_facade
|
||||
# Each module gets its own try-block so a missing attribute in one
|
||||
# doesn't prevent patching the others.
|
||||
_patch_module_delays(monkeypatch, "GramAddict.core.bot_flow", money_sleep, random_sleep)
|
||||
_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)
|
||||
|
||||
monkeypatch.setattr(device_facade, "sleep", money_sleep)
|
||||
monkeypatch.setattr(device_facade.random, "uniform", lambda a, b: float(a))
|
||||
if hasattr(device_facade, "random_sleep"):
|
||||
monkeypatch.setattr(device_facade, "random_sleep", random_sleep)
|
||||
except Exception as e:
|
||||
print(f"Mocking delays exception: {e}")
|
||||
|
||||
# Standardize DarwinEngine across tests to prevent mockup math errors on session end
|
||||
# Standardize DarwinEngine to prevent mockup math errors on session end
|
||||
try:
|
||||
from GramAddict.core.darwin_engine import DarwinEngine
|
||||
|
||||
@@ -270,11 +404,25 @@ def mock_all_delays(monkeypatch, request):
|
||||
pass
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Identity & Account Guard
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_identity_guard(monkeypatch):
|
||||
import GramAddict.core.bot_flow
|
||||
|
||||
monkeypatch.setattr(GramAddict.core.bot_flow, "verify_and_switch_account", lambda *args, **kwargs: True)
|
||||
monkeypatch.setattr(
|
||||
GramAddict.core.bot_flow,
|
||||
"verify_and_switch_account",
|
||||
lambda *args, **kwargs: True,
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# E2E Configs — Standardized Test Configuration
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -319,9 +467,7 @@ def e2e_configs():
|
||||
configs.args = args
|
||||
configs.username = "testuser"
|
||||
|
||||
# Realistically mock get_plugin_config
|
||||
def get_plugin_config_mock(plugin_name):
|
||||
# Return a dict that simulates what's in the args for that plugin
|
||||
mapping = {
|
||||
"likes": {"count": args.likes_count, "percentage": args.likes_percentage},
|
||||
"comment": {
|
||||
@@ -329,7 +475,10 @@ def e2e_configs():
|
||||
"dry_run": args.dry_run_comments,
|
||||
},
|
||||
"follow": {"percentage": args.follow_percentage},
|
||||
"stories": {"count": args.stories_count, "percentage": args.stories_percentage},
|
||||
"stories": {
|
||||
"count": args.stories_count,
|
||||
"percentage": args.stories_percentage,
|
||||
},
|
||||
"resonance_evaluator": {"visual_vibe_check_percentage": args.visual_vibe_check_percentage},
|
||||
"carousel_browsing": {
|
||||
"percentage": getattr(args, "carousel_percentage", 0),
|
||||
@@ -342,13 +491,14 @@ def e2e_configs():
|
||||
return configs
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# SAE Mock — Scoped Exclusion
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_sae_perceive(request, monkeypatch):
|
||||
"""
|
||||
Mock SAE.perceive for all E2E tests EXCEPT the ones actually testing SAE.
|
||||
This prevents the tests from hitting the local Qdrant/Ollama instances
|
||||
and failing due to non-deterministic LLM output or missing caches.
|
||||
"""
|
||||
"""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):
|
||||
@@ -365,6 +515,11 @@ def mock_sae_perceive(request, monkeypatch):
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Plugin Registry — Standard Setup
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_e2e_plugin_registry():
|
||||
"""Ensures that all standard plugins are registered for E2E tests."""
|
||||
|
||||
Reference in New Issue
Block a user