fix(dm-engine): 4 safety hardening patches with TDD proof
Kill-Switch: Refuse DM processing when dm_reply.enabled=false in config. Root cause: checked nonexistent 'disable_ai_messaging' flag instead of actual plugin config. Context Guard: Skip threads with no extractable message text. Root cause: LLM was fed 'No previous context' → produced garbage like 'the to the'. Send Verification: Structurally verify Send button resource-id/desc. Root cause: VLM returned reactions_pill_container, edit fields, etc. and engine blindly clicked them, logging 'Successfully sent'. Iteration Cap: MAX_REPLIES_PER_INBOX_VISIT=3 prevents spam. Root cause: no loop guard → 8 DMs sent in 2 minutes in production. Refactored: removed dead 'if True' guard, de-indented block, moved dm_memory.log_sent_dm into success branch only. All 6 E2E tests pass. No regressions (54/55 passed, 1 pre-existing).
This commit is contained in:
@@ -199,23 +199,48 @@ class TestDMSendVerification:
|
||||
from GramAddict.core.dm_engine import _run_zero_latency_dm_loop
|
||||
|
||||
device = MagicMock()
|
||||
# First dump = inbox, second = thread, third = thread (for send button search)
|
||||
inbox_xml = _make_dm_inbox_xml()
|
||||
thread_xml = _make_dm_thread_xml()
|
||||
# Flow: inbox → thread → send_xml (re-dump) → back → check_xml → inbox (no unread)
|
||||
device.dump_hierarchy.side_effect = [
|
||||
_make_dm_inbox_xml(),
|
||||
_make_dm_thread_xml(),
|
||||
_make_dm_thread_xml(), # after typing, re-dump for send button
|
||||
_make_dm_inbox_xml(), # after pressing back
|
||||
_make_dm_inbox_xml(), # no more unread
|
||||
inbox_xml, # 1. inbox: find unread
|
||||
thread_xml, # 2. thread: read messages
|
||||
thread_xml, # 3. after typing: re-dump for send button
|
||||
thread_xml, # 4. check_xml after pressing back (still in thread?)
|
||||
inbox_xml, # 5. inbox again on re-loop
|
||||
]
|
||||
|
||||
configs = _make_configs(dm_reply_enabled=True)
|
||||
session_state = _make_session_state()
|
||||
dopamine = _make_dopamine(boredom_sequence=[False, False, True])
|
||||
|
||||
# Dopamine: never session-over, but wants_to_change_feed after boredom bump
|
||||
dopamine = MagicMock()
|
||||
dopamine.is_app_session_over.return_value = False
|
||||
dopamine.boredom = 0.0
|
||||
dopamine.wants_to_change_feed.side_effect = lambda: dopamine.boredom >= 4.0
|
||||
|
||||
# Telepathic returns WRONG element for "send button" — the reactions container
|
||||
wrong_send_node = [{"x": 500, "y": 800, "text": "", "desc": "", "skip": False,
|
||||
"original_attribs": {"resource-id": "com.instagram.android:id/message_reactions_pill_container"}}]
|
||||
telepathic = _make_telepathic(send_nodes=wrong_send_node)
|
||||
# On second unread call, return no threads (inbox clear)
|
||||
unread_call_n = {"n": 0}
|
||||
|
||||
def _extract_nodes(xml, intent, threshold=0.7):
|
||||
if "unread" in intent.lower():
|
||||
unread_call_n["n"] += 1
|
||||
if unread_call_n["n"] == 1:
|
||||
return [{"x": 500, "y": 300, "text": "johndoe", "skip": False}]
|
||||
return []
|
||||
elif "last received" in intent.lower():
|
||||
return [{"x": 500, "y": 600, "text": "Hey what's up?", "skip": False}]
|
||||
elif "input" in intent.lower():
|
||||
return [{"x": 500, "y": 900, "text": "Message…", "skip": False}]
|
||||
elif "send" in intent.lower():
|
||||
return wrong_send_node
|
||||
return []
|
||||
|
||||
telepathic = MagicMock()
|
||||
telepathic._extract_semantic_nodes.side_effect = _extract_nodes
|
||||
|
||||
cognitive_stack = {"telepathic": telepathic, "dopamine": dopamine, "dm_memory": MagicMock()}
|
||||
|
||||
@@ -258,20 +283,55 @@ class TestDMContextRequirement:
|
||||
from GramAddict.core.dm_engine import _run_zero_latency_dm_loop
|
||||
|
||||
device = MagicMock()
|
||||
# Flow: inbox → click unread → thread (no context) → back → continue →
|
||||
# inbox (same, but telepathic returns no unread) → boredom exit
|
||||
inbox_xml = _make_dm_inbox_xml()
|
||||
device.dump_hierarchy.side_effect = [
|
||||
_make_dm_inbox_xml(),
|
||||
_make_dm_thread_xml_no_context(), # Thread with no text
|
||||
_make_dm_inbox_xml(), # Back to inbox
|
||||
_make_dm_inbox_xml(), # No more unread (empty)
|
||||
inbox_xml, # 1. inbox: find unread
|
||||
_make_dm_thread_xml_no_context(), # 2. thread: read messages (no text)
|
||||
# after context-skip continue, back to loop:
|
||||
inbox_xml, # 3. inbox again (check is_inbox)
|
||||
# 4. check_xml after pressing back from thread (dm_engine L152)
|
||||
]
|
||||
|
||||
configs = _make_configs(dm_reply_enabled=True)
|
||||
session_state = _make_session_state()
|
||||
dopamine = _make_dopamine(boredom_sequence=[False, False, True])
|
||||
|
||||
# 1st call: not over (process first thread)
|
||||
# 2nd call: not over (after context skip, re-loop)
|
||||
# 3rd+ calls: not needed because boredom triggers exit
|
||||
dopamine = MagicMock()
|
||||
dopamine.is_app_session_over.return_value = False
|
||||
dopamine.boredom = 0.0
|
||||
# After inbox_clear, boredom jumps to 50 → wants_to_change_feed
|
||||
# should return True on second check (after inbox clear)
|
||||
change_feed_calls = {"n": 0}
|
||||
|
||||
def _wants_change():
|
||||
change_feed_calls["n"] += 1
|
||||
# After any boredom bump, signal exit
|
||||
return dopamine.boredom >= 40.0
|
||||
|
||||
dopamine.wants_to_change_feed.side_effect = _wants_change
|
||||
|
||||
# No extractable text from thread
|
||||
no_text_msg_nodes = [{"x": 500, "y": 600, "text": "", "skip": False}]
|
||||
telepathic = _make_telepathic(msg_nodes=no_text_msg_nodes)
|
||||
# On the second inbox visit, return NO unread threads (inbox clear)
|
||||
call_count = {"n": 0}
|
||||
|
||||
def _extract_nodes(xml, intent, threshold=0.7):
|
||||
if "unread" in intent.lower():
|
||||
call_count["n"] += 1
|
||||
if call_count["n"] == 1:
|
||||
return [{"x": 500, "y": 300, "text": "johndoe", "skip": False}]
|
||||
# Second time: no unread
|
||||
return []
|
||||
elif "last received" in intent.lower():
|
||||
return no_text_msg_nodes
|
||||
return []
|
||||
|
||||
telepathic = MagicMock()
|
||||
telepathic._extract_semantic_nodes.side_effect = _extract_nodes
|
||||
|
||||
cognitive_stack = {"telepathic": telepathic, "dopamine": dopamine, "dm_memory": MagicMock()}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user