feat(navigation): Implement 100% autonomous Blank Start architecture; purge heuristics

This commit is contained in:
2026-04-22 00:05:03 +02:00
parent fff9ec5b0a
commit 75009d91a2
41 changed files with 1875 additions and 231 deletions

View 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"

View 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.")

View 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