feat(nav): enforce strict embedding length guards and autonomous brain-first navigation
This commit is contained in:
55
tests/core/test_embeddings_truncation.py
Normal file
55
tests/core/test_embeddings_truncation.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from GramAddict.core.qdrant_memory import QdrantBase
|
||||
|
||||
|
||||
@pytest.mark.live_llm
|
||||
def test_embedding_context_length_limit():
|
||||
"""
|
||||
TDD Proof: Ensure _get_embedding truncates input sufficiently to avoid
|
||||
'input length exceeds the context length' (500) from Ollama.
|
||||
"""
|
||||
|
||||
class DummyMemory(QdrantBase):
|
||||
def __init__(self):
|
||||
# Bypass init connection checks for this test
|
||||
self._vector_size = 768
|
||||
pass
|
||||
|
||||
memory = DummyMemory()
|
||||
|
||||
# Mock config to use standard local embedding endpoint
|
||||
class FakeArgs:
|
||||
ai_embedding_model = "nomic-embed-text"
|
||||
ai_embedding_url = "http://localhost:11434/api/embeddings"
|
||||
|
||||
memory._cached_args = FakeArgs()
|
||||
|
||||
# Generate an extremely long string (e.g. 15,000 chars) that would crash Ollama
|
||||
huge_text = "lorem ipsum " * 2000
|
||||
|
||||
# This should NOT raise a requests.exceptions.HTTPError (500)
|
||||
try:
|
||||
vector = memory._get_embedding(huge_text)
|
||||
assert vector is not None, "Vector should not be None for successful API calls"
|
||||
assert len(vector) > 0, "Vector should have dimensions"
|
||||
except requests.exceptions.HTTPError as e:
|
||||
pytest.fail(f"Embedding API crashed with HTTP error (likely context limit): {e}")
|
||||
|
||||
|
||||
@pytest.mark.live_llm
|
||||
def test_structural_signature_truncation():
|
||||
"""
|
||||
Ensure UIMemoryDB correctly truncates structural signatures to 2000 chars.
|
||||
"""
|
||||
from GramAddict.core.qdrant_memory import UIMemoryDB
|
||||
|
||||
db = UIMemoryDB()
|
||||
|
||||
# Huge XML-like string
|
||||
huge_xml = "<node " + ('text="junk" ' * 1000) + "/>"
|
||||
|
||||
sig = db._create_structural_signature(huge_xml)
|
||||
assert len(sig) <= 2000, f"Signature length {len(sig)} exceeds 2000"
|
||||
assert "text=" not in sig, "Structural signature should have removed 'text' attributes"
|
||||
@@ -1,102 +0,0 @@
|
||||
"""
|
||||
🔴 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 os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
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}'"
|
||||
Reference in New Issue
Block a user