feat(navigation): Implement 100% autonomous Blank Start architecture; purge heuristics
This commit is contained in:
101
tests/integration/test_dynamic_discovery.py
Normal file
101
tests/integration/test_dynamic_discovery.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@pytest.fixture
|
||||
def mock_device():
|
||||
device = MagicMock()
|
||||
# Initial screen: Home
|
||||
device.dump_hierarchy.side_effect = [
|
||||
"<home_xml/>", # Initial perceive
|
||||
"<messages_xml/>", # After click
|
||||
"<messages_xml/>" # Final check
|
||||
]
|
||||
device.app_id = "com.instagram.android"
|
||||
return device
|
||||
|
||||
@pytest.fixture
|
||||
def mock_nav_db(monkeypatch):
|
||||
"""
|
||||
Bulletproof mock for Qdrant isolation.
|
||||
"""
|
||||
storage = {} # collection -> seed -> payload
|
||||
|
||||
class MockDB:
|
||||
def __init__(self, collection_name, **kwargs):
|
||||
self.collection_name = collection_name
|
||||
self.is_connected = True
|
||||
self._storage = storage
|
||||
|
||||
def _get_embedding(self, text):
|
||||
return [0.1] * 768
|
||||
|
||||
def upsert_point(self, seed, payload, **kwargs):
|
||||
if self.collection_name not in self._storage:
|
||||
self._storage[self.collection_name] = {}
|
||||
self._storage[self.collection_name][seed] = payload
|
||||
return True
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
client_mock = MagicMock()
|
||||
def mock_query(collection_name, query, **kwargs):
|
||||
mock_points = MagicMock()
|
||||
points_list = []
|
||||
coll_data = self._storage.get(collection_name, {})
|
||||
for payload in coll_data.values():
|
||||
p = MagicMock()
|
||||
p.payload = payload
|
||||
points_list.append(p)
|
||||
mock_points.points = points_list
|
||||
return mock_points
|
||||
client_mock.query_points.side_effect = mock_query
|
||||
client_mock.delete_collection.side_effect = lambda c: self._storage.pop(c, None)
|
||||
return client_mock
|
||||
|
||||
import GramAddict.core.goap
|
||||
monkeypatch.setattr(GramAddict.core.goap, "QdrantBase", MockDB)
|
||||
yield storage
|
||||
|
||||
def test_dynamic_discovery_learning(device, mock_nav_db):
|
||||
"""
|
||||
TDD: Start blank, achieve a goal, verify knowledge is gained.
|
||||
"""
|
||||
from GramAddict.core.goap import GoalExecutor, ScreenType
|
||||
username = "test_discovery_user"
|
||||
# We need to mock TelepathicEngine.get_instance to avoid it failing in execute_action
|
||||
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance") as mock_te:
|
||||
mock_te.return_value.verify_success.return_value = True
|
||||
mock_te.return_value.find_best_node.return_value = {"x": 100, "y": 200}
|
||||
|
||||
executor = GoalExecutor(device, username)
|
||||
executor.planner.knowledge.wipe() # Start clean
|
||||
|
||||
# 1. Execute 'open messages'
|
||||
# We mock perceive to return HOME then DM_INBOX
|
||||
with patch.object(executor, "perceive") as mock_perceive:
|
||||
mock_perceive.side_effect = [
|
||||
{"screen_type": ScreenType.HOME_FEED, "available_actions": ["tap messages tab"]},
|
||||
{"screen_type": ScreenType.DM_INBOX, "available_actions": []},
|
||||
{"screen_type": ScreenType.DM_INBOX, "available_actions": []}
|
||||
]
|
||||
|
||||
# Using real achieve/execute logic
|
||||
success = executor.achieve("open messages")
|
||||
assert success is True
|
||||
|
||||
# 2. Verify knowledge was LEARNED automatically
|
||||
reqs = executor.planner.knowledge.get_requirements("open messages")
|
||||
assert ScreenType.DM_INBOX in reqs
|
||||
|
||||
def test_tab_mapping_learning(device, mock_nav_db):
|
||||
"""Verify that tapping a tab records its destination."""
|
||||
from GramAddict.core.goap import GoalExecutor, ScreenType
|
||||
username = "test_tab_user"
|
||||
executor = GoalExecutor(mock_device, username)
|
||||
executor.planner.knowledge.wipe()
|
||||
|
||||
# Tapping 'reels tab' should land on REELS_FEED
|
||||
executor.planner.knowledge.learn_screen_mapping("clips_tab", ScreenType.REELS_FEED)
|
||||
|
||||
tab = executor.planner.knowledge.get_tab_for_screen(ScreenType.REELS_FEED)
|
||||
assert tab == "clips_tab"
|
||||
35
tests/integration/test_hardware_autonomy.py
Normal file
35
tests/integration/test_hardware_autonomy.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import pytest
|
||||
import os
|
||||
from GramAddict.core.goap import GoalExecutor, ScreenType
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get("RUN_LIVE_HARDWARE_TESTS"), reason="Requires physical hardware and RUN_LIVE_HARDWARE_TESTS=1")
|
||||
def test_autonomous_navigation_to_messages(device):
|
||||
"""
|
||||
E2E Hardness Test:
|
||||
1. Start from Home screen.
|
||||
2. Command 'open messages'.
|
||||
3. Verify bot autonomous discovers the path and executes it.
|
||||
4. Verify final state is DM_INBOX.
|
||||
"""
|
||||
username = "marcmintel" # use current config user
|
||||
executor = GoalExecutor(device, username)
|
||||
executor.planner.knowledge.wipe() # Start with 'Blank Start' to force discovery
|
||||
|
||||
# Ensure we are in Instagram
|
||||
# device.start_app("com.instagram.android")
|
||||
|
||||
print("🚀 Initializing Autonomous Discovery on Hardware...")
|
||||
|
||||
# The achieve loop will:
|
||||
# - Perceive HOME_FEED (hopefully)
|
||||
# - See 'messages' intent -> tap dm_tab or top-right icon
|
||||
# - Verify DM_INBOX
|
||||
success = executor.achieve("open messages")
|
||||
|
||||
assert success is True, "Autonomous navigation failed to reach DMs on live device"
|
||||
|
||||
# Final check of the state
|
||||
screen = executor.perceive()
|
||||
assert screen["screen_type"] == ScreenType.DM_INBOX, f"Expected DM_INBOX, but bot thinks it is on {screen['screen_type']}"
|
||||
|
||||
print("✅ Autonomous hardware test PASSED. Bot discovered and navigated to DMs.")
|
||||
57
tests/integration/test_telepathic_hardening.py
Normal file
57
tests/integration/test_telepathic_hardening.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
import re
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
|
||||
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
@pytest.fixture
|
||||
def engine():
|
||||
# Instantiate directly to avoid singleton contamination from mocks
|
||||
return TelepathicEngine()
|
||||
|
||||
def test_keyword_nav_threshold(engine):
|
||||
"""
|
||||
TDD Case: "tap messages tab" should NOT match "Reels" (clips_tab).
|
||||
Intent words: {"messages", "tab"}
|
||||
Reels node description: "Reels", resource_id: "clips_tab"
|
||||
Matches "tab" -> score 0.5.
|
||||
Current threshold 0.45 -> matches (WRONG).
|
||||
New threshold for nav intents should be 1.0.
|
||||
"""
|
||||
reels_node = {
|
||||
"x": 500, "y": 2000, "area": 100,
|
||||
"semantic_string": "description: 'Reels', id context: 'clips tab'",
|
||||
"resource_id": "com.instagram.android:id/clips_tab",
|
||||
"original_attribs": {
|
||||
"desc": "Reels",
|
||||
"text": "",
|
||||
"resource-id": "com.instagram.android:id/clips_tab"
|
||||
}
|
||||
}
|
||||
|
||||
# Intent: "tap messages tab"
|
||||
# Result should be None because "messages" is missing.
|
||||
res = engine._keyword_match_score("tap messages tab", [reels_node])
|
||||
assert res is None
|
||||
|
||||
def test_direct_tab_fast_path(engine):
|
||||
"""
|
||||
Verify that "tap messages tab" now hits the core_nav_fast_path.
|
||||
"""
|
||||
direct_node = {
|
||||
"x": 800, "y": 2300, "area": 100,
|
||||
"semantic_string": "Direct",
|
||||
"resource_id": "com.instagram.android:id/direct_tab",
|
||||
"original_attribs": {
|
||||
"resource-id": "com.instagram.android:id/direct_tab"
|
||||
}
|
||||
}
|
||||
|
||||
res = engine._core_navigation_fast_path("tap messages tab", [direct_node])
|
||||
assert res is not None
|
||||
assert res["source"] == "core_nav"
|
||||
assert res["x"] == 800
|
||||
Reference in New Issue
Block a user