fix: eliminate 3 critical nav failures — DM guard, GOAP unlearn, ad escape
- Extract NAV_INTENT_KEYWORDS constant (DRY) with 'direct message', 'inbox', 'dm', 'notification', 'heart icon' to fix structural guard self-sabotage where 'tap direct message icon inbox' was rejected as non-nav intent - Add goal-achieved pre-check in GOAP _execute_recalled_path to skip stale paths when the bot is already on the target screen - Add already-there detection in _execute_action to prevent false unlearning when navigation produces no UI change because goal is already met - Implement 3-tier ad escape cascade: normal skip -> double scroll -> GOAP force-navigate to HomeFeed after 6+ consecutive ad cycles - 92 unit tests pass, 223/224 integration tests pass (1 pre-existing flaky)
This commit is contained in:
132
tests/unit/test_goap_false_unlearn.py
Normal file
132
tests/unit/test_goap_false_unlearn.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
🔴 RED TDD: GOAP False Unlearning Fix
|
||||
|
||||
Reproduces Bug 3: The bot taps 'home tab' while ALREADY on home_feed,
|
||||
detects 'no UI change', and destructively unlearns the correct mapping.
|
||||
|
||||
These tests MUST FAIL before the fix and PASS after.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from GramAddict.core.goap import GoalExecutor, ScreenType
|
||||
|
||||
|
||||
def _make_goap(screen_type=ScreenType.HOME_FEED, available_actions=None):
|
||||
"""Create a GoalExecutor with a mocked device that returns a fixed screen state."""
|
||||
device = MagicMock()
|
||||
device.app_id = "com.instagram.android"
|
||||
device._get_current_app.return_value = "com.instagram.android"
|
||||
|
||||
goap = GoalExecutor(device, bot_username="testbot")
|
||||
goap._sae = MagicMock()
|
||||
goap._sae.ensure_clear_screen.return_value = True
|
||||
|
||||
# Mock perceive to return a fixed screen state
|
||||
if available_actions is None:
|
||||
available_actions = ["tap profile tab", "tap explore tab", "scroll down", "press back", "tap home tab"]
|
||||
|
||||
goap.perceive = MagicMock(
|
||||
return_value={
|
||||
"screen_type": screen_type,
|
||||
"available_actions": available_actions,
|
||||
"context": {},
|
||||
"selected_tab": "feed_tab",
|
||||
}
|
||||
)
|
||||
|
||||
return goap
|
||||
|
||||
|
||||
class TestGoapRecalledPathPreCheck:
|
||||
"""Bug 3 Part A: Recalled path should skip execution when goal is already achieved."""
|
||||
|
||||
def test_recalled_path_skips_when_goal_already_achieved(self):
|
||||
"""
|
||||
If the bot is already on HOME_FEED and the goal is 'open home feed',
|
||||
_execute_recalled_path must return True WITHOUT executing any steps.
|
||||
"""
|
||||
goap = _make_goap(screen_type=ScreenType.HOME_FEED)
|
||||
|
||||
steps = [{"action": "tap home tab"}]
|
||||
|
||||
# Mock _execute_action to track if it was called
|
||||
goap._execute_action = MagicMock(return_value=True)
|
||||
|
||||
result = goap._execute_recalled_path(steps, "open home feed")
|
||||
|
||||
assert result is True, "Recalled path should succeed immediately when goal is already achieved."
|
||||
(
|
||||
goap._execute_action.assert_not_called(),
|
||||
("_execute_action should NOT have been called — goal was already achieved."),
|
||||
)
|
||||
|
||||
|
||||
class TestGoapNoUnlearnWhenAlreadyOnTarget:
|
||||
"""Bug 3 Part B: Navigation to current screen should not trigger reject_click."""
|
||||
|
||||
def test_no_unlearn_when_tapping_home_on_home_feed(self):
|
||||
"""
|
||||
If the bot is on HOME_FEED and taps 'home tab', the XML won't change.
|
||||
But since the goal 'open home feed' is already achieved, the action
|
||||
should be treated as SUCCESS, not FAILURE.
|
||||
"""
|
||||
goap = _make_goap(screen_type=ScreenType.HOME_FEED)
|
||||
|
||||
# Make dump_hierarchy return the same XML (no change)
|
||||
static_xml = "<hierarchy><node resource-id='feed_tab' text='Home' /></hierarchy>"
|
||||
goap.device.dump_hierarchy.return_value = static_xml
|
||||
|
||||
# Mock the TelepathicEngine
|
||||
mock_engine = MagicMock()
|
||||
mock_engine.find_best_node.return_value = {
|
||||
"x": 540,
|
||||
"y": 2300,
|
||||
"score": 0.95,
|
||||
"semantic": "description: 'Home', id context: 'feed tab'",
|
||||
"source": "qdrant_nav",
|
||||
"original_attribs": {},
|
||||
}
|
||||
|
||||
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance", return_value=mock_engine):
|
||||
result = goap._execute_action("tap home tab", goal="open home feed")
|
||||
|
||||
# The action should succeed (we're already where we need to be)
|
||||
assert result is True, (
|
||||
"GOAP incorrectly treated 'tap home tab' on home_feed as a failure. "
|
||||
"This causes destructive unlearning of valid navigation knowledge."
|
||||
)
|
||||
|
||||
# Critically: reject_click should NEVER have been called
|
||||
(
|
||||
mock_engine.reject_click.assert_not_called(),
|
||||
("reject_click was called — this destroys the correct 'tap home tab' → 'feed tab' mapping!"),
|
||||
)
|
||||
|
||||
def test_genuine_navigation_failure_still_triggers_reject(self):
|
||||
"""
|
||||
If the bot is on HOME_FEED and taps 'tap explore tab' but nothing changes,
|
||||
that IS a genuine failure and reject_click SHOULD be called.
|
||||
"""
|
||||
goap = _make_goap(screen_type=ScreenType.HOME_FEED)
|
||||
|
||||
static_xml = "<hierarchy><node resource-id='feed_tab' text='Home' /></hierarchy>"
|
||||
goap.device.dump_hierarchy.return_value = static_xml
|
||||
|
||||
mock_engine = MagicMock()
|
||||
mock_engine.find_best_node.return_value = {
|
||||
"x": 540,
|
||||
"y": 2300,
|
||||
"score": 0.95,
|
||||
"semantic": "description: 'Explore', id context: 'explore_tab'",
|
||||
"source": "qdrant_nav",
|
||||
"original_attribs": {},
|
||||
}
|
||||
|
||||
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance", return_value=mock_engine):
|
||||
result = goap._execute_action("tap explore tab", goal="open explore feed")
|
||||
|
||||
# This should fail — we expected to go to explore but UI didn't change
|
||||
assert result is False, (
|
||||
"GOAP should reject a navigation that produced no UI change " "when the goal is NOT already achieved."
|
||||
)
|
||||
101
tests/unit/test_nav_intent_classification.py
Normal file
101
tests/unit/test_nav_intent_classification.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
🔴 RED TDD: DM Structural Guard Self-Sabotage Fix
|
||||
|
||||
Reproduces Bug 2: The intent 'tap direct message icon inbox' is NOT classified
|
||||
as a nav intent, causing the Structural Guard to reject the correct VLM match
|
||||
in the nav bar zone.
|
||||
|
||||
These tests MUST FAIL before the fix and PASS after.
|
||||
"""
|
||||
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
|
||||
class TestNavIntentClassification:
|
||||
"""Verifies that all navigation-related intents are correctly classified."""
|
||||
|
||||
def test_dm_intent_is_classified_as_nav_intent(self):
|
||||
"""
|
||||
The intent 'tap direct message icon inbox' MUST be treated as a nav intent
|
||||
so the structural guard allows clicking elements in the nav bar zone.
|
||||
"""
|
||||
engine = TelepathicEngine()
|
||||
screen_height = 2400
|
||||
|
||||
# DM icon is in the nav bar zone (top right, but the 'direct tab'
|
||||
# element is at the bottom nav bar on some Instagram layouts)
|
||||
dm_node = {
|
||||
"semantic_string": "description: 'Message', id context: 'direct tab'",
|
||||
"y": int(screen_height * 0.95), # Bottom nav bar zone
|
||||
"area": 3000,
|
||||
"class_name": "android.widget.ImageView",
|
||||
"resource_id": "direct_tab",
|
||||
}
|
||||
|
||||
intent = "tap direct message icon inbox"
|
||||
|
||||
# The node should be viable — it's a nav intent targeting the nav bar
|
||||
is_valid = engine._structural_sanity_check(dm_node, intent, screen_height)
|
||||
|
||||
assert is_valid is True, (
|
||||
"Structural Guard rejected 'direct tab' for DM intent. "
|
||||
"This is the exact bug: 'tap direct message icon inbox' is not classified as nav intent."
|
||||
)
|
||||
|
||||
def test_inbox_intent_is_classified_as_nav_intent(self):
|
||||
"""Variant: 'tap inbox' should also be treated as navigation."""
|
||||
engine = TelepathicEngine()
|
||||
screen_height = 2400
|
||||
|
||||
inbox_node = {
|
||||
"semantic_string": "description: 'Inbox', id context: 'direct_inbox'",
|
||||
"y": int(screen_height * 0.95),
|
||||
"area": 2500,
|
||||
"class_name": "android.widget.ImageView",
|
||||
"resource_id": "direct_inbox",
|
||||
}
|
||||
|
||||
intent = "tap inbox"
|
||||
|
||||
is_valid = engine._structural_sanity_check(inbox_node, intent, screen_height)
|
||||
assert is_valid is True, "Structural Guard rejected inbox node for 'tap inbox' intent."
|
||||
|
||||
def test_notification_intent_is_classified_as_nav_intent(self):
|
||||
"""'tap heart icon notifications' should also be treated as navigation."""
|
||||
engine = TelepathicEngine()
|
||||
screen_height = 2400
|
||||
|
||||
notification_node = {
|
||||
"semantic_string": "description: 'Activity', id context: 'notification_tab'",
|
||||
"y": int(screen_height * 0.95),
|
||||
"area": 2500,
|
||||
"class_name": "android.widget.ImageView",
|
||||
"resource_id": "notification_tab",
|
||||
}
|
||||
|
||||
intent = "tap heart icon notifications"
|
||||
|
||||
is_valid = engine._structural_sanity_check(notification_node, intent, screen_height)
|
||||
assert is_valid is True, "Structural Guard rejected notification node for heart icon intent."
|
||||
|
||||
def test_regular_post_intent_still_blocked_in_nav_zone(self):
|
||||
"""
|
||||
Non-nav intents (like 'tap like button') targeting elements in the nav bar
|
||||
zone must STILL be rejected. We're not weakening the guard.
|
||||
"""
|
||||
engine = TelepathicEngine()
|
||||
screen_height = 2400
|
||||
|
||||
misplaced_like_node = {
|
||||
"semantic_string": "description: 'Like', id context: 'some_like_button'",
|
||||
"y": int(screen_height * 0.95),
|
||||
"area": 2000,
|
||||
"class_name": "android.widget.ImageView",
|
||||
}
|
||||
|
||||
intent = "tap like button"
|
||||
|
||||
is_valid = engine._structural_sanity_check(misplaced_like_node, intent, screen_height)
|
||||
assert is_valid is False, (
|
||||
"Structural Guard allowed a like button in the nav bar zone. " "Non-nav intents should still be blocked."
|
||||
)
|
||||
Reference in New Issue
Block a user