feat: ScreenTopology HD Map + graph-aware GOAP routing + NAV keyword split
FUNDAMENTAL ARCHITECTURE OVERHAUL: 1. ScreenTopology HD Map (NEW): Pure-data BFS pathfinding between Instagram screens. Zero runtime dependencies. The GOAP planner's GPS. Knows: HOME_FEED → tap profile tab → OWN_PROFILE → tap following list → FOLLOW_LIST 2. Graph-Aware GOAP Planning: GoalPlanner._plan_navigation() now consults ScreenTopology FIRST. From HOME_FEED, goal 'open following list' returns 'tap profile tab' (intermediate hop) instead of blind 'open following list' (impossible). Autonomous discovery and Qdrant knowledge kept as fallbacks. 3. NAV Keyword Split: NAV_INTENT_KEYWORDS → NAV_ZONE_BYPASS_KEYWORDS + NAV_TAB_KEYWORDS Ends the Structural Guard civil war where 'following' was both 'allowed in nav zone' AND 'must be at bottom' simultaneously. 4. QNavGraph Deduplication: _find_path() delegates to ScreenTopology.find_route() (SSOT). core_nodes seed generated from ScreenTopology.TRANSITIONS. 115 unit tests pass. Zero regressions.
This commit is contained in:
@@ -3,10 +3,12 @@ from unittest.mock import MagicMock
|
||||
from GramAddict.core.goap import GoalPlanner, ScreenType
|
||||
|
||||
|
||||
def test_goal_planner_linguistic_match_blank_start():
|
||||
def test_goal_planner_uses_hd_map_over_linguistic_match():
|
||||
"""
|
||||
Tests that the GoalPlanner correctly matches a linguistic target
|
||||
during Blank Start discovery without being blocked by a known target check.
|
||||
Tests that the GoalPlanner uses the HD Map (ScreenTopology) as its
|
||||
primary routing strategy, even during Blank Start discovery.
|
||||
The HD Map returns canonical action intents that the TelepathicEngine
|
||||
interprets — not just available_actions from the XML dump.
|
||||
"""
|
||||
planner = GoalPlanner("test_user")
|
||||
|
||||
@@ -26,4 +28,8 @@ def test_goal_planner_linguistic_match_blank_start():
|
||||
|
||||
result = planner.plan_next_step(goal, screen, explored_nav_actions=set())
|
||||
|
||||
assert result == "tap explore grid", "Failed to linguistically match explore during Blank Start"
|
||||
# HD Map routes HOME_FEED → EXPLORE_GRID via "tap explore tab"
|
||||
assert result == "tap explore tab", (
|
||||
f"Expected HD Map to route via 'tap explore tab', got '{result}'. "
|
||||
"The HD Map should override linguistic matching as the primary strategy."
|
||||
)
|
||||
|
||||
103
tests/unit/test_goap_graph_routing.py
Normal file
103
tests/unit/test_goap_graph_routing.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
🔴 RED TDD: GOAP Graph-Aware Routing
|
||||
|
||||
The GoalPlanner must use the ScreenTopology HD Map as its PRIMARY
|
||||
routing strategy. When asked to reach FollowingList from HomeFeed,
|
||||
it should return "tap profile tab" (first step of the BFS route),
|
||||
NOT "open following list" (impossible direct action).
|
||||
"""
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
|
||||
|
||||
from GramAddict.core.goap import GoalPlanner, ScreenType
|
||||
|
||||
|
||||
class TestGoapGraphRouting:
|
||||
"""GOAP planner must use the HD Map for multi-step navigation."""
|
||||
|
||||
@pytest.fixture
|
||||
def planner(self):
|
||||
p = GoalPlanner("testbot")
|
||||
# Ensure blank start (no learned knowledge)
|
||||
p.knowledge._goal_requirements = {}
|
||||
p.knowledge._learned_screen_mappings = {}
|
||||
p.knowledge._learned_traps = set()
|
||||
return p
|
||||
|
||||
def test_planner_routes_to_profile_first_for_following_list(self, planner):
|
||||
"""
|
||||
From HOME_FEED, goal 'open following list' should return
|
||||
'tap profile tab' (navigate to OwnProfile first),
|
||||
NOT 'open following list' as a raw intent.
|
||||
"""
|
||||
screen = {
|
||||
"screen_type": ScreenType.HOME_FEED,
|
||||
"available_actions": [
|
||||
"tap explore tab",
|
||||
"tap home tab",
|
||||
"tap messages tab",
|
||||
"tap reels tab",
|
||||
"press back",
|
||||
],
|
||||
"context": {},
|
||||
"selected_tab": "feed_tab",
|
||||
}
|
||||
action = planner.plan_next_step("open following list", screen)
|
||||
assert action == "tap profile tab", (
|
||||
f"Planner returned '{action}' instead of 'tap profile tab'. "
|
||||
"It should use the HD Map to route HOME_FEED → OWN_PROFILE → FOLLOW_LIST."
|
||||
)
|
||||
|
||||
def test_planner_returns_final_action_on_intermediate_screen(self, planner):
|
||||
"""
|
||||
From OWN_PROFILE, goal 'open following list' should return
|
||||
'tap following list' directly (we're already on the right screen).
|
||||
"""
|
||||
screen = {
|
||||
"screen_type": ScreenType.OWN_PROFILE,
|
||||
"available_actions": [
|
||||
"tap explore tab",
|
||||
"tap home tab",
|
||||
"tap reels tab",
|
||||
"tap following list",
|
||||
"press back",
|
||||
],
|
||||
"context": {},
|
||||
"selected_tab": "profile_tab",
|
||||
}
|
||||
action = planner.plan_next_step("open following list", screen)
|
||||
assert action == "tap following list", (
|
||||
f"Planner returned '{action}' instead of 'tap following list'. "
|
||||
"On OWN_PROFILE, it should directly execute the final action."
|
||||
)
|
||||
|
||||
def test_planner_detects_goal_already_achieved(self, planner):
|
||||
"""On FOLLOW_LIST, goal 'open following list' should return None (achieved)."""
|
||||
screen = {
|
||||
"screen_type": ScreenType.FOLLOW_LIST,
|
||||
"available_actions": ["press back"],
|
||||
"context": {},
|
||||
}
|
||||
action = planner.plan_next_step("open following list", screen)
|
||||
assert action is None, "Goal is already achieved — planner should return None."
|
||||
|
||||
def test_planner_routes_explore_to_following_list(self, planner):
|
||||
"""From EXPLORE_GRID, route should be: Explore → Profile → FollowList."""
|
||||
screen = {
|
||||
"screen_type": ScreenType.EXPLORE_GRID,
|
||||
"available_actions": [
|
||||
"tap home tab",
|
||||
"tap profile tab",
|
||||
"tap reels tab",
|
||||
],
|
||||
"context": {},
|
||||
"selected_tab": "explore_tab",
|
||||
}
|
||||
action = planner.plan_next_step("open following list", screen)
|
||||
assert action == "tap profile tab", (
|
||||
f"From EXPLORE_GRID, planner should route via OWN_PROFILE, got '{action}'"
|
||||
)
|
||||
98
tests/unit/test_screen_topology.py
Normal file
98
tests/unit/test_screen_topology.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
🔴 RED TDD: ScreenTopology — The Instagram HD Map
|
||||
|
||||
Tests for BFS pathfinding between Instagram screens.
|
||||
The killer test: HOME_FEED → OWN_PROFILE → FOLLOW_LIST must be a 2-step route.
|
||||
"""
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
|
||||
|
||||
from GramAddict.core.goap import ScreenType
|
||||
from GramAddict.core.screen_topology import ScreenTopology
|
||||
|
||||
|
||||
class TestScreenTopologyRouting:
|
||||
"""BFS pathfinding must compute correct multi-step routes."""
|
||||
|
||||
def test_route_home_to_following_list(self):
|
||||
"""THE killer test: HomeFeed → OwnProfile → FollowList (2 hops)."""
|
||||
route = ScreenTopology.find_route(ScreenType.HOME_FEED, ScreenType.FOLLOW_LIST)
|
||||
assert route is not None, "Route from HOME_FEED to FOLLOW_LIST must exist"
|
||||
assert len(route) == 2, f"Expected 2 hops, got {len(route)}: {route}"
|
||||
assert route[0] == ("tap profile tab", ScreenType.OWN_PROFILE)
|
||||
assert route[1] == ("tap following list", ScreenType.FOLLOW_LIST)
|
||||
|
||||
def test_route_already_there(self):
|
||||
"""Same screen = empty route (no steps needed)."""
|
||||
route = ScreenTopology.find_route(ScreenType.HOME_FEED, ScreenType.HOME_FEED)
|
||||
assert route == []
|
||||
|
||||
def test_route_single_hop(self):
|
||||
"""Direct neighbor = 1 step."""
|
||||
route = ScreenTopology.find_route(ScreenType.HOME_FEED, ScreenType.EXPLORE_GRID)
|
||||
assert route is not None
|
||||
assert len(route) == 1
|
||||
assert route[0] == ("tap explore tab", ScreenType.EXPLORE_GRID)
|
||||
|
||||
def test_route_reverse_direction(self):
|
||||
"""FollowList back to HomeFeed: FollowList → OwnProfile → HomeFeed (2 hops)."""
|
||||
route = ScreenTopology.find_route(ScreenType.FOLLOW_LIST, ScreenType.HOME_FEED)
|
||||
assert route is not None
|
||||
assert len(route) == 2
|
||||
assert route[0][1] == ScreenType.OWN_PROFILE
|
||||
assert route[1][1] == ScreenType.HOME_FEED
|
||||
|
||||
def test_no_route_from_unreachable(self):
|
||||
"""FOREIGN_APP has no outbound transitions — route should be None."""
|
||||
route = ScreenTopology.find_route(ScreenType.FOREIGN_APP, ScreenType.FOLLOW_LIST)
|
||||
assert route is None
|
||||
|
||||
|
||||
class TestGoalToTargetScreen:
|
||||
"""Goal string → target ScreenType mapping."""
|
||||
|
||||
def test_following_list_goal(self):
|
||||
assert ScreenTopology.goal_to_target_screen("open following list") == ScreenType.FOLLOW_LIST
|
||||
|
||||
def test_followers_list_goal(self):
|
||||
assert ScreenTopology.goal_to_target_screen("open followers list") == ScreenType.FOLLOW_LIST
|
||||
|
||||
def test_profile_goal(self):
|
||||
assert ScreenTopology.goal_to_target_screen("open profile") == ScreenType.OWN_PROFILE
|
||||
|
||||
def test_home_feed_goal(self):
|
||||
assert ScreenTopology.goal_to_target_screen("open home feed") == ScreenType.HOME_FEED
|
||||
|
||||
def test_explore_goal(self):
|
||||
assert ScreenTopology.goal_to_target_screen("open explore feed") == ScreenType.EXPLORE_GRID
|
||||
|
||||
def test_messages_goal(self):
|
||||
assert ScreenTopology.goal_to_target_screen("open messages") == ScreenType.DM_INBOX
|
||||
|
||||
def test_interaction_goal_returns_none(self):
|
||||
"""Non-navigation goals should return None."""
|
||||
assert ScreenTopology.goal_to_target_screen("like this post") is None
|
||||
|
||||
def test_unknown_goal_returns_none(self):
|
||||
assert ScreenTopology.goal_to_target_screen("do something random") is None
|
||||
|
||||
|
||||
class TestGetTransitions:
|
||||
"""Get available transitions from a screen."""
|
||||
|
||||
def test_home_feed_has_profile_tab(self):
|
||||
transitions = ScreenTopology.get_transitions(ScreenType.HOME_FEED)
|
||||
assert "tap profile tab" in transitions
|
||||
assert transitions["tap profile tab"] == ScreenType.OWN_PROFILE
|
||||
|
||||
def test_own_profile_has_following_list(self):
|
||||
transitions = ScreenTopology.get_transitions(ScreenType.OWN_PROFILE)
|
||||
assert "tap following list" in transitions
|
||||
assert transitions["tap following list"] == ScreenType.FOLLOW_LIST
|
||||
|
||||
def test_unknown_screen_returns_empty(self):
|
||||
transitions = ScreenTopology.get_transitions(ScreenType.FOREIGN_APP)
|
||||
assert transitions == {}
|
||||
Reference in New Issue
Block a user