feat: stabilize autonomous instagram bot suite (100% green)
Summary of work: - Resolved mass SystemExit: 2 failures by hardening Config against pytest CLI args. - Fixed state leakage in test suite by implementing aggressive cache wiping in conftest.py. - Fixed TypeErrors and UnboundLocalErrors in TelepathicEngine and bot_flow. - Aligned MockTelepathicEngine signatures to resolve Mock Drift. - Achieved 100% pass rate across 498 tests.
This commit is contained in:
63
tests/anomalies/test_goap_learning.py
Normal file
63
tests/anomalies/test_goap_learning.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
TDD Tests for Zero-Hardcode Screen Classification and Situational Awareness
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
|
||||
|
||||
from GramAddict.core.goap import ScreenIdentity, ScreenType
|
||||
|
||||
@pytest.fixture
|
||||
def mock_screen_memory():
|
||||
with patch("GramAddict.core.qdrant_memory.ScreenMemoryDB") as mock_db:
|
||||
instance = mock_db.return_value
|
||||
instance.is_connected = True
|
||||
yield instance
|
||||
|
||||
@pytest.fixture
|
||||
def mock_query_llm():
|
||||
with patch("GramAddict.core.llm_provider.query_llm") as mock_llm:
|
||||
yield mock_llm
|
||||
|
||||
def test_classify_screen_uses_memory(mock_screen_memory, mock_query_llm):
|
||||
"""
|
||||
Test that _classify_screen FIRST tries to hit the ScreenMemoryDB.
|
||||
"""
|
||||
si = ScreenIdentity("testbot")
|
||||
|
||||
# Mock that memory ALREADY knows this screen
|
||||
mock_screen_memory.get_screen_type.return_value = ScreenType.MODAL.name
|
||||
|
||||
# We pass random strings that would previously fail or hit hardcoded checks
|
||||
res = si._classify_screen(
|
||||
ids=set(), descs=[], texts=["totally ambiguous text"],
|
||||
selected_tab=None, desc_lower="", text_lower="",
|
||||
ids_str="random_id", signature="MOCK_SIGNATURE"
|
||||
)
|
||||
|
||||
assert res == ScreenType.MODAL
|
||||
mock_screen_memory.get_screen_type.assert_called_once_with("MOCK_SIGNATURE", similarity_threshold=0.92)
|
||||
# Should not fall back to LLM if memory hits
|
||||
mock_query_llm.assert_not_called()
|
||||
|
||||
def test_classify_screen_uses_llm_fallback_and_learns(mock_screen_memory, mock_query_llm):
|
||||
"""
|
||||
Test that if memory misses, it uses LLM fallback and caches the result.
|
||||
"""
|
||||
si = ScreenIdentity("testbot")
|
||||
mock_screen_memory.get_screen_type.return_value = None
|
||||
mock_query_llm.return_value = {"response": "HOME_FEED"}
|
||||
|
||||
res = si._classify_screen(
|
||||
ids={'random'}, descs=[], texts=[],
|
||||
selected_tab=None, desc_lower="", text_lower="",
|
||||
ids_str="random", signature="MOCK_SIGNATURE_2"
|
||||
)
|
||||
|
||||
assert res == ScreenType.HOME_FEED
|
||||
mock_query_llm.assert_called_once()
|
||||
mock_screen_memory.store_screen.assert_called_once_with("MOCK_SIGNATURE_2", "HOME_FEED")
|
||||
61
tests/anomalies/test_hardware_anomalies_gauss.py
Normal file
61
tests/anomalies/test_hardware_anomalies_gauss.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Hardware Anomaly Traps: Mathematical Verification of Gaussian Clicks
|
||||
Instagram can detect standard `uniform` distributed clicks as bot-like.
|
||||
This test ensures our click distributions follow a proper biological Gaussian curve.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Ensure the GramAddict module is reachable
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
|
||||
|
||||
import numpy as np
|
||||
from GramAddict.core.device_facade import DeviceFacade
|
||||
|
||||
class MockDeviceFacade(DeviceFacade):
|
||||
def __init__(self):
|
||||
self.clicks = []
|
||||
|
||||
def human_click(self, x, y):
|
||||
self.clicks.append((x, y))
|
||||
|
||||
class MockNode:
|
||||
def bounds(self):
|
||||
# returns left, top, right, bottom
|
||||
return (100, 500, 300, 600) # Width = 200, Height = 100
|
||||
|
||||
def test_gaussian_distribution():
|
||||
device = MockDeviceFacade()
|
||||
node = MockNode()
|
||||
|
||||
# Simulate 10,000 clicks
|
||||
for _ in range(10000):
|
||||
device.click(obj=node)
|
||||
|
||||
xs = [c[0] for c in device.clicks]
|
||||
ys = [c[1] for c in device.clicks]
|
||||
|
||||
mean_x = np.mean(xs)
|
||||
std_x = np.std(xs)
|
||||
|
||||
mean_y = np.mean(ys)
|
||||
std_y = np.std(ys)
|
||||
|
||||
print(f"Total Clicks: {len(device.clicks)}")
|
||||
print(f"X -> Mean: {mean_x:.2f} (Expected ~190 based on thumb bias), StdDev: {std_x:.2f} (Expected ~30)")
|
||||
print(f"Y -> Mean: {mean_y:.2f} (Expected ~555 based on thumb bias), StdDev: {std_y:.2f} (Expected ~15)")
|
||||
|
||||
# Assertions
|
||||
assert 185 <= mean_x <= 195, "X Mean does not reflect the 45% thumb bias."
|
||||
assert 550 <= mean_y <= 560, "Y Mean does not reflect the 55% thumb bias."
|
||||
|
||||
# Check for Normal Distribution using a simple heuristic (68-95-99.7 rule)
|
||||
within_1_std = sum(1 for x in xs if mean_x - std_x <= x <= mean_x + std_x) / len(xs)
|
||||
print(f"{within_1_std*100:.2f}% of X clicks within 1 standard deviation (should be ~68%)")
|
||||
assert 0.65 <= within_1_std <= 0.72, "Distribution is not Gaussian!"
|
||||
|
||||
print("SUCCESS: Clicks pass the hardware anti-bot anomaly check!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_gaussian_distribution()
|
||||
@@ -25,6 +25,10 @@ def test_tap_home_tab_recovery_from_homescreen():
|
||||
|
||||
# 4. Patch TelepathicEngine.get_instance to return a mock engine
|
||||
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance") as mock_get_instance, \
|
||||
patch("GramAddict.core.goap.PathMemory.learn_path"), \
|
||||
patch("GramAddict.core.goap.PathMemory.recall_path", return_value=None), \
|
||||
patch("GramAddict.core.qdrant_memory.ScreenMemoryDB._get_embedding", return_value=[0]*1536), \
|
||||
patch("GramAddict.core.situational_awareness.SituationalAwarenessEngine.ensure_clear_screen", return_value=False), \
|
||||
patch("GramAddict.core.q_nav_graph.time.sleep"):
|
||||
mock_engine = MagicMock()
|
||||
mock_get_instance.return_value = mock_engine
|
||||
|
||||
@@ -113,12 +113,12 @@ class TestQNavGraphEdgeCases:
|
||||
zero_engine = MagicMock()
|
||||
|
||||
# Mock transitions completely failing
|
||||
with patch.object(self.graph, '_execute_transition', return_value=False):
|
||||
with patch.object(self.graph.goap, 'navigate_to_screen', return_value=False):
|
||||
# Recovery attempts maxed out
|
||||
assert self.graph.navigate_to("ExploreFeed", zero_engine, recovery_attempts=3) == False
|
||||
|
||||
# Start logic where path is None and direct fallback also fails
|
||||
self.graph.current_state = "IsolatedNode"
|
||||
# It should trigger fallback and then return False because `_execute_transition` always returns False
|
||||
# It should trigger fallback and then return False because `navigate_to_screen` always returns False
|
||||
assert self.graph.navigate_to("ExploreFeed", zero_engine, recovery_attempts=0) == False
|
||||
|
||||
|
||||
45
tests/anomalies/test_trap_radome.py
Normal file
45
tests/anomalies/test_trap_radome.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import pytest
|
||||
import xml.etree.ElementTree as ET
|
||||
from GramAddict.core.sensors.honeypot_radome import HoneypotRadome
|
||||
|
||||
@pytest.fixture
|
||||
def radome():
|
||||
# Provide dummy screen dimensions for the Radome
|
||||
return HoneypotRadome(display_width=1080, display_height=2400)
|
||||
|
||||
def create_node(bounds: str, clickable="true", visible_to_user="true", text="", cdesc="", res_id="") -> ET.Element:
|
||||
node = ET.Element("node", {
|
||||
"bounds": bounds,
|
||||
"clickable": clickable,
|
||||
"visible-to-user": visible_to_user,
|
||||
"text": text,
|
||||
"content-desc": cdesc,
|
||||
"resource-id": res_id
|
||||
})
|
||||
return node
|
||||
|
||||
def test_zero_point_trap(radome):
|
||||
node = create_node("[0,0][0,0]")
|
||||
assert radome._is_honeypot(node) is True
|
||||
|
||||
def test_micro_pixel_trap(radome):
|
||||
node = create_node("[100,100][101,101]", clickable="true")
|
||||
assert radome._is_honeypot(node) is True
|
||||
|
||||
def test_safe_normal_button(radome):
|
||||
node = create_node("[500,500][600,600]", text="Like", clickable="true")
|
||||
assert radome._is_honeypot(node) is False
|
||||
|
||||
def test_transparent_interceptor_trap(radome):
|
||||
# A full screen clickable node with NO text/id/desc is a trap!
|
||||
node = create_node("[0,0][1080,2400]", text="", cdesc="", res_id="", clickable="true")
|
||||
assert radome._is_honeypot(node) is True
|
||||
|
||||
# If it has text (e.g. a legit full screen modal), it's NOT flagged by this specific trap rule
|
||||
safe_modal = create_node("[0,0][1080,2400]", text="Warning", clickable="true")
|
||||
assert radome._is_honeypot(safe_modal) is False
|
||||
|
||||
def test_accessibility_trap(radome):
|
||||
# Visible-to-user is false but it is clickable
|
||||
node = create_node("[100,100][300,300]", visible_to_user="false", clickable="true")
|
||||
assert radome._is_honeypot(node) is True
|
||||
@@ -8,7 +8,7 @@ from unittest.mock import patch, MagicMock
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
# Path to real xml dumps
|
||||
DUMPS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "debug", "xml_dumps")
|
||||
DUMPS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "debug", "xml_dumps")
|
||||
|
||||
# Gather all XML files
|
||||
xml_files = glob.glob(os.path.join(DUMPS_DIR, "*.xml"))
|
||||
@@ -55,6 +55,8 @@ def test_xml_parser_does_not_crash(xml_path):
|
||||
|
||||
# Phase 2: Query resolution stability (Keyword + Vector + VLM Fallbacks)
|
||||
device_mock = MagicMock()
|
||||
device_mock.get_info.return_value = {"displayHeight": 2400, "displayWidth": 1080}
|
||||
|
||||
# Find completely arbitrary intent, just to trigger full resolution path
|
||||
best_node = engine.find_best_node(xml_content, "dismiss this modal immediately or try clicking like", device=device_mock)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user