Hardening: Streamlined testing infrastructure, unified toolkit, and established English TDD standards
This commit is contained in:
@@ -66,12 +66,6 @@ def start_bot(**kwargs):
|
||||
prewarm_ollama_models(configs)
|
||||
except Exception as e:
|
||||
logger.debug(f"Prewarm failed: {e}")
|
||||
|
||||
if getattr(configs.args, "capture_e2e_dumps", False):
|
||||
device = create_device(configs.device_id, configs.app_id, configs.args)
|
||||
from GramAddict.core.dump_capturer import capture_all
|
||||
capture_all(device)
|
||||
return
|
||||
|
||||
sessions = PersistentList("sessions", SessionStateEncoder)
|
||||
device = create_device(configs.device_id, configs.app_id, configs.args)
|
||||
|
||||
@@ -141,7 +141,6 @@ class Config:
|
||||
self.parser.add_argument("--time-delta-session", help="Time delta between sessions", default=None)
|
||||
self.parser.add_argument("--restart-atx-agent", action="store_true", help="Restart atx agent")
|
||||
self.parser.add_argument("--allow-untested-ig-version", action="store_true", help="Allow untested IG version")
|
||||
self.parser.add_argument("--capture-e2e-dumps", action="store_true", help="Automatically navigate through the app and capture missing XML dumps for the test suite")
|
||||
|
||||
# Interaction settings
|
||||
self.parser.add_argument("--likes-count", help="Likes count", default="2-3")
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def capture_all(device):
|
||||
"""
|
||||
Automated E2E Dump Capturer Sequence.
|
||||
Navigates through the Instagram UI and securely saves exact XML representations
|
||||
to satisfy the `e2e_device_dump_injector` test requirements.
|
||||
|
||||
Warning: Requires a logged-in session and active device connection.
|
||||
"""
|
||||
logger.info("📸 Initiating E2E Dump Capture Sequence!")
|
||||
|
||||
FIX_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "tests", "fixtures")
|
||||
os.makedirs(FIX_DIR, exist_ok=True)
|
||||
|
||||
def _save_dump(filename, description):
|
||||
logger.info(f"⏳ Waiting for UI to settle for [{description}]...")
|
||||
time.sleep(3.5) # ensure animations finish
|
||||
xml_data = device.dump_hierarchy()
|
||||
path = os.path.join(FIX_DIR, filename)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(xml_data)
|
||||
logger.info(f"✅ Saved ECHTEN DUMP to {filename}")
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("🤖 MANUAL E2E DUMP CAPTURE SEQUENCE")
|
||||
print("="*50)
|
||||
print("Please follow the instructions below to capture the required fixtures.")
|
||||
print("If an IG update changed the layout, you can navigate there naturally.")
|
||||
print("="*50 + "\n")
|
||||
|
||||
try:
|
||||
# Pre-condition: Device connected
|
||||
logger.info("Verifying device connection...")
|
||||
device.info
|
||||
|
||||
# 1. Comment Sheet
|
||||
input("\n👉 1. COMMENT SHEET:\nOpen Instagram, scroll to any post on the HomeFeed, and open the comment section.\nWhen the comment sheet is fully visible, press ENTER to capture...")
|
||||
_save_dump("comment_sheet.xml", "Post Comment Sheet")
|
||||
|
||||
# 2. Stories Feed
|
||||
input("\n👉 2. STORIES FEED:\nGo to the HomeFeed and tap any user's story right at the top.\nWhile the story is playing (video/photo is visible), press ENTER to capture...")
|
||||
_save_dump("stories_feed_dump.xml", "Active Story Playback")
|
||||
|
||||
# 3. DM Inbox
|
||||
input("\n👉 3. DM INBOX:\nGo back to the HomeFeed and tap the message icon in the top right to open your inbox.\nWhen your list of chats is visible, press ENTER to capture...")
|
||||
_save_dump("dm_inbox_dump.xml", "DM Inbox / Threads List")
|
||||
|
||||
# 4. Profile Scraping & Unfollow List
|
||||
input("\n👉 4. OWN PROFILE:\nGo to your OWN profile by tapping your avatar in the bottom right corner.\nWhen your bio and grid are fully visible, press ENTER to capture...")
|
||||
_save_dump("scraping_profile_dump.xml", "Own Profile Root (User Info)")
|
||||
|
||||
input("\n👉 4.b FOLLOWING LIST:\nFrom your profile, tap your 'Following' (Abonniert) count to open the list of people you follow.\nWhen the list is fully loaded, press ENTER to capture...")
|
||||
_save_dump("unfollow_list_dump.xml", "Following List Iteration View")
|
||||
|
||||
# 5. Search Feed
|
||||
input("\n👉 5. EXPLORE SEARCH:\nTap the magnifying glass (Explore) tab at the bottom. Then, tap into the top 'Search' bar so your keyboard opens.\nWhen you are in the search state, press ENTER to capture...")
|
||||
_save_dump("search_feed_dump.xml", "Explore Search Input Focus")
|
||||
|
||||
# 6. Reels Feed
|
||||
input("\n👉 6. REELS FEED:\nTap the Reels (Video) tab at the bottom center. Let a video start playing.\nPress ENTER to capture...")
|
||||
_save_dump("reels_feed_dump.xml", "Reels Video Feed")
|
||||
|
||||
# 7. Notifications
|
||||
input("\n👉 7. NOTIFICATIONS (ACTIVITY):\nGo to the HomeFeed and tap the Heart icon in the top right to open notifications.\nPress ENTER to capture...")
|
||||
_save_dump("notifications_dump.xml", "Activity / Notifications tab")
|
||||
|
||||
# 8. Explore Grid
|
||||
input("\n👉 8. EXPLORE GRID:\nTap the magnifying glass (Explore) tab, but do NOT tap the search bar.\nWhen the grid of images/videos is visible, press ENTER to capture...")
|
||||
_save_dump("explore_feed_dump.xml", "Explore Discovery Grid")
|
||||
|
||||
# 9. Other User's Profile
|
||||
input("\n👉 9. ALIEN PROFILE:\nNavigate to ANY OTHER user's profile (e.g. from your Feed or Search).\nWhen their bio and grid are visible, press ENTER to capture...")
|
||||
_save_dump("user_profile_dump.xml", "Alien Profile Root")
|
||||
|
||||
# 10. Followers List
|
||||
input("\n👉 10. FOLLOWERS LIST:\nFrom that profile (or your own), tap the 'Followers' (Abonnenten) count.\nWhen the list of followers is visible, press ENTER to capture...")
|
||||
_save_dump("followers_list_dump.xml", "Followers List Iteration View")
|
||||
|
||||
# 11. Carousel Post
|
||||
input("\n👉 11. CAROUSEL POST:\nScroll your Feed until you see a Carousel (a post with multiple swipable images/videos).\nWhen it is visible, press ENTER to capture...")
|
||||
_save_dump("carousel_post_dump.xml", "Carousel Post Wrapper")
|
||||
|
||||
# 12. Sponsored Post / Ad
|
||||
input("\n👉 12. SPONSORED AD:\nScroll your Feed or Stories until you see a Sponsored / Gesponsert Post with an action button.\nWhen the Ad is visible, press ENTER to capture...")
|
||||
_save_dump("home_feed_with_ad.xml", "Sponsored Ad Post")
|
||||
|
||||
# 13. Inside DM Chat
|
||||
input("\n👉 13. DM CHAT THREAD:\nOpen any message thread in your DM inbox.\nWhen the chat messages and text input field are visible, press ENTER to capture...")
|
||||
_save_dump("dm_thread_dump.xml", "Direct Message Chat Thread")
|
||||
|
||||
print("\n" + "="*50)
|
||||
logger.info("🎉 Capture Sequence Complete! All 13 E2E dumps have been placed into tests/fixtures/")
|
||||
print("="*50 + "\n")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n")
|
||||
logger.info("🛑 Capture Sequence Interrupted by User.")
|
||||
except Exception as e:
|
||||
logger.error(f"💥 Capture Sequence crashed: {e}", exc_info=True)
|
||||
129
TESTING.md
Normal file
129
TESTING.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 🧪 Instagram Bot Testing Standards
|
||||
|
||||
This project follows a strict **Test-Driven Development (TDD)** philosophy. We do not write features blindly; we ground our development in real-world observations and automated verification.
|
||||
|
||||
## 🔴🟢🔵 The TDD Workflow (Red-Green-Refactor)
|
||||
|
||||
Every new feature or bugfix should follow this cycle:
|
||||
|
||||
1. **RED**: Start by obtaining a **Real XML Dump** (using the Testing Toolkit) of the target UI state. Write a test that fails against this dump or a `--live` device.
|
||||
2. **GREEN**: Implement the minimum amount of code (logic in `TelepathicEngine`, `QNavGraph`, etc.) to make the test pass.
|
||||
3. **REFACTOR**: Clean up the code. Ensure it adheres to our [Best Practices](#-best-practices--no-gos), is well-documented, and doesn't introduce regressions.
|
||||
|
||||
> [!TIP]
|
||||
> TDD-specific tests, regression fixes, and edge-case hardening should be placed in the `tests/tdd/` directory.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 1. Mock Testing (Offline Mode)
|
||||
This is the **default mode** used in CI/CD pipelines and for rapid local development iterations.
|
||||
|
||||
- **Concept**: The `DeviceFacade` is fully mocked. UI states are loaded from static XML fixtures located in `tests/fixtures/`.
|
||||
- **Primary Benefit**: Extremely fast (<1s per test) and requires no physical device or emulator.
|
||||
- **Command**:
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Fixture Management (The Toolkit)
|
||||
To prevent offline tests from validating against outdated Instagram layouts, XML fixtures must be periodically synchronized. Use the **Testing Toolkit** located in `scripts/`.
|
||||
|
||||
### Interactive Guide (Full Sync)
|
||||
This guide walks you through 13 critical views (Home, Explore, DMs, etc.) and automatically captures the required dumps.
|
||||
- **Command**:
|
||||
```bash
|
||||
python3 scripts/sync_fixtures.py --config test_config.yml --interactive
|
||||
```
|
||||
|
||||
### Single Fixture Update
|
||||
If only a specific screen has changed:
|
||||
- **Command**:
|
||||
```bash
|
||||
python3 scripts/sync_fixtures.py --config test_config.yml --fixture explore_feed_dump.xml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Live Hardware Testing (Real Device Mode)
|
||||
Validates that the bot correctly interacts with a real device (emulator or physical phone) by performing actual clicks and swipes.
|
||||
|
||||
- **Concept**: Disables mocks. `pytest` connects via ADB to the active device.
|
||||
- **Primary Benefit**: Detects UI synchronization issues, animation delays, and ADB connection drops.
|
||||
- **Command**:
|
||||
```bash
|
||||
pytest --live
|
||||
```
|
||||
*(Note: Uses the device ID specified in your config or `conftest.py` defaults).*
|
||||
|
||||
---
|
||||
|
||||
## 4. AI & LLM Validation
|
||||
Verifies that the bot's "brain" (Telepathic Engine) still understands XML structures and correctly maps elements to actions (e.g., finding the "Like Button").
|
||||
|
||||
- **Concept**: Sends real prompts to your local LLM server (Ollama/Qwen).
|
||||
- **Primary Benefit**: Protects against "prompt drift" or performance regressions after model updates.
|
||||
- **Command**:
|
||||
```bash
|
||||
RUN_LIVE_AI_TESTS=1 pytest tests/integration/test_live_telepathy.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. E2E Functional Sequences
|
||||
These tests simulate full automation loops to ensure that different components (Goap, Navigation, SAE) play together correctly.
|
||||
|
||||
- **Concept**: Targeted scenario tests that verify a complete user flow from start to finish.
|
||||
- **Example Flows**:
|
||||
- `tests/e2e/test_e2e_explore_feed.py`: Validates the full "Explore -> Analyze -> Interact" loop.
|
||||
- `tests/e2e/test_e2e_dm_sequence.py`: Validates handling of message threads.
|
||||
- **Command**:
|
||||
```bash
|
||||
pytest tests/e2e/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Cognitive Benchmarking
|
||||
Measures the "IQ" and latency of your LLM models to ensure they are suitable for autonomous navigation.
|
||||
|
||||
- **Concept**: Runs a series of 13+ UI-parsing scenarios against your configured models and scores them on accuracy and speed.
|
||||
- **Benefit**: Identifies models that are too slow or "hallucinate" UI coordinates before you let them loose on your real account.
|
||||
- **Command (Ollama)**:
|
||||
```bash
|
||||
python3 benchmarks/run_competitive_benchmark.py --all-ollama
|
||||
```
|
||||
- **Command (Existing Config)**:
|
||||
```bash
|
||||
python3 benchmarks/run_competitive_benchmark.py --config test_config.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Troubleshooting
|
||||
- **Device offline**: Ensure that `adb devices` lists your device and it is authorized.
|
||||
- **LLM Timeout**: Verify that Ollama is running (`ollama list`) and the required model (e.g., `qwen3.5:latest`) is loaded.
|
||||
- **Missing Fixture**: If a test fails with `MISSING REAL DUMP`, use the Toolkit (Step 2) to capture the missing screen.
|
||||
- **Benchmark Failures**: If a model fails benchmarks, it is automatically marked as `is_unsuitable` and should not be used for critical navigation tasks.
|
||||
|
||||
---
|
||||
|
||||
## 💎 Best Practices & No-Gos
|
||||
|
||||
To maintain a high-fidelity test suite, all developers must adhere to these standards:
|
||||
|
||||
### ✅ Best Practices
|
||||
- **Use Golden Fixtures**: Always use real, freshly pulled XML dumps. If the Instagram UI changes, update the fixtures immediately using the Testing Toolkit.
|
||||
- **Singleton Isolation**: Ensure all core singletons (`TelepathicEngine`, `GoalExecutor`) are reset between tests in `conftest.py`.
|
||||
- **Hermetic Tests**: Each test must be independent. Ensure on-disk caches (JSON files) are wiped before each run.
|
||||
- **Layered Validation**: Start with fast mock tests for logic, then verify with `--live` hardware tests for physical interaction.
|
||||
- **Relative Pathing**: Use `os.path.join` relative to `__file__` for all fixture loading.
|
||||
|
||||
### ❌ No-Gos
|
||||
- **"Lying" Mocks**: Never create hand-written or "guessed" XML structures. If you don't have a dump, pull a real one.
|
||||
- **Hardcoded Absolute Paths**: Never use paths like `/Users/name/...`. These break CI and other developers' environments.
|
||||
- **State Leakage**: Never rely on the side effects of a previous test. If a test fails, it should not cause subsequent tests to fail.
|
||||
- **Implicit Timing in Mocks**: Do not use `time.sleep()` for UI waiting in offline tests. Rely on the `VirtualClock` or state-based assertions.
|
||||
- **Mocking Navigation Logic**: In E2E tests, do not mock the internal decision-making of the `TelepathicEngine` or `GrowthBrain`. Force them to process real (fixture) data.
|
||||
118
scripts/sync_fixtures.py
Executable file
118
scripts/sync_fixtures.py
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import time
|
||||
import logging
|
||||
|
||||
# Ensure GramAddict is in PYTHONPATH if script is run directly
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from GramAddict.core.device_facade import create_device
|
||||
from GramAddict.core.config import Config
|
||||
|
||||
# Setup a clean logger for the toolkit
|
||||
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(levelname)s | %(message)s', datefmt='%H:%M:%S')
|
||||
logger = logging.getLogger("TestingToolkit")
|
||||
|
||||
def _save_dump(device, fixture_dir, filename, description):
|
||||
logger.info(f"⏳ Waiting for UI to settle for [{description}]...")
|
||||
time.sleep(3.5) # ensure animations finish
|
||||
xml_data = device.dump_hierarchy()
|
||||
|
||||
if not xml_data or len(xml_data) < 100:
|
||||
logger.warning(f"⚠️ Received empty or exceptionally small XML dump for {filename}. Is the app open?")
|
||||
|
||||
path = os.path.join(fixture_dir, filename)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(xml_data)
|
||||
logger.info(f"✅ Saved REAL DUMP to {filename} ({len(xml_data)} bytes)")
|
||||
|
||||
def run_interactive_guide(device, fixture_dir):
|
||||
print("\n" + "="*60)
|
||||
print("🤖 AUTOMATED E2E DUMP CAPTURE GUIDE")
|
||||
print("="*60)
|
||||
print("This guide will help you refresh the golden fixtures for the E2E suite.")
|
||||
print("Please follow the instructions on your phone/emulator.")
|
||||
print("="*60 + "\n")
|
||||
|
||||
steps = [
|
||||
("comment_sheet.xml", "Post Comment Sheet", "Open any post and open its comment section."),
|
||||
("stories_feed_dump.xml", "Active Story Playback", "Tap any user's story ring on the HomeFeed."),
|
||||
("dm_inbox_dump.xml", "DM Inbox / Threads List", "Go to your DM inbox list."),
|
||||
("scraping_profile_dump.xml", "Own Profile Root", "Go to your OWN profile tab."),
|
||||
("unfollow_list_dump.xml", "Following List", "On your profile, tap 'Following' to open the list."),
|
||||
("search_feed_dump.xml", "Explore Search Focus", "Tap Explore, then tap into the Search bar."),
|
||||
("reels_feed_dump.xml", "Reels Video Feed", "Go to the Reels tab and let a video play."),
|
||||
("notifications_dump.xml", "Activity Feed", "Tap the Heart/Notifications icon."),
|
||||
("explore_feed_dump.xml", "Explore Grid", "Tap the Explore tab (just the grid visible)."),
|
||||
("user_profile_dump.xml", "Alien Profile Root", "Go to ANY OTHER user's profile."),
|
||||
("followers_list_dump.xml", "Followers List", "On that user's profile, tap 'Followers'."),
|
||||
("carousel_post_dump.xml", "Carousel Post", "Find a post with multiple images on your feed."),
|
||||
("home_feed_with_ad.xml", "Sponsored Ad", "Scroll until you see a 'Sponsored' post."),
|
||||
("dm_thread_dump.xml", "DM Chat Thread", "Open any specific chat conversation.")
|
||||
]
|
||||
|
||||
for i, (fname, desc, instr) in enumerate(steps, 1):
|
||||
try:
|
||||
print(f"\n👉 {i}/{len(steps)}. {desc.upper()}:")
|
||||
print(f" {instr}")
|
||||
input(" Press ENTER when ready to capture...")
|
||||
_save_dump(device, fixture_dir, fname, desc)
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Guide interrupted.")
|
||||
return
|
||||
|
||||
print("\n" + "="*60)
|
||||
logger.info("🎉 Capture Sequence Complete! All fixtures have been updated.")
|
||||
print("="*60 + "\n")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Instagram Bot Testing Toolkit - Fixture Management")
|
||||
parser.add_argument("--config", help="Path to your bot's .yml config (to auto-extract device ID)")
|
||||
parser.add_argument("--device", help="Manual ADB device ID (e.g., emulator-5554)")
|
||||
parser.add_argument("--fixture", help="Name of a single fixture to sync (e.g., explore_feed_dump.xml)")
|
||||
parser.add_argument("--interactive", action="store_true", help="Launch the full interactive capture guide")
|
||||
args = parser.parse_args()
|
||||
|
||||
device_id = args.device
|
||||
|
||||
# Try to extract device from config if provided
|
||||
if args.config:
|
||||
try:
|
||||
# We use a dummy Config instance to parse the file
|
||||
bot_config = Config(first_run=True, config=args.config)
|
||||
bot_config.parse_args()
|
||||
device_id = bot_config.device_id or device_id
|
||||
logger.info(f"Loaded device ID from config: {device_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not read config file {args.config}: {e}")
|
||||
|
||||
if not device_id:
|
||||
logger.error("No device ID provided! Use --device or --config to specify which device to connect to.")
|
||||
sys.exit(1)
|
||||
|
||||
fixture_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "tests", "fixtures")
|
||||
os.makedirs(fixture_dir, exist_ok=True)
|
||||
|
||||
print(f"Connecting to {device_id}...")
|
||||
try:
|
||||
device = create_device(device_id, "com.instagram.android")
|
||||
# Quick connectivity check
|
||||
device.get_info()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to device: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if args.interactive:
|
||||
run_interactive_guide(device, fixture_dir)
|
||||
elif args.fixture:
|
||||
fname = args.fixture
|
||||
if not fname.endswith(".xml"): fname += ".xml"
|
||||
_save_dump(device, fixture_dir, fname, f"Single Fixture: {fname}")
|
||||
else:
|
||||
logger.info("Nothing to do. Use --interactive or --fixture <name>.")
|
||||
parser.print_help()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -54,7 +54,7 @@ limits:
|
||||
max_comments_per_day: 40
|
||||
|
||||
# ── Infrastructure (Nur für Entwickler) ──
|
||||
device: 192.168.1.206:41441
|
||||
device: 192.168.1.206:45003
|
||||
app-id: com.instagram.android
|
||||
ai-model: qwen3.5:latest
|
||||
ai-model-url: http://localhost:11434/api/generate
|
||||
|
||||
@@ -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