feat(navigation): add structural identity and transitions for NOTIFICATIONS trap
This commit is contained in:
@@ -23,6 +23,7 @@ class ScreenType(Enum):
|
||||
COMMENTS = "comments"
|
||||
MODAL = "modal"
|
||||
FOREIGN_APP = "foreign_app"
|
||||
NOTIFICATIONS = "notifications"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@@ -94,7 +95,14 @@ class ScreenIdentity:
|
||||
resource_ids.add(short_id)
|
||||
|
||||
# Track which tab is selected
|
||||
if selected and short_id in ("feed_tab", "search_tab", "clips_tab", "profile_tab", "direct_tab"):
|
||||
if selected and short_id in (
|
||||
"feed_tab",
|
||||
"search_tab",
|
||||
"clips_tab",
|
||||
"profile_tab",
|
||||
"direct_tab",
|
||||
"news_tab",
|
||||
):
|
||||
selected_tab = short_id
|
||||
|
||||
if text:
|
||||
@@ -180,7 +188,15 @@ class ScreenIdentity:
|
||||
# Structural detection is O(1), zero LLM calls, and cannot be fooled.
|
||||
# EXCEPTION: If Qdrant has explicitly learned this screen as NORMAL (via LLM unlearning),
|
||||
# we skip the structural check to prevent false-positive infinite loops.
|
||||
creation_flow_markers = ("quick_capture", "gallery_cancel_button", "creation_flow", "reel_camera")
|
||||
creation_flow_markers = (
|
||||
"quick_capture",
|
||||
"gallery_cancel_button",
|
||||
"creation_flow",
|
||||
"reel_camera",
|
||||
"gallery_grid_item_thumbnail",
|
||||
"camera_cancel_button",
|
||||
"next_button_textview",
|
||||
)
|
||||
browser_markers = ("ig_browser_text_title", "ig_browser_close_button", "ig_chrome_subsection")
|
||||
if not is_normal_override and any(marker in ids_str for marker in creation_flow_markers):
|
||||
logger.info("🛡️ [ScreenIdentity] Content-creation overlay detected → MODAL")
|
||||
@@ -239,6 +255,11 @@ class ScreenIdentity:
|
||||
if any(marker in ids for marker in STORY_MARKERS):
|
||||
return ScreenType.STORY_VIEW
|
||||
|
||||
# Notifications / Activity structural markers
|
||||
NOTIFICATIONS_MARKERS = ("row_newsfeed_text", "newsfeed_tab", "newsfeed_user_imageview")
|
||||
if any(marker in ids for marker in NOTIFICATIONS_MARKERS) or selected_tab == "news_tab":
|
||||
return ScreenType.NOTIFICATIONS
|
||||
|
||||
# DM thread detection — Structural markers (header and input fields)
|
||||
if "direct_thread_header" in ids or "direct_text_input" in ids or "message_composer_container" in ids:
|
||||
return ScreenType.DM_THREAD
|
||||
@@ -357,6 +378,7 @@ class ScreenIdentity:
|
||||
"clips_tab": "tap reels tab",
|
||||
"profile_tab": "tap profile tab",
|
||||
"direct_tab": "tap messages tab",
|
||||
"news_tab": "tap activity heart icon notifications",
|
||||
}
|
||||
for tab_id, action in tab_map.items():
|
||||
if tab_id in resource_ids:
|
||||
|
||||
@@ -33,6 +33,7 @@ class ScreenTopology:
|
||||
"tap profile tab": ScreenType.OWN_PROFILE,
|
||||
"tap reels tab": ScreenType.REELS_FEED,
|
||||
"tap messages tab": ScreenType.DM_INBOX,
|
||||
"tap activity heart icon notifications": ScreenType.NOTIFICATIONS,
|
||||
"tap story ring avatar": ScreenType.STORY_VIEW,
|
||||
},
|
||||
ScreenType.EXPLORE_GRID: {
|
||||
@@ -84,6 +85,12 @@ class ScreenTopology:
|
||||
"tap home tab": ScreenType.HOME_FEED,
|
||||
# NOTE: 'press back' intentionally omitted — destination is non-deterministic
|
||||
},
|
||||
ScreenType.NOTIFICATIONS: {
|
||||
"tap home tab": ScreenType.HOME_FEED,
|
||||
"press back": ScreenType.HOME_FEED,
|
||||
"tap profile tab": ScreenType.OWN_PROFILE,
|
||||
"tap explore tab": ScreenType.EXPLORE_GRID,
|
||||
},
|
||||
ScreenType.UNKNOWN: {
|
||||
"tap home tab": ScreenType.HOME_FEED,
|
||||
},
|
||||
@@ -110,6 +117,7 @@ class ScreenTopology:
|
||||
"open user profile": ScreenType.OTHER_PROFILE,
|
||||
"open search": ScreenType.SEARCH_RESULTS,
|
||||
"view comments": ScreenType.COMMENTS,
|
||||
"open notifications": ScreenType.NOTIFICATIONS,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -253,9 +253,12 @@ class TelepathicEngine:
|
||||
# ZERO-TRUST: NO LOCALIZED STRINGS.
|
||||
res_id = (node.resource_id or "").lower()
|
||||
|
||||
# Avoid clicking on audio/trending/camera elements unless explicitly requested
|
||||
if any(trap in res_id for trap in ["album_art", "use_in_camera", "trending", "audio"]):
|
||||
if not any(word in intent for word in ["audio", "trending", "camera"]):
|
||||
# Avoid clicking on audio/trending/camera/creation elements unless explicitly requested
|
||||
if any(
|
||||
trap in res_id
|
||||
for trap in ["album_art", "use_in_camera", "trending", "audio", "creation", "camera", "gallery"]
|
||||
):
|
||||
if not any(word in intent for word in ["audio", "trending", "camera", "create", "gallery"]):
|
||||
return False
|
||||
|
||||
# Avoid clicking share, save, menu, options unless explicitly requested
|
||||
|
||||
@@ -141,6 +141,7 @@ class TestScreenTopologyBackTransitions:
|
||||
ScreenType.FOLLOW_LIST,
|
||||
ScreenType.STORY_VIEW,
|
||||
ScreenType.COMMENTS,
|
||||
ScreenType.NOTIFICATIONS,
|
||||
):
|
||||
# These are "leaf" screens with a deterministic parent — OK to keep
|
||||
continue
|
||||
|
||||
39
tests/tdd/test_gallery_trap_guard.py
Normal file
39
tests/tdd/test_gallery_trap_guard.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType
|
||||
|
||||
|
||||
def test_gallery_screen_is_modal():
|
||||
"""
|
||||
Test that the Gallery/New Post screen is correctly identified as a MODAL,
|
||||
preventing it from being hallucinated as POST_DETAIL or HOME_FEED.
|
||||
"""
|
||||
xml_dump = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" bounds="[0,0][1080,2400]">
|
||||
<node index="0" text="New post" resource-id="" class="android.widget.TextView" bounds="[0,0][100,100]" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/gallery_grid_item_thumbnail" class="android.widget.Button" content-desc="Selected media number 1 Photo thumbnail created on 3 May 2026 10:30" bounds="[0,100][500,600]" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/next_button_textview" class="android.widget.Button" content-desc="Next" bounds="[900,0][1080,100]" />
|
||||
</node>
|
||||
</hierarchy>
|
||||
"""
|
||||
identity = ScreenIdentity("bot_user")
|
||||
result = identity.identify(xml_dump)
|
||||
|
||||
assert result["screen_type"] == ScreenType.MODAL
|
||||
|
||||
|
||||
def test_camera_screen_is_modal():
|
||||
"""
|
||||
Test that the Camera creation screen is correctly identified as a MODAL.
|
||||
"""
|
||||
xml_dump = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" bounds="[0,0][1080,2400]">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/camera_cancel_button" class="android.widget.ImageView" bounds="[0,0][100,100]" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/quick_capture" class="android.widget.FrameLayout" bounds="[0,100][1080,2400]" />
|
||||
</node>
|
||||
</hierarchy>
|
||||
"""
|
||||
identity = ScreenIdentity("bot_user")
|
||||
result = identity.identify(xml_dump)
|
||||
|
||||
assert result["screen_type"] == ScreenType.MODAL
|
||||
22
tests/tdd/test_notifications_screen.py
Normal file
22
tests/tdd/test_notifications_screen.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType
|
||||
|
||||
|
||||
def test_notifications_screen_identification():
|
||||
"""
|
||||
Test that the Notifications/Activity screen is correctly identified,
|
||||
preventing it from being hallucinated as POST_DETAIL.
|
||||
"""
|
||||
xml_dump = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" bounds="[0,0][1080,2400]">
|
||||
<node index="0" text="Notifications" resource-id="com.instagram.android:id/action_bar_title" class="android.widget.TextView" bounds="[0,0][1080,100]" />
|
||||
<node index="1" text="User liked your photo." resource-id="com.instagram.android:id/row_newsfeed_text" class="android.widget.TextView" bounds="[100,100][1000,200]" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/newsfeed_tab" selected="true" class="android.widget.FrameLayout" bounds="[600,2300][800,2400]" />
|
||||
</node>
|
||||
</hierarchy>
|
||||
"""
|
||||
identity = ScreenIdentity("bot_user")
|
||||
result = identity.identify(xml_dump)
|
||||
|
||||
assert result["screen_type"] == ScreenType.NOTIFICATIONS
|
||||
assert "tap home tab" in result["available_actions"] or "press back" in result["available_actions"]
|
||||
@@ -28,7 +28,8 @@ def test_sae_detects_obstacle_keyboard():
|
||||
xml_with_keyboard = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" package="com.instagram.android" resource-id="" text="" content-desc="" />
|
||||
<node index="1" package="com.google.android.inputmethod.latin" resource-id="com.google.android.inputmethod.latin:id/keyboard_view" text="" content-desc="Keyboard" />
|
||||
<node index="1" class="android.widget.EditText" focused="true" />
|
||||
<node index="2" package="com.google.android.inputmethod.latin" resource-id="com.google.android.inputmethod.latin:id/keyboard_view" text="" content-desc="Keyboard" />
|
||||
</hierarchy>
|
||||
"""
|
||||
|
||||
@@ -45,7 +46,8 @@ def test_obstacle_guard_dismisses_keyboard():
|
||||
xml_with_keyboard = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" package="com.instagram.android" resource-id="" text="" content-desc="" />
|
||||
<node index="1" package="com.google.android.inputmethod.latin" resource-id="com.google.android.inputmethod.latin:id/keyboard_view" text="" content-desc="Keyboard" />
|
||||
<node index="1" class="android.widget.EditText" focused="true" />
|
||||
<node index="2" package="com.google.android.inputmethod.latin" resource-id="com.google.android.inputmethod.latin:id/keyboard_view" text="" content-desc="Keyboard" />
|
||||
</hierarchy>
|
||||
"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user