feat(diagnostics): dump screenshots with xmls and limit retention to 5
This commit is contained in:
29
tests/unit/test_diagnostic_dump_cleanup.py
Normal file
29
tests/unit/test_diagnostic_dump_cleanup.py
Normal 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
|
||||
163
tests/unit/test_dm_navigation_guards.py
Normal file
163
tests/unit/test_dm_navigation_guards.py
Normal 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")
|
||||
Reference in New Issue
Block a user