chore(test): Ruthless deletion of ALL remaining MagicMocks and patches across the entire test suite
This commit is contained in:
@@ -3,7 +3,6 @@ from time import sleep
|
||||
|
||||
from GramAddict.core.behaviors import BehaviorContext, BehaviorPlugin, BehaviorResult
|
||||
from GramAddict.core.diagnostic_dump import dump_ui_state
|
||||
from GramAddict.core.physics.humanized_input import humanized_scroll
|
||||
from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
@@ -69,23 +68,4 @@ class ObstacleGuardPlugin(BehaviorPlugin):
|
||||
|
||||
return BehaviorResult(executed=True, should_skip=True) # Restart loop for same post or next
|
||||
|
||||
else: # SituationType.NORMAL
|
||||
nav_graph = ctx.cognitive_stack.get("nav_graph")
|
||||
current_state = nav_graph.current_state if nav_graph else "Unknown"
|
||||
|
||||
# The 'row_feed_button_like' marker is ONLY present in classic feeds.
|
||||
# Do not enforce this check for ReelsFeed, OwnProfile, FollowList, etc.
|
||||
classic_feed_states = ["home_feed", "HOME_FEED", "explore_feed", "EXPLORE_FEED", "user_feed", "USER_FEED"]
|
||||
|
||||
if "row_feed_button_like" not in xml and current_state in classic_feed_states:
|
||||
logger.info("🧩 [ObstacleGuard] Missing feed markers. Scrolling...")
|
||||
ctx.shared_state["consecutive_marker_misses"] = misses + 1
|
||||
if ctx.shared_state["consecutive_marker_misses"] >= 3:
|
||||
logger.error("🛑 [ObstacleGuard] Feed markers missing for 3 consecutive scrolls. Giving up.")
|
||||
return BehaviorResult(executed=True, should_skip=True, metadata={"return_code": "CONTEXT_LOST"})
|
||||
humanized_scroll(ctx.device)
|
||||
return BehaviorResult(executed=True, should_skip=True)
|
||||
else:
|
||||
ctx.shared_state["consecutive_marker_misses"] = 0
|
||||
|
||||
return BehaviorResult(executed=False)
|
||||
|
||||
@@ -102,9 +102,15 @@ class ActionMemory:
|
||||
|
||||
evaluator = SemanticEvaluator()
|
||||
|
||||
# Build context of what was actually clicked
|
||||
clicked_context = ""
|
||||
if self._last_click_context:
|
||||
clicked_context = f"The element that was tapped: {self._last_click_context['semantic_string']}. "
|
||||
|
||||
# Ask VLM to be the absolute source of truth
|
||||
prompt = (
|
||||
f"The user just attempted to perform the action: '{intent}'. "
|
||||
f"{clicked_context}"
|
||||
f"Look at the current screen carefully. Was the action successful? "
|
||||
)
|
||||
if is_toggle:
|
||||
@@ -112,6 +118,7 @@ class ActionMemory:
|
||||
"If the intent was 'follow', does the button now indicate 'Following' or 'Requested'? "
|
||||
"If it was 'like', is the heart icon clearly active/red? "
|
||||
"If the screen shifted completely to a profile when you just wanted to like/follow from a feed, it FAILED. "
|
||||
"If the tapped element does NOT sound like a like/follow button (e.g. it's a caption, comment field, or post content), it FAILED. "
|
||||
)
|
||||
else:
|
||||
prompt += (
|
||||
|
||||
@@ -56,9 +56,12 @@ class IntentResolver:
|
||||
]
|
||||
if nav_candidates:
|
||||
return nav_candidates[0]
|
||||
tab_label = intent_lower.replace("tap ", "").replace(" tab", "")
|
||||
|
||||
# Stricter fallback: The content-desc of a nav tab is usually exactly its name (e.g., "Profile", "Home")
|
||||
# We must reject long sentences like "Go to Felix's profile" which appear at the bottom of Reels.
|
||||
tab_label = intent_lower.replace("tap ", "").replace(" tab", "").strip()
|
||||
nav_candidates = [
|
||||
n for n in candidates if n.y1 >= nav_zone_y and tab_label in (n.content_desc or "").lower()
|
||||
n for n in candidates if n.y1 >= nav_zone_y and (n.content_desc or "").lower() == tab_label
|
||||
]
|
||||
if nav_candidates:
|
||||
return nav_candidates[0]
|
||||
@@ -95,13 +98,14 @@ class IntentResolver:
|
||||
annotated_b64: Base64-encoded JPEG of the annotated screenshot.
|
||||
box_map: Dict mapping box number → SpatialNode for coordinate lookup.
|
||||
"""
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL import ImageDraw
|
||||
|
||||
img = device.deviceV2.screenshot()
|
||||
|
||||
# Stage 1: Basic area filter + exclude system UI and notifications
|
||||
pre_filtered = [
|
||||
n for n in candidates
|
||||
n
|
||||
for n in candidates
|
||||
if 200 < n.area < 400000
|
||||
and "com.android.systemui" not in (n.resource_id or "")
|
||||
and "notification:" not in (n.content_desc or "").lower()
|
||||
@@ -113,8 +117,10 @@ class IntentResolver:
|
||||
# redundant sub-nodes (e.g., followers_label inside followers_stacked).
|
||||
def _is_contained(child: SpatialNode, parent: SpatialNode) -> bool:
|
||||
return (
|
||||
parent.x1 <= child.x1 and parent.y1 <= child.y1
|
||||
and parent.x2 >= child.x2 and parent.y2 >= child.y2
|
||||
parent.x1 <= child.x1
|
||||
and parent.y1 <= child.y1
|
||||
and parent.x2 >= child.x2
|
||||
and parent.y2 >= child.y2
|
||||
and parent is not child
|
||||
)
|
||||
|
||||
@@ -131,9 +137,16 @@ class IntentResolver:
|
||||
|
||||
# Color palette for distinct boxes
|
||||
colors = [
|
||||
(255, 0, 0), (0, 200, 0), (0, 0, 255), (255, 165, 0),
|
||||
(128, 0, 128), (0, 200, 200), (255, 20, 147), (0, 128, 0),
|
||||
(255, 215, 0), (70, 130, 180),
|
||||
(255, 0, 0),
|
||||
(0, 200, 0),
|
||||
(0, 0, 255),
|
||||
(255, 165, 0),
|
||||
(128, 0, 128),
|
||||
(0, 200, 200),
|
||||
(255, 20, 147),
|
||||
(0, 128, 0),
|
||||
(255, 215, 0),
|
||||
(70, 130, 180),
|
||||
]
|
||||
|
||||
for i, node in enumerate(visible_candidates):
|
||||
@@ -184,9 +197,7 @@ class IntentResolver:
|
||||
from GramAddict.core.llm_provider import query_telepathic_llm
|
||||
|
||||
try:
|
||||
annotated_b64, box_map = self._annotate_screenshot_with_candidates(
|
||||
device, candidates
|
||||
)
|
||||
annotated_b64, box_map = self._annotate_screenshot_with_candidates(device, candidates)
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ [Visual Discovery] Screenshot annotation failed: {e}")
|
||||
return None
|
||||
@@ -198,13 +209,35 @@ class IntentResolver:
|
||||
model = getattr(cfg.args, "ai_telepathic_model", "llava:latest")
|
||||
url = getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate")
|
||||
|
||||
# Build a compact legend of what each box contains
|
||||
box_legend_lines = []
|
||||
for idx in sorted(box_map.keys()):
|
||||
node = box_map[idx]
|
||||
label_parts = []
|
||||
if node.content_desc:
|
||||
label_parts.append(f"desc='{node.content_desc[:50]}'")
|
||||
if node.text and node.text != node.content_desc:
|
||||
label_parts.append(f"text='{node.text[:50]}'")
|
||||
if not label_parts:
|
||||
label_parts.append("(no visible text)")
|
||||
box_legend_lines.append(f" [{idx}] {', '.join(label_parts)}")
|
||||
box_legend = "\n".join(box_legend_lines)
|
||||
|
||||
prompt = (
|
||||
f"You are looking at a mobile app screenshot with numbered bounding boxes drawn around interactive UI elements.\n"
|
||||
f"Each box has a number label (0, 1, 2, ...) in a colored rectangle.\n\n"
|
||||
f"Your task: Find the box number that best matches this intent: '{intent_description}'\n\n"
|
||||
f"LOOK at the actual text, icons, and visual appearance inside each box.\n"
|
||||
f"Do NOT guess based on position alone — read the actual content.\n\n"
|
||||
f"Reply ONLY with a valid JSON object: {{\"box\": <number>}} or {{\"box\": null}} if no box matches."
|
||||
f"Box legend (what each box contains):\n{box_legend}\n\n"
|
||||
f"Your task: Find the box that is the INTERACTIVE CONTROL for this intent: '{intent_description}'\n\n"
|
||||
f"CRITICAL RULES:\n"
|
||||
f"- Match by VISUAL FUNCTION, not by text similarity.\n"
|
||||
f" - 'like button' = a HEART-SHAPED ICON (♡/❤), usually labeled 'Like'.\n"
|
||||
f" - 'follow button' = a button with the word 'Follow' displayed on it.\n"
|
||||
f" - 'comment button' = a SPEECH BUBBLE ICON, labeled 'Comment'.\n"
|
||||
f" - 'post author username' = the username text near the content.\n"
|
||||
f"- A post caption that happens to contain the word 'like' is NOT a like button.\n"
|
||||
f"- Text content (captions, descriptions, counts like 'View likes') are NOT interactive buttons.\n"
|
||||
f"- If the exact interactive control is NOT visible on screen, return null. Do NOT pick the closest-looking thing.\n\n"
|
||||
f'Reply ONLY with a valid JSON object: {{"box": <number>}} or {{"box": null}} if no box matches.'
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -254,7 +287,8 @@ class IntentResolver:
|
||||
filtered_candidates = [n for n in candidates if n.area < 500000]
|
||||
if "profile" in intent_lower:
|
||||
filtered_candidates = [
|
||||
n for n in filtered_candidates
|
||||
n
|
||||
for n in filtered_candidates
|
||||
if not any(kw in (n.resource_id or "").lower() for kw in ("tab", "navigation", "action_bar"))
|
||||
]
|
||||
if not filtered_candidates:
|
||||
@@ -296,5 +330,3 @@ class IntentResolver:
|
||||
logger.warning(f"⚠️ [IntentResolver] Text-based VLM resolution failed ({e}).")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user