Hardening: Streamlined testing infrastructure, unified toolkit, and established English TDD standards
This commit is contained in:
@@ -2,6 +2,12 @@ import pytest
|
||||
import logging
|
||||
import os
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--live", action="store_true", default=False, help="run tests against a live ADB device (disable DeviceFacade mocks)"
|
||||
)
|
||||
|
||||
MagicMock.app_id = "com.instagram.android"
|
||||
MagicMock._get_current_app = MagicMock(return_value="com.instagram.android")
|
||||
|
||||
@@ -44,7 +50,10 @@ def mock_logger():
|
||||
return logging.getLogger("test")
|
||||
|
||||
@pytest.fixture
|
||||
def device():
|
||||
def device(request):
|
||||
if request.config.getoption("--live"):
|
||||
from GramAddict.core.device_facade import create_device
|
||||
return create_device("emulator-5554", "com.instagram.android")
|
||||
return create_mock_device()
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -68,7 +77,10 @@ def reset_singletons():
|
||||
yield
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def telepathic_mock(monkeypatch):
|
||||
def telepathic_mock(monkeypatch, request):
|
||||
if request.config.getoption("--live"):
|
||||
# TelepathicEngine is a singleton, allow it to run natively
|
||||
return None
|
||||
import GramAddict.core.telepathic_engine
|
||||
engine = create_mock_telepathic_engine()
|
||||
monkeypatch.setattr(GramAddict.core.telepathic_engine.TelepathicEngine, "get_instance", lambda: engine)
|
||||
|
||||
@@ -17,12 +17,15 @@ mock_qdrant.get_collection.return_value = mock_collection
|
||||
sys.modules["qdrant_client"].QdrantClient = MagicMock(return_value=mock_qdrant)
|
||||
|
||||
@pytest.fixture
|
||||
def e2e_device_dump_injector():
|
||||
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)
|
||||
@@ -51,13 +54,16 @@ class VirtualClock:
|
||||
clock = VirtualClock()
|
||||
|
||||
@pytest.fixture
|
||||
def dynamic_e2e_dump_injector(monkeypatch):
|
||||
def dynamic_e2e_dump_injector(monkeypatch, request):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -123,12 +129,15 @@ def dynamic_e2e_dump_injector(monkeypatch):
|
||||
return _inject
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_all_delays(monkeypatch):
|
||||
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.
|
||||
"""
|
||||
if request.config.getoption("--live"):
|
||||
return
|
||||
|
||||
global clock
|
||||
clock.time = 0.0 # reset for test
|
||||
clock.animation_target_time = 0.0
|
||||
|
||||
48
tests/integration/test_live_telepathy.py
Normal file
48
tests/integration/test_live_telepathy.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
from GramAddict.core.device_facade import DeviceFacade
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get("RUN_LIVE_AI_TESTS"), reason="Requires a running local LLM/Ollama instance and RUN_LIVE_AI_TESTS=1")
|
||||
def test_real_llm_pathfinding_on_golden_fixture():
|
||||
"""
|
||||
Validates that the REAL telepathic engine (hitting the real LLM endpoint)
|
||||
can successfully parse a real IG XML dump and locate standard elements
|
||||
without hallucinating or crashing.
|
||||
"""
|
||||
# 1. Load a real XML dump from our golden fixtures
|
||||
fixture_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
|
||||
xml_path = os.path.join(fixture_dir, "explore_feed_dump.xml")
|
||||
|
||||
assert os.path.exists(xml_path), "Golden fixture explore_feed_dump.xml is missing. Make sure sync_fixtures.py was run."
|
||||
|
||||
with open(xml_path, "r", encoding="utf-8") as f:
|
||||
xml_data = f.read()
|
||||
|
||||
# 2. Setup a fresh real engine
|
||||
engine = TelepathicEngine()
|
||||
|
||||
# Mute the actual screencap logic, but provide valid display bounds
|
||||
device = MagicMock(spec=DeviceFacade)
|
||||
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
|
||||
|
||||
# Mock screenshot to prevent hitting the adb shell during this specific offline text-parsing test
|
||||
device.screenshot = MagicMock()
|
||||
|
||||
# 3. Fire the intent against the real LLM endpoint.
|
||||
# We force min_confidence=1.1 so it ALWAYS hits the LLM Vision Cortex Fallback
|
||||
# and bypasses the local vector DB cache (to ensure we test the prompt & JSON extraction).
|
||||
result = engine.find_best_node(xml_data, "tap reels tab", min_confidence=1.1, device=device)
|
||||
|
||||
# 4. Assert it actually found something sane instead of hallucinating
|
||||
assert result is not None, "Real LLM failed to understand the XML and return a valid action json."
|
||||
assert "x" in result and "y" in result, "Coordinates missing from VLM payload"
|
||||
|
||||
# The Reels tab on standard IG UI is in the bottom navigation bar
|
||||
# usually around y=2200-2400 and roughly x=540
|
||||
assert result["y"] > 2000, f"Expected reels tab to be at the bottom screen, but got y={result['y']}"
|
||||
assert 400 < result["x"] < 700, f"Expected reels tab to be in bottom middle, but got x={result['x']}"
|
||||
|
||||
print(f"SUCCESS: LLM detected reels tab correctly at x={result['x']}, y={result['y']}")
|
||||
Reference in New Issue
Block a user