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:
2026-04-24 21:42:27 +02:00
parent 5266b8b290
commit 8f8efe6f2a
6 changed files with 455 additions and 57 deletions

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

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