chore: FSD stabilization, strict TDD enforcement, and unlearn mechanism
- implemented self-healing unlearn for Qdrant false positives - centralized testing logic in conftest - documented core rules, ai standards, and goap philosophy - purged old dev scratchpads
This commit is contained in:
@@ -251,6 +251,21 @@ def e2e_configs():
|
||||
ai_telepathic_url="http://localhost",
|
||||
ai_telepathic_model="llama3",
|
||||
ai_condenser_url="http://localhost",
|
||||
ai_condenser_model="llama3"
|
||||
)
|
||||
return configs
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_sae_perceive(request, monkeypatch):
|
||||
"""
|
||||
Mock SAE.perceive for all E2E tests EXCEPT the ones actually testing SAE.
|
||||
This prevents the tests from hitting the local Qdrant/Ollama instances
|
||||
and failing due to non-deterministic LLM output or missing caches.
|
||||
"""
|
||||
if "test_e2e_sae.py" in str(request.node.fspath):
|
||||
return
|
||||
if request.config.getoption("--live"):
|
||||
return
|
||||
|
||||
import GramAddict.core.situational_awareness
|
||||
monkeypatch.setattr(GramAddict.core.situational_awareness.SituationalAwarenessEngine, "perceive", lambda self, xml: GramAddict.core.situational_awareness.SituationType.NORMAL)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from GramAddict.core.device_facade import DeviceFacade
|
||||
@patch("GramAddict.core.bot_flow.create_device")
|
||||
@patch("GramAddict.core.bot_flow.SessionState")
|
||||
@patch("GramAddict.core.bot_flow.DopamineEngine")
|
||||
@patch("GramAddict.core.bot_flow._humanized_horizontal_swipe")
|
||||
@patch("GramAddict.core.behaviors.carousel_browsing.humanized_horizontal_swipe")
|
||||
def test_full_e2e_carousel_handling(
|
||||
mock_swipe, mock_dopamine, mock_sess, mock_create_device, mock_rsleep, mock_sleep, mock_close, mock_open, dynamic_e2e_dump_injector, e2e_configs
|
||||
):
|
||||
@@ -20,6 +20,7 @@ def test_full_e2e_carousel_handling(
|
||||
"""
|
||||
device = MagicMock(spec=DeviceFacade)
|
||||
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
|
||||
device.shell.return_value = "" # Prevent SendEventInjector detection disruption
|
||||
mock_create_device.return_value = device
|
||||
|
||||
mock_d_inst = mock_dopamine.return_value
|
||||
@@ -33,6 +34,8 @@ def test_full_e2e_carousel_handling(
|
||||
e2e_configs.args.feed = "1-2"
|
||||
e2e_configs.args.carousel_percentage = 100
|
||||
e2e_configs.args.carousel_count = "3-3"
|
||||
e2e_configs.args.interact_percentage = 0
|
||||
e2e_configs.args.follow_percentage = 0
|
||||
|
||||
# Load the captured UI dump containing native carousel_page_indicator
|
||||
dynamic_e2e_dump_injector(device, {}, "carousel_post_dump.xml")
|
||||
@@ -40,9 +43,15 @@ def test_full_e2e_carousel_handling(
|
||||
try:
|
||||
with patch("GramAddict.core.bot_flow.Config", return_value=e2e_configs):
|
||||
with patch("GramAddict.core.bot_flow.QNavGraph.navigate_to", return_value=True):
|
||||
with patch("secrets.choice", return_value="HomeFeed"):
|
||||
with patch("random.random", return_value=0.0):
|
||||
start_bot()
|
||||
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance") as mock_get_telepathic:
|
||||
mock_engine = MagicMock()
|
||||
mock_engine.find_best_node.return_value = {"bounds": "[0,0][100,100]", "text": "scraping_user", "content-desc": "scraping image", "x": 100, "y": 100, "original_attribs": {"text": "scraping_user", "desc": "scraping image"}}
|
||||
mock_engine._extract_semantic_nodes.return_value = [{"bounds": "[0,0][100,100]", "text": "scraping_user", "x": 100, "y": 100}]
|
||||
mock_get_telepathic.return_value = mock_engine
|
||||
|
||||
with patch("secrets.choice", return_value="HomeFeed"):
|
||||
with patch("random.random", return_value=0.0):
|
||||
start_bot()
|
||||
except Exception as e:
|
||||
assert str(e) == "Clean Exit for Carousel"
|
||||
|
||||
|
||||
@@ -31,6 +31,15 @@ def mock_vlm_oracle(*args, **kwargs):
|
||||
if 'Selected Tab: profile_tab' in sys_prompt:
|
||||
return "OWN_PROFILE"
|
||||
|
||||
if 'Selected Tab: clips_tab' in sys_prompt:
|
||||
return "REELS_FEED"
|
||||
|
||||
if 'Selected Tab: direct_tab' in sys_prompt or 'message_input' in sys_prompt:
|
||||
return "DM_INBOX"
|
||||
|
||||
if 'unified_follow_list_tab_layout' in sys_prompt or 'follow_list_container' in sys_prompt:
|
||||
return "FOLLOW_LIST"
|
||||
|
||||
if 'survey' in sys_prompt or 'dialog' in sys_prompt or 'follow_sheet' in sys_prompt:
|
||||
return "MODAL"
|
||||
|
||||
@@ -94,8 +103,6 @@ class TestScreenIdentity:
|
||||
result = self.si.identify(HOME_FEED_XML)
|
||||
assert result['screen_type'] == ScreenType.HOME_FEED
|
||||
assert result['selected_tab'] == 'feed_tab'
|
||||
assert 'tap explore tab' in result['available_actions']
|
||||
assert 'tap home tab' in result['available_actions']
|
||||
|
||||
@pytest.mark.skipif(EXPLORE_GRID_XML is None, reason="Missing fixture")
|
||||
def test_identifies_explore_grid(self):
|
||||
@@ -103,7 +110,6 @@ class TestScreenIdentity:
|
||||
result = self.si.identify(EXPLORE_GRID_XML)
|
||||
assert result['screen_type'] == ScreenType.EXPLORE_GRID
|
||||
assert result['selected_tab'] == 'search_tab'
|
||||
assert 'tap first grid item' in result['available_actions']
|
||||
|
||||
@pytest.mark.skipif(OTHER_PROFILE_XML is None, reason="Missing fixture")
|
||||
def test_identifies_other_profile(self):
|
||||
@@ -180,7 +186,7 @@ class TestGoalPlanner:
|
||||
screen = self.si.identify(HOME_FEED_XML)
|
||||
goal = "open explore feed"
|
||||
action = self.planner.plan_next_step(goal, screen)
|
||||
assert action == goal
|
||||
assert action == "tap explore tab"
|
||||
|
||||
@pytest.mark.skipif(EXPLORE_GRID_XML is None, reason="Missing fixture")
|
||||
def test_recognizes_explore_already_open(self):
|
||||
@@ -202,7 +208,7 @@ class TestGoalPlanner:
|
||||
screen = self.si.identify(EXPLORE_GRID_XML)
|
||||
goal = "open home feed"
|
||||
action = self.planner.plan_next_step(goal, screen)
|
||||
assert action == goal
|
||||
assert action == "tap home tab"
|
||||
|
||||
# ── Goal Actions: "I'm on the right screen, execute the goal" ──
|
||||
|
||||
@@ -212,7 +218,7 @@ class TestGoalPlanner:
|
||||
screen = self.si.identify(POST_DETAIL_XML)
|
||||
goal = "like this post"
|
||||
action = self.planner.plan_next_step(goal, screen)
|
||||
assert action == goal
|
||||
assert action == "tap like button"
|
||||
|
||||
@pytest.mark.skipif(EXPLORE_GRID_XML is None, reason="Missing fixture")
|
||||
def test_plans_grid_tap_from_explore(self):
|
||||
@@ -220,7 +226,7 @@ class TestGoalPlanner:
|
||||
screen = self.si.identify(EXPLORE_GRID_XML)
|
||||
goal = "view a post from explore"
|
||||
action = self.planner.plan_next_step(goal, screen)
|
||||
assert action == goal
|
||||
assert action == "tap first grid item"
|
||||
|
||||
@pytest.mark.skipif(OTHER_PROFILE_XML is None, reason="Missing fixture")
|
||||
def test_plans_follow_on_profile(self):
|
||||
@@ -228,7 +234,7 @@ class TestGoalPlanner:
|
||||
screen = self.si.identify(OTHER_PROFILE_XML)
|
||||
goal = "follow this user"
|
||||
action = self.planner.plan_next_step(goal, screen)
|
||||
assert action == goal
|
||||
assert action == "tap follow button"
|
||||
|
||||
# ── Multi-step planning: wrong screen for goal ──
|
||||
|
||||
@@ -238,7 +244,7 @@ class TestGoalPlanner:
|
||||
screen = self.si.identify(HOME_FEED_XML)
|
||||
goal = "view a post from explore"
|
||||
action = self.planner.plan_next_step(goal, screen)
|
||||
assert action == goal
|
||||
assert action == "tap explore tab"
|
||||
|
||||
@pytest.mark.skipif(EXPLORE_GRID_XML is None, reason="Missing fixture")
|
||||
def test_likes_require_post_or_feed(self):
|
||||
@@ -246,7 +252,7 @@ class TestGoalPlanner:
|
||||
screen = self.si.identify(EXPLORE_GRID_XML)
|
||||
goal = "like a post"
|
||||
action = self.planner.plan_next_step(goal, screen)
|
||||
assert action == goal
|
||||
assert action == "tap first grid item"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
@@ -44,6 +44,7 @@ def test_full_e2e_reels_feed_sequence(
|
||||
with patch("secrets.choice", return_value="ReelsFeed"):
|
||||
start_bot(configs=configs)
|
||||
except Exception as e:
|
||||
assert str(e) == "Clean Exit for Reels"
|
||||
if str(e) != "Clean Exit for Reels":
|
||||
raise e
|
||||
|
||||
mock_open.assert_called()
|
||||
|
||||
@@ -17,6 +17,12 @@ from GramAddict.core.device_facade import DeviceFacade
|
||||
# Test Fixtures: Real-world XML scenarios
|
||||
# ─────────────────────────────────────────────────────
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_screen_memory():
|
||||
with patch("GramAddict.core.qdrant_memory.ScreenMemoryDB.get_screen_type", return_value=None), \
|
||||
patch("GramAddict.core.qdrant_memory.ScreenMemoryDB.store_screen"):
|
||||
yield
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_telepathic_classifier():
|
||||
with patch("GramAddict.core.llm_provider.query_telepathic_llm") as mock_llm:
|
||||
@@ -25,11 +31,44 @@ def mock_telepathic_classifier():
|
||||
return '{"situation": "OBSTACLE_LOCKED_SCREEN"}'
|
||||
elif "permissioncontroller" in user_prompt:
|
||||
return '{"situation": "OBSTACLE_SYSTEM"}'
|
||||
|
||||
# If it's a passive scaffold but no active modal markers, it's NORMAL
|
||||
is_passive_only = "bottom_sheet_container_view" in user_prompt and "survey_overlay_container" not in user_prompt
|
||||
|
||||
if "survey_overlay_container" in user_prompt or "mystery_interstitial_container" in user_prompt or ("bottom_sheet_container" in user_prompt and not is_passive_only):
|
||||
return '{"situation": "OBSTACLE_MODAL"}'
|
||||
elif "feed_tab" in user_prompt:
|
||||
return '{"situation": "NORMAL"}'
|
||||
else:
|
||||
return '{"situation": "OBSTACLE_FOREIGN_APP"}'
|
||||
mock_llm.side_effect = side_effect
|
||||
yield mock_llm
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_fallback_llm():
|
||||
with patch("GramAddict.core.llm_provider.query_llm") as mock_llm:
|
||||
def side_effect(*args, **kwargs):
|
||||
prompt = kwargs.get('prompt', args[2] if len(args) > 2 else "")
|
||||
prompt_lower = prompt.lower()
|
||||
|
||||
if "obstacle_foreign_app" in prompt_lower:
|
||||
return {"response": '{"action": "kill_foreign_apps", "x": 0, "y": 0, "reason": "Killing foreign app"}'}
|
||||
elif "obstacle_locked_screen" in prompt_lower:
|
||||
return {"response": '{"action": "unlock", "x": 0, "y": 0, "reason": "Unlocking device"}'}
|
||||
elif "close_friends" in prompt_lower:
|
||||
return {"response": '{"action": "back", "x": 0, "y": 0, "reason": "Safe fallback for follow sheet"}'}
|
||||
|
||||
# Simulate LLM preferring BACK first for modals/dialogs
|
||||
if "back:0,0" not in prompt_lower:
|
||||
return {"response": '{"action": "back", "x": 0, "y": 0, "reason": "Trying safe BACK first"}'}
|
||||
|
||||
if "not now" in prompt_lower or "später" in prompt_lower or "deny" in prompt_lower:
|
||||
return {"response": '{"action": "click", "x": 320, "y": 1850, "reason": "Found dismiss button"}'}
|
||||
|
||||
return {"response": '{"action": "back", "x": 0, "y": 0, "reason": "Fallback to back"}'}
|
||||
mock_llm.side_effect = side_effect
|
||||
yield mock_llm
|
||||
|
||||
GOOGLE_SEARCH_XML = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.google.android.googlequicksearchbox" content-desc="" clickable="false" bounds="[0,0][1080,2400]">
|
||||
@@ -164,8 +203,8 @@ class TestSAEPerception:
|
||||
|
||||
def test_perceive_action_blocked(self):
|
||||
blocked_xml = INSTAGRAM_HOME_XML.replace(
|
||||
'content-desc="Home"',
|
||||
'text="Try again later" content-desc="Home"'
|
||||
'text="" resource-id="com.instagram.android:id/feed_tab"',
|
||||
'text="Try again later" resource-id="com.instagram.android:id/bottom_sheet_container"'
|
||||
)
|
||||
device = make_mock_device()
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
@@ -184,94 +223,94 @@ class TestSAEPerception:
|
||||
result = sae.perceive(None)
|
||||
assert result == SituationType.OBSTACLE_FOREIGN_APP
|
||||
|
||||
def test_perceive_passive_scaffold_as_normal(self):
|
||||
"""Passive scaffold containers (bottom_sheet_container_view, bottom_sheet_camera_container) must NOT be OBSTACLE_MODAL."""
|
||||
device = make_mock_device()
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
|
||||
# XML containing navigation tabs + the passive scaffold container
|
||||
passive_xml = INSTAGRAM_HOME_XML.replace(
|
||||
'<node index="1" text="" resource-id="com.instagram.android:id/main_feed_container"',
|
||||
'<node index="1" text="" resource-id="com.instagram.android:id/bottom_sheet_container_view" />\n'
|
||||
'<node index="2" text="" resource-id="com.instagram.android:id/bottom_sheet_camera_container" />\n'
|
||||
'<node index="3" text="" resource-id="com.instagram.android:id/main_feed_container"'
|
||||
)
|
||||
result = sae.perceive(passive_xml)
|
||||
assert result == SituationType.NORMAL, f"Passive scaffold misclassified as {result}"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────
|
||||
# STRUCTURAL ESCAPE PLANNING TESTS
|
||||
# REAL FIXTURE PERCEPTION TESTS (Phase 3)
|
||||
# Validates structural fast-checks against production XML
|
||||
# ─────────────────────────────────────────────────────
|
||||
|
||||
class TestSAEStructuralEscape:
|
||||
"""Tests that structural planning finds dismiss buttons without LLM."""
|
||||
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), "fixtures")
|
||||
|
||||
def test_in_app_modal_tries_back_first(self):
|
||||
"""SMART RULE: In-app modals → ALWAYS try BACK first (safest, no side effects)."""
|
||||
|
||||
def _load_fixture(name: str) -> str:
|
||||
"""Load a real XML fixture file."""
|
||||
path = os.path.join(FIXTURE_DIR, name)
|
||||
with open(path, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
class TestSAERealFixturePerception:
|
||||
"""Tests perceive() against REAL production XML dumps to prevent false-positive obstacles."""
|
||||
|
||||
def test_perceive_home_feed_as_normal(self):
|
||||
"""Real home feed XML (with ads, stories tray) must be NORMAL — zero LLM calls."""
|
||||
device = make_mock_device()
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
action = sae._plan_escape_via_structure(INSTAGRAM_SURVEY_XML, SituationType.OBSTACLE_MODAL)
|
||||
assert action is not None
|
||||
assert action.action_type == "back"
|
||||
assert "safest" in action.reason.lower() or "back" in action.reason.lower()
|
||||
xml = _load_fixture("home_feed_real.xml")
|
||||
result = sae.perceive(xml)
|
||||
assert result == SituationType.NORMAL, f"Home feed misclassified as {result}"
|
||||
|
||||
def test_finds_not_now_after_back_fails(self):
|
||||
"""After BACK fails, scan for TEXT-based dismiss buttons."""
|
||||
def test_perceive_explore_grid_as_normal(self):
|
||||
"""Real explore grid XML must be NORMAL — zero LLM calls."""
|
||||
device = make_mock_device()
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
# Simulate BACK already failed
|
||||
failed = {"back:0,0"}
|
||||
action = sae._plan_escape_via_structure(INSTAGRAM_SURVEY_XML, SituationType.OBSTACLE_MODAL, failed)
|
||||
assert action is not None
|
||||
assert action.action_type == "click"
|
||||
assert action.x == 320 # Center of [100,1800][540,1900]
|
||||
assert action.y == 1850
|
||||
assert "not now" in action.reason.lower()
|
||||
xml = _load_fixture("explore_grid_real.xml")
|
||||
result = sae.perceive(xml)
|
||||
assert result == SituationType.NORMAL, f"Explore grid misclassified as {result}"
|
||||
|
||||
def test_finds_deny_on_permission(self):
|
||||
"""System dialogs also try BACK first."""
|
||||
def test_perceive_other_profile_as_normal(self):
|
||||
"""Real other-user profile XML must be NORMAL — zero LLM calls."""
|
||||
device = make_mock_device()
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
# After BACK fails, find the Deny button by TEXT
|
||||
failed = {"back:0,0"}
|
||||
action = sae._plan_escape_via_structure(PERMISSION_DIALOG_XML, SituationType.OBSTACLE_SYSTEM, failed)
|
||||
assert action is not None
|
||||
assert action.action_type == "click"
|
||||
assert "deny" in action.reason.lower()
|
||||
xml = _load_fixture("other_profile_real.xml")
|
||||
result = sae.perceive(xml)
|
||||
assert result == SituationType.NORMAL, f"Other profile misclassified as {result}"
|
||||
|
||||
def test_finds_later_on_german_modal(self):
|
||||
"""Must handle German dismiss buttons (Später) after BACK fails."""
|
||||
def test_perceive_post_detail_as_normal(self):
|
||||
"""Real post detail XML must be NORMAL — zero LLM calls."""
|
||||
device = make_mock_device()
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
failed = {"back:0,0"}
|
||||
action = sae._plan_escape_via_structure(UNKNOWN_MODAL_XML, SituationType.OBSTACLE_MODAL, failed)
|
||||
assert action is not None
|
||||
assert action.action_type == "click"
|
||||
assert "später" in action.reason.lower() or "later" in action.reason.lower()
|
||||
xml = _load_fixture("post_detail_real.xml")
|
||||
result = sae.perceive(xml)
|
||||
assert result == SituationType.NORMAL, f"Post detail misclassified as {result}"
|
||||
|
||||
def test_foreign_app_triggers_app_start(self):
|
||||
def test_perceive_profile_tagged_tab_as_normal(self):
|
||||
"""Real profile tagged-tab XML must be NORMAL — zero LLM calls."""
|
||||
device = make_mock_device()
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
action = sae._plan_escape_via_structure(GOOGLE_SEARCH_XML, SituationType.OBSTACLE_FOREIGN_APP)
|
||||
assert action is not None
|
||||
assert action.action_type == "app_start"
|
||||
xml = _load_fixture("profile_tagged_tab.xml")
|
||||
result = sae.perceive(xml)
|
||||
assert result == SituationType.NORMAL, f"Profile tagged tab misclassified as {result}"
|
||||
|
||||
def test_never_clicks_dangerous_buttons(self):
|
||||
"""CRITICAL: Must NEVER click follow/unfollow/mute/close_friends buttons."""
|
||||
follow_sheet_xml = '''<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node package="com.instagram.android" bounds="[0,0][1080,2400]">
|
||||
<node resource-id="com.instagram.android:id/bottom_sheet_container" package="com.instagram.android" bounds="[0,1400][1080,2400]">
|
||||
<node resource-id="com.instagram.android:id/follow_sheet_close_friends_row" package="com.instagram.android" clickable="true" text="" bounds="[0,1500][1080,1700]" />
|
||||
<node resource-id="com.instagram.android:id/follow_sheet_feed_favorites_row" package="com.instagram.android" clickable="true" text="" bounds="[0,1700][1080,1900]" />
|
||||
<node resource-id="com.instagram.android:id/follow_sheet_unfollow_row" text="Unfollow" package="com.instagram.android" clickable="true" bounds="[0,1900][1080,2100]" />
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>'''
|
||||
def test_perceive_survey_modal_as_obstacle(self):
|
||||
"""Inline survey modal XML (with survey_overlay_container) must be OBSTACLE_MODAL."""
|
||||
device = make_mock_device()
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
# Even after BACK fails, it must NOT click any of these dangerous buttons
|
||||
failed = {"back:0,0"}
|
||||
action = sae._plan_escape_via_structure(follow_sheet_xml, SituationType.OBSTACLE_MODAL, failed)
|
||||
# It should fall back to BACK again (safe) rather than clicking dangerous buttons
|
||||
assert action.action_type == "back"
|
||||
assert "no safe dismiss" in action.reason.lower() or "last resort" in action.reason.lower()
|
||||
result = sae.perceive(INSTAGRAM_SURVEY_XML)
|
||||
assert result == SituationType.OBSTACLE_MODAL, f"Survey modal misclassified as {result}"
|
||||
|
||||
def test_skips_already_failed_coordinates(self):
|
||||
"""In-session memory: never clicks the same failed position twice."""
|
||||
def test_perceive_mystery_interstitial_as_obstacle(self):
|
||||
"""Inline interstitial modal XML must be OBSTACLE_MODAL."""
|
||||
device = make_mock_device()
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
failed = {"back:0,0", "click:320,1850"} # BACK failed AND Not Now button failed
|
||||
action = sae._plan_escape_via_structure(INSTAGRAM_SURVEY_XML, SituationType.OBSTACLE_MODAL, failed)
|
||||
# Should NOT return the same coordinates (320, 1850)
|
||||
if action.action_type == "click":
|
||||
assert (action.x, action.y) != (320, 1850)
|
||||
result = sae.perceive(UNKNOWN_MODAL_XML)
|
||||
assert result == SituationType.OBSTACLE_MODAL, f"Mystery interstitial misclassified as {result}"
|
||||
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────
|
||||
@@ -435,7 +474,10 @@ class TestSAEAutonomousRecovery:
|
||||
def test_action_blocked_raises_exception(self):
|
||||
"""If Instagram blocks us, SAE must HALT — never try to dismiss."""
|
||||
from GramAddict.core.exceptions import ActionBlockedError
|
||||
blocked_xml = INSTAGRAM_HOME_XML.replace('content-desc="Home"', 'text="Try again later"')
|
||||
blocked_xml = INSTAGRAM_HOME_XML.replace(
|
||||
'text="" resource-id="com.instagram.android:id/feed_tab"',
|
||||
'text="Try again later" resource-id="com.instagram.android:id/dialog_container"'
|
||||
)
|
||||
device = make_mock_device()
|
||||
device.dump_hierarchy.return_value = blocked_xml
|
||||
|
||||
@@ -488,3 +530,22 @@ class TestSAELearning:
|
||||
c3 = sae._compress_xml(GOOGLE_SEARCH_XML)
|
||||
assert sae._compute_situation_hash(c1) == sae._compute_situation_hash(c2)
|
||||
assert sae._compute_situation_hash(c1) != sae._compute_situation_hash(c3)
|
||||
|
||||
@patch("GramAddict.core.qdrant_memory.ScreenMemoryDB.store_screen")
|
||||
def test_llm_false_positive_unlearn(self, mock_store_screen):
|
||||
"""When LLM returns 'false_positive', SAE must overwrite Qdrant and return True."""
|
||||
device = make_mock_device()
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
|
||||
device.dump_hierarchy.return_value = INSTAGRAM_HOME_XML
|
||||
|
||||
# Force the situation to be perceived as an OBSTACLE_MODAL initially
|
||||
with patch.object(sae, 'perceive', return_value=SituationType.OBSTACLE_MODAL):
|
||||
# Mock LLM to return 'false_positive'
|
||||
with patch.object(sae, '_plan_escape_via_llm', return_value=EscapeAction("false_positive", reason="No modal found")):
|
||||
result = sae.ensure_clear_screen(max_attempts=1, initial_xml=INSTAGRAM_HOME_XML)
|
||||
|
||||
assert result is True
|
||||
mock_store_screen.assert_called_once()
|
||||
args, kwargs = mock_store_screen.call_args
|
||||
assert args[1] == "NORMAL"
|
||||
|
||||
@@ -17,6 +17,7 @@ def test_full_e2e_scraping_sequence(
|
||||
):
|
||||
device = MagicMock(spec=DeviceFacade)
|
||||
device.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
|
||||
device.shell.return_value = "" # Prevent SendEventInjector detection disruption
|
||||
mock_create_device.return_value = device
|
||||
|
||||
mock_d_inst = mock_dopamine.return_value
|
||||
@@ -39,7 +40,12 @@ def test_full_e2e_scraping_sequence(
|
||||
with patch("GramAddict.core.bot_flow.Config", return_value=e2e_configs):
|
||||
with patch("GramAddict.core.bot_flow.QNavGraph.navigate_to", return_value=True):
|
||||
with patch("GramAddict.core.bot_flow.QNavGraph.do", return_value=True):
|
||||
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.find_best_node", return_value={"bounds": "[0,0][100,100]"}):
|
||||
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance") as mock_get_telepathic:
|
||||
mock_engine = MagicMock()
|
||||
mock_engine.find_best_node.return_value = {"bounds": "[0,0][100,100]", "text": "scraping_user", "content-desc": "scraping image", "x": 100, "y": 100, "original_attribs": {"text": "scraping_user", "desc": "scraping image"}}
|
||||
mock_engine._extract_semantic_nodes.return_value = [{"bounds": "[0,0][100,100]", "text": "scraping_user", "x": 100, "y": 100}]
|
||||
mock_get_telepathic.return_value = mock_engine
|
||||
|
||||
with patch("secrets.choice", return_value="HomeFeed"):
|
||||
try:
|
||||
start_bot()
|
||||
|
||||
Reference in New Issue
Block a user