feat(diagnostics): dump screenshots with xmls and limit retention to 5

This commit is contained in:
2026-04-27 11:55:44 +02:00
parent ae046be3b1
commit e9201e0e30
10 changed files with 480 additions and 70 deletions

View File

@@ -0,0 +1,29 @@
import os
import shutil
import base64
from unittest.mock import MagicMock, patch
from GramAddict.core.behaviors.simulator import BehaviorSimulator
def test_device_facade_session_trace_cleanup(tmp_path):
facade = BehaviorSimulator()
facade.get_screenshot_b64 = MagicMock(return_value=base64.b64encode(b"fake_jpg_data").decode("utf-8"))
with patch("GramAddict.core.device_facade.os.path.join", side_effect=os.path.join), \
patch("GramAddict.core.device_facade.os.makedirs"):
def fake_join(*args):
if args[0] == "debug" and args[1] == "session_traces":
return str(tmp_path / "session_traces")
return os.path.join(*args)
with patch("GramAddict.core.device_facade.os.path.join", side_effect=fake_join):
traces_root = tmp_path / "session_traces"
traces_root.mkdir(parents=True, exist_ok=True)
for i in range(6):
d = traces_root / f"old_session_{i}"
d.mkdir()
facade.dump_hierarchy()
remaining_folders = [f for f in os.listdir(traces_root) if os.path.isdir(os.path.join(traces_root, f))]
assert len(remaining_folders) <= 5

View File

@@ -0,0 +1,163 @@
from unittest.mock import MagicMock
import pytest
from GramAddict.core.dm_engine import _run_zero_latency_dm_loop
from GramAddict.core.qdrant_memory import DMMemoryDB, ParasocialCRMDB
def test_parasocial_crm_missing_log_sent_dm():
"""
RED: This test proves that ParasocialCRMDB lacks log_sent_dm,
which caused the crash in the last production run.
"""
crm = ParasocialCRMDB()
with pytest.raises(AttributeError) as excinfo:
crm.log_sent_dm("test_user", "hello", "bio", [])
assert "'ParasocialCRMDB' object has no attribute 'log_sent_dm'" in str(excinfo.value)
def test_dm_engine_uses_correct_dm_memory_logging(monkeypatch):
"""
RED: This test checks if dm_engine correctly uses dm_memory from cognitive_stack.
Note: I am writing this to PROVE the fix is necessary.
"""
mock_device = MagicMock()
mock_device.dump_hierarchy.return_value = "<xml></xml>"
mock_telepathic = MagicMock()
# Mock unread thread found
mock_thread = {"x": 100, "y": 100, "text": "Mariischen"}
def side_effect_logging(*args, **kwargs):
res = next(iterator)
print(f"DEBUG: extract_semantic_nodes called with {args[1]}. Returning {res}")
return res
iterator = iter(
[
[mock_thread], # Step 1: unread threads
[{"text": "Hey"}], # Step 2: context
[{"x": 200, "y": 200}], # Step 3: input field
[{"x": 300, "y": 300}], # Step 4: send button
[], # Step 5: next iteration no unread
]
)
mock_telepathic._extract_semantic_nodes.side_effect = side_effect_logging
mock_dopamine = MagicMock()
mock_dopamine.is_app_session_over.return_value = False
mock_dopamine.wants_to_change_feed.return_value = True # exit after one
mock_dopamine.boredom = 0
mock_crm = ParasocialCRMDB()
mock_dm_memory = MagicMock(spec=DMMemoryDB)
mock_resonance = MagicMock()
mock_resonance.persona_prompt = "Persona prompt"
mock_resonance.args.ai_model = "test-model"
cognitive_stack = {
"telepathic": mock_telepathic,
"dopamine": mock_dopamine,
"crm": mock_crm,
"dm_memory": mock_dm_memory,
"resonance": mock_resonance,
}
mock_session = MagicMock()
mock_session.check_limit.return_value = False
mock_session.totalMessages = 0
# Mock LLM response
monkeypatch.setattr("GramAddict.core.llm_provider.query_llm", lambda **k: {"response": "hi"})
monkeypatch.setattr("GramAddict.core.bot_flow._humanized_click", lambda *a: None)
monkeypatch.setattr("GramAddict.core.bot_flow.sleep", lambda *a: None)
monkeypatch.setattr("GramAddict.core.stealth_typing.ghost_type", lambda *a, **k: None)
mock_configs = MagicMock()
mock_configs.args.disable_ai_messaging = False
mock_configs.args.ai_condenser_model = "test-model"
mock_configs.args.ai_condenser_url = "http://localhost:11434/api/generate"
# This should NOT crash now because I fixed it, but we are testing the logic.
_run_zero_latency_dm_loop(
mock_device, MagicMock(), MagicMock(), mock_configs, mock_session, "target", cognitive_stack
)
# Verify dm_memory was used, NOT crm
mock_dm_memory.log_sent_dm.assert_called_once()
def test_dm_navigation_double_back_guard():
"""
Verifies that if we are still in a thread after one back press,
we press back again.
"""
mock_device = MagicMock()
# Mocking hierarchy sequence
mock_device.dump_hierarchy.side_effect = [
"<xml>Inbox</xml>", # Loop 1 start
"<xml>Thread</xml>", # Context read
"<xml>Thread</xml>", # Send button find
"<xml><node resource-id='com.instagram.android:id/direct_thread_header'/></xml>", # Navigation check AFTER back
"<xml>Inbox</xml>", # Loop 2 start (exit)
"<xml>Inbox</xml>", # Buffer
]
mock_telepathic = MagicMock()
mock_telepathic._extract_semantic_nodes.side_effect = [
[{"x": 1, "y": 1}], # unread found in Loop 1
[{"text": "msg"}], # context
[{"x": 2, "y": 2}], # input
[{"x": 3, "y": 3}], # send
[], # Loop 2: no unread
[], # Buffer
]
mock_dopamine = MagicMock()
mock_dopamine.is_app_session_over.return_value = False
mock_dopamine.wants_to_change_feed.side_effect = [False, True, True] # exit after Loop 1
mock_dopamine.boredom = 0
mock_resonance = MagicMock()
mock_resonance.persona_prompt = "Persona"
mock_resonance.args.ai_model = "model"
cognitive_stack = {
"telepathic": mock_telepathic,
"dopamine": mock_dopamine,
"dm_memory": MagicMock(),
"resonance": mock_resonance,
}
mock_session = MagicMock()
mock_session.check_limit.return_value = False
mock_session.totalMessages = 0
mock_configs = MagicMock()
mock_configs.args.disable_ai_messaging = False
mock_configs.args.ai_condenser_model = "test-model"
mock_configs.args.ai_condenser_url = "http://localhost:11434/api/generate"
from unittest.mock import patch
import GramAddict.core.dm_engine as dm_engine
with (
patch("GramAddict.core.llm_provider.query_llm", return_value={"response": "hi"}),
patch("GramAddict.core.bot_flow._humanized_click"),
patch("GramAddict.core.bot_flow.sleep"),
patch("GramAddict.core.stealth_typing.ghost_type"),
):
dm_engine._run_zero_latency_dm_loop(
mock_device, MagicMock(), MagicMock(), mock_configs, mock_session, "target", cognitive_stack
)
# Expected calls:
# 1. First back from success flow (thread -> inbox)
# 2. Second back from guard check (if still in thread)
# 3. Third back from inbox exit (boredom check)
assert mock_device.press.call_count == 3
mock_device.press.assert_called_with("back")