Compare commits
142 Commits
fix/e2e-te
...
fix/keyboa
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f778d46c9 | |||
| 49f82d467f | |||
| a67072eec4 | |||
| 59ba330029 | |||
| 6f9da50ae2 | |||
| 720841103d | |||
| c98e2caaa1 | |||
| 32731ed7ec | |||
| 8a6c8a2249 | |||
| f384fbb749 | |||
| 565bdaa568 | |||
| c641204a6b | |||
| 4b645c6fb2 | |||
| c7c7ce29f8 | |||
| b36dde77d8 | |||
| 93b2140844 | |||
| 67c3d464e0 | |||
| d298f03891 | |||
| 604f2d7341 | |||
| cd8f35056c | |||
| 800fb1da98 | |||
| 6cd068f951 | |||
| f46b0b7bcb | |||
| 5fbbe3d273 | |||
| f85d0a8a76 | |||
| f0a54d4e20 | |||
| 0a73c35809 | |||
| e535c10b65 | |||
| 91effbc843 | |||
| 3da3849ca1 | |||
| 51ee7a6793 | |||
| 936da47f61 | |||
| d1e0995148 | |||
| 2f8eebb7e9 | |||
| 9c6f80de9d | |||
| cff7e976e0 | |||
| f6f15ebd9a | |||
| 1cc367697e | |||
| 738a59ac8d | |||
| cd6cecbe27 | |||
| 4af4ddb060 | |||
| f32ee46d8c | |||
| d2de5f91de | |||
| 9a13216064 | |||
| aa5184786e | |||
| 2e1edec56a | |||
| df18a48a84 | |||
| f1a8573be8 | |||
| da7201117c | |||
| fddf14fd67 | |||
| 392abff313 | |||
| 556bd181fa | |||
| 2c44331f03 | |||
| b83b55e02b | |||
| db226ed7c2 | |||
| 47f94b699c | |||
| 96fdbd7db7 | |||
| ca91ae4b33 | |||
| a560225dc9 | |||
| 849fb63426 | |||
| fc44633ebc | |||
| 0f5b71708d | |||
| 0ef2840f79 | |||
| 068a6a616a | |||
| effb1f5ae1 | |||
| 0e43996ccd | |||
| b6846ab0fe | |||
| 6db579f45b | |||
| 0ed12303ac | |||
| 6abb519e3b | |||
| 4e91db01c9 | |||
| e55abc5a8a | |||
| 03105437b8 | |||
| b9c29a5a2d | |||
| 71310b8e84 | |||
| 073a90c38c | |||
| 44fae37cc7 | |||
| 48071cc9b8 | |||
| 10a85a91f1 | |||
| 5bf0053884 | |||
| a846462d02 | |||
| 0dbafd0a82 | |||
| 83e5b94ddf | |||
| dd8285e1ce | |||
| ac5d5351a6 | |||
| ad012b4cd4 | |||
| 5fcf1f180b | |||
| 9a74d89477 | |||
| dc4b576bc1 | |||
| e94dfe8c5c | |||
| 7aa6bfccf6 | |||
| 5fef014cb4 | |||
| 0bdfd999d2 | |||
| 4ad559e107 | |||
| f220e09193 | |||
| de2a1c104f | |||
| 52c553827f | |||
| cd64794f55 | |||
| bd9148e6e9 | |||
| 1e1bba6b16 | |||
| 2b992cf2a8 | |||
| c051c3a4c3 | |||
| 3006020106 | |||
| 3b9465a3bc | |||
| 5d50228945 | |||
| 7277f27fae | |||
| ee3de811d3 | |||
| c93333928a | |||
| 12937cb2c1 | |||
| 097a5753f9 | |||
| 9ee6aab831 | |||
| da804b174a | |||
| 93175b7caf | |||
| e37d92cdfd | |||
| 1c38dabe79 | |||
| 7b8daa7670 | |||
| a7449a1db3 | |||
| 746eeb767d | |||
| 36a8683643 | |||
| 888136f733 | |||
| ae36b6e196 | |||
| e70ce0f52d | |||
| 22ca93c988 | |||
| 740f8f1f56 | |||
| f148efd2a0 | |||
| ac95dec9d8 | |||
| 0b68d4bc77 | |||
| 8c37290bc3 | |||
| b4bafb59be | |||
| 41450c4eaf | |||
| e9201e0e30 | |||
| ae046be3b1 | |||
| a2a4a75603 | |||
| 714c914432 | |||
| 294403d590 | |||
| 117e7a22e7 | |||
| 0fbd1b1678 | |||
| b5cca06ce2 | |||
| 3c4dd84a61 | |||
| 9ad49500f9 | |||
| 4de087ae45 | |||
| 42a11107fd |
18
.gitignore
vendored
18
.gitignore
vendored
@@ -10,6 +10,12 @@
|
||||
!test_config.yml
|
||||
*.json
|
||||
*.xml
|
||||
!tests/fixtures/*.xml
|
||||
!tests/fixtures/*.jpg
|
||||
!tests/fixtures/*.json
|
||||
!tests/e2e/fixtures/*.xml
|
||||
!tests/e2e/fixtures/*.jpg
|
||||
!tests/e2e/fixtures/*.json
|
||||
logs/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
@@ -25,8 +31,14 @@ Pipfile.lock
|
||||
*.ini
|
||||
*.db
|
||||
|
||||
# Debug artifacts
|
||||
# Debug artifacts & garbage scripts (Rule 5: KRIEG DEM MÜLL)
|
||||
scratch*.py
|
||||
rewrite_*.py
|
||||
test_*.py
|
||||
!tests/**
|
||||
update_*.py
|
||||
profile_dump.*
|
||||
resp_dump.*
|
||||
test_compress.py
|
||||
test_fixtures.py
|
||||
output.txt
|
||||
@@ -37,3 +49,7 @@ traceback.log
|
||||
htmlcov/
|
||||
.coverage
|
||||
coverage.xml
|
||||
.hypothesis/
|
||||
|
||||
# Local diagnostic traces
|
||||
debug/
|
||||
|
||||
Binary file not shown.
@@ -29,6 +29,15 @@ Found in `sensors/honeypot_radome.py`.
|
||||
- **Ghost Engagement Guard**: Strips DOM nodes explicitly tagged with `visible-to-user="false"` to prevent triggering Accessibility Hooks.
|
||||
- **VLM Sanity Guard**: Woven into `telepathic_engine.py`, it sends semantic matches for destructive actions (Like/Follow) through a Vision Language Model step to prevent executing semantic "Bait and Switch" tricks.
|
||||
|
||||
### 🧠 Situational Awareness Engine (SAE)
|
||||
Found in `situational_awareness.py`. Handles autonomous obstacle detection, recovery, and learning with **ZERO hardcoded UI element identifiers**.
|
||||
- **Autonomous 3-Layer Classification** (zero maintenance — no resource-ids, no button text, no localized strings):
|
||||
1. **Package-Based Foreign App Detection**: If our app's package is absent from the XML hierarchy, it's a foreign app. Uses Android package names (not Instagram UI elements).
|
||||
2. **Qdrant Semantic Cache**: Previously classified screens are instantly recalled from the vector database (O(1) latency). The bot learns from every first-encounter.
|
||||
3. **ScreenIdentity Structural Delegation**: The `ScreenIdentity` module classifies known screen types via its own structural logic. If it identifies a MODAL, the SAE trusts it.
|
||||
4. **LLM Autonomous Classification**: Unknown screens are classified by the LLM (OBSTACLE_MODAL, DANGER_ACTION_BLOCKED, or NORMAL). Results are cached in Qdrant — so each screen type is learned exactly once.
|
||||
- **Zero-Maintenance Guarantee**: When Instagram updates its UI (changes resource-ids, adds new modals), the bot discovers and learns the new patterns autonomously via the LLM. No code changes required.
|
||||
|
||||
### 🦾 Biometric Facade (Gaussian Clicks)
|
||||
Found in `device_facade.py`.
|
||||
- Human touches do not follow a flat mathematical uniform grid. The GramPilot simulates genuine **biometric dispersion** using `random.gauss(mu, sigma)`, strictly centering clicks inside a thumb-bias radius (bottom-left skew for right-handers). In tests, this hits a 68% standard deviation precision.
|
||||
@@ -37,3 +46,9 @@ Found in `device_facade.py`.
|
||||
Instead of hardcoding limits like `max_likes = 50`, the bot stops interacting based on **simulated boredom**.
|
||||
- The `ResonanceEngine` calculates the aesthetic score of content.
|
||||
- The `DopamineEngine` uses this score to modulate pace. High resonance = engagement. Low resonance over multiple posts = early session termination (simulating human fatigue).
|
||||
|
||||
## 4. The 100% Autonomy Directive (Zero Hardcoding)
|
||||
GramPilot is designed as a true agent, not a state-machine script. It operates on **absolute zero hardcoded UI states or edge cases**.
|
||||
- **No Manual Guards**: Features like `if "row_feed_button_like" not in xml:` or `if state == "ReelsFeed":` are strictly prohibited. The bot must understand the screen via its Vision-Language-Action (VLA) pipeline.
|
||||
- **No Hand-Holding**: If the LLM makes a mistake (e.g., clicking the wrong button in a DM), the solution is to improve the VLM prompt, the system architecture, or the Visual Critic. We never insert `if is_dm_thread:` hacks.
|
||||
- **Smart like a human**: The bot navigates by visually confirming targets, detecting obstacles when the UI organically stops responding, and inferring context precisely like a real user scrolling.
|
||||
|
||||
@@ -47,7 +47,7 @@ def verify_and_switch_account(device, nav_graph, target_username):
|
||||
telepath = TelepathicEngine.get_instance()
|
||||
|
||||
# We ask the semantic engine to find the profile tab, ensuring 100% ID-agnostic behavior
|
||||
profile_tab_node = telepath.find_best_node(xml_dump, "tap profile tab", min_threshold=0.3)
|
||||
profile_tab_node = telepath.find_best_node(xml_dump, "tap profile tab", min_threshold=0.3, device=device)
|
||||
if profile_tab_node:
|
||||
profile_tab = (profile_tab_node["x"], profile_tab_node["y"])
|
||||
except Exception as e:
|
||||
@@ -113,7 +113,7 @@ def verify_and_switch_account(device, nav_graph, target_username):
|
||||
dump_ui_state(
|
||||
device, "identity_guard", {"reason": "account_not_found_in_bottom_sheet", "target": target_username}
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
# Escape the bottom sheet
|
||||
device.press("back")
|
||||
|
||||
@@ -240,6 +240,7 @@ from GramAddict.core.behaviors.profile_visit import ProfileVisitPlugin # noqa:
|
||||
from GramAddict.core.behaviors.rabbit_hole import RabbitHolePlugin # noqa: E402
|
||||
from GramAddict.core.behaviors.repost import RepostPlugin # noqa: E402
|
||||
from GramAddict.core.behaviors.resonance_evaluator import ResonanceEvaluatorPlugin # noqa: E402
|
||||
from GramAddict.core.behaviors.scrape_profile import ScrapeProfilePlugin # noqa: E402
|
||||
|
||||
# Note: We do not automatically instantiate all of them globally here to avoid circular
|
||||
# dependencies during initial load. The bot_flow.py engine should explicitly register them.
|
||||
@@ -265,3 +266,4 @@ def load_all_plugins():
|
||||
registry.register(RabbitHolePlugin())
|
||||
registry.register(RepostPlugin())
|
||||
registry.register(ResonanceEvaluatorPlugin())
|
||||
registry.register(ScrapeProfilePlugin())
|
||||
|
||||
@@ -31,7 +31,7 @@ class AnomalyHandlerPlugin(BehaviorPlugin):
|
||||
return getattr(self, "_enabled", True)
|
||||
|
||||
def execute(self, ctx: BehaviorContext) -> BehaviorResult:
|
||||
telepathic = TelepathicEngine.get_instance()
|
||||
telepathic = ctx.cognitive_stack.get("telepathic") or TelepathicEngine.get_instance()
|
||||
xml = ctx.context_xml if ctx.context_xml else ctx.device.dump_hierarchy()
|
||||
nodes = telepathic._extract_semantic_nodes(xml)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class CloseFriendsGuardPlugin(BehaviorPlugin):
|
||||
return False
|
||||
|
||||
xml = ctx.context_xml if ctx.context_xml else ctx.device.dump_hierarchy()
|
||||
return "enge freunde" in xml.lower() or "close friend" in xml.lower()
|
||||
return "close friend" in xml.lower()
|
||||
|
||||
def execute(self, ctx: BehaviorContext) -> BehaviorResult:
|
||||
logger.info("💚 [CloseFriendsGuard] Close friends post detected. Skipping...")
|
||||
|
||||
@@ -32,6 +32,20 @@ class CommentPlugin(BehaviorPlugin):
|
||||
if ctx.session_state.check_limit(SessionState.Limit.COMMENTS):
|
||||
return False
|
||||
|
||||
# Safety Guard: Do not comment on stories or grids
|
||||
xml_lower = (ctx.context_xml or "").lower()
|
||||
STORY_MARKERS = (
|
||||
"reel_viewer_media_layout",
|
||||
"reel_viewer_header",
|
||||
"reel_viewer_progress_bar",
|
||||
"reel_viewer_root",
|
||||
)
|
||||
if any(marker in xml_lower for marker in STORY_MARKERS):
|
||||
return False
|
||||
|
||||
if "explore_action_bar" in xml_lower or "profile_tabs_container" in xml_lower:
|
||||
return False
|
||||
|
||||
config = self.get_config(ctx)
|
||||
comment_pct = float(config.get("percentage", getattr(ctx.configs.args, "comment_percentage", 0))) / 100.0
|
||||
|
||||
@@ -78,7 +92,7 @@ class CommentPlugin(BehaviorPlugin):
|
||||
# 4. Type and post
|
||||
if nav_graph.do("type and post comment", text=text):
|
||||
logger.info(f"💬 [Comment] Posted to @{ctx.username} ✓")
|
||||
ctx.session_state.add_interaction(source=ctx.username, succeed=True, followed=False, liked=False)
|
||||
ctx.session_state.add_interaction(source=ctx.username, succeed=True, followed=False, scraped=False)
|
||||
ctx.session_state.totalComments += 1
|
||||
return BehaviorResult(executed=True, interactions=1, metadata={"text": text})
|
||||
|
||||
|
||||
@@ -40,7 +40,15 @@ class DarwinDwellPlugin(BehaviorPlugin):
|
||||
logger.info("🐢 [DarwinDwell] Executing organic dwell behaviors...")
|
||||
darwin.execute_micro_wobble(ctx.device)
|
||||
res_score = ctx.shared_state.get("res_score", 1.0)
|
||||
darwin.execute_proof_of_resonance(ctx.device, res_score)
|
||||
darwin.execute_proof_of_resonance(
|
||||
ctx.device,
|
||||
res_score,
|
||||
nav_graph=ctx.cognitive_stack.get("nav_graph"),
|
||||
configs=ctx.configs,
|
||||
resonance_oracle=ctx.cognitive_stack.get("oracle"),
|
||||
username=ctx.username,
|
||||
context_xml=ctx.context_xml or ctx.device.dump_hierarchy(),
|
||||
)
|
||||
else:
|
||||
logger.info("🐢 [DarwinDwell] Darwin engine missing. Falling back to static sleep.")
|
||||
sleep(2.5 * ctx.sleep_mod)
|
||||
|
||||
@@ -49,9 +49,9 @@ class FollowPlugin(BehaviorPlugin):
|
||||
|
||||
nav_graph = QNavGraph(ctx.device)
|
||||
|
||||
if nav_graph.do("tap follow button"):
|
||||
if nav_graph.do("tap 'Follow' button") or nav_graph.do("tap 'Following' button"):
|
||||
logger.info(f"🤝 [Follow] Followed @{ctx.username} ✓")
|
||||
ctx.session_state.add_interaction(source=ctx.username, succeed=True, followed=True, liked=False)
|
||||
ctx.session_state.add_interaction(source=ctx.username, succeed=True, followed=True, scraped=False)
|
||||
|
||||
# Buffer for follow animations to close
|
||||
sleep(random.uniform(1.8, 3.2) * ctx.sleep_mod)
|
||||
|
||||
@@ -30,6 +30,7 @@ class LikePlugin(BehaviorPlugin):
|
||||
from GramAddict.core.session_state import SessionState
|
||||
|
||||
if ctx.session_state.check_limit(SessionState.Limit.LIKES):
|
||||
logger.error("LikePlugin: limit check failed")
|
||||
return False
|
||||
|
||||
config = self.get_config(ctx)
|
||||
@@ -54,7 +55,8 @@ class LikePlugin(BehaviorPlugin):
|
||||
|
||||
if nav_graph.do("tap like button"):
|
||||
logger.info(f"❤️ [Like] Liked post by @{ctx.username} ✓")
|
||||
ctx.session_state.add_interaction(source=ctx.username, succeed=True, followed=False, liked=True)
|
||||
ctx.session_state.add_interaction(source=ctx.username, succeed=True, followed=False, scraped=False)
|
||||
ctx.session_state.totalLikes += 1
|
||||
return BehaviorResult(executed=True, interactions=1)
|
||||
|
||||
return BehaviorResult(executed=False)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,10 +42,25 @@ class ObstacleGuardPlugin(BehaviorPlugin):
|
||||
|
||||
misses = ctx.shared_state.get("consecutive_marker_misses", 0)
|
||||
|
||||
# ── System Dialog / Permission Modal (e.g. "Allow Instagram to record audio?") ──
|
||||
if situation == SituationType.OBSTACLE_SYSTEM:
|
||||
logger.warning("⚠️ [ObstacleGuard] System permission dialog detected. Dismissing with BACK...")
|
||||
ctx.device.press("back")
|
||||
sleep(1.5 * ctx.sleep_mod)
|
||||
return BehaviorResult(executed=True, should_skip=True)
|
||||
|
||||
# ── Foreign App Takeover (e.g. browser opened, wrong app in foreground) ──
|
||||
if situation == SituationType.OBSTACLE_FOREIGN_APP:
|
||||
logger.warning("⚠️ [ObstacleGuard] Foreign app detected. Pressing BACK to recover...")
|
||||
ctx.device.press("back")
|
||||
sleep(1.5 * ctx.sleep_mod)
|
||||
return BehaviorResult(executed=True, should_skip=True)
|
||||
|
||||
# ── Instagram Modal / Overlay (survey, "Not Now" prompt, creation flow) ──
|
||||
if situation == SituationType.OBSTACLE_MODAL:
|
||||
if misses >= 2:
|
||||
logger.error("🛑 [ObstacleGuard] Failed to recover from OBSTACLE_MODAL after multiple attempts.")
|
||||
sae.unlearn_current_state()
|
||||
sae.unlearn_current_state(xml)
|
||||
dump_ui_state(ctx.device, f"fatal_obstacle_{ctx.session_state.job_target}")
|
||||
return BehaviorResult(executed=True, should_skip=True, metadata={"return_code": "CONTEXT_LOST"})
|
||||
|
||||
@@ -57,7 +71,7 @@ class ObstacleGuardPlugin(BehaviorPlugin):
|
||||
# Check recovery
|
||||
new_xml = ctx.device.dump_hierarchy()
|
||||
tele = TelepathicEngine.get_instance()
|
||||
best_node = tele.find_best_node(new_xml, intent_description="Dismiss obstacle")
|
||||
best_node = tele.find_best_node(new_xml, intent_description="Dismiss obstacle", device=ctx.device)
|
||||
if best_node:
|
||||
ctx.device.click(best_node.get("x", 0), best_node.get("y", 0))
|
||||
|
||||
@@ -69,16 +83,4 @@ class ObstacleGuardPlugin(BehaviorPlugin):
|
||||
|
||||
return BehaviorResult(executed=True, should_skip=True) # Restart loop for same post or next
|
||||
|
||||
else: # SituationType.NORMAL
|
||||
if "row_feed_button_like" not in xml:
|
||||
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)
|
||||
|
||||
@@ -26,7 +26,17 @@ class PerfectSnappingPlugin(BehaviorPlugin):
|
||||
return 90
|
||||
|
||||
def can_activate(self, ctx: BehaviorContext) -> bool:
|
||||
return getattr(self, "_enabled", True)
|
||||
if not getattr(self, "_enabled", True):
|
||||
return False
|
||||
|
||||
# Perfect snapping is only for feed posts.
|
||||
# Do not snap if we are on a profile page, explore grid, or modal.
|
||||
from GramAddict.core.perception.feed_analysis import has_feed_markers
|
||||
|
||||
if not has_feed_markers(ctx.context_xml):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, ctx: BehaviorContext) -> BehaviorResult:
|
||||
aligned = _align_active_post(ctx.device)
|
||||
|
||||
@@ -26,15 +26,24 @@ class PostDataExtractionPlugin(BehaviorPlugin):
|
||||
return 85
|
||||
|
||||
def can_activate(self, ctx: BehaviorContext) -> bool:
|
||||
return getattr(self, "_enabled", True) and ctx.context_xml is not None
|
||||
from GramAddict.core.perception.feed_analysis import has_feed_markers
|
||||
|
||||
return getattr(self, "_enabled", True) and ctx.context_xml is not None and has_feed_markers(ctx.context_xml)
|
||||
|
||||
def execute(self, ctx: BehaviorContext) -> BehaviorResult:
|
||||
logger.debug("🧩 [PostDataExtraction] Extracting post metadata...")
|
||||
post_data = extract_post_content(ctx.context_xml)
|
||||
post_data = extract_post_content(ctx.context_xml, device=ctx.device)
|
||||
|
||||
if post_data:
|
||||
ctx.post_data = post_data
|
||||
ctx.username = post_data.get("username", "")
|
||||
|
||||
if post_data.get("username_missing") or not ctx.username:
|
||||
logger.error(
|
||||
"❌ [PostDataExtraction] FAILED: Post author username is empty or missing! Halting interaction."
|
||||
)
|
||||
return BehaviorResult(executed=False, metadata={"error": "Empty username extracted"})
|
||||
|
||||
logger.info(f"📝 [PostDataExtraction] Post by @{ctx.username} extracted.")
|
||||
return BehaviorResult(executed=True)
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class ProfileGuardPlugin(BehaviorPlugin):
|
||||
|
||||
# Close friends guard
|
||||
if getattr(ctx.configs.args, "ignore_close_friends", False):
|
||||
if "enge freunde" in xml_check_lower or "close friend" in xml_check_lower:
|
||||
if "close friend" in xml_check_lower:
|
||||
logger.info(
|
||||
f"💚 [Profile Guard] @{ctx.username} is a Close Friend. Ignoring.", extra={"color": "\033[32m"}
|
||||
)
|
||||
|
||||
@@ -50,8 +50,11 @@ class RepostPlugin(BehaviorPlugin):
|
||||
|
||||
nav_graph = QNavGraph(ctx.device)
|
||||
|
||||
if nav_graph.do("share to story"):
|
||||
logger.info(f"📤 [Repost] Shared post by @{ctx.username} to story ✓")
|
||||
return BehaviorResult(executed=True, interactions=1)
|
||||
# We must click the send post button first
|
||||
if nav_graph.do("tap send post button"):
|
||||
# A modal should appear, now click add to story
|
||||
if nav_graph.do("tap add to story"):
|
||||
logger.info(f"📤 [Repost] Shared post by @{ctx.username} to story ✓")
|
||||
return BehaviorResult(executed=True, interactions=1)
|
||||
|
||||
return BehaviorResult(executed=False)
|
||||
|
||||
@@ -49,11 +49,36 @@ class ResonanceEvaluatorPlugin(BehaviorPlugin):
|
||||
tele = ctx.cognitive_stack.get("telepathic")
|
||||
if tele:
|
||||
logger.info("✨ [Resonance] Performing visual vibe check...")
|
||||
vibe = tele.evaluate_post_vibe()
|
||||
vibe_score = vibe.get("quality_score", 5) / 10.0
|
||||
if vibe.get("matches_niche"):
|
||||
vibe_score = min(1.0, vibe_score + 0.2)
|
||||
res_score = (res_score * 0.3) + (vibe_score * 0.7)
|
||||
|
||||
# BUG 5 Fix: Read target_audience or persona_interests
|
||||
raw_interests = getattr(ctx.configs.args, "persona_interests", "")
|
||||
if not raw_interests:
|
||||
raw_interests = getattr(ctx.configs.args, "target_audience", "")
|
||||
|
||||
if isinstance(raw_interests, list):
|
||||
persona_interests = [str(i).strip() for i in raw_interests if str(i).strip()]
|
||||
else:
|
||||
persona_interests = [i.strip() for i in str(raw_interests).split(",") if i.strip()]
|
||||
|
||||
vibe = tele.evaluate_post_vibe(ctx.device, persona_interests)
|
||||
if vibe is None:
|
||||
logger.warning(
|
||||
"✨ [Resonance] VLM vibe check returned None (truncated JSON?). Keeping neutral score."
|
||||
)
|
||||
else:
|
||||
if vibe.get("is_ad"):
|
||||
logger.info("🛡️ [Resonance Oracle] Visually identified post as an Ad! Skipping...")
|
||||
marker = vibe.get("ad_marker_text")
|
||||
if marker and marker.strip():
|
||||
from GramAddict.core.utils import learn_ad_marker
|
||||
learn_ad_marker(marker, ctx.context_xml)
|
||||
humanized_scroll(ctx.device)
|
||||
return BehaviorResult(executed=True, should_skip=True)
|
||||
|
||||
# BUG 6 Fix: VLM returns {"should_like": true/false}, not "quality_score"
|
||||
should_like = vibe.get("should_like", False)
|
||||
vibe_score = 1.0 if should_like else 0.2
|
||||
res_score = (res_score * 0.3) + (vibe_score * 0.7)
|
||||
|
||||
ctx.shared_state["res_score"] = res_score
|
||||
logger.info(f"📊 [Resonance] Post Score: {res_score:.2f}")
|
||||
|
||||
78
GramAddict/core/behaviors/scrape_profile.py
Normal file
78
GramAddict/core/behaviors/scrape_profile.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import logging
|
||||
|
||||
from GramAddict.core.behaviors import BehaviorContext, BehaviorPlugin, BehaviorResult
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ScrapeProfilePlugin(BehaviorPlugin):
|
||||
"""
|
||||
Extracts profile metadata (followers, following, bio) when visiting a profile.
|
||||
|
||||
Priority: 45. (Runs after ProfileGuard, before deep interactions like GridLike)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._enabled = True
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "scrape_profile"
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
return 45
|
||||
|
||||
def can_activate(self, ctx: BehaviorContext) -> bool:
|
||||
if not getattr(self, "_enabled", True):
|
||||
return False
|
||||
|
||||
# Only activate if scrape_profiles is True in config
|
||||
if not getattr(ctx.configs.args, "scrape_profiles", False):
|
||||
return False
|
||||
|
||||
# Only activate when we are actively visiting a profile (via ProfileVisitPlugin)
|
||||
nav_graph = ctx.cognitive_stack.get("nav_graph")
|
||||
if not nav_graph or nav_graph.current_state != "ProfileView":
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, ctx: BehaviorContext) -> BehaviorResult:
|
||||
from colorama import Fore
|
||||
|
||||
logger.info(f"📊 [Scraping] Extracting metadata for @{ctx.username}...", extra={"color": f"{Fore.CYAN}"})
|
||||
|
||||
telepathic = ctx.cognitive_stack.get("telepathic") or TelepathicEngine.get_instance()
|
||||
crm = ctx.cognitive_stack.get("crm")
|
||||
|
||||
xml_check = ctx.context_xml or ctx.device.dump_hierarchy()
|
||||
|
||||
f_node = telepathic.find_best_node(xml_check, "Followers count text or number", device=ctx.device)
|
||||
fg_node = telepathic.find_best_node(xml_check, "Following count text or number", device=ctx.device)
|
||||
bio_node = telepathic.find_best_node(xml_check, "User biography or description text", device=ctx.device)
|
||||
|
||||
scraped_data = {
|
||||
"username": ctx.username,
|
||||
"followers": f_node.get("text") if f_node else "unknown",
|
||||
"following": fg_node.get("text") if fg_node else "unknown",
|
||||
"bio": bio_node.get("text") if bio_node else "No bio",
|
||||
}
|
||||
|
||||
logger.info(
|
||||
f"✅ [Scraping] Data acquired: {scraped_data['followers']} followers, {scraped_data['following']} following."
|
||||
)
|
||||
|
||||
ctx.session_state.add_interaction(source=ctx.username, succeed=False, followed=False, scraped=True)
|
||||
|
||||
if crm:
|
||||
try:
|
||||
crm.enrich_lead(ctx.username, scraped_data)
|
||||
logger.info(f"💾 [CRM] Enriched lead @{ctx.username} in database.")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ [CRM] Failed to enrich lead @{ctx.username}: {e}")
|
||||
|
||||
# Return executed=True, but we don't return interactions=1 since it's just data extraction
|
||||
return BehaviorResult(executed=True)
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
|
||||
try:
|
||||
import psutil
|
||||
@@ -9,21 +10,6 @@ except ImportError:
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
|
||||
|
||||
def log_metabolic_rate():
|
||||
if psutil is None:
|
||||
logging.getLogger(__name__).debug("🧬 [Metabolism] psutil not installed. Skipping memory log.")
|
||||
return
|
||||
try:
|
||||
process = psutil.Process(os.getpid())
|
||||
mem_info = process.memory_info()
|
||||
logging.getLogger(__name__).info(
|
||||
f"🧬 [Metabolism] RSS: {mem_info.rss / 1024 / 1024:.2f} MB | VMS: {mem_info.vms / 1024 / 1024:.2f} MB"
|
||||
)
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).debug(f"🧬 [Metabolism] Failed to log memory: {e}")
|
||||
|
||||
|
||||
from colorama import Fore, Style
|
||||
|
||||
from GramAddict.core.account_switcher import verify_and_switch_account
|
||||
@@ -66,8 +52,6 @@ from GramAddict.core.physics.timing import (
|
||||
wait_for_story_loaded as _wait_for_story_loaded_impl,
|
||||
)
|
||||
from GramAddict.core.q_nav_graph import QNavGraph
|
||||
from GramAddict.core.qdrant_memory import ParasocialCRMDB
|
||||
from GramAddict.core.resonance_engine import ResonanceEngine
|
||||
from GramAddict.core.sensors.honeypot_radome import HoneypotRadome
|
||||
from GramAddict.core.session_state import SessionState, SessionStateEncoder
|
||||
from GramAddict.core.swarm_protocol import SwarmProtocol
|
||||
@@ -84,6 +68,21 @@ from GramAddict.core.utils import (
|
||||
)
|
||||
from GramAddict.core.zero_latency_engine import ZeroLatencyEngine
|
||||
|
||||
|
||||
def log_metabolic_rate():
|
||||
if psutil is None:
|
||||
logging.getLogger(__name__).debug("🧬 [Metabolism] psutil not installed. Skipping memory log.")
|
||||
return
|
||||
try:
|
||||
process = psutil.Process(os.getpid())
|
||||
mem_info = process.memory_info()
|
||||
logging.getLogger(__name__).info(
|
||||
f"🧬 [Metabolism] RSS: {mem_info.rss / 1024 / 1024:.2f} MB | VMS: {mem_info.vms / 1024 / 1024:.2f} MB"
|
||||
)
|
||||
except Exception as e:
|
||||
logging.getLogger(__name__).debug(f"🧬 [Metabolism] Failed to log memory: {e}")
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -94,9 +93,8 @@ def check_production_integrity():
|
||||
"""
|
||||
import sys
|
||||
|
||||
# If we are in a pytest session, we expect and allow mocks
|
||||
if "pytest" in sys.modules or "PYTEST_CURRENT_TEST" in os.environ:
|
||||
return
|
||||
# We no longer skip this in tests. Production integrity must hold everywhere.
|
||||
pass
|
||||
|
||||
try:
|
||||
from unittest.mock import MagicMock
|
||||
@@ -178,13 +176,27 @@ def start_bot(**kwargs):
|
||||
)
|
||||
persona_interests = [p.strip() for p in persona_raw.split(",") if p.strip()] if persona_raw else []
|
||||
|
||||
global_goal = getattr(configs.args, "goal", None)
|
||||
if global_goal:
|
||||
persona_interests.insert(0, global_goal)
|
||||
logger.info(
|
||||
f"🎯 [Autonomous Directive] Overriding target audience with high-level goal: {global_goal}",
|
||||
extra={"color": f"{Style.BRIGHT}{Fore.GREEN}"},
|
||||
)
|
||||
|
||||
from GramAddict.core.goap import GoalExecutor
|
||||
from GramAddict.core.interaction import LLMWriter
|
||||
from GramAddict.core.qdrant_memory import DMMemoryDB, ParasocialCRMDB
|
||||
from GramAddict.core.resonance_engine import ResonanceEngine
|
||||
|
||||
dopamine = DopamineEngine()
|
||||
crm_db = ParasocialCRMDB()
|
||||
dm_memory_db = DMMemoryDB()
|
||||
resonance_oracle = ResonanceEngine(username, persona_interests=persona_interests, crm=crm_db)
|
||||
writer = LLMWriter(username, persona_interests, configs)
|
||||
active_inference = ActiveInferenceEngine(username)
|
||||
|
||||
# Core Autonomous Engines
|
||||
from GramAddict.core.goap import GoalExecutor
|
||||
|
||||
GoalExecutor.get_instance(device, username)
|
||||
zero_engine = ZeroLatencyEngine(device)
|
||||
@@ -235,6 +247,8 @@ def start_bot(**kwargs):
|
||||
"telepathic": telepathic,
|
||||
"darwin": darwin,
|
||||
"crm": crm_db,
|
||||
"dm_memory": dm_memory_db,
|
||||
"writer": writer,
|
||||
}
|
||||
|
||||
from GramAddict.core.behaviors import PluginRegistry
|
||||
@@ -256,6 +270,7 @@ def start_bot(**kwargs):
|
||||
from GramAddict.core.behaviors.rabbit_hole import RabbitHolePlugin
|
||||
from GramAddict.core.behaviors.repost import RepostPlugin
|
||||
from GramAddict.core.behaviors.resonance_evaluator import ResonanceEvaluatorPlugin
|
||||
from GramAddict.core.behaviors.scrape_profile import ScrapeProfilePlugin
|
||||
from GramAddict.core.behaviors.story_view import StoryViewPlugin
|
||||
|
||||
PluginRegistry.reset()
|
||||
@@ -279,6 +294,7 @@ def start_bot(**kwargs):
|
||||
plugin_registry.register(CommentPlugin())
|
||||
plugin_registry.register(RepostPlugin())
|
||||
plugin_registry.register(PostInteractionPlugin())
|
||||
plugin_registry.register(ScrapeProfilePlugin())
|
||||
|
||||
cognitive_stack["plugin_registry"] = plugin_registry
|
||||
|
||||
@@ -290,7 +306,26 @@ def start_bot(**kwargs):
|
||||
cognitive_stack["dojo"] = dojo
|
||||
|
||||
try:
|
||||
bot_start_time = datetime.now()
|
||||
max_runtime = getattr(configs.args, "max_runtime_minutes", None)
|
||||
|
||||
dopamine.global_start_time = bot_start_time
|
||||
dopamine.global_max_runtime_minutes = max_runtime
|
||||
GoalExecutor.global_start_time = bot_start_time
|
||||
GoalExecutor.global_max_runtime_minutes = max_runtime
|
||||
|
||||
while True:
|
||||
if max_runtime:
|
||||
from datetime import timedelta
|
||||
|
||||
elapsed = datetime.now() - bot_start_time
|
||||
if elapsed > timedelta(minutes=max_runtime):
|
||||
logger.info(
|
||||
f"🛑 [Timeout] Maximum runtime of {max_runtime} minutes reached. Stopping bot.",
|
||||
extra={"color": f"{Fore.RED}"},
|
||||
)
|
||||
break
|
||||
|
||||
set_time_delta(configs.args)
|
||||
inside_working_hours, time_left = SessionState.inside_working_hours(
|
||||
configs.args.working_hours, configs.args.time_delta_session
|
||||
@@ -341,9 +376,7 @@ def start_bot(**kwargs):
|
||||
logger.info(
|
||||
f"🧠 [Agent Orchestrator] Session started. Strategy: {growth_brain.strategy} | Persona: {getattr(configs.args, 'agent_persona', 'unknown')}"
|
||||
)
|
||||
|
||||
from GramAddict.core.goap import GoalExecutor
|
||||
|
||||
# 1. Starten wir den GOAP Executor, um die UI-Struktur autonom zu erfassen
|
||||
goap = GoalExecutor.get_instance(device, username)
|
||||
|
||||
# --- PHASE 0: Autonomous Profile Scanning ---
|
||||
@@ -411,8 +444,8 @@ def start_bot(**kwargs):
|
||||
|
||||
from GramAddict.core.llm_provider import query_llm
|
||||
|
||||
model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b")
|
||||
url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate")
|
||||
model = getattr(configs.args, "ai_condenser_model")
|
||||
url = getattr(configs.args, "ai_condenser_url")
|
||||
|
||||
response_dict = query_llm(url=url, model=model, prompt=prompt, format_json=True, timeout=120)
|
||||
if response_dict and isinstance(response_dict, dict) and "persona" in response_dict:
|
||||
@@ -439,31 +472,68 @@ def start_bot(**kwargs):
|
||||
has_scanned_own_profile = True
|
||||
|
||||
while not dopamine.is_app_session_over():
|
||||
# 1. Ask the Growth Brain for a Desire
|
||||
current_desire = growth_brain.get_current_desire(dopamine)
|
||||
# ── 1. Generate available tasks from mission + plugins ──
|
||||
from GramAddict.core.goal_decomposer import GoalDecomposer
|
||||
|
||||
if current_desire == "ShiftContext":
|
||||
logger.info("🧠 [Free Will] Boredom critical. Forcing app restart to clear context.")
|
||||
device.app_stop(device.app_id)
|
||||
random_sleep(2.0, 4.0)
|
||||
device.app_start(device.app_id, use_monkey=True)
|
||||
random_sleep(4.0, 6.0)
|
||||
dopamine.boredom = max(0.0, dopamine.boredom * 0.2)
|
||||
continue
|
||||
decomposer = GoalDecomposer(
|
||||
plugins=configs.config.get("plugins", {}) if configs.config else {},
|
||||
actions={
|
||||
k: getattr(configs.args, k, None)
|
||||
for k in ("feed", "explore", "reels")
|
||||
if getattr(configs.args, k, None)
|
||||
},
|
||||
mission=configs.config.get("mission", {}) if configs.config else {},
|
||||
)
|
||||
available_tasks = decomposer.generate_tasks()
|
||||
|
||||
# 2. Map Desire to Sub-Feed
|
||||
target_map = {
|
||||
"DiscoverNewContent": ["ExploreFeed", "ReelsFeed"],
|
||||
"NurtureCommunity": ["HomeFeed", "StoriesFeed"],
|
||||
"SocialReciprocity": ["FollowingList", "MessageInbox"],
|
||||
}
|
||||
if not available_tasks:
|
||||
# No plugins enabled = nothing to do. Fall back to legacy desire system.
|
||||
current_desire = growth_brain.get_current_desire(dopamine)
|
||||
if current_desire == "ShiftContext":
|
||||
logger.info("🧠 [Free Will] Boredom critical. Forcing app restart.")
|
||||
device.app_stop(device.app_id)
|
||||
random_sleep(2.0, 4.0)
|
||||
device.app_start(device.app_id, use_monkey=True)
|
||||
random_sleep(4.0, 6.0)
|
||||
dopamine.boredom = max(0.0, dopamine.boredom * 0.2)
|
||||
continue
|
||||
|
||||
import secrets
|
||||
# Legacy desire → target mapping (kept for backward compatibility)
|
||||
target_map = {
|
||||
"DiscoverNewContent": ["ExploreFeed", "ReelsFeed"],
|
||||
"NurtureCommunity": ["HomeFeed", "StoriesFeed"],
|
||||
"SocialReciprocity": ["FollowingList"],
|
||||
}
|
||||
|
||||
options = target_map.get(current_desire, ["HomeFeed"])
|
||||
current_target = secrets.choice(options)
|
||||
dm_config = configs.get_plugin_config("dm_reply")
|
||||
if dm_config.get("enabled", False):
|
||||
target_map["SocialReciprocity"].append("MessageInbox")
|
||||
|
||||
logger.info(f"🧠 [Agent Orchestrator] Desire '{current_desire}' -> Routed to {current_target}")
|
||||
import secrets
|
||||
|
||||
options = target_map.get(current_desire, ["HomeFeed"])
|
||||
current_target = secrets.choice(options)
|
||||
else:
|
||||
# ── 2. Select a concrete Task ──
|
||||
selected_task = growth_brain.select_task(dopamine, available_tasks)
|
||||
|
||||
if selected_task is None:
|
||||
# ShiftContext signal from high boredom
|
||||
logger.info("🧠 [Free Will] Boredom critical. Forcing app restart to clear context.")
|
||||
device.app_stop(device.app_id)
|
||||
random_sleep(2.0, 4.0)
|
||||
device.app_start(device.app_id, use_monkey=True)
|
||||
random_sleep(4.0, 6.0)
|
||||
dopamine.boredom = max(0.0, dopamine.boredom * 0.2)
|
||||
continue
|
||||
|
||||
current_target = selected_task.target_screen
|
||||
logger.info(
|
||||
f"🎯 [GoalDecomposer] Task: {selected_task.intent} "
|
||||
f"→ {current_target} (budget={selected_task.budget_posts})"
|
||||
)
|
||||
|
||||
logger.info(f"🧠 [Agent Orchestrator] Routed to {current_target}")
|
||||
|
||||
logger.info(f"⚡ Navigating to {current_target}")
|
||||
success = nav_graph.navigate_to(current_target, zero_engine)
|
||||
@@ -488,7 +558,9 @@ def start_bot(**kwargs):
|
||||
continue
|
||||
elif current_target == "StoriesFeed":
|
||||
logger.info("📱 Locating story tray on HomeFeed...")
|
||||
nav_graph.do("tap story ring avatar")
|
||||
if not nav_graph.do("tap story ring avatar"):
|
||||
logger.warning("❌ Failed to tap story ring avatar. Retrying next loop.")
|
||||
continue
|
||||
post_loaded = _wait_for_story_loaded(device, timeout=5)
|
||||
if not post_loaded:
|
||||
logger.warning("❌ Stories failed to open from HomeFeed. Retrying next loop.")
|
||||
@@ -613,6 +685,7 @@ def _interact_with_profile(device, configs, username, session_state, sleep_mod,
|
||||
|
||||
if cognitive_stack is None:
|
||||
cognitive_stack = {}
|
||||
_validate_cognitive_stack(cognitive_stack, "ProfileInteraction")
|
||||
|
||||
if hasattr(session_state, "my_username") and username == session_state.my_username:
|
||||
logger.info(f"🤝 [Deep Interaction] Skipping own profile @{username} to prevent self-interactions.")
|
||||
@@ -642,7 +715,7 @@ def _interact_with_profile(device, configs, username, session_state, sleep_mod,
|
||||
return
|
||||
|
||||
if getattr(configs.args, "ignore_close_friends", False):
|
||||
if "enge freunde" in xml_check_lower or "close friend" in xml_check_lower:
|
||||
if "close friend" in xml_check_lower:
|
||||
logger.info(
|
||||
f"💚 [Profile Guard] @{username} is a Close Friend. Ignoring completely.", extra={"color": "\\033[32m"}
|
||||
)
|
||||
@@ -752,6 +825,7 @@ def _run_zero_latency_stories_loop(device, configs, session_state, cognitive_sta
|
||||
|
||||
from colorama import Fore
|
||||
|
||||
_validate_cognitive_stack(cognitive_stack, "StoriesLoop")
|
||||
logger.info("🎬 [StoriesFeed] Starting native story binging loop...", extra={"color": f"{Fore.CYAN}"})
|
||||
|
||||
dopamine = cognitive_stack.get("dopamine")
|
||||
@@ -786,8 +860,22 @@ def _run_zero_latency_stories_loop(device, configs, session_state, cognitive_sta
|
||||
logger.warning("Failed to dump UI hierarchy in StoriesFeed.")
|
||||
return "CONTEXT_LOST"
|
||||
|
||||
# ── Perimeter Guard: Verify we're still inside Instagram ──
|
||||
# Production bug 2026-05-03: A story's swipe-up link opened the Play Store,
|
||||
# and the loop kept tapping blindly on com.android.vending for 5+ iterations.
|
||||
packages = set(re.findall(r'package="([^"]+)"', xml_dump))
|
||||
app_id = getattr(device, "app_id", "com.instagram.android")
|
||||
if packages and app_id not in packages:
|
||||
logger.error(
|
||||
f"🚨 [StoriesFeed] FOREIGN APP DETECTED! Packages: {packages}. "
|
||||
f"A story link likely opened an external app. Aborting loop."
|
||||
)
|
||||
device.press("back")
|
||||
sleep(1.5)
|
||||
return "CONTEXT_LOST"
|
||||
|
||||
if getattr(configs.args, "ignore_close_friends", False):
|
||||
if "enge freunde" in xml_dump.lower() or "close friend" in xml_dump.lower():
|
||||
if "close friend" in xml_dump.lower():
|
||||
logger.info(
|
||||
"💚 [Anti-Friend] Story is from a Close Friend. Swiping horizontally to skip User.",
|
||||
extra={"color": "\\033[32m"},
|
||||
@@ -807,6 +895,36 @@ def _run_zero_latency_stories_loop(device, configs, session_state, cognitive_sta
|
||||
return "FEED_EXHAUSTED"
|
||||
|
||||
|
||||
def _validate_cognitive_stack(cognitive_stack, context_name):
|
||||
"""
|
||||
Validates that the cognitive stack has all required engine dependencies
|
||||
injected properly. This is the ultimate zero-trust guard for plugins.
|
||||
"""
|
||||
if not isinstance(cognitive_stack, dict):
|
||||
raise TypeError(f"[{context_name}] CognitiveStack must be a dict, got {type(cognitive_stack)}")
|
||||
|
||||
required_engines = [
|
||||
"dopamine",
|
||||
"darwin",
|
||||
"resonance",
|
||||
"active_inference",
|
||||
"growth_brain",
|
||||
"swarm",
|
||||
"writer",
|
||||
"nav_graph",
|
||||
"zero_engine",
|
||||
"telepathic",
|
||||
]
|
||||
|
||||
missing = [eng for eng in required_engines if eng not in cognitive_stack or cognitive_stack[eng] is None]
|
||||
if missing:
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"🚨 [{context_name}] CognitiveStack missing required engines: {missing}")
|
||||
raise ValueError(f"[{context_name}] CognitiveStack missing required engines: {missing}")
|
||||
|
||||
|
||||
def _run_zero_latency_feed_loop(
|
||||
device, zero_engine, nav_graph, configs, session_state, job_target, cognitive_stack, is_reels=False
|
||||
):
|
||||
@@ -820,6 +938,7 @@ def _run_zero_latency_feed_loop(
|
||||
- Darwin is the SOLE dwell controller → no duplicate sleep calls
|
||||
- SwarmProtocol emits pheromones after successful interactions
|
||||
"""
|
||||
_validate_cognitive_stack(cognitive_stack, "FeedLoop")
|
||||
logger.info(f"🔄 Entering Zero-Latency Interaction Pool. Feed: {job_target}")
|
||||
|
||||
dopamine = cognitive_stack.get("dopamine")
|
||||
@@ -855,7 +974,19 @@ def _run_zero_latency_feed_loop(
|
||||
|
||||
elif governance_decision == "CHECK_CURIOSITY":
|
||||
logger.info("👀 [Curiosity] Spontaneously checking DMs / Notifications...")
|
||||
explore_target = random.choice(["MessageInbox", "Notifications"])
|
||||
|
||||
# 🛡️ Structural Guard: Curiosity targets (DMs, Notifications) are ONLY available on HomeFeed.
|
||||
# We must navigate there first, breaking current context.
|
||||
if not nav_graph.navigate_to("HomeFeed", zero_engine):
|
||||
logger.warning("❌ [Curiosity] Failed to navigate to HomeFeed. Aborting curiosity check.")
|
||||
continue
|
||||
sleep(random.uniform(1.0, 2.5))
|
||||
|
||||
dm_config = configs.get_plugin_config("dm_reply")
|
||||
if dm_config.get("enabled", False):
|
||||
explore_target = random.choice(["MessageInbox", "Notifications"])
|
||||
else:
|
||||
explore_target = "Notifications"
|
||||
|
||||
if explore_target == "MessageInbox":
|
||||
nav_graph.do("tap direct message icon inbox")
|
||||
@@ -887,6 +1018,17 @@ def _run_zero_latency_feed_loop(
|
||||
if cognitive_stack.get("radome"):
|
||||
context_xml = cognitive_stack.get("radome").sanitize_xml(context_xml)
|
||||
|
||||
# ── Perimeter Guard: Verify we're still inside Instagram ──
|
||||
# Parity with story loop guard (production bug 2026-05-03).
|
||||
if context_xml:
|
||||
feed_packages = set(re.findall(r'package="([^"]+)"', context_xml))
|
||||
feed_app_id = getattr(device, "app_id", "com.instagram.android")
|
||||
if feed_packages and feed_app_id not in feed_packages:
|
||||
logger.error(f"🚨 [FeedLoop] FOREIGN APP DETECTED! Packages: {feed_packages}. Aborting loop.")
|
||||
device.press("back")
|
||||
sleep(1.5)
|
||||
return "CONTEXT_LOST"
|
||||
|
||||
# ── Execute Plugin Registry Behaviors (Feed Level) ──
|
||||
from GramAddict.core.behaviors import BehaviorContext, PluginRegistry
|
||||
|
||||
@@ -946,6 +1088,7 @@ def _run_zero_latency_search_loop(
|
||||
"""
|
||||
Executes the autonomous Search & Interact logic.
|
||||
"""
|
||||
_validate_cognitive_stack(cognitive_stack, "SearchLoop")
|
||||
logger.info("🧠 [Search Engine] Initiating keyword discovery...", extra={"color": f"{Style.BRIGHT}{Fore.CYAN}"})
|
||||
|
||||
import random
|
||||
@@ -971,6 +1114,16 @@ def _run_zero_latency_search_loop(
|
||||
xml = device.dump_hierarchy()
|
||||
telepathic = cognitive_stack.get("telepathic")
|
||||
|
||||
# ── Perimeter Guard: Verify we're still inside Instagram ──
|
||||
if xml:
|
||||
search_packages = set(re.findall(r'package="([^"]+)"', xml))
|
||||
search_app_id = getattr(device, "app_id", "com.instagram.android")
|
||||
if search_packages and search_app_id not in search_packages:
|
||||
logger.error(f"🚨 [SearchLoop] FOREIGN APP DETECTED! Packages: {search_packages}. Aborting loop.")
|
||||
device.press("back")
|
||||
sleep(1.5)
|
||||
return "CONTEXT_LOST"
|
||||
|
||||
# Find search bar
|
||||
search_bar = telepathic.find_best_node(xml, "Search edit text box or magnifying glass input", device=device)
|
||||
if search_bar:
|
||||
|
||||
@@ -30,13 +30,13 @@ class VLMCompilerEngine:
|
||||
extra={"color": "\x1b[1m\x1b[35m"},
|
||||
)
|
||||
|
||||
args = getattr(self.device, "args", None)
|
||||
model = getattr(args, "ai_telepathic_model", "llama3.2:1b") if args else "llama3.2:1b"
|
||||
url = (
|
||||
getattr(args, "ai_telepathic_url", "http://localhost:11434/api/generate")
|
||||
if args
|
||||
else "http://localhost:11434/api/generate"
|
||||
)
|
||||
from GramAddict.core.config import Config
|
||||
|
||||
cfg = Config()
|
||||
if not hasattr(cfg, "args"):
|
||||
raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.")
|
||||
model = getattr(cfg.args, "ai_telepathic_model")
|
||||
url = getattr(cfg.args, "ai_telepathic_url")
|
||||
use_local = "11434" in url or "localhost" in url
|
||||
|
||||
simplified_xml = self._simplify_xml(context_xml)
|
||||
|
||||
@@ -17,14 +17,12 @@ class Config:
|
||||
self.args = kwargs
|
||||
self.module = True
|
||||
else:
|
||||
# Avoid parsing sys.argv if we are running in a test environment (pytest)
|
||||
# as pytest arguments will cause argparse to fail with SystemExit: 2
|
||||
is_pytest = "pytest" in sys.modules
|
||||
if is_pytest:
|
||||
self.args = []
|
||||
else:
|
||||
self.args = sys.argv
|
||||
self.args = list(sys.argv)
|
||||
self.module = False
|
||||
|
||||
if not self.module and "--config" not in self.args:
|
||||
if os.path.exists("config.yml"):
|
||||
self.args.extend(["--config", "config.yml"])
|
||||
self.config = None
|
||||
self.config_list = None
|
||||
self.actions = {}
|
||||
@@ -81,6 +79,9 @@ class Config:
|
||||
self.username = self.username[0]
|
||||
self.debug = self.config.get("debug", False)
|
||||
self.app_id = self.config.get("app_id", "com.instagram.android")
|
||||
|
||||
# Autonomous goals removed — the bot now derives tasks from mission + plugins
|
||||
# via GoalDecomposer. See GramAddict/core/goal_decomposer.py.
|
||||
else:
|
||||
if "--debug" in self.args:
|
||||
self.debug = True
|
||||
@@ -137,12 +138,22 @@ class Config:
|
||||
self.parser.add_argument("--total-sessions", help="Total amount of sessions", default="-1")
|
||||
self.parser.add_argument("--working-hours", help="Working hours", default=None)
|
||||
self.parser.add_argument("--time-delta-session", help="Time delta between sessions", default=None)
|
||||
self.parser.add_argument(
|
||||
"--max-runtime-minutes", type=int, help="Maximum runtime in minutes before bot auto-exits", default=None
|
||||
)
|
||||
self.parser.add_argument("--restart-atx-agent", action="store_true", help="Restart atx agent")
|
||||
self.parser.add_argument("--allow-untested-ig-version", action="store_true", help="Allow untested IG version")
|
||||
self.parser.add_argument(
|
||||
"--blank-start",
|
||||
action="store_true",
|
||||
help="Wipe all learned navigation and telepathic memories on boot to start 100% blank.",
|
||||
help="Wipe all learned navigation and telepathic memories on boot to start 100%% blank.",
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
"--goal",
|
||||
type=str,
|
||||
help="High-level autonomous goal for the bot (Tesla-style). Overrides config.yml goals.",
|
||||
default=None,
|
||||
)
|
||||
|
||||
# Interaction settings
|
||||
@@ -308,7 +319,7 @@ class Config:
|
||||
logger.debug(f"Arguments used: {' '.join(sys.argv[1:])}")
|
||||
if self.config:
|
||||
logger.debug(f"Config used: {self.config}")
|
||||
if len(sys.argv) <= 1:
|
||||
if len(sys.argv) <= 1 and not self.config:
|
||||
self.parser.print_help()
|
||||
exit(0)
|
||||
if self.config:
|
||||
|
||||
@@ -84,7 +84,6 @@ class DarwinEngine(QdrantBase):
|
||||
resonance: float,
|
||||
text_length: int = 0,
|
||||
nav_graph=None,
|
||||
zero_engine=None,
|
||||
configs=None,
|
||||
resonance_oracle=None,
|
||||
username=None,
|
||||
@@ -142,12 +141,22 @@ class DarwinEngine(QdrantBase):
|
||||
cy = h // 2
|
||||
|
||||
dur_ms = int(random.uniform(200, 500))
|
||||
device.shell(f"input swipe {int(cx)} {int(cy)} {int(cx + noise_x)} {int(cy + slip_distance)} {dur_ms}")
|
||||
|
||||
# Use physics-based injector instead of algorithmic 'input swipe'
|
||||
body = PhysicsBody.get_session_instance(device)
|
||||
injector = SendEventInjector.get_instance(device)
|
||||
start_pt = (int(cx), int(cy))
|
||||
end_pt = (int(cx + noise_x), int(cy + slip_distance))
|
||||
|
||||
points = BezierGesture.scroll_curve(start_pt, end_pt, body, n_points=5)
|
||||
timing = BezierGesture.compute_sigmoid_timing(len(points), dur_ms)
|
||||
injector.inject_gesture(points, timing, touch_major=body.get_touch_major())
|
||||
|
||||
time.sleep(random.uniform(0.5, 1.2))
|
||||
|
||||
# 4. Comment depth simulation (probabilistic & resonance-correlated)
|
||||
if profile["comment_read_dwell"] > 1.0 and resonance > 0.4 and random.random() < 0.3:
|
||||
if nav_graph and zero_engine:
|
||||
if nav_graph:
|
||||
if not self._has_comments(context_xml):
|
||||
logger.debug(" -> 🚫 [Darwin Engine] Skipping comment depth simulation (Post has 0 comments).")
|
||||
else:
|
||||
@@ -334,27 +343,34 @@ class DarwinEngine(QdrantBase):
|
||||
"""
|
||||
Heuristic to check if a post actually has comments to read.
|
||||
If it has 0 comments, checking them is suspicious bot behavior.
|
||||
|
||||
Zero-Maintenance: Uses only English text and resource_id patterns.
|
||||
Resource IDs are locale-invariant. English text in content_desc
|
||||
is used by Instagram internally and is reliable.
|
||||
"""
|
||||
low_xml = xml_string.lower()
|
||||
|
||||
# 1. Explicit zero comments checks
|
||||
if re.search(r"\b0\s*kommentare?\b", low_xml) or re.search(r"\b0\s*comment(?:s)?\b", low_xml):
|
||||
# 1. Explicit zero comments check (resource_id based + English fallback)
|
||||
if re.search(r"\b0\s*comment(?:s)?\b", low_xml):
|
||||
return False
|
||||
|
||||
# 2. Check for "view all" or similar prominent comment link texts
|
||||
if "view all" in low_xml or ("alle " in low_xml and "kommentare ansehen" in low_xml):
|
||||
if "view all" in low_xml:
|
||||
return True
|
||||
if "view 1 comment" in low_xml or "1 kommentar ansehen" in low_xml:
|
||||
if "view 1 comment" in low_xml:
|
||||
return True
|
||||
if "comment number is" in low_xml:
|
||||
return True
|
||||
|
||||
# 3. Check for specific counter elements > 0 in content descriptors
|
||||
# e.g. "by username, 23 comments" or "1,234 comments"
|
||||
has_number_of_comments = re.search(r"\b([1-9][0-9.,]*)\s*(?:comment(?:s)?|kommentare?)\b", low_xml)
|
||||
# 3. Structural: comment_textview_layout is present with a count > 0
|
||||
has_number_of_comments = re.search(r"\b([1-9][0-9.,]*)\s*comment(?:s)?\b", low_xml)
|
||||
if has_number_of_comments:
|
||||
return True
|
||||
|
||||
# 4. Structural: The comment button resource_id exists and has content
|
||||
if "row_feed_comment_textview_layout" in low_xml:
|
||||
return True
|
||||
|
||||
# If no indicators are found, assume the post has 0 comments.
|
||||
# The comment button exists, but there are no comments to read.
|
||||
return False
|
||||
|
||||
|
||||
@@ -36,15 +36,38 @@ def create_device(device_id, app_id, args=None):
|
||||
try:
|
||||
return DeviceFacade(device_id, app_id, args)
|
||||
except Exception as e:
|
||||
str(e)
|
||||
err_msg = str(e)
|
||||
err_type = str(type(e))
|
||||
if (
|
||||
"ConnectError" in err_type
|
||||
or "ConnectionRefusedError" in err_type
|
||||
or "ConnectionError" in err_type
|
||||
or "Timeout" in err_type
|
||||
if any(
|
||||
keyword in err_type or keyword in err_msg
|
||||
for keyword in ["ConnectError", "ConnectionRefused", "ConnectionError", "Timeout"]
|
||||
):
|
||||
logger.error(f"⚠️ [ADB ConnectError] Could not connect to device '{device_id}'.")
|
||||
|
||||
# Proactive Discovery
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(["adb", "devices"], capture_output=True, text=True, timeout=2)
|
||||
lines = [
|
||||
line.strip()
|
||||
for line in result.stdout.split("\n")
|
||||
if line.strip() and not line.startswith("List of devices")
|
||||
]
|
||||
devices = [line.split("\t")[0] for line in lines if "device" in line]
|
||||
|
||||
if devices:
|
||||
logger.info("🔍 Proactive Discovery: I found the following devices connected:")
|
||||
for d in devices:
|
||||
if d.split(":")[0] == device_id.split(":")[0]:
|
||||
logger.info(f" 👉 {d} (MATCHING IP - Is this the same device with a different port?)")
|
||||
else:
|
||||
logger.info(f" - {d}")
|
||||
else:
|
||||
logger.warning("🔍 Proactive Discovery: No ADB devices found. Is your phone authorized?")
|
||||
except Exception as discovery_err:
|
||||
logger.debug(f"Proactive discovery failed: {discovery_err}")
|
||||
|
||||
logger.error("👉 Please verify:")
|
||||
logger.error(" 1. Your phone is connected via USB or Wi-Fi.")
|
||||
logger.error(" 2. 'USB Debugging' is enabled in Developer Options.")
|
||||
@@ -158,6 +181,10 @@ class DeviceFacade:
|
||||
def press(self, key):
|
||||
self.deviceV2.press(key)
|
||||
|
||||
@adb_retry()
|
||||
def back(self):
|
||||
self.deviceV2.press("back")
|
||||
|
||||
@adb_retry()
|
||||
def click(self, x=None, y=None, obj=None):
|
||||
if obj:
|
||||
@@ -299,19 +326,51 @@ class DeviceFacade:
|
||||
xml = self.deviceV2.dump_hierarchy(compressed=True)
|
||||
|
||||
# Continuous Session Tracing
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
traces_root = os.path.join("debug", "session_traces")
|
||||
if not hasattr(self, "_trace_counter"):
|
||||
self._trace_counter = 0
|
||||
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
self._trace_dir = os.path.join("debug", "session_traces", ts)
|
||||
self._trace_dir = os.path.join(traces_root, ts)
|
||||
os.makedirs(self._trace_dir, exist_ok=True)
|
||||
|
||||
# Cleanup: keep only last 5 session folders
|
||||
try:
|
||||
if os.path.exists(traces_root):
|
||||
folders = [
|
||||
os.path.join(traces_root, d)
|
||||
for d in os.listdir(traces_root)
|
||||
if os.path.isdir(os.path.join(traces_root, d))
|
||||
]
|
||||
folders.sort(key=os.path.getmtime)
|
||||
while len(folders) > 5:
|
||||
oldest = folders.pop(0)
|
||||
shutil.rmtree(oldest, ignore_errors=True)
|
||||
logger.info(f"🧹 [Cleanup] Removed old session trace: {oldest}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to cleanup old traces: {e}")
|
||||
|
||||
self._trace_counter += 1
|
||||
trace_path = os.path.join(self._trace_dir, f"{self._trace_counter:05d}.xml")
|
||||
with open(trace_path, "w", encoding="utf-8") as f:
|
||||
f.write(xml)
|
||||
|
||||
# Dump screenshot as well
|
||||
try:
|
||||
import base64
|
||||
|
||||
screenshot_b64 = self.get_screenshot_b64()
|
||||
if screenshot_b64:
|
||||
screenshot_data = base64.b64decode(screenshot_b64)
|
||||
screenshot_path = trace_path.replace(".xml", ".jpg")
|
||||
with open(screenshot_path, "wb") as f:
|
||||
f.write(screenshot_data)
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to capture screenshot for session trace: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to write session trace: {e}")
|
||||
|
||||
@@ -323,6 +382,8 @@ class DeviceFacade:
|
||||
from io import BytesIO
|
||||
|
||||
img = self.deviceV2.screenshot()
|
||||
if img is None:
|
||||
return None
|
||||
buffered = BytesIO()
|
||||
img.save(buffered, format="JPEG", quality=70) # Compressed for target latency
|
||||
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
|
||||
@@ -18,19 +18,12 @@ from datetime import datetime
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DUMP_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "debug", "xml_dumps")
|
||||
MAX_DUMPS_PER_CATEGORY = 50
|
||||
MAX_DUMPS_PER_CATEGORY = 5
|
||||
|
||||
|
||||
def dump_ui_state(device, reason: str, extra_context: dict = None):
|
||||
"""
|
||||
Capture and save the current UI hierarchy to disk for debugging.
|
||||
|
||||
Args:
|
||||
device: The uiautomator2 device facade.
|
||||
reason: Short tag for the failure type. Used for filename grouping.
|
||||
Examples: 'context_lost', 'vlm_hallucination', 'nav_failure',
|
||||
'stuck_on_post', 'unexpected_screen'
|
||||
extra_context: Optional dict with additional metadata (intent, expected state, etc.)
|
||||
Capture and save the current UI hierarchy and screenshot to disk for debugging.
|
||||
"""
|
||||
try:
|
||||
os.makedirs(DUMP_DIR, exist_ok=True)
|
||||
@@ -48,11 +41,25 @@ def dump_ui_state(device, reason: str, extra_context: dict = None):
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(xml)
|
||||
|
||||
# Capture and write screenshot
|
||||
try:
|
||||
import base64
|
||||
|
||||
screenshot_b64 = device.get_screenshot_b64()
|
||||
if screenshot_b64:
|
||||
screenshot_data = base64.b64decode(screenshot_b64)
|
||||
screenshot_path = filepath.replace(".xml", ".jpg")
|
||||
with open(screenshot_path, "wb") as f:
|
||||
f.write(screenshot_data)
|
||||
except Exception as e:
|
||||
logger.debug(f"[Diagnostic] Could not capture screenshot: {e}")
|
||||
|
||||
# Write companion metadata JSON
|
||||
meta = {
|
||||
"reason": reason,
|
||||
"timestamp": ts,
|
||||
"xml_file": filename,
|
||||
"screenshot_file": filename.replace(".xml", ".jpg"),
|
||||
}
|
||||
# Capture the session log if available
|
||||
try:
|
||||
@@ -77,7 +84,7 @@ def dump_ui_state(device, reason: str, extra_context: dict = None):
|
||||
with open(meta_path, "w", encoding="utf-8") as f:
|
||||
json.dump(meta, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.info(f"📸 [Diagnostic] UI state and session log dumped for '{reason}': {filepath}")
|
||||
logger.info(f"📸 [Diagnostic] UI state, screenshot, and session log dumped for '{reason}': {filepath}")
|
||||
|
||||
# Rotate old dumps for this category
|
||||
_rotate_dumps(safe_reason)
|
||||
@@ -90,18 +97,50 @@ def dump_ui_state(device, reason: str, extra_context: dict = None):
|
||||
return None
|
||||
|
||||
|
||||
def _rotate_dumps(category_prefix: str):
|
||||
"""Keep only the last MAX_DUMPS_PER_CATEGORY dumps per category."""
|
||||
def _rotate_dumps(category_prefix: str = None):
|
||||
"""Keep only the last MAX_DUMPS_PER_CATEGORY dumps per category. If no category, cleans all."""
|
||||
try:
|
||||
all_files = sorted([f for f in os.listdir(DUMP_DIR) if f.startswith(category_prefix) and f.endswith(".xml")])
|
||||
if not os.path.exists(DUMP_DIR):
|
||||
return
|
||||
|
||||
if len(all_files) > MAX_DUMPS_PER_CATEGORY:
|
||||
files_to_remove = all_files[: len(all_files) - MAX_DUMPS_PER_CATEGORY]
|
||||
for f in files_to_remove:
|
||||
xml_path = os.path.join(DUMP_DIR, f)
|
||||
meta_path = xml_path.replace(".xml", ".meta.json")
|
||||
os.remove(xml_path)
|
||||
if os.path.exists(meta_path):
|
||||
os.remove(meta_path)
|
||||
except Exception:
|
||||
pass
|
||||
# Get all unique timestamps/prefixes
|
||||
all_files = os.listdir(DUMP_DIR)
|
||||
prefixes = set()
|
||||
for f in all_files:
|
||||
# Format is usually reason__timestamp.ext
|
||||
if "__" in f:
|
||||
prefix = f.split(".")[0]
|
||||
prefixes.add(prefix)
|
||||
|
||||
# Group prefixes by category
|
||||
categories = {}
|
||||
for p in prefixes:
|
||||
parts = p.split("__")
|
||||
if len(parts) >= 2:
|
||||
cat = parts[0]
|
||||
if cat not in categories:
|
||||
categories[cat] = []
|
||||
categories[cat].append(p)
|
||||
|
||||
for cat, prefs in categories.items():
|
||||
if category_prefix and cat != category_prefix:
|
||||
continue
|
||||
|
||||
prefs.sort() # chronological
|
||||
if len(prefs) > MAX_DUMPS_PER_CATEGORY:
|
||||
prefs_to_remove = prefs[: len(prefs) - MAX_DUMPS_PER_CATEGORY]
|
||||
for p_rm in prefs_to_remove:
|
||||
for ext in [".xml", ".jpg", ".log", ".meta.json"]:
|
||||
fp = os.path.join(DUMP_DIR, p_rm + ext)
|
||||
if os.path.exists(fp):
|
||||
os.remove(fp)
|
||||
|
||||
# Also clean orphaned files that don't match any known prefix pattern
|
||||
for f in all_files:
|
||||
if "__" not in f:
|
||||
fp = os.path.join(DUMP_DIR, f)
|
||||
if os.path.isfile(fp):
|
||||
os.remove(fp)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"[Diagnostic] Error during dump rotation: {e}")
|
||||
|
||||
@@ -7,12 +7,53 @@ from GramAddict.core.session_state import SessionState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Hard cap: maximum DM replies per inbox visit to prevent spam.
|
||||
MAX_REPLIES_PER_INBOX_VISIT = 3
|
||||
|
||||
# Sentinel values that indicate missing message context.
|
||||
_EMPTY_CONTEXT_SENTINELS = frozenset({"no previous context", "", "none", "n/a"})
|
||||
|
||||
|
||||
def _is_send_button(node: dict) -> bool:
|
||||
"""
|
||||
Structural verification: returns True if the node is identified as a Send button.
|
||||
NO hardcoded UI strings (no localized 'send' or 'absenden').
|
||||
"""
|
||||
rid = (node.get("id") or node.get("resource_id", "")).lower()
|
||||
|
||||
# 1. Structural Resource IDs (Language agnostic)
|
||||
if "send" in rid or "composer_button" in rid or "direct_send_button_text" in rid:
|
||||
return True
|
||||
|
||||
# 2. Spatial Guard: The send button in a DM thread is ALWAYS on the far right side of the screen
|
||||
# next to the composer input.
|
||||
x = node.get("x", 0)
|
||||
if int(x) > (1080 * 0.75):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_state, current_target, cognitive_stack):
|
||||
"""
|
||||
Executes the autonomous Direct Messaging logic in the Zero-Latency architecture.
|
||||
Assumes the bot is already at the "MessageInbox" UI state.
|
||||
|
||||
Safety guarantees:
|
||||
- Refuses to execute if dm_reply plugin is disabled in config.
|
||||
- Skips threads with no extractable text context.
|
||||
- Structurally verifies the Send button before logging success.
|
||||
- Hard-caps replies per inbox visit to MAX_REPLIES_PER_INBOX_VISIT.
|
||||
"""
|
||||
# ── Kill-Switch: Respect dm_reply.enabled config ──
|
||||
dm_plugin_config = configs.get_plugin_config("dm_reply")
|
||||
if not dm_plugin_config.get("enabled", False):
|
||||
logger.warning(
|
||||
"🛑 [DM Engine] dm_reply plugin is DISABLED in config. Refusing to process inbox.",
|
||||
extra={"color": f"{Fore.RED}"},
|
||||
)
|
||||
return "BOREDOM_CHANGE_FEED"
|
||||
|
||||
logger.info(
|
||||
f"🧠 [DM Engine] Initiating inbox processing in {current_target}...",
|
||||
extra={"color": f"{Style.BRIGHT}{Fore.CYAN}"},
|
||||
@@ -20,7 +61,6 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s
|
||||
|
||||
telepathic = cognitive_stack.get("telepathic")
|
||||
dopamine = cognitive_stack.get("dopamine")
|
||||
crm = cognitive_stack.get("crm")
|
||||
|
||||
from GramAddict.core.bot_flow import _humanized_click, sleep
|
||||
from GramAddict.core.llm_provider import query_llm
|
||||
@@ -31,6 +71,7 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s
|
||||
session_state.totalMessages = 0
|
||||
|
||||
failed_attempts = 0
|
||||
replies_this_visit = 0
|
||||
|
||||
while not dopamine.is_app_session_over():
|
||||
# Limits check
|
||||
@@ -44,6 +85,33 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s
|
||||
try:
|
||||
xml_dump = device.dump_hierarchy()
|
||||
|
||||
# --- Zero Trust Structural Guard ---
|
||||
from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType
|
||||
|
||||
identity_engine = ScreenIdentity(getattr(configs.args, "username", ""))
|
||||
identity_engine.device = device
|
||||
screen_info = identity_engine.identify(xml_dump)
|
||||
|
||||
screen_type = screen_info["screen_type"]
|
||||
is_inbox = screen_type == ScreenType.DM_INBOX
|
||||
is_thread = screen_type == ScreenType.DM_THREAD
|
||||
|
||||
if is_thread:
|
||||
logger.warning("⚠️ [Structural Guard] DM Engine trapped in an open thread. Escaping...")
|
||||
device.press("back")
|
||||
from GramAddict.core.bot_flow import sleep
|
||||
|
||||
sleep(1.5)
|
||||
continue
|
||||
|
||||
if not is_inbox:
|
||||
# We have drifted somewhere entirely alien (like Privacy Settings)
|
||||
logger.error(
|
||||
f"🛑 [Structural Guard] Alien context detected ({screen_type}). Not in Inbox. Triggering CONTEXT_LOST."
|
||||
)
|
||||
return "CONTEXT_LOST"
|
||||
# -----------------------------------
|
||||
|
||||
# Step 1: Find unread conversation threads
|
||||
unread_threads = telepathic._extract_semantic_nodes(
|
||||
xml_dump, "find unread message threads or unread badges", threshold=0.7
|
||||
@@ -67,60 +135,101 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s
|
||||
|
||||
logger.debug(f"Last received message context: {context_text}")
|
||||
|
||||
# Verify we aren't at limits before sending
|
||||
if not getattr(configs.args, "disable_ai_messaging", False):
|
||||
# Configure models
|
||||
model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b")
|
||||
url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate")
|
||||
|
||||
# Generate response
|
||||
prompt = f"You are replying to a direct message on Instagram. The last message you received was: '{context_text}'. Keep it short, casual, and friendly. Do not use hashtags."
|
||||
|
||||
response_dict = query_llm(
|
||||
url=url,
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
format_json=False,
|
||||
timeout=120,
|
||||
max_tokens=100,
|
||||
temperature=0.7,
|
||||
# ── Context Guard: Skip threads with no extractable message ──
|
||||
if context_text.strip().lower() in _EMPTY_CONTEXT_SENTINELS:
|
||||
logger.warning(
|
||||
"⏭️ [DM Engine] Thread has no extractable message context (story reply / media-only). Skipping."
|
||||
)
|
||||
device.press("back")
|
||||
sleep(1.5)
|
||||
continue
|
||||
|
||||
if response_dict and "response" in response_dict:
|
||||
response_text = response_dict["response"].strip()
|
||||
# Find the input field
|
||||
input_nodes = telepathic._extract_semantic_nodes(
|
||||
thread_xml, "find the message input text field", threshold=0.7
|
||||
# Verify we aren't at limits before sending
|
||||
# ── Iteration Cap: Prevent DM spam ──
|
||||
if replies_this_visit >= MAX_REPLIES_PER_INBOX_VISIT:
|
||||
logger.info(
|
||||
f"🛑 [DM Engine] Reached max replies per inbox visit ({MAX_REPLIES_PER_INBOX_VISIT}). Exiting."
|
||||
)
|
||||
device.press("back")
|
||||
sleep(1.0)
|
||||
return "BOREDOM_CHANGE_FEED"
|
||||
|
||||
# Configure models
|
||||
model = getattr(configs.args, "ai_condenser_model")
|
||||
url = getattr(configs.args, "ai_condenser_url")
|
||||
|
||||
# Generate response
|
||||
prompt = f"You are replying to a direct message on Instagram. The last message you received was: '{context_text}'. Keep it short, casual, and friendly. Do not use hashtags."
|
||||
|
||||
logger.info(">>> [DM Engine] ABOUT TO CALL LLM")
|
||||
response_dict = query_llm(
|
||||
url=url,
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
format_json=False,
|
||||
timeout=120,
|
||||
max_tokens=100,
|
||||
temperature=0.7,
|
||||
)
|
||||
logger.info(f">>> [DM Engine] LLM RETURNED: {response_dict}")
|
||||
|
||||
if response_dict and "response" in response_dict:
|
||||
response_text = response_dict["response"].strip()
|
||||
# Find the input field
|
||||
input_nodes = telepathic._extract_semantic_nodes(
|
||||
thread_xml, "find the message input text field", threshold=0.7
|
||||
)
|
||||
if input_nodes and not input_nodes[0].get("skip"):
|
||||
in_node = input_nodes[0]
|
||||
_humanized_click(device, in_node["x"], in_node["y"])
|
||||
sleep(1.0)
|
||||
|
||||
# Type the message
|
||||
ghost_type(device, response_text, speed="fast")
|
||||
sleep(1.0)
|
||||
|
||||
# Find Send button
|
||||
send_xml = device.dump_hierarchy()
|
||||
send_nodes = telepathic._extract_semantic_nodes(
|
||||
send_xml, "find the send message button", threshold=0.8
|
||||
)
|
||||
if input_nodes and not input_nodes[0].get("skip"):
|
||||
in_node = input_nodes[0]
|
||||
_humanized_click(device, in_node["x"], in_node["y"])
|
||||
sleep(1.0)
|
||||
|
||||
# Type the message
|
||||
ghost_type(device, response_text, speed="fast")
|
||||
sleep(1.0)
|
||||
if send_nodes and not send_nodes[0].get("skip"):
|
||||
s_node = send_nodes[0]
|
||||
|
||||
# Find Send button
|
||||
send_xml = device.dump_hierarchy()
|
||||
send_nodes = telepathic._extract_semantic_nodes(
|
||||
send_xml, "find the send message button", threshold=0.8
|
||||
)
|
||||
|
||||
if send_nodes and not send_nodes[0].get("skip"):
|
||||
s_node = send_nodes[0]
|
||||
# ── Send Button Structural Verification ──
|
||||
if not _is_send_button(s_node):
|
||||
s_rid = s_node.get("original_attribs", {}).get("resource-id", "unknown")
|
||||
logger.warning(
|
||||
f"⚠️ [DM Engine] Refused to click non-Send element: {s_rid}. Aborting reply."
|
||||
)
|
||||
else:
|
||||
_humanized_click(device, s_node["x"], s_node["y"])
|
||||
logger.info(
|
||||
"✅ [DM Engine] Successfully sent a generated reply.", extra={"color": Fore.GREEN}
|
||||
"✅ [DM Engine] Successfully sent a generated reply.",
|
||||
extra={"color": Fore.GREEN},
|
||||
)
|
||||
|
||||
session_state.totalMessages += 1
|
||||
if crm:
|
||||
crm.log_sent_dm("unknown_target", response_text, "", [])
|
||||
replies_this_visit += 1
|
||||
dm_memory = cognitive_stack.get("dm_memory")
|
||||
if dm_memory:
|
||||
dm_memory.log_sent_dm("unknown_target", response_text, "", [])
|
||||
|
||||
# Return back to inbox
|
||||
device.press("back")
|
||||
sleep(1.0)
|
||||
sleep(1.5)
|
||||
|
||||
# If keyboard was open, the first back only closed it. Check if still in thread.
|
||||
check_xml = device.dump_hierarchy()
|
||||
from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType
|
||||
|
||||
check_identity = ScreenIdentity(getattr(configs.args, "username", ""))
|
||||
check_identity.device = device
|
||||
check_screen = check_identity.identify(check_xml)
|
||||
|
||||
if check_screen["screen_type"] == ScreenType.DM_THREAD:
|
||||
device.press("back")
|
||||
sleep(1.0)
|
||||
|
||||
dopamine.boredom += random.uniform(5.0, 15.0)
|
||||
failed_attempts = 0
|
||||
@@ -136,6 +245,19 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s
|
||||
except Exception as e:
|
||||
logger.error(f"⚠️ [Anomaly Handler] Exception in DM Loop: {e}")
|
||||
device.press("back")
|
||||
sleep(1.0)
|
||||
|
||||
check_xml = device.dump_hierarchy()
|
||||
from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType
|
||||
|
||||
check_identity = ScreenIdentity(getattr(configs.args, "username", ""))
|
||||
check_identity.device = device
|
||||
check_screen = check_identity.identify(check_xml)
|
||||
|
||||
if check_screen["screen_type"] == ScreenType.DM_THREAD:
|
||||
device.press("back")
|
||||
sleep(1.0)
|
||||
|
||||
failed_attempts += 1
|
||||
if failed_attempts > 2:
|
||||
return "CONTEXT_LOST"
|
||||
|
||||
@@ -93,6 +93,18 @@ class DopamineEngine:
|
||||
)
|
||||
|
||||
def is_app_session_over(self):
|
||||
# Global Hard Kill check
|
||||
if getattr(self, "global_max_runtime_minutes", None):
|
||||
if hasattr(self, "global_start_time"):
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
if datetime.now() - self.global_start_time > timedelta(minutes=self.global_max_runtime_minutes):
|
||||
logger.info(
|
||||
f"🛑 [Timeout] Maximum runtime of {self.global_max_runtime_minutes} minutes reached (checked by DopamineEngine). Force-stopping session.",
|
||||
extra={"color": f"{Fore.RED}"},
|
||||
)
|
||||
return True
|
||||
|
||||
# True if we have scrolled too long or hit absolute burnout
|
||||
return (time.time() - self.session_start) > self.session_limit_seconds or self.boredom >= 100.0
|
||||
|
||||
|
||||
276
GramAddict/core/goal_decomposer.py
Normal file
276
GramAddict/core/goal_decomposer.py
Normal file
@@ -0,0 +1,276 @@
|
||||
"""
|
||||
GoalDecomposer — Mission-Driven Task Planning
|
||||
|
||||
Translates the bot's `mission` config + `plugins` capabilities into
|
||||
concrete, weighted Task objects. Pure logic — no LLM, no device,
|
||||
no network, no side effects.
|
||||
|
||||
This is the bridge between:
|
||||
- "What does the user WANT?" (mission.strategy)
|
||||
- "What CAN the bot DO?" (enabled plugins + actions)
|
||||
- "What SHOULD it do NOW?" (weighted Task selection)
|
||||
|
||||
Tesla analogy: FSD doesn't have a "goal: drive safely" config.
|
||||
It derives behavior from destination + road rules + sensor capabilities.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ── Strategy Weight Tables ──
|
||||
# Each strategy defines relative weights for screen targets.
|
||||
# Higher weight = more likely to be selected by GrowthBrain.
|
||||
STRATEGY_WEIGHTS: Dict[str, Dict[str, float]] = {
|
||||
"aggressive_growth": {
|
||||
"HomeFeed": 0.15,
|
||||
"ExploreFeed": 0.45,
|
||||
"ReelsFeed": 0.15,
|
||||
"StoriesFeed": 0.10,
|
||||
"MessageInbox": 0.10,
|
||||
"FollowingList": 0.05,
|
||||
},
|
||||
"community_builder": {
|
||||
"HomeFeed": 0.40,
|
||||
"ExploreFeed": 0.10,
|
||||
"ReelsFeed": 0.05,
|
||||
"StoriesFeed": 0.25,
|
||||
"MessageInbox": 0.15,
|
||||
"FollowingList": 0.05,
|
||||
},
|
||||
"passive_learning": {
|
||||
"HomeFeed": 0.20,
|
||||
"ExploreFeed": 0.50,
|
||||
"ReelsFeed": 0.20,
|
||||
"StoriesFeed": 0.05,
|
||||
"MessageInbox": 0.00,
|
||||
"FollowingList": 0.05,
|
||||
},
|
||||
"stealth_lurker": {
|
||||
"HomeFeed": 0.35,
|
||||
"ExploreFeed": 0.25,
|
||||
"ReelsFeed": 0.15,
|
||||
"StoriesFeed": 0.15,
|
||||
"MessageInbox": 0.05,
|
||||
"FollowingList": 0.05,
|
||||
},
|
||||
}
|
||||
|
||||
# ── Plugin → Screen Mapping ──
|
||||
# Which plugins enable which screen targets.
|
||||
# A screen is only viable if at least one enabling plugin is active.
|
||||
# Some plugins work on MULTIPLE screens (likes work on home, explore, reels).
|
||||
PLUGIN_SCREENS_MAP: Dict[str, set] = {
|
||||
"likes": {"HomeFeed", "ExploreFeed", "ReelsFeed"},
|
||||
"comment": {"HomeFeed", "ExploreFeed"},
|
||||
"follow": {"HomeFeed", "ExploreFeed"},
|
||||
"repost": {"HomeFeed", "ExploreFeed"},
|
||||
"profile_visit": {"HomeFeed", "ExploreFeed"},
|
||||
"grid_like": {"HomeFeed"},
|
||||
"carousel_browsing": {"HomeFeed"},
|
||||
"rabbit_hole": {"HomeFeed", "ExploreFeed"},
|
||||
"story_view": {"StoriesFeed"},
|
||||
"dm_reply": {"MessageInbox"},
|
||||
}
|
||||
|
||||
# ── Action → Screen Mapping ──
|
||||
# The `actions:` config section maps directly to screens.
|
||||
ACTION_SCREEN_MAP: Dict[str, str] = {
|
||||
"feed": "HomeFeed",
|
||||
"explore": "ExploreFeed",
|
||||
"reels": "ReelsFeed",
|
||||
}
|
||||
|
||||
# ── Screen → Verb Mapping ──
|
||||
SCREEN_VERB_MAP: Dict[str, str] = {
|
||||
"HomeFeed": "browse_feed",
|
||||
"ExploreFeed": "browse_explore",
|
||||
"ReelsFeed": "browse_reels",
|
||||
"StoriesFeed": "view_stories",
|
||||
"MessageInbox": "check_messages",
|
||||
"FollowingList": "manage_following",
|
||||
}
|
||||
|
||||
# ── Screen → Human Intent ──
|
||||
SCREEN_INTENT_MAP: Dict[str, str] = {
|
||||
"HomeFeed": "Interact with posts in the home feed",
|
||||
"ExploreFeed": "Discover and engage with new content",
|
||||
"ReelsFeed": "Browse and interact with reels",
|
||||
"StoriesFeed": "View and react to stories",
|
||||
"MessageInbox": "Reply to unread direct messages",
|
||||
"FollowingList": "Review and manage following list",
|
||||
}
|
||||
|
||||
DEFAULT_BUDGET = 5
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Task:
|
||||
"""A concrete, executable unit of work for the bot.
|
||||
|
||||
Unlike abstract goals ("nurture community"), a Task has:
|
||||
- A specific screen to navigate to
|
||||
- A measurable budget (how many posts/items to process)
|
||||
- A weight for probabilistic selection
|
||||
- A human-readable intent for logging
|
||||
"""
|
||||
|
||||
verb: str
|
||||
target_screen: str
|
||||
intent: str
|
||||
budget_posts: int
|
||||
weight: float
|
||||
|
||||
|
||||
class GoalDecomposer:
|
||||
"""Translates mission + plugins → weighted Task list.
|
||||
|
||||
Pure logic, zero side effects. Call generate_tasks() to get
|
||||
the bot's action menu for the current session.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
plugins: Dict[str, dict],
|
||||
actions: Dict[str, str],
|
||||
mission: Dict[str, str],
|
||||
):
|
||||
self._plugins = plugins
|
||||
self._actions = actions
|
||||
self._strategy = mission.get("strategy", "aggressive_growth")
|
||||
|
||||
def generate_tasks(self) -> List[Task]:
|
||||
"""Generate weighted tasks from config.
|
||||
|
||||
Returns an empty list if no plugins are enabled —
|
||||
the bot literally has nothing to do.
|
||||
"""
|
||||
viable_screens = self._discover_viable_screens()
|
||||
if not viable_screens:
|
||||
return []
|
||||
|
||||
strategy_weights = STRATEGY_WEIGHTS.get(self._strategy, STRATEGY_WEIGHTS["aggressive_growth"])
|
||||
|
||||
tasks = []
|
||||
for screen in viable_screens:
|
||||
weight = strategy_weights.get(screen, 0.1)
|
||||
if weight <= 0:
|
||||
continue
|
||||
|
||||
budget = self._budget_for_screen(screen)
|
||||
verb = SCREEN_VERB_MAP.get(screen, "browse")
|
||||
intent = SCREEN_INTENT_MAP.get(screen, f"Interact on {screen}")
|
||||
|
||||
tasks.append(
|
||||
Task(
|
||||
verb=verb,
|
||||
target_screen=screen,
|
||||
intent=intent,
|
||||
budget_posts=budget,
|
||||
weight=weight,
|
||||
)
|
||||
)
|
||||
|
||||
return tasks
|
||||
|
||||
def _discover_viable_screens(self) -> set:
|
||||
"""Determine which screens the bot can meaningfully interact on.
|
||||
|
||||
A screen is viable if it has BOTH:
|
||||
1. A route (action config or plugin-implied), AND
|
||||
2. At least one active plugin that can DO something there.
|
||||
|
||||
Without an active plugin, navigating to a screen is pointless —
|
||||
the bot would just scroll with nothing to interact on.
|
||||
"""
|
||||
# 1. Collect screens with active plugins
|
||||
plugin_screens: set = set()
|
||||
for plugin_name, screens in PLUGIN_SCREENS_MAP.items():
|
||||
plugin_cfg = self._plugins.get(plugin_name, {})
|
||||
if not plugin_cfg:
|
||||
continue
|
||||
if not self._is_plugin_active(plugin_cfg):
|
||||
continue
|
||||
plugin_screens.update(screens)
|
||||
|
||||
# 2. Screens from actions are only viable if plugins exist for them
|
||||
action_screens: set = set()
|
||||
for action_key, screen in ACTION_SCREEN_MAP.items():
|
||||
if action_key in self._actions and self._actions[action_key]:
|
||||
action_screens.add(screen)
|
||||
|
||||
# 3. A screen must have plugin coverage to be viable
|
||||
# Action-enabled screens need at least one active plugin
|
||||
viable = action_screens & plugin_screens
|
||||
|
||||
# 4. Plugin-only screens (story_view, dm_reply) are viable
|
||||
# even without an explicit action config
|
||||
viable |= plugin_screens
|
||||
|
||||
return viable
|
||||
|
||||
def _is_plugin_active(self, plugin_cfg: dict) -> bool:
|
||||
"""Check if a plugin config represents an active plugin.
|
||||
|
||||
A plugin is active if:
|
||||
- It has `enabled: true` (explicit), OR
|
||||
- It has `percentage` > 0 (implicit enable), OR
|
||||
- It has any config keys and `enabled` is not explicitly False
|
||||
"""
|
||||
# Explicit disable
|
||||
if plugin_cfg.get("enabled") is False:
|
||||
return False
|
||||
|
||||
# Explicit enable
|
||||
if plugin_cfg.get("enabled") is True:
|
||||
return True
|
||||
|
||||
# Percentage-based: 0% means disabled
|
||||
pct = plugin_cfg.get("percentage")
|
||||
if pct is not None:
|
||||
try:
|
||||
return float(pct) > 0
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
# Has config keys but no explicit enabled/percentage = active
|
||||
return bool(plugin_cfg)
|
||||
|
||||
def _budget_for_screen(self, screen: str) -> int:
|
||||
"""Determine the post budget for a screen.
|
||||
|
||||
Reads from actions config (e.g. feed: "5-10") and parses
|
||||
the range string into a random integer within bounds.
|
||||
"""
|
||||
# Map screen back to action key
|
||||
reverse_map = {v: k for k, v in ACTION_SCREEN_MAP.items()}
|
||||
action_key = reverse_map.get(screen)
|
||||
|
||||
if action_key and action_key in self._actions:
|
||||
return _parse_range(self._actions[action_key])
|
||||
|
||||
# Special screens get fixed budgets from plugin config
|
||||
if screen == "StoriesFeed":
|
||||
story_cfg = self._plugins.get("story_view", {})
|
||||
count_str = story_cfg.get("count", "1-3")
|
||||
return _parse_range(str(count_str))
|
||||
|
||||
if screen == "MessageInbox":
|
||||
return DEFAULT_BUDGET
|
||||
|
||||
return DEFAULT_BUDGET
|
||||
|
||||
|
||||
def _parse_range(range_str: str) -> int:
|
||||
"""Parse a range string like '5-10' into a random int within bounds."""
|
||||
try:
|
||||
if "-" in str(range_str):
|
||||
parts = str(range_str).split("-")
|
||||
low, high = int(parts[0]), int(parts[1])
|
||||
return random.randint(low, high)
|
||||
return int(range_str)
|
||||
except (ValueError, IndexError):
|
||||
return DEFAULT_BUDGET
|
||||
@@ -17,15 +17,13 @@ import logging
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from GramAddict.core.utils import random_sleep
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from GramAddict.core.navigation.knowledge import NavigationKnowledge
|
||||
from GramAddict.core.navigation.path_memory import PathMemory
|
||||
from GramAddict.core.navigation.planner import GoalPlanner
|
||||
from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType
|
||||
from GramAddict.core.utils import random_sleep
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Re-export for backward compatibility (optional but helps minimize import breakage)
|
||||
__all__ = ["GoalExecutor", "ScreenIdentity", "ScreenType", "PathMemory", "NavigationKnowledge", "GoalPlanner"]
|
||||
@@ -45,6 +43,8 @@ class GoalExecutor:
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
global_start_time = None
|
||||
global_max_runtime_minutes = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, device=None, bot_username=""):
|
||||
@@ -63,6 +63,7 @@ class GoalExecutor:
|
||||
self.device = device
|
||||
self.username = bot_username
|
||||
self.screen_id = ScreenIdentity(bot_username)
|
||||
self.screen_id.device = device
|
||||
self.planner = GoalPlanner(bot_username)
|
||||
self.path_memory = PathMemory(bot_username)
|
||||
self.max_steps = 15 # Safety: never execute more than 15 steps
|
||||
@@ -119,10 +120,25 @@ class GoalExecutor:
|
||||
consecutive_back_presses = 0
|
||||
MAX_CONSECUTIVE_BACK = 3
|
||||
explored_nav_actions = set()
|
||||
visited_screens = set()
|
||||
for step_num in range(max_steps):
|
||||
# ── Global Hard Kill Check ──
|
||||
max_rt = GoalExecutor.global_max_runtime_minutes
|
||||
start_time = GoalExecutor.global_start_time
|
||||
if max_rt and start_time:
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
if datetime.now() - start_time > timedelta(minutes=max_rt):
|
||||
logger.error(
|
||||
f"🛑 [Timeout] Maximum runtime of {max_rt} minutes reached during GOAP execution. Hard stopping planner.",
|
||||
extra={"color": "\\033[31m"},
|
||||
)
|
||||
return False
|
||||
|
||||
# PERCEIVE
|
||||
screen = self.perceive()
|
||||
screen_type = screen["screen_type"]
|
||||
visited_screens.add(screen_type)
|
||||
|
||||
if last_screen_type and screen_type != last_screen_type:
|
||||
logger.debug(
|
||||
@@ -136,7 +152,7 @@ class GoalExecutor:
|
||||
original_available = screen.get("available_actions", []).copy()
|
||||
masked_available = []
|
||||
for act in original_available:
|
||||
fail_count = self.action_failures.get(act, 0)
|
||||
fail_count = self.action_failures.get((screen_type, act), 0)
|
||||
if fail_count >= MAX_RETRIES:
|
||||
logger.warning(
|
||||
f"🚫 [GOAP] Masking action '{act}' due to {fail_count} consecutive failures to prevent loops."
|
||||
@@ -146,7 +162,7 @@ class GoalExecutor:
|
||||
screen["available_actions"] = masked_available
|
||||
|
||||
logger.debug(
|
||||
f"📍 [GOAP Step {step_num + 1}] On: {screen_type.value} | "
|
||||
f"📍 [GOAP Step {step_num + 1}] Goal: '{goal}' | On: {screen_type.value} | "
|
||||
f"Available: {screen.get('available_actions', [])[:5]}"
|
||||
)
|
||||
|
||||
@@ -158,13 +174,23 @@ class GoalExecutor:
|
||||
# SAE Feedback Loop!
|
||||
# If we hit this, the LAST action caused an obstacle! Mask it!
|
||||
if last_action and last_screen_type:
|
||||
self.action_failures[last_action] = (
|
||||
self.action_failures.get(last_action, 0) + MAX_RETRIES
|
||||
) # Instantly mask it
|
||||
self.planner.knowledge.learn_trap(last_screen_type, last_action, f"caused_obstacle_{obstacle_name}")
|
||||
logger.warning(
|
||||
f"🛡️ [SAE Feedback] Action '{last_action}' caused an obstacle. Masking aggressively and learned trap."
|
||||
)
|
||||
self.action_failures[(last_screen_type, last_action)] = (
|
||||
self.action_failures.get((last_screen_type, last_action), 0) + MAX_RETRIES
|
||||
) # Instantly mask it for this session
|
||||
from GramAddict.core.screen_topology import ScreenTopology
|
||||
|
||||
if ScreenTopology.is_structural_action(last_screen_type, last_action):
|
||||
logger.warning(
|
||||
f"🛡️ [SAE Feedback] Structural action '{last_action}' caused an obstacle. "
|
||||
f"Masking for this session. (Never burned permanently)"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"🛡️ [SAE Feedback] Content action '{last_action}' caused an obstacle. "
|
||||
f"Masking for this session to break loop, but preventing permanent Qdrant poisoning."
|
||||
)
|
||||
# We specifically DO NOT call self.planner.knowledge.learn_trap here anymore!
|
||||
# Burning dynamic actions like "tap follow button" permanently destroys the bot's capabilities across sessions.
|
||||
|
||||
if not self._get_sae().ensure_clear_screen():
|
||||
if screen_type == ScreenType.FOREIGN_APP:
|
||||
@@ -173,7 +199,13 @@ class GoalExecutor:
|
||||
continue
|
||||
|
||||
# PLAN
|
||||
action = self.planner.plan_next_step(goal, screen, explored_nav_actions=explored_nav_actions)
|
||||
action = self.planner.plan_next_step(
|
||||
goal,
|
||||
screen,
|
||||
explored_nav_actions=explored_nav_actions,
|
||||
action_failures=self.action_failures,
|
||||
visited_screens=visited_screens,
|
||||
)
|
||||
|
||||
if action is None:
|
||||
# Goal achieved!
|
||||
@@ -193,51 +225,80 @@ class GoalExecutor:
|
||||
|
||||
if success:
|
||||
steps_taken.append({"action": action})
|
||||
|
||||
if action == "force start instagram":
|
||||
logger.info("🔄 [GOAP State] App restarted. Purging memory/traps to attempt fresh routing.")
|
||||
self.action_failures.clear()
|
||||
explored_nav_actions.clear()
|
||||
visited_screens.clear()
|
||||
consecutive_back_presses = 0
|
||||
continue
|
||||
|
||||
# Check if it was a navigation action (vs a goal action). If we are not on the required screen,
|
||||
# any action taken is essentially a navigation attempt.
|
||||
explored_nav_actions.add(action)
|
||||
# Reset failures for this action since it eventually succeeded
|
||||
self.action_failures[action] = 0
|
||||
self.action_failures[(screen_type, action)] = 0
|
||||
|
||||
# ── Back-Press Circuit Breaker ──
|
||||
if "scroll" in action.lower():
|
||||
logger.debug(
|
||||
"📍 [GOAP State] Scrolled successfully. Clearing explored actions to allow retrying off-screen elements."
|
||||
)
|
||||
explored_nav_actions.clear()
|
||||
# Keep action_failures for synthetic intents, but clear them for structural actions
|
||||
# so that the HD Map can retry route actions that might now be visible!
|
||||
from GramAddict.core.screen_topology import ScreenTopology
|
||||
|
||||
keys_to_clear = [
|
||||
k
|
||||
for k in self.action_failures.keys()
|
||||
if k[0] == screen_type and ScreenTopology.is_structural_action(screen_type, k[1])
|
||||
]
|
||||
for k in keys_to_clear:
|
||||
del self.action_failures[k]
|
||||
|
||||
# ── Back-Press Circuit Breaker → Escalation ──
|
||||
if action == "press back":
|
||||
consecutive_back_presses += 1
|
||||
if consecutive_back_presses >= MAX_CONSECUTIVE_BACK:
|
||||
logger.error(
|
||||
logger.warning(
|
||||
f"🛑 [GOAP] Back-pressed {MAX_CONSECUTIVE_BACK} times with no screen transition. "
|
||||
f"Aborting goal '{goal}' to prevent app exit."
|
||||
f"Escalating to force restart."
|
||||
)
|
||||
self.path_memory.learn_path(goal, start_screen, steps_taken, False)
|
||||
|
||||
# Phase 3 GREEN: Unlearn the trap path
|
||||
# Unlearn the trap path
|
||||
from GramAddict.core.qdrant_memory import NavigationMemoryDB
|
||||
|
||||
# We don't know the exact action that got us here easily without analyzing steps_taken,
|
||||
# but we can grab the first action taken from start_screen in this chain if available.
|
||||
if len(steps_taken) > consecutive_back_presses:
|
||||
last_real_action = steps_taken[-consecutive_back_presses - 1]["action"]
|
||||
logger.debug(
|
||||
f"[GOAP Unlearn] last_real_action={last_real_action}, " f"start_screen={start_screen}"
|
||||
)
|
||||
NavigationMemoryDB().unlearn_transition(start_screen, last_real_action)
|
||||
else:
|
||||
logger.debug(
|
||||
f"[GOAP Unlearn] No real action to unlearn. "
|
||||
f"steps={len(steps_taken)}, back_presses={consecutive_back_presses}"
|
||||
)
|
||||
|
||||
return False
|
||||
# ── ESCALATION: Force restart instead of aborting ──
|
||||
app_id = getattr(self.device, "app_id", "com.instagram.android")
|
||||
self.device.app_start(app_id, use_monkey=True)
|
||||
random_sleep(2.0, 3.5)
|
||||
steps_taken.append({"action": "force start instagram"})
|
||||
|
||||
logger.info("🔄 [GOAP Escalation] App restarted. Purging all failure state for fresh attempt.")
|
||||
self.action_failures.clear()
|
||||
explored_nav_actions.clear()
|
||||
visited_screens.clear()
|
||||
consecutive_back_presses = 0
|
||||
continue
|
||||
else:
|
||||
consecutive_back_presses = 0
|
||||
else:
|
||||
self.action_failures[action] = self.action_failures.get(action, 0) + 1
|
||||
self.action_failures[(screen_type, action)] = self.action_failures.get((screen_type, action), 0) + 1
|
||||
# Track failed actions in explored_nav_actions so the planner
|
||||
# knows NOT to return the same synthetic intent again.
|
||||
# Without this, synthetic intents (not in available_actions)
|
||||
# bypass the masking logic and loop forever.
|
||||
explored_nav_actions.add(action)
|
||||
|
||||
if self.action_failures[action] >= MAX_RETRIES:
|
||||
if self.action_failures[(screen_type, action)] >= MAX_RETRIES:
|
||||
# ── Topology Guard: Never poison structural HD Map actions ──
|
||||
from GramAddict.core.screen_topology import ScreenTopology
|
||||
|
||||
@@ -268,7 +329,7 @@ class GoalExecutor:
|
||||
|
||||
return False
|
||||
|
||||
def _execute_action(self, action: str, goal: str = None) -> bool:
|
||||
def _execute_action(self, action: str, goal: str = None, **kwargs) -> bool:
|
||||
"""Execute a single natural-language action using the TelepathicEngine."""
|
||||
|
||||
if action == "press back":
|
||||
@@ -288,6 +349,9 @@ class GoalExecutor:
|
||||
random_sleep(2.0, 3.5)
|
||||
return True
|
||||
|
||||
if action == "type and post comment":
|
||||
return self._execute_type_and_post_comment(kwargs.get("text", ""))
|
||||
|
||||
# Use TelepathicEngine for any semantic click
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
@@ -306,25 +370,65 @@ class GoalExecutor:
|
||||
self._get_sae().ensure_clear_screen(max_attempts=3)
|
||||
return False
|
||||
|
||||
# ── Pre-Click Semantic Match Guard ──
|
||||
# For toggle intents (follow/like/save), verify the selected node
|
||||
# semantically matches the intent BEFORE clicking. This prevents
|
||||
# VLM hallucinations from clicking photo grid items when looking
|
||||
# for follow buttons.
|
||||
from GramAddict.core.perception.action_memory import _intent_matches_node
|
||||
|
||||
node_semantic = (
|
||||
f"text: '{best_node.get('text', '')}', "
|
||||
f"desc: '{best_node.get('description', '')}', "
|
||||
f"id: '{best_node.get('id', '')}'"
|
||||
)
|
||||
if not _intent_matches_node(action, node_semantic):
|
||||
logger.warning(
|
||||
f"🛡️ [GOAP Execute] Pre-click rejection: node does not match intent '{action}'. "
|
||||
f"Node: {node_semantic}"
|
||||
)
|
||||
return False
|
||||
|
||||
# Execute click
|
||||
self.device.click(obj=best_node)
|
||||
import random
|
||||
|
||||
time.sleep(random.uniform(1.6, 2.8))
|
||||
|
||||
# Verify success via Goal Context + Screen Feedback
|
||||
post_xml = self.device.dump_hierarchy()
|
||||
# ── Smart UI Stabilization Poll ──
|
||||
# Instead of a static sleep (which is either too long on fast devices or
|
||||
# too short on slow ones), we poll dump_hierarchy multiple times.
|
||||
# As soon as the XML changes, we know the UI has transitioned.
|
||||
MAX_POLLS = 5
|
||||
POLL_INTERVAL = 0.5 # seconds between polls
|
||||
post_xml = xml_dump # Start with pre-click state
|
||||
for poll in range(MAX_POLLS):
|
||||
time.sleep(POLL_INTERVAL)
|
||||
post_xml = self.device.dump_hierarchy()
|
||||
if post_xml != xml_dump:
|
||||
logger.debug(f"[GOAP Poll] UI change detected on poll {poll + 1}/{MAX_POLLS}.")
|
||||
break
|
||||
else:
|
||||
logger.debug(f"[GOAP Poll] No UI change after {MAX_POLLS} polls ({MAX_POLLS * POLL_INTERVAL}s).")
|
||||
pre_action_screen = self.perceive(xml_dump) # Screen state BEFORE the click
|
||||
post_screen = self.perceive(post_xml)
|
||||
post_screen_type = post_screen["screen_type"]
|
||||
pre_action_screen_type = pre_action_screen["screen_type"]
|
||||
|
||||
# Determine if this was a navigation or an interaction
|
||||
from GramAddict.core.screen_topology import ScreenTopology
|
||||
|
||||
is_navigation = any(k in action.lower() for k in ["tab", "open", "go to", "navigate", "following list"])
|
||||
if not is_navigation:
|
||||
is_navigation = ScreenTopology.is_structural_action(pre_action_screen_type, action)
|
||||
action_success = False
|
||||
ui_changed = post_xml != xml_dump
|
||||
|
||||
# ── UI Change Detection with Noise Threshold ──
|
||||
# Raw string diffs of < 50 bytes are noise (timestamps, whitespace, counters).
|
||||
# A real navigation changes the XML by hundreds/thousands of bytes.
|
||||
MIN_UI_CHANGE_BYTES = 50
|
||||
xml_delta = abs(len(post_xml) - len(xml_dump))
|
||||
ui_changed = post_xml != xml_dump and xml_delta >= MIN_UI_CHANGE_BYTES
|
||||
logger.debug(
|
||||
f"[GOAP Verify] ui_changed={ui_changed}, " f"xml_len_pre={len(xml_dump)}, xml_len_post={len(post_xml)}"
|
||||
f"[GOAP Verify] ui_changed={ui_changed}, "
|
||||
f"xml_len_pre={len(xml_dump)}, xml_len_post={len(post_xml)}, delta={xml_delta}b"
|
||||
)
|
||||
|
||||
if is_navigation:
|
||||
@@ -380,8 +484,18 @@ class GoalExecutor:
|
||||
action_success = False
|
||||
else:
|
||||
# For interactions (like, follow) or unknown goals, use XML delta + semantic verify
|
||||
if ui_changed:
|
||||
verification = engine.verify_success(action, post_xml)
|
||||
# REGRESSION FIX 2026-05-01: Toggle actions (like/save) produce tiny XML deltas
|
||||
# (e.g. checked="false" → "true" = 1 byte). We must NOT gate interactions on
|
||||
# MIN_UI_CHANGE_BYTES. ANY change at all warrants semantic verification.
|
||||
interaction_xml_changed = post_xml != xml_dump
|
||||
if post_screen_type == ScreenType.FOREIGN_APP:
|
||||
logger.error(
|
||||
f"❌ [GOAP Verify] Interaction '{action}' caused navigation to FOREIGN_APP (e.g. Play Store). Rejecting as catastrophic failure."
|
||||
)
|
||||
action_success = False
|
||||
elif interaction_xml_changed:
|
||||
score = best_node.get("score", 0.0) if best_node else 0.0
|
||||
verification = engine.verify_success(action, post_xml, device=self.device, confidence=score)
|
||||
if verification is True:
|
||||
action_success = True
|
||||
logger.info(f"✅ [GOAP Step] Interaction '{action}' successful.")
|
||||
@@ -412,8 +526,12 @@ class GoalExecutor:
|
||||
return False
|
||||
else:
|
||||
# action_success is None (INCONCLUSIVE)
|
||||
# We decay the memory so it unlearns if it repeatedly fails to produce a definitive success.
|
||||
logger.warning(f"⚠️ [GOAP Execute] Applying AGGRESSIVE PENALTY for inconclusive action '{action}'.")
|
||||
engine.decay_click(action)
|
||||
# Double penalty to burn ambiguous paths faster (outer loop adds +1, so total +2 = instantly hits MAX_RETRIES)
|
||||
self.action_failures[(pre_action_screen_type, action)] = (
|
||||
self.action_failures.get((pre_action_screen_type, action), 0) + 1
|
||||
)
|
||||
return False
|
||||
|
||||
def _execute_recalled_path(self, steps: List[Dict], goal: str) -> bool:
|
||||
@@ -440,6 +558,83 @@ class GoalExecutor:
|
||||
achieved = self.planner.plan_next_step(goal, screen) is None
|
||||
return achieved
|
||||
|
||||
def _execute_type_and_post_comment(self, fallback_text: str) -> bool:
|
||||
"""Handles the specific typing interaction, prioritizing Meta AI chips if available."""
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
logger.info("💬 [GOAP] Executing 'type and post comment'...")
|
||||
engine = TelepathicEngine.get_instance()
|
||||
|
||||
# 1. Tap the comment input field to open the keyboard
|
||||
xml_dump = self.device.dump_hierarchy()
|
||||
if "com.google.android.inputmethod.latin" not in xml_dump:
|
||||
logger.info("⌨️ [GOAP] Tapping comment composer to open keyboard...")
|
||||
best_node = engine.find_best_node(
|
||||
xml_dump, "tap comment input field", min_confidence=0.7, device=self.device
|
||||
)
|
||||
if best_node:
|
||||
self.device.click(obj=best_node)
|
||||
random_sleep(1.5, 2.5)
|
||||
xml_dump = self.device.dump_hierarchy()
|
||||
else:
|
||||
logger.warning("⚠️ [GOAP] Could not find comment input field.")
|
||||
return False
|
||||
|
||||
# 2. Check for Meta AI chips (Supportive, Funny, etc.)
|
||||
meta_ai_chips = []
|
||||
try:
|
||||
root = ET.fromstring(xml_dump.encode("utf-8"))
|
||||
for node in root.iter("node"):
|
||||
node_text = node.attrib.get("text", "")
|
||||
if node_text in ["Supportive", "Rewrite", "Absurd", "Casual", "Funny", "Heartfelt", "Professional"]:
|
||||
meta_ai_chips.append(node_text)
|
||||
except Exception as e:
|
||||
logger.debug(f"XML parse error for Meta AI chips: {e}")
|
||||
|
||||
if meta_ai_chips:
|
||||
logger.info(f"✨ [Meta AI] Detected Meta AI chips: {meta_ai_chips}")
|
||||
logger.info("🧠 [Meta AI] Asking VLM to select the best tone...")
|
||||
|
||||
# Use Telepathic Engine to pick the best chip via VLM
|
||||
chip_node = engine.find_best_node(
|
||||
xml_dump,
|
||||
"tap the best Meta AI tone chip to generate a comment (e.g. Supportive, Funny, Casual)",
|
||||
min_confidence=0.6,
|
||||
device=self.device,
|
||||
)
|
||||
|
||||
if chip_node:
|
||||
logger.info(f"✨ [Meta AI] VLM selected chip: '{chip_node.get('text', 'Unknown')}'")
|
||||
self.device.click(obj=chip_node)
|
||||
random_sleep(3.0, 4.5) # Wait for Meta AI to generate the text
|
||||
else:
|
||||
logger.warning("⚠️ [Meta AI] VLM failed to pick a chip, falling back to manual typing.")
|
||||
from GramAddict.core.stealth_typing import ghost_type
|
||||
|
||||
ghost_type(self.device, fallback_text)
|
||||
random_sleep(1.0, 2.0)
|
||||
else:
|
||||
# 3. Fallback: Type the text provided by our own VLM writer
|
||||
logger.info(f"⌨️ [GOAP] Typing comment manually: {fallback_text}")
|
||||
from GramAddict.core.stealth_typing import ghost_type
|
||||
|
||||
ghost_type(self.device, fallback_text)
|
||||
random_sleep(1.0, 2.0)
|
||||
|
||||
# 4. Click the Post button
|
||||
xml_dump = self.device.dump_hierarchy()
|
||||
post_btn = engine.find_best_node(xml_dump, "tap post comment button", min_confidence=0.7, device=self.device)
|
||||
if post_btn:
|
||||
self.device.click(obj=post_btn)
|
||||
random_sleep(1.5, 2.5)
|
||||
logger.info("✅ [GOAP] Comment posted successfully.")
|
||||
return True
|
||||
else:
|
||||
logger.warning("⚠️ [GOAP] Could not find post button after typing.")
|
||||
return False
|
||||
|
||||
# ── Convenience methods (backward compatibility with navigate_to) ──
|
||||
|
||||
def navigate_to_screen(self, target: str) -> bool:
|
||||
|
||||
@@ -94,6 +94,63 @@ class GrowthBrain:
|
||||
logger.info(f"🧠 [GrowthBrain] Strategy '{self.strategy}' dictated Desire: {selected_desire}")
|
||||
return selected_desire
|
||||
|
||||
def get_current_goal(self, dopamine_engine, available_goals: list[str], success_rates: dict = None) -> str:
|
||||
"""
|
||||
Autonomously selects the next strategic goal.
|
||||
If no goals are configured, falls back to legacy desires.
|
||||
Weights goals based on session success rates if provided.
|
||||
|
||||
.. deprecated::
|
||||
Use select_task() instead for concrete, plugin-linked task selection.
|
||||
"""
|
||||
import random
|
||||
|
||||
if not available_goals:
|
||||
# Legacy Desire Mapping (Fallback)
|
||||
return self.get_current_desire(dopamine_engine)
|
||||
|
||||
if dopamine_engine.boredom > 80:
|
||||
return "ShiftContext" # High boredom triggers a context shift
|
||||
|
||||
if not success_rates:
|
||||
return random.choice(available_goals)
|
||||
|
||||
weights = []
|
||||
for goal in available_goals:
|
||||
base_weight = 1.0
|
||||
success_count = success_rates.get(goal, 0)
|
||||
weight = base_weight + float(success_count)
|
||||
weights.append(weight)
|
||||
|
||||
return random.choices(available_goals, weights=weights, k=1)[0]
|
||||
|
||||
def select_task(self, dopamine_engine, available_tasks: list) -> "Optional[Task]":
|
||||
"""Select the next concrete Task using weighted random selection.
|
||||
|
||||
This is the primary interface for the orchestrator. Unlike get_current_goal()
|
||||
which returns abstract strings, this returns a Task object with a specific
|
||||
target_screen, budget, and success metric.
|
||||
|
||||
Returns:
|
||||
Task: The selected task to execute.
|
||||
None: If no tasks available or boredom is too high (ShiftContext signal).
|
||||
"""
|
||||
if not available_tasks:
|
||||
return None
|
||||
|
||||
# High boredom = ShiftContext (take a break, switch feed)
|
||||
if dopamine_engine.boredom > 85.0:
|
||||
logger.info("🧠 [GrowthBrain] Boredom too high for task selection. ShiftContext.")
|
||||
return None
|
||||
|
||||
weights = [task.weight for task in available_tasks]
|
||||
selected = random.choices(available_tasks, weights=weights, k=1)[0]
|
||||
logger.info(
|
||||
f"🧠 [GrowthBrain] Selected task: {selected.verb} → {selected.target_screen} "
|
||||
f"(weight={selected.weight:.2f}, budget={selected.budget_posts})"
|
||||
)
|
||||
return selected
|
||||
|
||||
def get_circadian_pacing(self) -> float:
|
||||
"""
|
||||
Adjusts activity levels based on the current local time
|
||||
|
||||
86
GramAddict/core/interaction.py
Normal file
86
GramAddict/core/interaction.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from GramAddict.core.llm_provider import query_llm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LLMWriter:
|
||||
"""
|
||||
The Creative Engine — Content Generation for Interactions.
|
||||
|
||||
Generates high-fidelity, persona-aligned comments and messages.
|
||||
Replaces legacy static 'comment_list' with dynamic, contextual resonance.
|
||||
"""
|
||||
|
||||
def __init__(self, username: str, persona_interests: list[str], configs):
|
||||
self.username = username
|
||||
self.persona_interests = persona_interests
|
||||
self.configs = configs
|
||||
self.args = getattr(configs, "args", None)
|
||||
|
||||
def generate_comment(self, post_data: Dict) -> str:
|
||||
"""
|
||||
Generates a human-like comment based on post data and persona interests.
|
||||
"""
|
||||
if not post_data:
|
||||
logger.warning("✍️ [Writer] No post data provided. Using generic fallback.")
|
||||
return "Cool!"
|
||||
|
||||
caption = post_data.get("caption", "")
|
||||
description = post_data.get("description", "")
|
||||
target_username = post_data.get("username", "the user")
|
||||
|
||||
# Build context for the LLM
|
||||
context = f"Post by @{target_username}\n"
|
||||
if caption:
|
||||
context += f"Caption: {caption}\n"
|
||||
if description:
|
||||
context += f"Visual Description: {description}\n"
|
||||
|
||||
interests_str = ", ".join(self.persona_interests) if self.persona_interests else "general interesting things"
|
||||
|
||||
prompt = (
|
||||
f"You are an Instagram user interested in: {interests_str}.\n"
|
||||
f"You want to leave a brief, friendly, and authentic comment on the following post:\n\n"
|
||||
f"{context}\n"
|
||||
f"INSTRUCTIONS:\n"
|
||||
f"1. Keep it under 10 words.\n"
|
||||
f"2. Be casual and human. Avoid overly formal language or sounding like a bot.\n"
|
||||
f"3. Do NOT use more than one emoji.\n"
|
||||
f"4. Do NOT use hashtags.\n"
|
||||
f"5. Focus on something specific in the post if possible.\n"
|
||||
f"6. Reply with ONLY the comment text."
|
||||
)
|
||||
|
||||
model = getattr(self.args, "ai_writer_model", getattr(self.args, "ai_model"))
|
||||
url = getattr(
|
||||
self.args, "ai_writer_url", getattr(self.args, "ai_model_url")
|
||||
)
|
||||
|
||||
logger.info(f"✍️ [Writer] Generating comment for @{target_username} using {model}...")
|
||||
|
||||
try:
|
||||
response_dict = query_llm(
|
||||
url=url,
|
||||
model=model,
|
||||
prompt=prompt,
|
||||
system="You are a friendly Instagram user. You write short, authentic comments.",
|
||||
format_json=False,
|
||||
timeout=60,
|
||||
temperature=0.7, # Add some variety to avoid 'the to the' loops
|
||||
)
|
||||
|
||||
if response_dict and "response" in response_dict:
|
||||
comment = response_dict["response"].strip().strip('"')
|
||||
# Basic cleaning to remove LLM artifacts
|
||||
comment = comment.split("\n")[0] # Take only first line
|
||||
if not comment:
|
||||
return "Nice!"
|
||||
return comment
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"✍️ [Writer] Failed to generate comment: {e}")
|
||||
|
||||
return "Great post! 🔥"
|
||||
@@ -287,6 +287,12 @@ def query_llm(
|
||||
req_data["images"] = images_b64
|
||||
if format_json:
|
||||
req_data["format"] = "json"
|
||||
else:
|
||||
# For free-text calls (Brain action extraction), explicitly disable
|
||||
# thinking mode. Reasoning models like qwen3.5 put EVERYTHING in
|
||||
# the thinking block and return response='', which is useless for
|
||||
# action extraction. think=false forces a direct response.
|
||||
req_data["think"] = False
|
||||
|
||||
# Ollama passes configs inside 'options'
|
||||
if temperature is not None or max_tokens is not None:
|
||||
@@ -344,16 +350,27 @@ def query_llm(
|
||||
return {"response": content}
|
||||
else:
|
||||
# Ollama returns response OR thinking (for reasoning models)
|
||||
content = resp_json.get("response") or resp_json.get("thinking") or ""
|
||||
raw_response = resp_json.get("response", "")
|
||||
raw_thinking = resp_json.get("thinking", "")
|
||||
|
||||
logger.debug(f"DEBUG LLM PAYLOAD: response='{raw_response}', thinking='{raw_thinking}'")
|
||||
|
||||
# CRITICAL: For free-text mode (format_json=False), do NOT substitute
|
||||
# thinking for empty response. The thinking block is REASONING, not
|
||||
# a decision. The Brain parser would extract random actions from it.
|
||||
# For JSON mode (format_json=True), falling back to thinking IS correct
|
||||
# because reasoning models may place structured output in the thinking block.
|
||||
if format_json:
|
||||
content = raw_response or raw_thinking or ""
|
||||
extracted = extract_json(content)
|
||||
if not extracted:
|
||||
# Log more context if JSON extraction fails
|
||||
logger.debug(f"Ollama raw content (for JSON extraction): {content[:200]}...")
|
||||
raise ValueError("Ollama returned non-JSON content when JSON was expected.")
|
||||
resp_json["response"] = extracted
|
||||
logger.warning(f"Failed to extract JSON from content: {content[:100]}")
|
||||
else:
|
||||
content = extracted
|
||||
else:
|
||||
content = raw_response
|
||||
|
||||
return resp_json
|
||||
return {"response": content}
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.error(f"⚠️ [LLM Provider] Connection refused for {model} at {url}. Is the service running?")
|
||||
except Exception as e:
|
||||
@@ -378,14 +395,11 @@ def query_llm(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Last resort defaults
|
||||
# Last resort: If no fallback config exists, don't silently use hardcoded defaults.
|
||||
# Config() defines these via argparse defaults — if they're missing, there's nothing to fallback to.
|
||||
if not f_model or not f_url:
|
||||
if is_openai_compat:
|
||||
f_model = f_model or "llama3.2:1b"
|
||||
f_url = f_url or "http://localhost:11434/api/generate"
|
||||
else:
|
||||
f_model = f_model or "llama3.2:1b"
|
||||
f_url = f_url or "http://localhost:11434/api/generate"
|
||||
logger.warning("⚠️ [Circuit Breaker] No fallback model/URL configured. Cannot retry.")
|
||||
return None
|
||||
|
||||
# Circuit Breaker: If fallback is identical to primary, don't waste time retrying
|
||||
if f_model == model and f_url == url:
|
||||
@@ -441,11 +455,12 @@ def query_telepathic_llm(
|
||||
|
||||
try:
|
||||
args = Config().args
|
||||
target_url = getattr(args, "ai_fallback_url", "http://localhost:11434/api/generate")
|
||||
target_model = getattr(args, "ai_fallback_model", "llama3.2:1b")
|
||||
target_url = getattr(args, "ai_fallback_url")
|
||||
target_model = getattr(args, "ai_fallback_model")
|
||||
except Exception:
|
||||
target_url = "http://localhost:11434/api/generate"
|
||||
target_model = "llama3.2:1b"
|
||||
raise RuntimeError(
|
||||
"Config().args not initialized — cannot resolve fallback AI model. Fail Fast."
|
||||
)
|
||||
|
||||
is_local = "localhost" in target_url or "127.0.0.1" in target_url
|
||||
calc_timeout = 180 if is_local else 45
|
||||
|
||||
85
GramAddict/core/navigation/brain.py
Normal file
85
GramAddict/core/navigation/brain.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from GramAddict.core.config import Config
|
||||
from GramAddict.core.llm_provider import query_llm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def ask_brain_for_action(
|
||||
goal: str, screen_type: str, available_actions: List[str], explored_actions: set, context: dict = None
|
||||
) -> Optional[str]:
|
||||
"""Asks the VLM to decide the best available action to reach the goal, considering failures."""
|
||||
if not available_actions:
|
||||
return None
|
||||
|
||||
cfg = Config()
|
||||
if not hasattr(cfg, "args"):
|
||||
raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.")
|
||||
url = getattr(cfg.args, "ai_model_url")
|
||||
model = getattr(cfg.args, "ai_model")
|
||||
|
||||
prompt = (
|
||||
f"You are an autonomous Instagram agent. Your ultimate goal is: '{goal}'.\n"
|
||||
f"You are currently on the screen: {screen_type}.\n"
|
||||
f"These actions are available to you right now: {available_actions}\n"
|
||||
)
|
||||
if explored_actions:
|
||||
prompt += f"You recently tried these actions but they failed or didn't help: {list(explored_actions)}\n"
|
||||
if context:
|
||||
prompt += f"Context: {context}\n"
|
||||
|
||||
prompt += (
|
||||
"INSTRUCTIONS:\n"
|
||||
"1. Reason about where you are. Consider the screen type and what actions make sense on that screen.\n"
|
||||
"2. If the goal requires navigating away from the current screen, choose the action that moves you closest to the goal.\n"
|
||||
"3. 'scroll down' reveals more UI elements on scrollable screens (feeds, profiles, lists). If your target is likely on this screen but not currently visible, you MUST choose 'scroll down'.\n"
|
||||
"4. 'press back' exits the current screen and returns to the previous one. Use it when you are on a screen that doesn't lead to your goal.\n"
|
||||
"5. DO NOT hallucinate actions. Reply ONLY with the exact string from the available actions list.\n"
|
||||
"6. Reply with ONLY the action string, nothing else."
|
||||
)
|
||||
|
||||
try:
|
||||
response = query_llm(
|
||||
url=url,
|
||||
model=model,
|
||||
prompt="Choose the next best action.",
|
||||
system=prompt,
|
||||
format_json=False,
|
||||
max_tokens=250,
|
||||
)
|
||||
if response:
|
||||
result = response if isinstance(response, str) else response.get("response", "")
|
||||
result = result.strip().strip("'\"")
|
||||
|
||||
# 1. Exact match check (ideal case)
|
||||
for act in available_actions:
|
||||
if act.lower() == result.lower():
|
||||
return act
|
||||
|
||||
# 2. Strict line-by-line check (often the model outputs the action on the last line)
|
||||
for line in reversed(result.splitlines()):
|
||||
line = line.strip().strip("'\"")
|
||||
for act in available_actions:
|
||||
if act.lower() == line.lower():
|
||||
return act
|
||||
|
||||
# 3. Fuzzy match (find the LAST mentioned action in the text, assuming it's the conclusion)
|
||||
best_act = None
|
||||
best_idx = -1
|
||||
for act in available_actions:
|
||||
idx = result.lower().rfind(act.lower())
|
||||
if idx > best_idx:
|
||||
best_idx = idx
|
||||
best_act = act
|
||||
|
||||
if best_act:
|
||||
logger.warning(f"🧠 [Brain] Extracted action '{best_act}' from verbose LLM output.")
|
||||
return best_act
|
||||
|
||||
logger.warning(f"🧠 [Brain] LLM returned an invalid action or no action found: '{result[:100]}...'. Falling back.")
|
||||
except Exception as e:
|
||||
logger.debug(f"🧠 [Brain] Error querying LLM: {e}")
|
||||
|
||||
return None
|
||||
@@ -109,7 +109,7 @@ class PathMemory:
|
||||
try:
|
||||
from qdrant_client import models
|
||||
|
||||
point_id = self._db._get_id(seed)
|
||||
point_id = self._db.generate_uuid(seed)
|
||||
self._db.client.delete(
|
||||
collection_name=self._db.collection_name, points_selector=models.PointIdsList(points=[point_id])
|
||||
)
|
||||
|
||||
@@ -17,7 +17,14 @@ class GoalPlanner:
|
||||
def __init__(self, username: str):
|
||||
self.knowledge = NavigationKnowledge(username)
|
||||
|
||||
def plan_next_step(self, goal: str, screen: Dict[str, Any], explored_nav_actions: set = None) -> Optional[str]:
|
||||
def plan_next_step(
|
||||
self,
|
||||
goal: str,
|
||||
screen: Dict[str, Any],
|
||||
explored_nav_actions: set = None,
|
||||
action_failures: dict = None,
|
||||
visited_screens: set = None,
|
||||
) -> Optional[str]:
|
||||
"""Plans the NEXT single action to take toward the goal."""
|
||||
screen_type = screen["screen_type"]
|
||||
available = screen.get("available_actions", [])
|
||||
@@ -34,7 +41,9 @@ class GoalPlanner:
|
||||
|
||||
# ── 3. Am I on the right screen? If not, navigate there ──
|
||||
selected_tab = screen.get("selected_tab")
|
||||
nav_action = self._plan_navigation(goal_lower, screen_type, available, selected_tab, explored_nav_actions)
|
||||
nav_action = self._plan_navigation(
|
||||
goal_lower, screen_type, available, selected_tab, explored_nav_actions, action_failures, visited_screens
|
||||
)
|
||||
if nav_action:
|
||||
return nav_action
|
||||
|
||||
@@ -70,6 +79,8 @@ class GoalPlanner:
|
||||
available: List[str],
|
||||
selected_tab: Optional[str] = None,
|
||||
explored_nav_actions: set = None,
|
||||
action_failures: dict = None,
|
||||
visited_screens: set = None,
|
||||
) -> Optional[str]:
|
||||
"""If we're on the wrong screen, figure out how to navigate.
|
||||
|
||||
@@ -89,10 +100,67 @@ class GoalPlanner:
|
||||
logger.debug(f"🛡️ [Aversive Filter] Masking trapped action: '{action}'")
|
||||
available = safe_available
|
||||
|
||||
# ── 1. HD Map Routing (Primary Strategy) ──
|
||||
visited_screens = visited_screens or set()
|
||||
|
||||
# 0b. No-Op Guard & Anti-Loop Guard:
|
||||
# - Strip tab actions that navigate to the CURRENT screen.
|
||||
# - Strip actions that navigate to PREVIOUSLY VISITED screens (except back-tracking).
|
||||
noop_actions = set()
|
||||
for action in available:
|
||||
expected = ScreenTopology.expected_screen_for_action(action, screen_type)
|
||||
if expected == screen_type:
|
||||
noop_actions.add(action)
|
||||
logger.debug(f"🛡️ [No-Op Guard] Stripping '{action}' — leads back to {screen_type.name}")
|
||||
elif expected in visited_screens and action != "press back":
|
||||
noop_actions.add(action)
|
||||
logger.debug(f"🛡️ [Anti-Loop Guard] Stripping '{action}' — leads to visited {expected.name}")
|
||||
|
||||
# Also strip actions where the HD Map says they go TO the current screen from OTHER screens
|
||||
for src_screen, transitions in ScreenTopology.TRANSITIONS.items():
|
||||
if src_screen == screen_type:
|
||||
continue # We already handled this screen's own transitions
|
||||
for action, dest in transitions.items():
|
||||
if dest == screen_type and action in available:
|
||||
noop_actions.add(action)
|
||||
logger.debug(
|
||||
f"🛡️ [No-Op Guard] Stripping '{action}' — known to navigate to current {screen_type.name}"
|
||||
)
|
||||
elif dest in visited_screens and action in available and action != "press back":
|
||||
noop_actions.add(action)
|
||||
logger.debug(f"🛡️ [Anti-Loop Guard] Stripping '{action}' — known to navigate to visited {dest.name}")
|
||||
|
||||
available = [a for a in available if a not in noop_actions]
|
||||
|
||||
# Build avoid_actions for HD Map route planning
|
||||
avoid_actions = (explored_nav_actions or set()).copy()
|
||||
if action_failures:
|
||||
for key, count in action_failures.items():
|
||||
if isinstance(key, tuple) and len(key) == 2:
|
||||
scr, act = key
|
||||
if scr == screen_type and count >= 2: # MAX_RETRIES is 2 in goap
|
||||
avoid_actions.add(act)
|
||||
else:
|
||||
if count >= 2:
|
||||
avoid_actions.add(key)
|
||||
|
||||
target_screen = ScreenTopology.goal_to_target_screen(goal)
|
||||
|
||||
# ── 1. HD Map Pre-Check for Dead Ends ──
|
||||
# If the topological map KNOWS the target is unreachable due to action_failures,
|
||||
# we must preempt the Brain from blindly routing into a dead end.
|
||||
if target_screen and target_screen != screen_type:
|
||||
route = ScreenTopology.find_route(screen_type, target_screen, avoid_actions=avoid_actions)
|
||||
if route is None and ScreenTopology.find_route(screen_type, target_screen):
|
||||
logger.warning(
|
||||
f"🛡️ [HD Map] Target {target_screen.name} is unreachable due to masked edges! Preventing Brain from blind routing."
|
||||
)
|
||||
return None
|
||||
|
||||
# ── 2. HD Map Routing (Primary Strategy for Navigation) ──
|
||||
# Ground UI transitions in structural invariants. If the topological map knows the route, use it.
|
||||
target_screen = ScreenTopology.goal_to_target_screen(goal)
|
||||
if target_screen and target_screen != screen_type:
|
||||
route = ScreenTopology.find_route(screen_type, target_screen)
|
||||
route = ScreenTopology.find_route(screen_type, target_screen, avoid_actions=avoid_actions)
|
||||
if route:
|
||||
next_action, next_screen = route[0]
|
||||
# Verify action isn't explored/trapped
|
||||
@@ -104,9 +172,20 @@ class GoalPlanner:
|
||||
)
|
||||
return next_action
|
||||
else:
|
||||
logger.warning(f"🛡️ [HD Map] Route action '{next_action}' is trapped. Falling back.")
|
||||
logger.warning(f"🛡️ [HD Map] Route action '{next_action}' is trapped. Skipping HD Map.")
|
||||
else:
|
||||
logger.debug(f"🛡️ [HD Map] Route action '{next_action}' already explored. Falling back.")
|
||||
logger.debug(
|
||||
f"🛡️ [HD Map] Route action '{next_action}' already explored and failed. Skipping HD Map."
|
||||
)
|
||||
|
||||
# ── 3. Brain-Driven Decision Making (Fallback / Discovery) ──
|
||||
# For non-navigation goals or when the HD Map is incomplete.
|
||||
from GramAddict.core.navigation.brain import ask_brain_for_action
|
||||
|
||||
brain_action = ask_brain_for_action(goal, screen_type.name, available, avoid_actions)
|
||||
if brain_action:
|
||||
logger.info(f"🧠 [Brain] Decided to execute: '{brain_action}' (to achieve: '{goal}')")
|
||||
return brain_action
|
||||
|
||||
# ── 2. Learned Knowledge (Qdrant) ──
|
||||
required_screens = self.knowledge.get_requirements(goal)
|
||||
@@ -131,7 +210,7 @@ class GoalPlanner:
|
||||
# 5. Find the action we need to take (from learned knowledge or HD map)
|
||||
for target_screen in required_screens:
|
||||
# Try HD Map first!
|
||||
route = ScreenTopology.find_route(screen_type, target_screen)
|
||||
route = ScreenTopology.find_route(screen_type, target_screen, avoid_actions=avoid_actions)
|
||||
if route:
|
||||
next_action, next_screen = route[0]
|
||||
if next_action not in (explored_nav_actions or set()):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
@@ -6,6 +7,55 @@ from GramAddict.core.perception.spatial_parser import SpatialNode
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_yes_no(response: str) -> Optional[bool]:
|
||||
"""Parses a VLM response to find a definitive YES or NO without substring-matching 'not' or 'now'."""
|
||||
text = response.strip()
|
||||
|
||||
# Try parsing as JSON first
|
||||
if text.startswith("{"):
|
||||
try:
|
||||
data = json.loads(text)
|
||||
for k, v in data.items():
|
||||
if str(k).strip().upper() == "YES" or str(v).strip().upper() == "YES":
|
||||
return True
|
||||
if str(k).strip().upper() == "NO" or str(v).strip().upper() == "NO":
|
||||
return False
|
||||
if str(k).strip().lower() == "success" and isinstance(v, bool):
|
||||
return v
|
||||
|
||||
# If it is valid JSON but we couldn't definitively find YES/NO,
|
||||
# do NOT fall through to text matching
|
||||
return None
|
||||
except Exception:
|
||||
# Prevent JSON parsing fall-throughs
|
||||
return None
|
||||
|
||||
text_lower = text.lower()
|
||||
if text_lower.startswith("yes"):
|
||||
return True
|
||||
if text_lower.startswith("no") and not text_lower.startswith("now") and not text_lower.startswith("not"):
|
||||
return False
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Semantic Match Keywords — SSOT for intent → element validation
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
# Maps toggle-intent keywords to required element markers.
|
||||
# If the intent contains the key, the clicked element MUST
|
||||
# contain at least one of the corresponding markers in its
|
||||
# text, content_desc, or resource_id.
|
||||
# ZERO MAINTENANCE: Only English words and resource_id fragments allowed.
|
||||
# No localized strings — the bot must work on any device language.
|
||||
TOGGLE_INTENT_MARKERS = {
|
||||
"follow": ["follow", "button_follow"],
|
||||
"like": ["like", "heart", "button_like"],
|
||||
"save": ["save", "saved", "bookmark"],
|
||||
}
|
||||
|
||||
|
||||
class ActionMemory:
|
||||
"""
|
||||
Handles the caching, tracking, and negative reinforcement (unlearning) of UI interactions.
|
||||
@@ -36,7 +86,11 @@ class ActionMemory:
|
||||
logger.debug(f"🧠 [ActionMemory] Tracking tentative click for intent: '{intent}' -> {semantic_string}")
|
||||
|
||||
def confirm_click(self, intent: str = None):
|
||||
"""Positive Reinforcement: Confirms the last click was successful."""
|
||||
"""Positive Reinforcement: Confirms the last click was successful.
|
||||
|
||||
Guard: Refuses to store in Qdrant if the clicked element does not
|
||||
semantically match the intent. Prevents memory poisoning.
|
||||
"""
|
||||
ctx = self._last_click_context
|
||||
if not ctx:
|
||||
return
|
||||
@@ -44,7 +98,19 @@ class ActionMemory:
|
||||
if intent and ctx["intent"] != intent:
|
||||
return
|
||||
|
||||
logger.info(f"✅ [ActionMemory] Confirming success for '{ctx['intent']}'. Boosting confidence.")
|
||||
# ── Semantic Mismatch Guard ──
|
||||
if not _intent_matches_node(ctx["intent"], ctx["semantic_string"]):
|
||||
logger.warning(
|
||||
f"🛡️ [ActionMemory] BLOCKED confirm_click for '{ctx['intent']}' — "
|
||||
f"clicked element does not match intent: {ctx['semantic_string']}"
|
||||
)
|
||||
self._last_click_context = None
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"✅ [ActionMemory] Confirming success for '{ctx['intent']}'. Boosting confidence.",
|
||||
extra={"color": "\x1b[32m"},
|
||||
)
|
||||
|
||||
# Store or boost in Qdrant
|
||||
try:
|
||||
@@ -68,7 +134,9 @@ class ActionMemory:
|
||||
if intent and ctx["intent"] != intent:
|
||||
return
|
||||
|
||||
logger.warning(f"❌ [ActionMemory] Click failed for '{ctx['intent']}'. Applying penalty.")
|
||||
logger.warning(
|
||||
f"❌ [ActionMemory] Click failed for '{ctx['intent']}'. Applying penalty.", extra={"color": "\x1b[31m"}
|
||||
)
|
||||
|
||||
try:
|
||||
self.ui_memory.decay_confidence(ctx["intent"], ctx["xml_context"])
|
||||
@@ -77,20 +145,225 @@ class ActionMemory:
|
||||
|
||||
self._last_click_context = None
|
||||
|
||||
def verify_success(self, intent: str, pre_click_xml: str, post_click_xml: str) -> Optional[bool]:
|
||||
def verify_success(
|
||||
self, intent: str, pre_click_xml: str, post_click_xml: str, device=None, confidence: float = 0.0
|
||||
) -> Optional[bool]:
|
||||
"""
|
||||
Structural verification: Did the UI actually change after the click?
|
||||
Structural and Visual verification: Did the UI actually change after the click?
|
||||
"""
|
||||
# Specific check for explore grid
|
||||
if "first image in explore grid" in intent or "grid item" in intent:
|
||||
if "row_feed_photo_imageview" in post_click_xml or "row_feed_button_like" in post_click_xml:
|
||||
intent_lower = intent.lower()
|
||||
post_xml_lower = post_click_xml.lower()
|
||||
|
||||
# Specific check for opening a post (from explore/profile grid)
|
||||
if "view a post" in intent_lower or "first image" in intent_lower or "grid item" in intent_lower:
|
||||
if (
|
||||
"row_feed_photo_imageview" in post_xml_lower
|
||||
or "row_feed_button_like" in post_xml_lower
|
||||
or "clips_viewer_view_pager" in post_xml_lower
|
||||
):
|
||||
return True
|
||||
if "explore_action_bar" in post_click_xml and "row_feed_button_like" not in post_click_xml:
|
||||
return None # Still on grid, inconclusive
|
||||
if (
|
||||
"explore_action_bar" in post_xml_lower
|
||||
and "row_feed_button_like" not in post_xml_lower
|
||||
and "clips_viewer" not in post_xml_lower
|
||||
):
|
||||
logger.warning(f"⚠️ [ActionMemory] Still on grid after trying to '{intent}'. Verification FAIL.")
|
||||
return False # Still on grid, definitely failed
|
||||
|
||||
if abs(len(pre_click_xml) - len(post_click_xml)) > 50:
|
||||
logger.debug(f"🧠 [ActionMemory] Structural change detected for '{intent}'. Verification PASS.")
|
||||
return True
|
||||
# Specific check for opening a profile
|
||||
if "profile" in intent_lower or "author" in intent_lower or "username" in intent_lower:
|
||||
if "profile_header_container" in post_xml_lower:
|
||||
logger.info("✅ [ActionMemory] Structural check confirmed profile navigation success.")
|
||||
return True
|
||||
else:
|
||||
logger.warning(
|
||||
f"⚠️ [ActionMemory] Profile header NOT found after trying to '{intent}'. Verification FAIL."
|
||||
)
|
||||
return False
|
||||
|
||||
logger.warning(f"⚠️ [ActionMemory] No structural change detected for '{intent}'. Verification FAIL.")
|
||||
return False
|
||||
# Specific check for navigating to Home Feed
|
||||
if "home feed" in intent_lower or "home tab" in intent_lower:
|
||||
if "main_feed_action_bar" in post_xml_lower:
|
||||
logger.info("✅ [ActionMemory] Structural check confirmed Home Feed navigation success.")
|
||||
return True
|
||||
|
||||
# Specific check for navigating to Explore Feed
|
||||
if "explore feed" in intent_lower or "explore tab" in intent_lower or "search" in intent_lower:
|
||||
if "explore_action_bar" in post_xml_lower or "action_bar_search_edit_text" in post_xml_lower:
|
||||
logger.info("✅ [ActionMemory] Structural check confirmed Explore Feed navigation success.")
|
||||
return True
|
||||
|
||||
state_toggles = ["like", "save", "follow", "heart"]
|
||||
is_toggle = any(t in intent_lower for t in state_toggles)
|
||||
|
||||
# ── VLM Verification Fallback ──
|
||||
|
||||
# If we are highly confident (e.g. pulled from Qdrant memory), bypass heavy VLM
|
||||
if device and confidence < 0.95:
|
||||
logger.info(
|
||||
f"👁️ [ActionMemory] Confidence ({confidence:.2f}) < 0.95. Handing over verification for '{intent}' to VLM visual analysis..."
|
||||
)
|
||||
from GramAddict.core.perception.semantic_evaluator import SemanticEvaluator
|
||||
|
||||
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:
|
||||
prompt += (
|
||||
"If the intent was 'follow', did the button change its visual state to indicate an active subscription? "
|
||||
"If it was 'like', is the heart icon clearly active/filled? "
|
||||
"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 += (
|
||||
f"Does the current screen match the expected outcome of '{intent}'? "
|
||||
f"For example, if the intent was to open a post/photo, are you looking at a post view (not a user profile or story)? "
|
||||
f"If the intent was to open a profile, are you on a profile page? "
|
||||
f"If the intent was to go back, are you on the previous screen? "
|
||||
)
|
||||
prompt += "Answer ONLY with the word YES or NO."
|
||||
|
||||
try:
|
||||
screenshot = device.get_screenshot_b64()
|
||||
if not screenshot:
|
||||
raise ValueError("No screenshot available from device")
|
||||
response = evaluator._query_vlm(prompt, screenshot)
|
||||
|
||||
decision = _parse_yes_no(response) if response else None
|
||||
|
||||
if decision is True:
|
||||
logger.debug(f"🧠 [ActionMemory] VLM visually confirmed success for '{intent}'.")
|
||||
return True
|
||||
elif decision is False:
|
||||
logger.warning(
|
||||
f"⚠️ [ActionMemory] VLM visual verification FAILED for '{intent}'. VLM replied: '{response}'"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
# VLM returned ambiguous response (JSON, mixed signals, etc.)
|
||||
# Don't treat as hard failure — fall through to structural delta verification
|
||||
logger.info(
|
||||
f"🧠 [ActionMemory] VLM response for '{intent}' was not YES/NO "
|
||||
f"(got: '{response[:80]}...'). Falling through to structural verification."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to query VLM for visual verification: {e}")
|
||||
# Fallthrough to structural delta if VLM crashes
|
||||
|
||||
# ── Pre-Structural Semantic Gate ──
|
||||
if is_toggle and self._last_click_context:
|
||||
if not _intent_matches_node(intent, self._last_click_context["semantic_string"]):
|
||||
logger.warning(
|
||||
f"🛡️ [ActionMemory] Semantic mismatch: '{intent}' does not match "
|
||||
f"clicked element {self._last_click_context['semantic_string']}. Verification FAIL."
|
||||
)
|
||||
return False
|
||||
|
||||
# Fallback to structural delta
|
||||
logger.info(f"DEBUG: len(pre_click_xml)={len(pre_click_xml)} len(post_click_xml)={len(post_click_xml)}")
|
||||
diff = abs(len(pre_click_xml) - len(post_click_xml))
|
||||
logger.info(f"DEBUG: diff={diff}")
|
||||
|
||||
if is_toggle:
|
||||
if diff > 1000:
|
||||
logger.warning(
|
||||
f"⚠️ [ActionMemory] Massive structural shift ({diff} chars) for state-toggle '{intent}'. Navigated away by mistake? Verification FAIL."
|
||||
)
|
||||
return False
|
||||
if diff > 0:
|
||||
logger.debug(f"🧠 [ActionMemory] Structural delta detected for toggle '{intent}'. Verification PASS.")
|
||||
return True
|
||||
logger.warning(
|
||||
f"⚠️ [ActionMemory] Zero structural shift (diff={diff}) for state-toggle '{intent}'. Verification FAIL."
|
||||
)
|
||||
return False
|
||||
# If the intent is an abstract goal (like "find customers"), diff > 50 is NOT enough.
|
||||
# We must force visual VLM confirmation because clicking the wrong thing (like "Create highlight")
|
||||
# also produces a large diff but achieves the wrong goal.
|
||||
if diff > 50:
|
||||
# Is it a standard structural transition?
|
||||
from GramAddict.core.screen_topology import ScreenTopology
|
||||
|
||||
# We don't have screen type here, so we just check if it's in the HD Map keys
|
||||
logger.info(f"DEBUG: intent is '{intent}'")
|
||||
logger.info(
|
||||
f"DEBUG: TRANSITIONS keys are: {[list(t.keys()) for t in ScreenTopology.TRANSITIONS.values()]}"
|
||||
)
|
||||
is_standard = any(intent in transitions for transitions in ScreenTopology.TRANSITIONS.values())
|
||||
logger.info(f"DEBUG: is_standard={is_standard}")
|
||||
|
||||
if is_standard:
|
||||
logger.debug(
|
||||
f"🧠 [ActionMemory] Structural change detected for known navigation '{intent}'. Verification PASS."
|
||||
)
|
||||
return True
|
||||
else:
|
||||
logger.info(
|
||||
f"👁️ [ActionMemory] Abstract intent '{intent}' caused UI change. Forcing VLM visual verification..."
|
||||
)
|
||||
# For abstract intents, we must visually verify if it actually helped!
|
||||
# If device is available, we use VLM. If not, we fail safe.
|
||||
if device:
|
||||
from GramAddict.core.perception.semantic_evaluator import SemanticEvaluator
|
||||
|
||||
evaluator = SemanticEvaluator()
|
||||
prompt = f"The user just attempted to perform the action: '{intent}'. Does the current screen match the expected outcome? Answer ONLY with the word YES or NO."
|
||||
try:
|
||||
response = evaluator._query_vlm(prompt, device.get_screenshot_b64())
|
||||
decision = _parse_yes_no(response) if response else None
|
||||
if decision is True:
|
||||
return True
|
||||
else:
|
||||
logger.warning(
|
||||
f"⚠️ [ActionMemory] VLM rejected success for abstract intent '{intent}'. Response: '{response}'"
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"VLM visual verification failed: {e}")
|
||||
|
||||
logger.warning(f"⚠️ [ActionMemory] Cannot visually verify abstract intent '{intent}'. Failing safe.")
|
||||
return False
|
||||
|
||||
# If diff <= 50 for non-toggle
|
||||
logger.warning(
|
||||
f"⚠️ [ActionMemory] Insufficient structural change (diff={diff}) for non-toggle '{intent}'. Verification FAIL."
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _intent_matches_node(intent: str, semantic_string: str) -> bool:
|
||||
"""Checks if the clicked element semantically matches the toggle intent.
|
||||
|
||||
For toggle intents (follow, like, save), the clicked element MUST contain
|
||||
at least one of the required keywords in its text/desc/id. This prevents
|
||||
photo grid items, captions, and other unrelated elements from being
|
||||
falsely confirmed as successful interactions.
|
||||
|
||||
For non-toggle intents, returns True (no restriction).
|
||||
"""
|
||||
intent_lower = intent.lower()
|
||||
semantic_lower = semantic_string.lower()
|
||||
|
||||
for intent_keyword, required_markers in TOGGLE_INTENT_MARKERS.items():
|
||||
if intent_keyword in intent_lower:
|
||||
if any(marker in semantic_lower for marker in required_markers):
|
||||
return True
|
||||
logger.debug(
|
||||
f"🛡️ [SemanticGuard] Intent '{intent}' requires markers "
|
||||
f"{required_markers} but element has: {semantic_string}"
|
||||
)
|
||||
return False
|
||||
|
||||
# Non-toggle intents pass through
|
||||
return True
|
||||
|
||||
@@ -46,15 +46,15 @@ def has_carousel_in_view(xml_dump: str) -> bool:
|
||||
return any(ind in xml_dump for ind in CAROUSEL_INDICATORS)
|
||||
|
||||
|
||||
def extract_post_content(context_xml: str) -> dict:
|
||||
def extract_post_content(context_xml: str, device=None) -> dict:
|
||||
"""
|
||||
Extracts meaningful content data from the current feed post's XML.
|
||||
This is the BOT'S EYES — what it actually "sees" about each post.
|
||||
|
||||
Returns:
|
||||
{'username': str, 'description': str, 'caption': str}
|
||||
{'username': str, 'description': str, 'caption': str, 'username_missing': bool}
|
||||
"""
|
||||
result = {"username": "", "description": "", "caption": ""}
|
||||
result = {"username": "", "description": "", "caption": "", "username_missing": False}
|
||||
|
||||
try:
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
@@ -62,16 +62,87 @@ def extract_post_content(context_xml: str) -> dict:
|
||||
telepath = TelepathicEngine.get_instance()
|
||||
|
||||
# 1. Learn/extract post author dynamically
|
||||
author_node = telepath.find_best_node(context_xml, "post author username header", min_confidence=0.75)
|
||||
# 🛡️ Structural Fast-Path: Prioritize deterministic IDs over AI guesses
|
||||
# Try structural ID fast-path first (100% deterministic)
|
||||
author_node = None
|
||||
try:
|
||||
root = ET.fromstring(context_xml)
|
||||
for node in root.iter("node"):
|
||||
res_id = node.attrib.get("resource-id", "")
|
||||
if "row_feed_photo_profile_name" in res_id or "clips_author_username" in res_id or "profile_header_name" in res_id:
|
||||
author_node = {"original_attribs": node.attrib}
|
||||
logger.debug(f"Identified author_node via structural ID: {res_id}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"XML parse error in author structural fast-path: {e}")
|
||||
|
||||
# 🛡️ Anti-Hallucination Guard: The author header is always near the top. Ignore names in the comment section.
|
||||
if author_node and author_node.get("y", 0) < 1000 and author_node.get("original_attribs", {}).get("text"):
|
||||
result["username"] = author_node["original_attribs"]["text"].strip()
|
||||
# Fallback to Telepathic Engine if structural ID is missing
|
||||
if not author_node:
|
||||
author_node = telepath.find_best_node(
|
||||
context_xml, "post author username text (exclude bottom tabs)", min_confidence=0.75, device=device
|
||||
)
|
||||
logger.debug(f"Telepathic fallback for author_node: {author_node}")
|
||||
|
||||
# 🛡️ Anti-Hallucination Guard: Ensure we actually found text.
|
||||
if author_node:
|
||||
attribs = author_node.get("original_attribs", {})
|
||||
text = attribs.get("text", "").strip()
|
||||
desc = attribs.get("content_desc", "").strip()
|
||||
|
||||
if text:
|
||||
result["username"] = text
|
||||
elif desc:
|
||||
result["username"] = desc
|
||||
else:
|
||||
# If the VLM selected a container (like clips_author_info_component),
|
||||
# extract text from its children.
|
||||
logger.debug("Author node lacks text/desc. Searching children for username...")
|
||||
bounds = attribs.get("bounds")
|
||||
if bounds:
|
||||
try:
|
||||
# Re-parse to find children within bounds
|
||||
import re
|
||||
match = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds)
|
||||
if match:
|
||||
l, t, r, b = map(int, match.groups())
|
||||
|
||||
# Fallback: scan all nodes in XML and see if they are inside these bounds
|
||||
possible_texts = []
|
||||
possible_descs = []
|
||||
for n in ET.fromstring(context_xml).iter("node"):
|
||||
child_bounds = n.attrib.get("bounds")
|
||||
child_text = n.attrib.get("text", "").strip()
|
||||
child_desc = n.attrib.get("content-desc", "").strip()
|
||||
|
||||
if child_bounds and (child_text or child_desc):
|
||||
cm = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", child_bounds)
|
||||
if cm:
|
||||
cl, ct, cr, cb = map(int, cm.groups())
|
||||
# Check if child is strictly inside the container
|
||||
if cl >= l and ct >= t and cr <= r and cb <= b:
|
||||
if child_text:
|
||||
possible_texts.append(child_text)
|
||||
if child_desc and "Profile picture" not in child_desc:
|
||||
possible_descs.append(child_desc)
|
||||
|
||||
if possible_texts:
|
||||
result["username"] = possible_texts[0]
|
||||
logger.debug(f"Extracted username '{result['username']}' from child node text.")
|
||||
elif possible_descs:
|
||||
result["username"] = possible_descs[0]
|
||||
logger.debug(f"Extracted username '{result['username']}' from child node desc.")
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to extract username from children: {e}")
|
||||
|
||||
# 2. Learn/extract post media description dynamically
|
||||
media_node = telepath.find_best_node(context_xml, "post media content", min_confidence=0.35)
|
||||
if media_node and media_node.get("original_attribs", {}).get("desc"):
|
||||
result["description"] = media_node["original_attribs"]["desc"].strip()
|
||||
media_node = telepath.find_best_node(
|
||||
context_xml,
|
||||
"post media content (the actual image or video, exclude bottom tabs)",
|
||||
min_confidence=0.35,
|
||||
device=device,
|
||||
)
|
||||
if media_node and media_node.get("original_attribs", {}).get("content_desc"):
|
||||
result["description"] = media_node["original_attribs"]["content_desc"].strip()
|
||||
|
||||
# 3. Visible caption text (heuristic fallback if node isn't explicitly found)
|
||||
# Search all nodes for text that contains the username to find the caption body
|
||||
@@ -85,6 +156,11 @@ def extract_post_content(context_xml: str) -> dict:
|
||||
except Exception as e:
|
||||
logger.warning(f"Error extracting post content autonomously: {e}")
|
||||
|
||||
# REGRESSION FIX 2026-05-01: Flag unreliable data when username is empty
|
||||
if not result["username"]:
|
||||
result["username_missing"] = True
|
||||
logger.warning("⚠️ [PostDataExtraction] Username is empty — data may be unreliable.")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -1,113 +1,844 @@
|
||||
from typing import List, Optional
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from GramAddict.core.perception.spatial_parser import SpatialNode
|
||||
|
||||
# Navigation tab intent → resource_id keyword mapping
|
||||
_NAV_TAB_MAP = {
|
||||
"tap home tab": "feed_tab",
|
||||
"tap explore tab": "search_tab",
|
||||
"tap reels tab": "clips_tab",
|
||||
"tap profile tab": "profile_tab",
|
||||
"tap messages tab": "direct_tab",
|
||||
}
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _humanize_desc(desc: str) -> str:
|
||||
"""
|
||||
Inserts a space between numbers and letters to fix Instagram's concatenated content-desc.
|
||||
Example: "991following" -> "991 following", "140Kfollowers" -> "140K followers"
|
||||
"""
|
||||
if not desc:
|
||||
return ""
|
||||
import re
|
||||
|
||||
return re.sub(r"(\d[KMBkmb]?)([a-z])", r"\1 \2", desc)
|
||||
|
||||
|
||||
class IntentResolver:
|
||||
"""
|
||||
Translates natural language intents into spatial constraints and node filtering.
|
||||
Replaces the generic text/regex matching with structural intelligence.
|
||||
Vision-First Intent Resolver.
|
||||
|
||||
Resolves UI intents by SEEING the screen, not by parsing text descriptions.
|
||||
Uses Set-of-Mark (SoM) visual prompting: annotates a screenshot with numbered
|
||||
bounding boxes around clickable candidates, sends the annotated image to the VLM,
|
||||
and lets the VLM visually decide which box to tap.
|
||||
|
||||
CRITICAL ARCHITECTURE RULE: Language Agnosticism & Structural Determinism
|
||||
- NO hardcoded localized UI strings (e.g. "follow", "message", "like") are permitted.
|
||||
- All non-VLM resolution logic MUST rely strictly on spatial coordinates (e.g. center_x bounds)
|
||||
or structural Android resource IDs.
|
||||
|
||||
Architecture:
|
||||
1. Navigation tabs → structural zone guard (bottom 15%, resource-id)
|
||||
2. Everything else → Visual Discovery (screenshot + numbered boxes + VLM)
|
||||
3. Fallback → text-based VLM (when no device/screenshot available)
|
||||
"""
|
||||
|
||||
def resolve(
|
||||
self, intent_description: str, candidates: List[SpatialNode], screen_height: int = 2400
|
||||
) -> Optional[SpatialNode]:
|
||||
"""
|
||||
Finds the best matching node for a given intent autonomously.
|
||||
# ──────────────────────────────────────────────
|
||||
# Structural Guards
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
Navigation tab intents use a structural Zone Guard (bottom 15% of screen)
|
||||
to guarantee we click the actual nav bar, not a content-area element.
|
||||
All other intents delegate to VLM resolution.
|
||||
def filter_navigation_conflicts(
|
||||
self, candidates: List[SpatialNode], intent_description: str, screen_height: int = 2400
|
||||
) -> List[SpatialNode]:
|
||||
"""
|
||||
Prevents VLM from confusing navigation-bar buttons (Back, Close)
|
||||
with bottom tab-bar buttons (Home, Profile, Search).
|
||||
|
||||
Production bug 2026-04-30: VLM picked action_bar_button_back
|
||||
for "tap profile tab" → account switch failed.
|
||||
|
||||
Production bug 2026-05-01: VLM picked profile_tab (desc='Profile')
|
||||
for "post author username text" → navigated to own profile instead.
|
||||
|
||||
Rules:
|
||||
- For tab intents: exclude nodes with "back" in resource_id or
|
||||
content_desc == "Back"
|
||||
- For back/close intents: no filtering (Back is the correct target)
|
||||
- For author/username intents: exclude bottom navigation tabs
|
||||
"""
|
||||
intent_lower = intent_description.lower()
|
||||
|
||||
# Only apply for REAL tab navigation intents.
|
||||
# REGRESSION FIX 2026-05-02: "tab" as a substring was too broad.
|
||||
# Intent "post author username text (exclude bottom tabs)" matched
|
||||
# because it contained "tab" → Tab Height Guard nuked the author node.
|
||||
# Now we require specific tab navigation patterns:
|
||||
# - "tap profile tab", "tap home tab", "explore tab"
|
||||
# - NOT "exclude bottom tabs", "tabbar", random mentions
|
||||
import re
|
||||
|
||||
_TAB_PATTERN = re.compile(
|
||||
r"\btap\s+\w+\s+tab\b" # "tap profile tab", "tap home tab"
|
||||
r"|\b\w+\s+tab\b" # "profile tab", "explore tab"
|
||||
r"|^tab\b", # "tab" at start of intent
|
||||
re.IGNORECASE,
|
||||
)
|
||||
filtered = []
|
||||
is_tab_intent = bool(_TAB_PATTERN.search(intent_lower)) and "back" not in intent_lower
|
||||
is_create_intent = "create" in intent_lower or "camera" in intent_lower or "story" in intent_lower
|
||||
# REGRESSION FIX 2026-05-01: Author/username intents must never pick nav tabs
|
||||
is_author_intent = any(kw in intent_lower for kw in ["author", "username", "post media"])
|
||||
|
||||
# Known bottom navigation tab resource_id suffixes
|
||||
NAV_TAB_SUFFIXES = ("_tab", "tab_icon", "navigation_bar")
|
||||
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
desc = (node.content_desc or "").lower()
|
||||
|
||||
is_back = "back" in rid or desc == "back"
|
||||
is_close = "close" in rid or desc == "close"
|
||||
is_create = "camera" in rid or "create" in rid or desc == "camera" or desc == "create" or "creation" in rid
|
||||
is_nav_tab = any(rid.endswith(s) for s in NAV_TAB_SUFFIXES)
|
||||
|
||||
if is_tab_intent and (is_back or is_close):
|
||||
logger.debug(
|
||||
f"🛡️ [Nav Conflict Guard] Excluded '{node.resource_id}' "
|
||||
f"(desc='{node.content_desc}') for tab intent '{intent_description}'"
|
||||
)
|
||||
continue
|
||||
|
||||
# NEW REGRESSION FIX 2026-05-01: Tab intents must never pick top-screen elements or headers
|
||||
# UPDATE: Actually, tabs are always at the very bottom. Filter out anything above 85% of screen height.
|
||||
if is_tab_intent:
|
||||
is_not_at_bottom = node.center_y < (screen_height * 0.85)
|
||||
if is_not_at_bottom:
|
||||
logger.debug(
|
||||
f"🛡️ [Tab Height Guard] Excluded non-bottom element '{node.resource_id}' "
|
||||
f"(desc='{node.content_desc}', y={node.center_y}) for tab intent '{intent_description}'"
|
||||
)
|
||||
continue
|
||||
# Tab intents should NEVER be content items
|
||||
if any(kw in desc for kw in ["reel by", "photo by", "photos by", "row ", "column "]):
|
||||
logger.debug(
|
||||
f"🛡️ [Content Tab Guard] Excluded content item '{node.resource_id}' "
|
||||
f"(desc='{node.content_desc}') for tab intent '{intent_description}'"
|
||||
)
|
||||
continue
|
||||
|
||||
if is_author_intent and is_nav_tab:
|
||||
logger.debug(
|
||||
f"🛡️ [Author Tab Guard] Excluded nav tab '{node.resource_id}' "
|
||||
f"(desc='{node.content_desc}') for author intent '{intent_description}'"
|
||||
)
|
||||
continue
|
||||
|
||||
if not is_create_intent and is_create:
|
||||
logger.debug(
|
||||
f"🛡️ [Creation Conflict Guard] Excluded '{node.resource_id}' "
|
||||
f"(desc='{node.content_desc}') for intent '{intent_description}'"
|
||||
)
|
||||
continue
|
||||
|
||||
# NEW REGRESSION FIX: Exclude interaction buttons (comment, like, share) when looking for author or media
|
||||
# This prevents the weak VLM from hallucinating bounding box numbers that point to "Comment".
|
||||
interaction_suffixes = ["comment", "like", "share", "send", "save", "button_icon", "scrubber"]
|
||||
is_interaction = any(s in rid for s in interaction_suffixes) or any(s in desc for s in interaction_suffixes)
|
||||
|
||||
# SPATIAL GUARD (NO HARDCODED STRINGS): The post author (avatar/name) is always strictly on the left side of the screen.
|
||||
# The "More options" (Report menu) button is strictly on the far right.
|
||||
# We enforce a mathematical boundary to prevent LLM hallucinations from clicking the report menu.
|
||||
if is_author_intent and node.center_x > (1080 * 0.75):
|
||||
logger.debug(
|
||||
f"🛡️ [Author Spatial Guard] Excluded far-right element '{node.resource_id}' "
|
||||
f"(x={node.center_x}) for author intent '{intent_description}'"
|
||||
)
|
||||
continue
|
||||
is_follow = "button_follow" in rid or "ufi_follow" in rid
|
||||
is_media_intent = "media content" in intent_lower or "image" in intent_lower or "video" in intent_lower
|
||||
|
||||
if (is_author_intent or is_media_intent) and (is_interaction or is_follow):
|
||||
logger.debug(
|
||||
f"🛡️ [Interaction Guard] Excluded non-target element '{node.resource_id}' "
|
||||
f"(desc='{node.content_desc}', text='{node.text}') for intent '{intent_description}'"
|
||||
)
|
||||
continue
|
||||
|
||||
# NEW REGRESSION FIX: Exclude stories and reels tray when looking for a POST author
|
||||
# This prevents the VLM from selecting the user's own story at the top of the feed
|
||||
is_post_author_intent = is_author_intent and "post" in intent_lower
|
||||
node_text_lower = (node.text or "").lower()
|
||||
is_story_or_reel = (
|
||||
"story" in rid
|
||||
or "story" in desc
|
||||
or "story" in node_text_lower
|
||||
or "reel" in rid
|
||||
or "reel" in desc
|
||||
or "reel" in node_text_lower
|
||||
)
|
||||
|
||||
if is_post_author_intent and is_story_or_reel:
|
||||
logger.debug(
|
||||
f"🛡️ [Post Author Story Guard] Excluded story/reel '{node.resource_id}' "
|
||||
f"(desc='{node.content_desc}') for post author intent '{intent_description}'"
|
||||
)
|
||||
continue
|
||||
|
||||
# NEW REGRESSION FIX: Exclude action bar titles (like 'For you') when looking for an author
|
||||
if is_author_intent and "action_bar_title" in rid:
|
||||
logger.debug(
|
||||
f"🛡️ [Author Action Bar Guard] Excluded action bar title '{node.resource_id}' "
|
||||
f"(desc='{node.content_desc}') for author intent '{intent_description}'"
|
||||
)
|
||||
continue
|
||||
|
||||
filtered.append(node)
|
||||
|
||||
return filtered
|
||||
|
||||
def pre_filter_candidates(self, candidates: List[SpatialNode]) -> List[SpatialNode]:
|
||||
"""
|
||||
Filters candidates by area, system UI, keyboard packages, and notifications.
|
||||
|
||||
Production Bug 2026-05-04: The Android soft keyboard (com.google.android.inputmethod.*)
|
||||
flooded the candidate pool with 30+ single-letter nodes (A, B, C, N, ...),
|
||||
causing the VLM to hallucinate keyboard keys as valid UI targets.
|
||||
"""
|
||||
return [
|
||||
n
|
||||
for n in candidates
|
||||
if 200 < n.area < 400000
|
||||
and "com.android.systemui" not in (n.resource_id or "")
|
||||
and "inputmethod" not in (n.resource_id or "")
|
||||
and "notification:" not in (n.content_desc or "").lower()
|
||||
and "per cent" not in (n.content_desc or "").lower()
|
||||
]
|
||||
|
||||
def has_keyboard_open(self, candidates: List[SpatialNode]) -> bool:
|
||||
"""
|
||||
Detects if the Android soft keyboard is currently visible
|
||||
by checking for input method package nodes in the candidate list.
|
||||
"""
|
||||
return any("inputmethod" in (n.resource_id or "") for n in candidates)
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Public API
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def resolve(
|
||||
self, intent_description: str, candidates: List[SpatialNode], device=None, screen_height: int = 2400
|
||||
) -> Optional[SpatialNode]:
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
intent_lower = intent_description.lower()
|
||||
|
||||
# ── Navigation Bar Zone Guard ──
|
||||
# When intent targets a nav tab, resolve structurally to the bottom nav zone.
|
||||
# This prevents the VLM from selecting content profile pictures instead of tabs.
|
||||
# The bottom navigation bar is always in the bottom 15% of the screen.
|
||||
tab_keyword = _NAV_TAB_MAP.get(intent_lower)
|
||||
if tab_keyword:
|
||||
nav_zone_y = int(screen_height * 0.85)
|
||||
nav_candidates = [
|
||||
n for n in candidates if n.y1 >= nav_zone_y and tab_keyword in (n.resource_id or "").lower()
|
||||
]
|
||||
if nav_candidates:
|
||||
return nav_candidates[0]
|
||||
# Fallback: broader search in nav zone by content_desc
|
||||
tab_label = intent_lower.replace("tap ", "").replace(" tab", "")
|
||||
nav_candidates = [
|
||||
n for n in candidates if n.y1 >= nav_zone_y and tab_label in (n.content_desc or "").lower()
|
||||
]
|
||||
if nav_candidates:
|
||||
return nav_candidates[0]
|
||||
return None
|
||||
|
||||
# If the intent is a high-level GOAL that accidentally leaked into the IntentResolver,
|
||||
# we explicitly block it from clicking random nodes.
|
||||
# IMPORTANT: Use exact match to avoid blocking "tap profile tab" when filtering "open profile"
|
||||
# Block abstract goals from leaking into node clicks
|
||||
abstract_goals = ["open profile", "open explore", "open following", "learn own profile"]
|
||||
if intent_lower in abstract_goals:
|
||||
return None
|
||||
|
||||
# 1. Ask the Telepathic VLM to find the best node
|
||||
import json
|
||||
# --- Strict Structural Fast-Paths ---
|
||||
# Bypass VLM for deterministically identifiable UI components
|
||||
if "message text box" in intent_lower or "message input" in intent_lower or "type message" in intent_lower:
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
text = (node.text or "").lower()
|
||||
if "composer_edittext" in rid or "message…" in text or "message..." in text:
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found message input field: {rid}")
|
||||
return node
|
||||
|
||||
if "last received message text" in intent_lower or "received message" in intent_lower:
|
||||
# Gather all message text views
|
||||
msg_nodes = [n for n in candidates if "direct_text_message_text_view" in (n.resource_id or "").lower()]
|
||||
if msg_nodes:
|
||||
# The last one in the XML is typically the most recent message at the bottom of the screen
|
||||
latest_msg = msg_nodes[-1]
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found last received message text: '{latest_msg.text}'")
|
||||
return latest_msg
|
||||
|
||||
if "send message button" in intent_lower:
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
desc = (node.content_desc or "").lower()
|
||||
text = (node.text or "").lower()
|
||||
if "send" in rid or "composer_button" in rid:
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found send button: {rid or desc or text}")
|
||||
return node
|
||||
|
||||
if "post author username" in intent_lower or "tap post username" in intent_lower:
|
||||
# 1. Feed Posts
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
if "row_feed_photo_profile_imageview" in rid:
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found post author avatar image: {node.content_desc}")
|
||||
return node
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
if "row_feed_photo_profile_name" in rid:
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found post author username text: {node.text}")
|
||||
return node
|
||||
|
||||
# 2. Reels (Clips)
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
if "clips_author_username" in rid or "clips_author_container" in rid:
|
||||
logger.info(
|
||||
f"🎯 [Structural Fast-Path] Found Reel author username: {node.text or node.content_desc}"
|
||||
)
|
||||
return node
|
||||
|
||||
if "feed post content" in intent_lower or "post media content" in intent_lower:
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
if "row_feed_photo_imageview" in rid or "zoomable_view_container" in rid:
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found feed post content: {rid}")
|
||||
return node
|
||||
|
||||
if "comment" in intent_lower and "button" in intent_lower:
|
||||
# First try the View all comments button
|
||||
for node in candidates:
|
||||
if (
|
||||
"view all comments" in (node.text or "").lower()
|
||||
or "view all comments" in (node.content_desc or "").lower()
|
||||
):
|
||||
logger.info(
|
||||
f"🎯 [Structural Fast-Path] Found comment button text: {node.text or node.content_desc}"
|
||||
)
|
||||
return node
|
||||
# Then try the icon itself if somehow clickable
|
||||
for node in candidates:
|
||||
if (
|
||||
"row_feed_button_comment" in (node.resource_id or "").lower()
|
||||
or "row_feed_textview_comments" in (node.resource_id or "").lower()
|
||||
):
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found comment button: {node.resource_id}")
|
||||
return node
|
||||
|
||||
if "like" in intent_lower and ("button" in intent_lower or "post" in intent_lower):
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
if "row_feed_button_like" in rid:
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found like button: {rid}")
|
||||
return node
|
||||
|
||||
if ("send" in intent_lower or "share" in intent_lower) and "post" in intent_lower and "button" in intent_lower:
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
if "row_feed_button_share" in rid or "direct_share_button" in rid or "button_share" in rid:
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found send/share post button: {rid}")
|
||||
return node
|
||||
|
||||
if "add to story" in intent_lower:
|
||||
# We skip structural fast-path for 'add to story' since it relies heavily on language/text strings
|
||||
# and let the VLM figure it out or rely on purely visual indicators.
|
||||
pass
|
||||
|
||||
if "share" in intent_lower and ("button" in intent_lower or "post" in intent_lower):
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
if "row_feed_button_share" in rid:
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found share button: {rid}")
|
||||
return node
|
||||
|
||||
if "save" in intent_lower and ("button" in intent_lower or "post" in intent_lower):
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
if "row_feed_button_save" in rid:
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found save button: {rid}")
|
||||
return node
|
||||
|
||||
if "follow" in intent_lower and "button" in intent_lower:
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
if "profile_header_follow_button" in rid or "inline_follow_button" in rid:
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found follow/following button: {rid}")
|
||||
return node
|
||||
|
||||
if "first post" in intent_lower or "first item" in intent_lower or "first search result" in intent_lower:
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
if "grid_card_layout_container" in rid or "image_button" in rid or "row_search_user" in rid:
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found first post/item: {rid}")
|
||||
return node
|
||||
|
||||
if "story ring" in intent_lower or "story tray" in intent_lower:
|
||||
story_nodes = []
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
desc = (node.content_desc or "").lower()
|
||||
text = (node.text or "").lower()
|
||||
# Instagram story tray avatars usually have this resource id and 'story' in the content description
|
||||
if ("avatar_image_view" in rid or "row_profile_header_imageview" in rid) and "story" in desc:
|
||||
# Ignore the user's explicit "Add to story" ring
|
||||
if "add to story" not in desc and "your story" not in text:
|
||||
story_nodes.append(node)
|
||||
|
||||
if story_nodes:
|
||||
# Sort horizontally (left-to-right)
|
||||
story_nodes.sort(key=lambda n: n.x1)
|
||||
|
||||
# Check if this is the home feed story tray (avatar_image_view without 'highlight' in desc)
|
||||
is_highlight = any("highlight" in (n.content_desc or "").lower() for n in story_nodes)
|
||||
if "avatar_image_view" in (story_nodes[0].resource_id or "").lower() and not is_highlight:
|
||||
if len(story_nodes) > 1:
|
||||
logger.info(
|
||||
f"🎯 [Structural Fast-Path] Found {len(story_nodes)} story rings. Skipping own profile. Picking second: '{story_nodes[1].content_desc}'"
|
||||
)
|
||||
return story_nodes[1]
|
||||
else:
|
||||
logger.warning(
|
||||
"🎯 [Structural Fast-Path] Only 1 story ring found on feed (likely own profile). Skipping to avoid modal trap."
|
||||
)
|
||||
return None
|
||||
else:
|
||||
# Profile header or other single-story views
|
||||
logger.info(
|
||||
f"🎯 [Structural Fast-Path] Found story ring avatar: {story_nodes[0].resource_id} (desc: '{story_nodes[0].content_desc}')"
|
||||
)
|
||||
return story_nodes[0]
|
||||
|
||||
# --- Navigation Tab Fast-Paths ---
|
||||
# Deterministically identify bottom navigation tabs to prevent VLM confusion
|
||||
tab_map = {
|
||||
"home tab": "feed_tab",
|
||||
"feed tab": "feed_tab",
|
||||
"reels tab": "clips_tab",
|
||||
"clips tab": "clips_tab",
|
||||
"explore tab": "search_tab",
|
||||
"search tab": "search_tab",
|
||||
"profile tab": "profile_tab",
|
||||
"message tab": "direct_tab",
|
||||
"direct tab": "direct_tab",
|
||||
}
|
||||
for intent_key, resource_suffix in tab_map.items():
|
||||
if intent_key in intent_lower:
|
||||
for node in candidates:
|
||||
rid = (node.resource_id or "").lower()
|
||||
if rid.endswith(f":id/{resource_suffix}"):
|
||||
logger.info(f"🎯 [Structural Fast-Path] Found {intent_key}: {rid}")
|
||||
return node
|
||||
|
||||
# --- Semantic Match Guard ---
|
||||
# If the intent explicitly quotes a target (e.g., "tap 'New Message'"),
|
||||
# we strictly filter candidates to those whose text or content_desc contains the quote.
|
||||
import re
|
||||
|
||||
quotes = re.findall(r"['\"](.*?)['\"]", intent_description)
|
||||
if quotes:
|
||||
target_text = quotes[0].lower()
|
||||
|
||||
# Only use the exact target string (no manual localized translation dictionaries!)
|
||||
localized_targets = [target_text]
|
||||
|
||||
semantic_candidates = []
|
||||
for node in candidates:
|
||||
n_text = (node.text or "").lower()
|
||||
n_desc = (node.content_desc or "").lower()
|
||||
|
||||
# Check if any of the localized targets match
|
||||
for loc_target in localized_targets:
|
||||
pattern = r"\b" + re.escape(loc_target) + r"\b"
|
||||
if re.search(pattern, n_text) or re.search(pattern, n_desc):
|
||||
semantic_candidates.append(node)
|
||||
break # Found a match, no need to check other localized targets
|
||||
|
||||
if semantic_candidates:
|
||||
if len(semantic_candidates) == 1:
|
||||
logger.info(f"🎯 [Semantic Guard] Exact match found for '{target_text}', skipping VLM.")
|
||||
return semantic_candidates[0]
|
||||
else:
|
||||
logger.info(
|
||||
f"🎯 [Semantic Guard] {len(semantic_candidates)} matches found for '{target_text}'. Reducing candidates for VLM."
|
||||
)
|
||||
candidates = semantic_candidates
|
||||
else:
|
||||
logger.warning(
|
||||
f"⚠️ [Semantic Guard] No candidates found containing '{target_text}'. Returning None to prevent hallucination."
|
||||
)
|
||||
return None
|
||||
|
||||
# ── PRIMARY PATH: Visual Discovery ──
|
||||
# If we have a device, the VLM SEES the screen and decides.
|
||||
if device is not None and (
|
||||
hasattr(device, "screenshot") or hasattr(getattr(device, "deviceV2", None), "screenshot")
|
||||
):
|
||||
print(f"DEBUG_INTENT: Entering Visual Discovery for '{intent_description}'")
|
||||
logger.info("📸 Device screenshot capability detected. Enforcing visual discovery.")
|
||||
return self._visual_discovery(intent_description, candidates, device, screen_height=screen_height)
|
||||
|
||||
print(f"DEBUG_INTENT: Falling back to Text-based VLM for '{intent_description}'")
|
||||
# --- Strict VLM Hallucination Guard (Text-only Fallback) ---
|
||||
# For known structural targets that the text-based VLM frequently hallucinates when they are missing,
|
||||
# we enforce a strict failure.
|
||||
if "following list" in intent_lower or "followers list" in intent_lower or "tap message button" in intent_lower:
|
||||
logger.warning(
|
||||
f"🛡️ [Hallucination Guard] Intent '{intent_description}' is a strict structural target. "
|
||||
"Since it wasn't resolved by fast-paths, it is missing. Rejecting VLM fallback."
|
||||
)
|
||||
return None
|
||||
|
||||
# ── FALLBACK: Text-based VLM resolution ──
|
||||
# Only used when device is unavailable (e.g., unit tests without screenshots).
|
||||
return self._text_based_resolve(intent_description, candidates, device, screen_height=screen_height)
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Visual Discovery (Set-of-Mark Prompting)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def _annotate_screenshot_with_candidates(
|
||||
self, device, candidates: List[SpatialNode]
|
||||
) -> Tuple[str, Dict[int, SpatialNode]]:
|
||||
"""
|
||||
Takes a screenshot and draws numbered bounding boxes around clickable candidates.
|
||||
|
||||
Returns:
|
||||
annotated_b64: Base64-encoded JPEG of the annotated screenshot.
|
||||
box_map: Dict mapping box number → SpatialNode for coordinate lookup.
|
||||
"""
|
||||
from PIL import ImageDraw
|
||||
|
||||
img = device.deviceV2.screenshot()
|
||||
|
||||
# Stage 1: Basic area filter + exclude system UI and notifications (ALREADY HANDLED in _visual_discovery)
|
||||
pre_filtered = candidates
|
||||
|
||||
# Stage 2: Spatial deduplication
|
||||
# A node could completely contain another.
|
||||
# If parent is clickable and child is not: suppress child (e.g. text inside button)
|
||||
# If parent is not clickable and child is: suppress parent (e.g. layout container around button)
|
||||
# If both are not clickable: suppress parent (keep the smaller, more specific text)
|
||||
# If both are clickable: keep both! (e.g. nested buttons like row and camera icon)
|
||||
def _contains(parent: SpatialNode, child: SpatialNode) -> bool:
|
||||
return (
|
||||
parent.x1 <= child.x1
|
||||
and parent.y1 <= child.y1
|
||||
and parent.x2 >= child.x2
|
||||
and parent.y2 >= child.y2
|
||||
and parent.node_id != child.node_id
|
||||
)
|
||||
|
||||
to_suppress = set()
|
||||
# Sort by area DESCENDING so we process largest (parents) first
|
||||
pre_filtered.sort(key=lambda n: n.area, reverse=True)
|
||||
|
||||
for i, parent in enumerate(pre_filtered):
|
||||
for j in range(i + 1, len(pre_filtered)):
|
||||
child = pre_filtered[j]
|
||||
if _contains(parent, child):
|
||||
if parent.clickable and not child.clickable:
|
||||
to_suppress.add(child.node_id)
|
||||
# Merge semantic info from child to parent if missing
|
||||
if (
|
||||
child.text
|
||||
and child.text not in (parent.text or "")
|
||||
and child.text not in (parent.content_desc or "")
|
||||
):
|
||||
parent.content_desc = f"{(parent.content_desc or '')} {child.text}".strip()
|
||||
if (
|
||||
child.content_desc
|
||||
and child.content_desc not in (parent.text or "")
|
||||
and child.content_desc not in (parent.content_desc or "")
|
||||
):
|
||||
parent.content_desc = f"{(parent.content_desc or '')} {child.content_desc}".strip()
|
||||
elif not parent.clickable and child.clickable:
|
||||
to_suppress.add(parent.node_id)
|
||||
# Pass any semantic info down just in case
|
||||
if parent.content_desc and not child.content_desc:
|
||||
child.content_desc = parent.content_desc
|
||||
if parent.text and not child.text:
|
||||
child.text = parent.text
|
||||
elif not parent.clickable and not child.clickable:
|
||||
to_suppress.add(parent.node_id)
|
||||
if parent.content_desc and not child.content_desc:
|
||||
child.content_desc = parent.content_desc
|
||||
elif parent.clickable and child.clickable:
|
||||
# Keep both, distinct nested interactables
|
||||
pass
|
||||
|
||||
visible_candidates = [n for n in pre_filtered if n.node_id not in to_suppress]
|
||||
|
||||
draw = ImageDraw.Draw(img)
|
||||
box_map: Dict[int, SpatialNode] = {}
|
||||
|
||||
# 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),
|
||||
]
|
||||
|
||||
for i, node in enumerate(visible_candidates):
|
||||
color = colors[i % len(colors)]
|
||||
|
||||
# Draw bounding box
|
||||
draw.rectangle(
|
||||
[node.x1, node.y1, node.x2, node.y2],
|
||||
outline=color,
|
||||
width=3,
|
||||
)
|
||||
|
||||
# Draw number label with background for readability
|
||||
label = str(i)
|
||||
label_x = node.x1 + 2
|
||||
label_y = max(node.y1 - 18, 0)
|
||||
|
||||
# Draw label background
|
||||
bbox = draw.textbbox((label_x, label_y), label)
|
||||
draw.rectangle(
|
||||
[bbox[0] - 2, bbox[1] - 2, bbox[2] + 2, bbox[3] + 2],
|
||||
fill=color,
|
||||
)
|
||||
draw.text((label_x, label_y), label, fill=(255, 255, 255))
|
||||
|
||||
box_map[i] = node
|
||||
|
||||
# Encode to base64 JPEG
|
||||
buffered = BytesIO()
|
||||
img.save(buffered, format="JPEG", quality=85)
|
||||
annotated_b64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||
|
||||
return annotated_b64, box_map
|
||||
|
||||
def _visual_discovery(
|
||||
self, intent_description: str, candidates: List[SpatialNode], device, screen_height: int = 2400
|
||||
) -> Optional[SpatialNode]:
|
||||
"""
|
||||
Vision-first intent resolution via Set-of-Mark (SoM) prompting.
|
||||
|
||||
1. Takes a screenshot
|
||||
2. Draws numbered bounding boxes on clickable candidates
|
||||
3. Sends the annotated screenshot to the VLM
|
||||
4. VLM SEES the UI and picks which numbered box matches the intent
|
||||
5. Maps box number back to SpatialNode for precise coordinates
|
||||
"""
|
||||
from GramAddict.core.config import Config
|
||||
from GramAddict.core.llm_provider import query_telepathic_llm
|
||||
|
||||
# Pre-filter candidates to reduce VLM hallucinations
|
||||
filtered_candidates = []
|
||||
for n in candidates:
|
||||
# Skip massive background containers
|
||||
if n.area > 500000:
|
||||
continue
|
||||
# Pre-filter candidates by area, system UI, and keyboard packages
|
||||
candidates = self.pre_filter_candidates(candidates)
|
||||
|
||||
# Structural heuristic: if looking for profile, prioritize nodes that might be profiles
|
||||
# and exclude obvious bottom tabs/navigation
|
||||
if "profile" in intent_lower:
|
||||
res = (n.resource_id or "").lower()
|
||||
if "tab" in res or "navigation" in res or "action_bar" in res:
|
||||
continue
|
||||
filtered_candidates.append(n)
|
||||
# --- Navigation Conflict Guard ---
|
||||
# Prevents VLM from confusing Back buttons with tab buttons
|
||||
# Production bug 2026-04-30: VLM picked Back for "tap profile tab"
|
||||
candidates = self.filter_navigation_conflicts(candidates, intent_description, screen_height=screen_height)
|
||||
|
||||
if not filtered_candidates:
|
||||
filtered_candidates = candidates
|
||||
# --- Strict Button Guard ---
|
||||
# If the intent specifically asks for a "button", "icon", or "tab",
|
||||
# filter out candidates that contain long text (e.g. captions, comments)
|
||||
# to prevent the VLM from hallucinating text nodes as interactive buttons.
|
||||
intent_lower = intent_description.lower()
|
||||
if "button" in intent_lower or "icon" in intent_lower or "tab" in intent_lower:
|
||||
filtered_candidates = []
|
||||
for node in candidates:
|
||||
text_len = len(node.text or "")
|
||||
if text_len < 40:
|
||||
filtered_candidates.append(node)
|
||||
else:
|
||||
logger.debug(f"🛡️ [Strict Button Guard] Filtered out node with long text: '{node.text[:20]}...'")
|
||||
candidates = filtered_candidates
|
||||
|
||||
# --- Post/Grid Item Guard ---
|
||||
# VLMs frequently hallucinate 'Search' when asked to tap a post. We must pre-filter.
|
||||
if "first post" in intent_lower or "grid item" in intent_lower:
|
||||
grid_candidates = []
|
||||
for node in candidates:
|
||||
desc = (node.content_desc or "").lower()
|
||||
# Posts/grid items usually have 'row X, column Y', 'photos by', or 'reel by'
|
||||
if "row 1" in desc or "column" in desc or "photos by" in desc or "reel by" in desc:
|
||||
grid_candidates.append(node)
|
||||
|
||||
if grid_candidates:
|
||||
logger.info(f"🎯 [Grid Guard] Filtered to {len(grid_candidates)} actual grid candidates.")
|
||||
candidates = grid_candidates
|
||||
|
||||
# --- Author/Username Guard ---
|
||||
# Prevents VLM from picking the "Profile" nav tab when asked for "post author username".
|
||||
if "author" in intent_lower or "username" in intent_lower or "profile name" in intent_lower:
|
||||
filtered_candidates = []
|
||||
for node in candidates:
|
||||
res_id = (node.resource_id or "").lower()
|
||||
desc = (node.content_desc or "").lower()
|
||||
if (
|
||||
"tab" in res_id
|
||||
or "navigation" in res_id
|
||||
or "tabbar" in res_id
|
||||
or desc in ["home", "search", "reels", "profile"]
|
||||
):
|
||||
logger.debug(
|
||||
f"🛡️ [Author Guard] Filtered out navigation tab: '{node.content_desc}' ({node.resource_id})"
|
||||
)
|
||||
else:
|
||||
filtered_candidates.append(node)
|
||||
candidates = filtered_candidates
|
||||
|
||||
try:
|
||||
annotated_b64, box_map = self._annotate_screenshot_with_candidates(device, candidates)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.warning(f"⚠️ [Visual Discovery] Screenshot annotation failed: {e}")
|
||||
return None
|
||||
|
||||
if not box_map:
|
||||
return None
|
||||
|
||||
self.last_box_map = box_map
|
||||
|
||||
cfg = Config()
|
||||
model = getattr(cfg.args, "ai_telepathic_model", "qwen3.5:latest")
|
||||
url = getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate")
|
||||
model = getattr(cfg.args, "ai_telepathic_model")
|
||||
url = getattr(cfg.args, "ai_telepathic_url")
|
||||
|
||||
# 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:
|
||||
desc = _humanize_desc(node.content_desc)
|
||||
label_parts.append(f"desc='{desc[:50]}'")
|
||||
if node.text and node.text != node.content_desc:
|
||||
text = _humanize_desc(node.text)
|
||||
label_parts.append(f"text='{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)
|
||||
logger.debug(f"BOX LEGEND:\n{box_legend}")
|
||||
|
||||
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 in a colored rectangle.\n\n"
|
||||
f"Box legend (what each box contains):\n{box_legend}\n\n"
|
||||
f"Your task: Find the exact box number that corresponds to this intent: '{intent_description}'\n\n"
|
||||
f"CRITICAL RULES:\n"
|
||||
f"1. If the intent contains a word in quotes (e.g., 'Search', 'New Message'), you MUST look at the Box legend and pick the box that contains that word (case-insensitive). Do not pick anything else.\n"
|
||||
f"2. For icons without text:\n"
|
||||
f" - 'like button' = HEART-SHAPED ICON (♡/❤), look for id containing 'button_like' or 'ufi_heart'.\n"
|
||||
f" - 'comment button' = SPEECH BUBBLE ICON, look for id containing 'button_comment' or 'ufi_comment'.\n"
|
||||
f"3. Do NOT select text, captions, or view counts if looking for an icon.\n"
|
||||
f"4. Ignore numbers inside the text itself. Do not confuse the text '19' with Box [19].\n"
|
||||
f"5. If the intent contains 'following', you MUST pick the box containing id 'button_following' or 'profile_header_following'. Do NOT pick 'followers' or 'Follow'.\n"
|
||||
f"6. If the intent is to tap a 'post', 'first post', or 'grid item':\n"
|
||||
f" - Look for boxes with ids like 'image_button' inside a grid, or visual grid thumbnails.\n"
|
||||
f" - Pick the FIRST matching box index.\n"
|
||||
f" - Do NOT pick navigation buttons like 'Search'.\n"
|
||||
f"7. If the intent is a bottom navigation tab (e.g. 'profile tab', 'home tab'):\n"
|
||||
f" - These are always at the BOTTOM edge of the screen.\n"
|
||||
f" - 'profile tab' is usually the furthest right icon (your avatar).\n"
|
||||
f" - 'home tab' is the furthest left icon (house).\n"
|
||||
f" - 'explore tab' is the magnifying glass.\n"
|
||||
f" - 'reels tab' is the video clapperboard.\n"
|
||||
f"8. If the intent involves 'author username' or 'author profile':\n"
|
||||
f" - Pick the profile picture or the username text.\n"
|
||||
f" - NEVER pick a 'Follow' button. Do NOT pick 'button_follow'.\n"
|
||||
f"9. If the intent is 'save post':\n"
|
||||
f" - The save icon is the bookmark icon on the bottom right of the post image/video.\n"
|
||||
f" - Look for id containing 'button_save' or 'ufi_save'. Do NOT pick the post text or other action buttons.\n"
|
||||
f"10. DISTINGUISHING BOTTOM TABS vs CONTENT BUTTONS:\n"
|
||||
f" - Bottom Navigation Tabs (Home, Search, Reels, Profile) are ALWAYS at the very bottom (y > 2100).\n"
|
||||
f" - Content Interaction Buttons (Like, Comment, Share, Reactions, Message Input) are attached to posts or threads, NOT the bottom nav bar.\n"
|
||||
f" - If looking for 'message input' or 'type message', do NOT select 'reactions' or emoji icons. Look for an empty text box or id 'message_content'.\n"
|
||||
f"11. If the intent is 'feed post content' or 'post media content':\n"
|
||||
f" - Pick the largest box that contains the actual image or video. Look for id 'zoomable_view_container' or 'media_frame'.\n"
|
||||
f"12. If the exact control is NOT visible, return null. Do NOT guess.\n\n"
|
||||
f'Reply ONLY with a valid JSON object: {{"box": <number>}} or {{"box": null}}'
|
||||
)
|
||||
|
||||
try:
|
||||
res = query_telepathic_llm(
|
||||
model=model,
|
||||
url=url,
|
||||
system_prompt="Strict visual JSON box selector. Respond only with JSON.",
|
||||
user_prompt=prompt,
|
||||
use_local_edge=True,
|
||||
images_b64=[annotated_b64],
|
||||
)
|
||||
print(f"DEBUG_INTENT: VLM RAW RESPONSE for '{intent_description}': {res}")
|
||||
data = json.loads(res)
|
||||
box_idx = data.get("box")
|
||||
if box_idx is None:
|
||||
box_idx = data.get("selected_index")
|
||||
if box_idx is None:
|
||||
box_idx = data.get("box_index")
|
||||
if box_idx is None:
|
||||
box_idx = data.get("index")
|
||||
|
||||
if box_idx is not None and box_idx in box_map:
|
||||
selected = box_map[box_idx]
|
||||
logger.info(
|
||||
f"👁️ [Visual Discovery] VLM selected box [{box_idx}] → "
|
||||
f"id='{selected.resource_id}', desc='{selected.content_desc}'"
|
||||
)
|
||||
return selected
|
||||
else:
|
||||
logger.warning(
|
||||
f"👁️ [Visual Discovery] VLM returned box={box_idx} which is not in box_map ({list(box_map.keys())[:5]}...)"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ [Visual Discovery] VLM call failed: {e}")
|
||||
|
||||
return None
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Text-based Fallback (no device/screenshot)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def _text_based_resolve(
|
||||
self, intent_description: str, candidates: List[SpatialNode], device=None, screen_height: int = 2400
|
||||
) -> Optional[SpatialNode]:
|
||||
"""
|
||||
Fallback resolution via text descriptions of XML nodes.
|
||||
Used only when no device is available for screenshots.
|
||||
"""
|
||||
from GramAddict.core.config import Config
|
||||
from GramAddict.core.llm_provider import query_telepathic_llm
|
||||
|
||||
intent_lower = intent_description.lower()
|
||||
|
||||
filtered_candidates = [n for n in candidates if n.area < 500000]
|
||||
filtered_candidates = self.filter_navigation_conflicts(
|
||||
filtered_candidates, intent_description, screen_height=screen_height
|
||||
)
|
||||
if "profile" in intent_lower:
|
||||
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:
|
||||
filtered_candidates = [n for n in candidates if n.area < 500000]
|
||||
|
||||
cfg = Config()
|
||||
model = getattr(cfg.args, "ai_telepathic_model")
|
||||
url = getattr(cfg.args, "ai_telepathic_url")
|
||||
|
||||
# Prepare context
|
||||
node_context = []
|
||||
for i, node in enumerate(filtered_candidates):
|
||||
text = node.text or ""
|
||||
desc = node.content_desc or ""
|
||||
text = _humanize_desc(node.text or "")
|
||||
desc = _humanize_desc(node.content_desc or "")
|
||||
res_id = node.resource_id or ""
|
||||
node_context.append(f"[{i}] text='{text}', desc='{desc}', id='{res_id}', bounds=[{node.y1},{node.y2}]")
|
||||
|
||||
prompt = (
|
||||
f"You are a Spatial UI Intent Resolver.\n"
|
||||
f"Goal: Find the single best UI element to interact with to satisfy the intent: '{intent_description}'.\n"
|
||||
f"CRITICAL RULES:\n"
|
||||
f"- If the intent is about opening the 'post author', STRICTLY require 'row_feed_photo_profile' in the ID. Do not select comment authors.\n"
|
||||
f"- If the intent is about opening a user profile generally, prioritize nodes containing 'profile_name' or 'profile_image' in their ID, NOT generic action bars or tabs.\n"
|
||||
f"- Ignore bottom navigation tabs (home, search, profile) UNLESS the intent explicitly asks to navigate to a primary feed.\n"
|
||||
f"Candidates:\n" + "\n".join(node_context) + "\n\n"
|
||||
"CRITICAL RULES:\n"
|
||||
"1. If the intent is a bottom navigation tab (e.g. 'profile tab', 'home tab'):\n"
|
||||
" - These are always at the BOTTOM of the screen (typically y > 2100).\n"
|
||||
" - 'profile tab' is usually the furthest right.\n"
|
||||
" - 'home tab' is the furthest left.\n"
|
||||
" - Do NOT select 'Go to <user>'s profile' or other header text.\n"
|
||||
"2. If none of the candidates clearly and safely match the intent, return null.\n\n"
|
||||
"Reply ONLY with a valid JSON object strictly matching this schema:\n"
|
||||
'{"selected_index": <integer or null>}\n'
|
||||
"If none of the candidates match the intent, return null."
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -118,13 +849,12 @@ class IntentResolver:
|
||||
user_prompt=prompt,
|
||||
use_local_edge=True,
|
||||
)
|
||||
print(f"DEBUG_INTENT: TEXT LLM RAW RESPONSE for '{intent_description}': {res}")
|
||||
data = json.loads(res)
|
||||
idx = data.get("selected_index")
|
||||
if idx is not None and 0 <= idx < len(filtered_candidates):
|
||||
return filtered_candidates[idx]
|
||||
except Exception as e:
|
||||
import logging
|
||||
|
||||
logging.getLogger(__name__).warning(f"⚠️ [IntentResolver] VLM resolution failed ({e}).")
|
||||
logger.warning(f"⚠️ [IntentResolver] Text-based VLM resolution failed ({e}).")
|
||||
|
||||
return None
|
||||
|
||||
@@ -22,6 +22,7 @@ class ScreenType(Enum):
|
||||
FOLLOW_LIST = "follow_list"
|
||||
COMMENTS = "comments"
|
||||
MODAL = "modal"
|
||||
DANGER_ACTION_BLOCKED = "danger_action_blocked"
|
||||
FOREIGN_APP = "foreign_app"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
@@ -43,7 +44,7 @@ class ScreenIdentity:
|
||||
except ImportError:
|
||||
self.screen_memory = None
|
||||
|
||||
def identify(self, xml_dump: str) -> Dict[str, Any]:
|
||||
def identify(self, xml_dump: str, screenshot_b64: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyzes an XML dump and returns a complete screen description.
|
||||
|
||||
@@ -116,6 +117,11 @@ class ScreenIdentity:
|
||||
}
|
||||
)
|
||||
|
||||
from GramAddict.core.situational_awareness import SituationalAwarenessEngine
|
||||
|
||||
sae = SituationalAwarenessEngine.get_instance()
|
||||
signature = sae._compress_xml(xml_dump) if sae else self._compute_signature(resource_ids, content_descs, texts)
|
||||
|
||||
# ── Foreign app check ──
|
||||
if app_id not in packages:
|
||||
return {
|
||||
@@ -123,18 +129,16 @@ class ScreenIdentity:
|
||||
"available_actions": ["press back", "force start instagram"],
|
||||
"selected_tab": None,
|
||||
"context": {"packages": list(packages)},
|
||||
"signature": self._compute_signature(resource_ids, content_descs, texts),
|
||||
"signature": signature,
|
||||
}
|
||||
|
||||
desc_lower = " ".join(content_descs).lower()
|
||||
text_lower = " ".join(texts).lower()
|
||||
ids_str = " ".join(resource_ids).lower()
|
||||
|
||||
signature = self._compute_signature(resource_ids, content_descs, texts)
|
||||
|
||||
# ── Identify screen type from structural signals ──
|
||||
screen_type = self._classify_screen(
|
||||
resource_ids, content_descs, texts, selected_tab, desc_lower, text_lower, ids_str, signature
|
||||
resource_ids, content_descs, texts, selected_tab, desc_lower, text_lower, ids_str, signature, screenshot_b64
|
||||
)
|
||||
|
||||
# ── Extract available actions from clickable elements ──
|
||||
@@ -153,32 +157,74 @@ class ScreenIdentity:
|
||||
"signature": signature,
|
||||
}
|
||||
|
||||
def _classify_screen(self, ids, descs, texts, selected_tab, desc_lower, text_lower, ids_str, signature=None):
|
||||
"""Classify screen type using Semantic Memory with LLM fallback — NO hardcoded states."""
|
||||
def _classify_screen(
|
||||
self, ids, descs, texts, selected_tab, desc_lower, text_lower, ids_str, signature=None, screenshot_b64=None
|
||||
):
|
||||
"""
|
||||
Classify screen type using Semantic Memory with LLM fallback — NO hardcoded states."""
|
||||
|
||||
# Priority 0: Content-creation overlays that block ALL navigation.
|
||||
# These full-screen Instagram UIs have no navigation tabs and trap the bot.
|
||||
# Structural detection is O(1), zero LLM calls, and cannot be fooled.
|
||||
creation_flow_markers = ("quick_capture", "gallery_cancel_button", "creation_flow", "reel_camera")
|
||||
if any(marker in ids_str for marker in creation_flow_markers):
|
||||
logger.info("🛡️ [ScreenIdentity] Content-creation overlay detected → MODAL")
|
||||
return ScreenType.MODAL
|
||||
|
||||
# Priority 1: Check Qdrant Semantic Cache
|
||||
# Priority 0: Fetch Qdrant Semantic Cache
|
||||
# We fetch this early to see if there is a 'NORMAL' override for the MODAL check.
|
||||
# We DO NOT let this override deterministic structural heuristics! Fuzzy vector matching
|
||||
# can easily confuse HOME_FEED and OWN_PROFILE if the bottom navigation bar is identical.
|
||||
cached_type_str = None
|
||||
if signature and self.screen_memory and self.screen_memory.is_connected:
|
||||
cached_type_str = self.screen_memory.get_screen_type(signature, similarity_threshold=0.92)
|
||||
if cached_type_str:
|
||||
try:
|
||||
return ScreenType[cached_type_str]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Priority 2: Structural Heuristics (Instant, for core tabs)
|
||||
is_normal_override = cached_type_str == "NORMAL"
|
||||
|
||||
# Priority 1: Content-creation overlays that block ALL navigation.
|
||||
# These full-screen Instagram UIs have no navigation tabs and trap the bot.
|
||||
# Structural detection is O(1), zero LLM calls, and cannot be fooled.
|
||||
if not is_normal_override:
|
||||
modal_markers = (
|
||||
"quick_capture",
|
||||
"gallery_cancel_button",
|
||||
"creation_flow",
|
||||
"reel_camera",
|
||||
"survey_overlay_container",
|
||||
"interstitial_container",
|
||||
"nux_overlay",
|
||||
"rating_prompt",
|
||||
"feedback_dialog",
|
||||
"action_bar_browser_container",
|
||||
)
|
||||
if any(marker in ids_str for marker in modal_markers):
|
||||
logger.info("🛡️ [ScreenIdentity] Modal/Interstitial overlay detected → MODAL")
|
||||
return ScreenType.MODAL
|
||||
|
||||
# Action Blocked Detection (O(1) fast-path)
|
||||
# Prevents LLM hallucinations for system-level traps that block the entire flow.
|
||||
danger_markers = (
|
||||
"try again later",
|
||||
"action blocked",
|
||||
"we restrict certain activity",
|
||||
"to protect our community",
|
||||
)
|
||||
if "bottom_sheet_container" in ids and any(d in text_lower or d in desc_lower for d in danger_markers):
|
||||
logger.info("🛡️ [ScreenIdentity] Critical obstacle detected → DANGER_ACTION_BLOCKED")
|
||||
return ScreenType.DANGER_ACTION_BLOCKED
|
||||
|
||||
# Menu Trap Detection (O(1) fast-path)
|
||||
# Overrides hallucinated LLM cache using STRICTLY STRUCTURAL IDs (no localized strings).
|
||||
if "bottom_sheet_container" in ids and "action_sheet_row_text_view" in ids:
|
||||
logger.info("🛡️ [ScreenIdentity] Options/Report menu trap detected → MODAL")
|
||||
return ScreenType.MODAL
|
||||
|
||||
# Priority 2: Structural Heuristics (100% Deterministic)
|
||||
if "unified_follow_list_tab_layout" in ids or "follow_list_container" in ids:
|
||||
return ScreenType.FOLLOW_LIST
|
||||
|
||||
if "profile_header_container" in ids:
|
||||
if selected_tab == "profile_tab":
|
||||
# Profile structural markers
|
||||
PROFILE_MARKERS = (
|
||||
"profile_header_container",
|
||||
"row_profile_header_imageview",
|
||||
"profile_tabs_container",
|
||||
"profile_header_name",
|
||||
)
|
||||
if any(marker in ids for marker in PROFILE_MARKERS):
|
||||
own_profile_texts = ("edit profile", "share profile")
|
||||
if selected_tab == "profile_tab" or any(m in desc_lower or m in text_lower for m in own_profile_texts):
|
||||
return ScreenType.OWN_PROFILE
|
||||
return ScreenType.OTHER_PROFILE
|
||||
|
||||
@@ -188,12 +234,36 @@ class ScreenIdentity:
|
||||
if any(marker in ids for marker in REELS_MARKERS):
|
||||
return ScreenType.REELS_FEED
|
||||
|
||||
# DM thread detection — structural markers present inside DM conversations
|
||||
if "direct_thread_header" in ids or "row_thread_composer_edittext" in ids:
|
||||
# DM thread detection — Semantic app-agnostic markers (chat input fields)
|
||||
chat_input_markers = ["Message...", "Nachricht...", "Type a message", "Nachricht senden", "Send a message"]
|
||||
if any(marker in texts for marker in chat_input_markers) or "direct_thread_header" in ids:
|
||||
return ScreenType.DM_THREAD
|
||||
|
||||
if "row_feed_button_like" in ids and "row_feed_photo_profile_name" in ids and not selected_tab:
|
||||
return ScreenType.POST_DETAIL
|
||||
# POST_DETAIL vs HOME_FEED: Both have row_feed_* markers. The differentiator
|
||||
# is that HOME_FEED has the main_feed_action_bar (top bar with 'Instagram' title).
|
||||
# POST_DETAIL lacks this because it shows a single expanded post.
|
||||
# Note: We MUST NOT use `not selected_tab` here — posts opened from feed
|
||||
# retain the feed_tab as selected, which previously caused misclassification.
|
||||
if "row_feed_button_like" in ids and "row_feed_photo_profile_name" in ids:
|
||||
if "main_feed_action_bar" not in ids:
|
||||
return ScreenType.POST_DETAIL
|
||||
|
||||
# Story view structural markers — present in full-screen story viewer.
|
||||
# Stories hide the navigation tab bar, so selected_tab is always None.
|
||||
# Must be checked BEFORE tab-based fallbacks to prevent UNKNOWN classification.
|
||||
STORY_MARKERS = (
|
||||
"reel_viewer_media_layout",
|
||||
"reel_viewer_header",
|
||||
"reel_viewer_progress_bar",
|
||||
"reel_viewer_root",
|
||||
"story_viewer_container",
|
||||
"reel_viewer_content_layout",
|
||||
)
|
||||
if any(marker in ids for marker in STORY_MARKERS):
|
||||
return ScreenType.STORY_VIEW
|
||||
# Fallback: content-desc "Like Story" or "Send story" confirms story context
|
||||
if "like story" in desc_lower or "send story" in desc_lower or "nachricht senden" in desc_lower:
|
||||
return ScreenType.STORY_VIEW
|
||||
|
||||
if selected_tab == "feed_tab":
|
||||
return ScreenType.HOME_FEED
|
||||
@@ -201,6 +271,8 @@ class ScreenIdentity:
|
||||
return ScreenType.REELS_FEED
|
||||
if selected_tab == "search_tab":
|
||||
return ScreenType.EXPLORE_GRID
|
||||
if "action_bar_search_edit_text" in ids:
|
||||
return ScreenType.EXPLORE_GRID
|
||||
if selected_tab == "profile_tab":
|
||||
return ScreenType.OWN_PROFILE
|
||||
if selected_tab == "direct_tab":
|
||||
@@ -208,41 +280,73 @@ class ScreenIdentity:
|
||||
if "message_input" in ids:
|
||||
return ScreenType.DM_INBOX # Fallback for DM thread as inbox
|
||||
|
||||
# Priority 3: Semantic VLM Classification Fallback
|
||||
# End of structural heuristics
|
||||
|
||||
# Priority 3: Cached Semantic Type (If deterministic heuristics failed)
|
||||
if cached_type_str and cached_type_str != "NORMAL":
|
||||
try:
|
||||
cached_type = ScreenType[cached_type_str]
|
||||
# Enforce absolute structural parity: Story and Reels must have their structural markers.
|
||||
# If they reached Priority 3, it means Priority 2 failed to find their markers.
|
||||
# Therefore, any cache telling us this is a Story/Reel without those markers is hallucinating.
|
||||
if cached_type in (ScreenType.STORY_VIEW, ScreenType.REELS_FEED):
|
||||
logger.warning(
|
||||
f"⚠️ [ScreenIdentity] Rejecting cached {cached_type.name} due to missing structural markers."
|
||||
)
|
||||
else:
|
||||
return cached_type
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Priority 4: Semantic VLM Classification Fallback
|
||||
if not screenshot_b64 and getattr(self, "device", None) is not None:
|
||||
screenshot_b64 = self.device.get_screenshot_b64()
|
||||
|
||||
from GramAddict.core.config import Config
|
||||
from GramAddict.core.llm_provider import query_llm
|
||||
from GramAddict.core.llm_provider import query_telepathic_llm
|
||||
|
||||
cfg = Config()
|
||||
url = (
|
||||
getattr(cfg.args, "ai_embedding_url", "http://localhost:11434/api/chat")
|
||||
if hasattr(cfg, "args")
|
||||
else "http://localhost:11434/api/chat"
|
||||
)
|
||||
model = getattr(cfg.args, "ai_embedding_model", "llama3") if hasattr(cfg, "args") else "llama3"
|
||||
if not hasattr(cfg, "args"):
|
||||
raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.")
|
||||
url = getattr(cfg.args, "ai_telepathic_url")
|
||||
model = getattr(cfg.args, "ai_telepathic_model")
|
||||
|
||||
layout_context = (
|
||||
f"Selected Tab: {selected_tab}\nResource IDs: {list(ids)}\nVisible Texts context: {texts[:10]}\n"
|
||||
)
|
||||
prompt = (
|
||||
f"Identify the Instagram screen layout type based on these DOM structural signals.\n"
|
||||
f"Identify the Instagram screen layout type based on the provided screenshot and structural signals.\n"
|
||||
f"Valid types: {[t.name for t in ScreenType]}\n"
|
||||
f"Context:\n{layout_context}\n"
|
||||
f"Reply ONLY with the exact matching enum Type Name string, or 'UNKNOWN' if no type matches."
|
||||
)
|
||||
|
||||
try:
|
||||
response = query_llm(
|
||||
url=url, model=model, prompt="Classify this screen layout.", system=prompt, format_json=False
|
||||
response = query_telepathic_llm(
|
||||
model=model,
|
||||
url=url,
|
||||
system_prompt=prompt,
|
||||
user_prompt="Classify this screen layout.",
|
||||
images_b64=[screenshot_b64] if screenshot_b64 else None,
|
||||
temperature=0.0,
|
||||
use_local_edge=True,
|
||||
)
|
||||
if response and isinstance(response, str):
|
||||
result = response.strip().upper()
|
||||
elif response and isinstance(response, dict) and "response" in response:
|
||||
result = response["response"].strip().upper()
|
||||
else:
|
||||
return ScreenType.UNKNOWN
|
||||
|
||||
result = response.strip().upper() if response else "UNKNOWN"
|
||||
|
||||
for t in ScreenType:
|
||||
if t.name in result:
|
||||
if is_normal_override and t == ScreenType.MODAL:
|
||||
# Prevent the LLM from hallucinating an obstacle if explicitly verified as NORMAL
|
||||
return ScreenType.UNKNOWN
|
||||
|
||||
# Enforce absolute structural parity: Story and Reels must have their structural markers.
|
||||
if t in (ScreenType.STORY_VIEW, ScreenType.REELS_FEED):
|
||||
logger.warning(
|
||||
f"⚠️ [ScreenIdentity] Rejecting VLM hallucinated {t.name} due to missing structural markers."
|
||||
)
|
||||
return ScreenType.UNKNOWN
|
||||
|
||||
if signature and self.screen_memory:
|
||||
self.screen_memory.store_screen(signature, t.name)
|
||||
return t
|
||||
@@ -269,40 +373,39 @@ class ScreenIdentity:
|
||||
if tab_id in resource_ids:
|
||||
actions.append(action)
|
||||
|
||||
# Screen-specific actions
|
||||
desc_lower = " ".join(content_descs).lower()
|
||||
text_lower = " ".join(texts).lower()
|
||||
# Screen-specific actions (Purely structural, NO localized strings)
|
||||
ids_str = " ".join(resource_ids).lower()
|
||||
|
||||
if "like" in desc_lower:
|
||||
if "button_like" in ids_str or "ufi_heart" in ids_str:
|
||||
actions.append("tap like button")
|
||||
if "comment" in desc_lower:
|
||||
if "button_comment" in ids_str or "ufi_comment" in ids_str:
|
||||
actions.append("tap comment button")
|
||||
if "share" in desc_lower:
|
||||
if "button_share" in ids_str or "ufi_share" in ids_str or "direct_share" in ids_str:
|
||||
actions.append("tap share button")
|
||||
if "save" in desc_lower or "bookmark" in desc_lower:
|
||||
if "button_save" in ids_str or "ufi_save" in ids_str:
|
||||
actions.append("tap save button")
|
||||
if "back" in desc_lower:
|
||||
if "back" in ids_str or "action_bar_button_back" in ids_str:
|
||||
actions.append("tap back button")
|
||||
if any("follow" in e.get("text", "").lower() for e in clickable_elements):
|
||||
|
||||
has_following = "button_following" in ids_str or "profile_header_following" in ids_str
|
||||
if has_following:
|
||||
actions.append("tap following button")
|
||||
elif "button_follow" in ids_str:
|
||||
actions.append("tap follow button")
|
||||
|
||||
if screen_type == ScreenType.OWN_PROFILE or screen_type == ScreenType.OTHER_PROFILE:
|
||||
if "message" in desc_lower or "nachricht" in desc_lower:
|
||||
if "button_message" in ids_str or "direct_message" in ids_str:
|
||||
actions.append("tap message button")
|
||||
if (
|
||||
"following" in desc_lower
|
||||
or "abonniert" in desc_lower
|
||||
or "following" in text_lower
|
||||
or "profile_header_following" in " ".join(resource_ids).lower()
|
||||
):
|
||||
if "profile_header_following" in ids_str:
|
||||
actions.append("tap following list")
|
||||
|
||||
# Grid items
|
||||
if screen_type == ScreenType.EXPLORE_GRID:
|
||||
actions.append("tap first grid item")
|
||||
actions.append("tap first post")
|
||||
|
||||
# Scroll
|
||||
actions.append("scroll down")
|
||||
actions.append("scroll up")
|
||||
actions.append("press back")
|
||||
|
||||
return list(set(actions)) # Deduplicate
|
||||
|
||||
@@ -28,8 +28,8 @@ class SemanticEvaluator:
|
||||
logger.warning("👁️ [Vision Core] No config available. Cannot query VLM.")
|
||||
return None
|
||||
|
||||
model = getattr(self.args, "ai_telepathic_model", "llama3.2-vision")
|
||||
url = getattr(self.args, "ai_telepathic_url", "http://localhost:11434/api/generate")
|
||||
model = getattr(self.args, "ai_telepathic_model")
|
||||
url = getattr(self.args, "ai_telepathic_url")
|
||||
|
||||
try:
|
||||
res = query_telepathic_llm(
|
||||
@@ -117,12 +117,13 @@ class SemanticEvaluator:
|
||||
You are a user with the following interests: {', '.join(persona_interests)}.
|
||||
You are looking at an Instagram post.
|
||||
Evaluate if this post is highly relevant to your interests and if you should like/comment on it.
|
||||
CRITICAL: Check if this post is an advertisement or sponsored content (look for "Sponsored", "Ad", or promotional product placement).
|
||||
|
||||
Reply ONLY in valid JSON format:
|
||||
{{
|
||||
"should_like": true/false,
|
||||
"should_comment": true/false,
|
||||
"reasoning": "brief explanation"
|
||||
"is_ad": true/false
|
||||
}}
|
||||
"""
|
||||
response = self._query_vlm(prompt, screenshot_b64)
|
||||
@@ -131,7 +132,17 @@ class SemanticEvaluator:
|
||||
json_str = response.split("```json")[1].split("```")[0].strip()
|
||||
else:
|
||||
json_str = response.strip()
|
||||
return json.loads(json_str)
|
||||
try:
|
||||
return json.loads(json_str)
|
||||
except json.JSONDecodeError:
|
||||
# Try to close potential unclosed JSON strings
|
||||
if not json_str.endswith("}"):
|
||||
json_str += "}"
|
||||
try:
|
||||
return json.loads(json_str)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
logger.warning(f"👁️ [Vision Core] VLM returned malformed JSON: {response}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to evaluate post vibe: {e}")
|
||||
return None
|
||||
|
||||
@@ -149,10 +149,15 @@ class SpatialParser:
|
||||
# Filter zero-area nodes early
|
||||
if right > left and bottom > top:
|
||||
self._node_counter += 1
|
||||
text_val = attrib.get("text", "").strip()
|
||||
hint_val = attrib.get("hint", "").strip()
|
||||
if not text_val and hint_val:
|
||||
text_val = hint_val
|
||||
|
||||
node = SpatialNode(
|
||||
node_id=f"n_{self._node_counter}",
|
||||
class_name=attrib.get("class", ""),
|
||||
text=attrib.get("text", "").strip(),
|
||||
text=text_val,
|
||||
content_desc=attrib.get("content-desc", "").strip(),
|
||||
resource_id=attrib.get("resource-id", "").strip(),
|
||||
bounds=(left, top, right, bottom),
|
||||
@@ -179,7 +184,7 @@ class SpatialParser:
|
||||
for n in all_nodes:
|
||||
has_semantic = bool(n.text or n.content_desc)
|
||||
semantic_res = n.resource_id and any(
|
||||
x in n.resource_id.lower() for x in ["button", "tab", "icon", "action", "menu"]
|
||||
x in n.resource_id.lower() for x in ["button", "tab", "icon", "action", "menu", "imageview"]
|
||||
)
|
||||
|
||||
if n.clickable or n.scrollable or semantic_res or (has_semantic and n.area < 500000 and n.area > 0):
|
||||
|
||||
@@ -13,7 +13,8 @@ class PersistentList(list):
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
path = f"accounts/{self.filename}.json"
|
||||
base_dir = os.environ.get("GRAMADDICT_ACCOUNTS_DIR", "accounts")
|
||||
path = f"{base_dir}/{self.filename}.json"
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
@@ -27,9 +28,8 @@ class PersistentList(list):
|
||||
self.persist()
|
||||
|
||||
def persist(self, directory=None):
|
||||
if os.environ.get("PYTEST_CURRENT_TEST"):
|
||||
return
|
||||
folder = f"accounts/{directory}" if directory else "accounts"
|
||||
base_dir = os.environ.get("GRAMADDICT_ACCOUNTS_DIR", "accounts")
|
||||
folder = f"{base_dir}/{directory}" if directory else base_dir
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
path = f"{folder}/{self.filename}.json"
|
||||
try:
|
||||
|
||||
@@ -151,16 +151,12 @@ def humanized_scroll(device, is_skip=False, resonance_score=None):
|
||||
|
||||
def humanized_click(device, x, y, double=False, sleep_mod=1.0):
|
||||
"""Simulates a human tap with biomechanical jitter and micro-drift."""
|
||||
body = PhysicsBody.get_session_instance(device)
|
||||
injector = SendEventInjector.get_instance(device)
|
||||
|
||||
def single_tap():
|
||||
points = BezierGesture.tap_curve(x, y, body)
|
||||
# Tap timing: 40-90ms contact time
|
||||
tap_duration = random.uniform(40, 90)
|
||||
timing = BezierGesture.compute_sigmoid_timing(len(points), tap_duration)
|
||||
|
||||
injector.inject_gesture(points, timing, touch_major=body.get_touch_major())
|
||||
# Apply biomechanical jitter
|
||||
jx = int(x + random.gauss(0, 5))
|
||||
jy = int(y + random.gauss(0, 5))
|
||||
device.shell(f"input tap {jx} {jy}")
|
||||
|
||||
if double:
|
||||
# For double tap, the timing is extremely critical (<300ms between taps).
|
||||
|
||||
@@ -18,7 +18,6 @@ correct /dev/input/eventX and the axis ranges on first use.
|
||||
|
||||
import logging
|
||||
import re
|
||||
from time import sleep
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -179,6 +178,9 @@ class SendEventInjector:
|
||||
scale_x = self.x_max / display_w
|
||||
scale_y = self.y_max / display_h
|
||||
|
||||
# Build batch command list
|
||||
cmds = []
|
||||
|
||||
# --- Touch Down (first point) ---
|
||||
x, y, pressure = points[0]
|
||||
ix = int(x * scale_x)
|
||||
@@ -186,8 +188,6 @@ class SendEventInjector:
|
||||
ip = int(pressure * self.pressure_max)
|
||||
itm = min(touch_major, self.touch_major_max)
|
||||
|
||||
# Build batch command for touch-down
|
||||
cmds = []
|
||||
cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_TRACKING_ID} 0")
|
||||
cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_X} {ix}")
|
||||
cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_Y} {iy}")
|
||||
@@ -196,38 +196,36 @@ class SendEventInjector:
|
||||
cmds.append(f"sendevent {dev} {self.EV_KEY} {self.BTN_TOUCH} 1")
|
||||
cmds.append(f"sendevent {dev} {self.EV_SYN} {self.SYN_REPORT} 0")
|
||||
|
||||
# Execute touch-down
|
||||
self.device.shell(" && ".join(cmds))
|
||||
|
||||
# --- Move through intermediate points ---
|
||||
for i in range(1, len(points) - 1):
|
||||
if i - 1 < len(timing_intervals):
|
||||
sleep(timing_intervals[i - 1])
|
||||
delay = timing_intervals[i - 1]
|
||||
if delay > 0.001:
|
||||
cmds.append(f"sleep {delay:.3f}")
|
||||
|
||||
x, y, pressure = points[i]
|
||||
ix = int(x * scale_x)
|
||||
iy = int(y * scale_y)
|
||||
ip = int(pressure * self.pressure_max)
|
||||
|
||||
cmds = []
|
||||
cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_X} {ix}")
|
||||
cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_Y} {iy}")
|
||||
cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_PRESSURE} {ip}")
|
||||
cmds.append(f"sendevent {dev} {self.EV_SYN} {self.SYN_REPORT} 0")
|
||||
|
||||
self.device.shell(" && ".join(cmds))
|
||||
|
||||
# --- Touch Up (last point) ---
|
||||
if len(timing_intervals) >= len(points) - 1:
|
||||
sleep(timing_intervals[-1])
|
||||
delay = timing_intervals[-1]
|
||||
else:
|
||||
sleep(0.01)
|
||||
delay = 0.01
|
||||
|
||||
if delay > 0.001:
|
||||
cmds.append(f"sleep {delay:.3f}")
|
||||
|
||||
x, y, pressure = points[-1]
|
||||
ix = int(x * scale_x)
|
||||
iy = int(y * scale_y)
|
||||
|
||||
cmds = []
|
||||
cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_X} {ix}")
|
||||
cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_POSITION_Y} {iy}")
|
||||
cmds.append(f"sendevent {dev} {self.EV_ABS} {self.ABS_MT_PRESSURE} 0")
|
||||
@@ -235,6 +233,7 @@ class SendEventInjector:
|
||||
cmds.append(f"sendevent {dev} {self.EV_KEY} {self.BTN_TOUCH} 0")
|
||||
cmds.append(f"sendevent {dev} {self.EV_SYN} {self.SYN_REPORT} 0")
|
||||
|
||||
# Execute ALL events in one atomic batch to eliminate ADB latency
|
||||
self.device.shell(" && ".join(cmds))
|
||||
|
||||
except Exception as e:
|
||||
@@ -253,4 +252,12 @@ class SendEventInjector:
|
||||
ex, ey, _ = points[-1]
|
||||
total_ms = int(sum(timing_intervals) * 1000) if timing_intervals else 300
|
||||
|
||||
self.device.shell(f"input swipe {int(sx)} {int(sy)} {int(ex)} {int(ey)} {total_ms}")
|
||||
dist_x = abs(ex - sx)
|
||||
dist_y = abs(ey - sy)
|
||||
|
||||
# Android sometimes interprets a low-duration swipe with minimal movement as a long press or cancels it.
|
||||
# If it's physically a tap (minimal movement, short duration), use native input tap.
|
||||
if dist_x < 15 and dist_y < 15 and total_ms < 150:
|
||||
self.device.shell(f"input tap {int(sx)} {int(sy)}")
|
||||
else:
|
||||
self.device.shell(f"input swipe {int(sx)} {int(sy)} {int(ex)} {int(ey)} {total_ms}")
|
||||
|
||||
@@ -135,64 +135,116 @@ def align_active_post(device):
|
||||
"""
|
||||
aligned = False
|
||||
attempts = 0
|
||||
max_attempts = 3
|
||||
max_attempts = 5 # Increased for structural retry loop
|
||||
failed_bounds = set()
|
||||
|
||||
# Intents for structural discovery
|
||||
intents = [
|
||||
"post author username text (exclude follow buttons)",
|
||||
"post author header profile",
|
||||
"row_feed_photo_profile_name", # ID fallback
|
||||
"clips_viewer_author_container", # Reels fallback
|
||||
"feed post content", # Final desperation
|
||||
]
|
||||
|
||||
while not aligned and attempts < max_attempts:
|
||||
attempts += 1
|
||||
try:
|
||||
xml = device.dump_hierarchy()
|
||||
if "clips_video_container" in xml or "clips_viewer_container" in xml:
|
||||
logger.info("🎯 [Alignment] Reels view detected. Auto-snapping is native.")
|
||||
return True
|
||||
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
telepath = TelepathicEngine.get_instance()
|
||||
target_node = telepath.find_best_node(xml, "post author header profile", min_confidence=0.4, device=device)
|
||||
|
||||
target_node = None
|
||||
for intent in intents:
|
||||
target_node = telepath.find_best_node(
|
||||
xml, intent, min_confidence=0.35, device=device, track=False, exclude_bounds=list(failed_bounds)
|
||||
)
|
||||
if target_node:
|
||||
break
|
||||
|
||||
if target_node:
|
||||
original_attribs = target_node.get("original_attribs", {})
|
||||
bounds = original_attribs.get("bounds", "")
|
||||
if not bounds:
|
||||
bounds = target_node.get("bounds", "")
|
||||
bounds = original_attribs.get("bounds")
|
||||
|
||||
m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds)
|
||||
if m:
|
||||
l, t, r, b = map(int, m.groups())
|
||||
header_y = (t + b) // 2
|
||||
|
||||
# Instagram's optimal top margin for a snapped post is ~200-280px
|
||||
target_y = 250
|
||||
diff = header_y - target_y
|
||||
|
||||
# If target is off-center (> 100px), execute precise correction swipe
|
||||
if abs(diff) > 100:
|
||||
info = device.get_info()
|
||||
w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
|
||||
cx = w // 2
|
||||
|
||||
max_safe_swipe = int(h * 0.4)
|
||||
|
||||
if diff > 0:
|
||||
# Content is too LOW. Move it UP.
|
||||
dist = min(diff, max_safe_swipe)
|
||||
start_y = int(h * 0.7)
|
||||
end_y = start_y - dist
|
||||
else:
|
||||
# Content is too HIGH. Move it DOWN.
|
||||
dist = min(abs(diff), max_safe_swipe)
|
||||
start_y = int(h * 0.3)
|
||||
end_y = start_y + dist
|
||||
|
||||
# Duration 1.0s = precise mechanical drag with ZERO momentum
|
||||
device.swipe(cx, start_y, cx, end_y, duration=1.0)
|
||||
sleep(1.0)
|
||||
logger.debug(f"📐 [Alignment] Snapping attempt {attempts}: Shifted {diff}px.")
|
||||
bounds_str = ""
|
||||
# If bounds is a tuple from SpatialNode.to_dict()
|
||||
if isinstance(bounds, (tuple, list)) and len(bounds) == 4:
|
||||
left, t, r, b = bounds
|
||||
bounds_str = f"[{left},{t}][{r},{b}]"
|
||||
else:
|
||||
# Fallback to string parsing
|
||||
if not bounds:
|
||||
bounds = target_node.get("bounds", "")
|
||||
m = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", str(bounds))
|
||||
if m:
|
||||
left, t, r, b = map(int, m.groups())
|
||||
bounds_str = f"[{left},{t}][{r},{b}]"
|
||||
else:
|
||||
aligned = True
|
||||
logger.warning(f"📐 [Alignment] Could not parse bounds: {bounds}")
|
||||
continue
|
||||
|
||||
# Check if this is a false positive (e.g. bottom bar item misclassified)
|
||||
# Post headers should be in the top half usually, or at least not at the very bottom
|
||||
info = device.get_info()
|
||||
h = info.get("displayHeight", 2400)
|
||||
if t > h * 0.85:
|
||||
logger.debug(f"📐 [Alignment] Rejecting node at y={t} (too low, likely bottom bar)")
|
||||
failed_bounds.add(bounds_str)
|
||||
continue
|
||||
|
||||
header_y = (t + b) // 2
|
||||
target_y = 250 # Top margin for headers
|
||||
diff = header_y - target_y
|
||||
|
||||
# If target is off-center (> 50px for higher precision), execute precise correction swipe
|
||||
if abs(diff) > 50:
|
||||
info = device.get_info()
|
||||
w = info.get("displayWidth", 1080)
|
||||
cx = w // 2
|
||||
|
||||
max_safe_swipe = int(h * 0.4)
|
||||
|
||||
# Calculate movement
|
||||
dist = min(abs(diff), max_safe_swipe)
|
||||
if diff > 0:
|
||||
# Content is too LOW. Move it UP (Swipe UP).
|
||||
start_y = int(h * 0.7)
|
||||
end_y = start_y - dist
|
||||
else:
|
||||
# Content is too HIGH. Move it DOWN (Swipe DOWN).
|
||||
start_y = int(h * 0.3)
|
||||
end_y = start_y + dist
|
||||
|
||||
logger.debug(f"📐 [Alignment] Attempt {attempts}: Snapping {diff}px (Swipe {start_y} -> {end_y})")
|
||||
# Duration 1.5s = ultra-precise mechanical drag with ZERO momentum
|
||||
device.swipe(cx, start_y, cx, end_y, duration=1.5)
|
||||
sleep(1.0)
|
||||
|
||||
# Refresh XML for next iteration check
|
||||
continue
|
||||
else:
|
||||
logger.info(f"🎯 [Alignment] Perfect snap achieved after {attempts} attempts.")
|
||||
aligned = True
|
||||
else:
|
||||
break # No header found, cannot align
|
||||
logger.debug(f"📐 [Alignment] No structural markers found on attempt {attempts}.")
|
||||
# If we can't find any markers, maybe we are stuck in a transition.
|
||||
# Micro-wobble to force a layout update.
|
||||
if attempts < 3:
|
||||
info = device.get_info()
|
||||
w, h = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
|
||||
device.swipe(w // 2, h // 2, w // 2, h // 2 - 20, duration=0.2)
|
||||
sleep(0.5)
|
||||
device.swipe(w // 2, h // 2 - 20, w // 2, h // 2, duration=0.2)
|
||||
sleep(1.0)
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"📐 [Alignment] Snapping correction failed: {e}")
|
||||
break
|
||||
|
||||
if aligned and attempts > 1:
|
||||
logger.debug(f"📐 [Alignment] Snapped post cleanly into view after {attempts} attempts.")
|
||||
return True
|
||||
return aligned
|
||||
|
||||
@@ -121,31 +121,11 @@ class QNavGraph:
|
||||
GOAP-powered action execution.
|
||||
Replaces _execute_transition() for post interactions.
|
||||
|
||||
Screen-aware: refuses to attempt actions that don't exist on the current screen.
|
||||
|
||||
Usage:
|
||||
nav_graph.do("like this post") # instead of _execute_transition("tap_like_button")
|
||||
nav_graph.do("follow this user") # instead of _execute_transition("tap_follow_button")
|
||||
nav_graph.do("tap first grid item") # instead of _execute_transition("tap_explore_grid_item")
|
||||
"""
|
||||
# ── Screen sanity check: is this action possible here? ──
|
||||
screen = self.goap.perceive()
|
||||
available = screen.get("available_actions", [])
|
||||
screen_type = screen["screen_type"]
|
||||
|
||||
# Map goal to the action that should be available
|
||||
action_checks = {
|
||||
"like": "tap like button",
|
||||
"comment": "tap comment button",
|
||||
"share": "tap share button",
|
||||
}
|
||||
for keyword, required_action in action_checks.items():
|
||||
if keyword in goal.lower() and required_action not in available:
|
||||
logger.warning(
|
||||
f"🚫 [GOAP] Cannot '{goal}' on {screen_type.value} "
|
||||
f"('{required_action}' not available on this screen)"
|
||||
)
|
||||
return False
|
||||
|
||||
return self.goap._execute_action(goal)
|
||||
|
||||
@@ -171,13 +151,13 @@ class QNavGraph:
|
||||
success = self.sae.ensure_clear_screen(max_attempts=max_attempts + 5, initial_xml=xml_dump)
|
||||
return success
|
||||
|
||||
def _execute_transition(self, action: str, mock_semantic_engine=None, max_retries: int = 2) -> bool:
|
||||
def _execute_transition(self, action: str, max_retries: int = 2) -> bool:
|
||||
"""
|
||||
Executes a transition (e.g. 'tap_explore_tab') using the Telepathic Semantic Engine.
|
||||
"""
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
engine = mock_semantic_engine or TelepathicEngine.get_instance()
|
||||
engine = TelepathicEngine.get_instance()
|
||||
|
||||
failed_positions = set() # Track (x, y) of clicks that failed, for grid retry diversity
|
||||
|
||||
@@ -209,7 +189,7 @@ class QNavGraph:
|
||||
# Grid & Profile
|
||||
"tap_explore_grid_item": "first image in explore grid",
|
||||
"tap_story_tray_item": "profile picture avatar story ring",
|
||||
"tap_follow_button": "tap follow button on profile",
|
||||
"tap_follow_button": "tap 'Follow' button on profile",
|
||||
"tap_grid_first_post": "first image post in profile grid",
|
||||
"tap_back": "tap back button icon arrow",
|
||||
"tap_message_icon": "tap direct message icon inbox",
|
||||
|
||||
@@ -29,7 +29,10 @@ class QdrantBase:
|
||||
|
||||
try:
|
||||
qdrant_url = os.environ.get("QDRANT_URL", "http://localhost:6344")
|
||||
self.client = QdrantClient(url=qdrant_url, timeout=10.0)
|
||||
if qdrant_url == ":memory:":
|
||||
self.client = QdrantClient(location=":memory:")
|
||||
else:
|
||||
self.client = QdrantClient(url=qdrant_url, timeout=10.0)
|
||||
|
||||
if self.client:
|
||||
if self.client.collection_exists(collection_name):
|
||||
@@ -112,8 +115,8 @@ class QdrantBase:
|
||||
args = self._cached_args
|
||||
|
||||
# Pull specific embedding config or fallback to defaults
|
||||
model = getattr(args, "ai_embedding_model", "nomic-embed-text")
|
||||
url = getattr(args, "ai_embedding_url", "http://localhost:11434/api/embeddings")
|
||||
model = getattr(args, "ai_embedding_model")
|
||||
url = getattr(args, "ai_embedding_url")
|
||||
|
||||
try:
|
||||
# Generate embeddings
|
||||
@@ -125,10 +128,10 @@ class QdrantBase:
|
||||
if key:
|
||||
headers["Authorization"] = f"Bearer {key}"
|
||||
# OpenAI/OpenRouter use 'input' instead of 'prompt'
|
||||
payload = {"model": model, "input": str(text)[:8000]}
|
||||
payload = {"model": model, "input": str(text)[:2000]}
|
||||
else:
|
||||
# Local Ollama
|
||||
payload = {"model": model, "prompt": str(text)[:8000]}
|
||||
payload = {"model": model, "prompt": str(text)[:2000]}
|
||||
|
||||
# Log to prevent user from thinking the bot is hung during model swap in VRAM
|
||||
if not getattr(self, "_has_logged_embedding", False):
|
||||
@@ -141,7 +144,7 @@ class QdrantBase:
|
||||
url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=12,
|
||||
timeout=30,
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
logger.debug(f"Embedding API Error {resp.status_code}: {resp.text}")
|
||||
@@ -155,8 +158,8 @@ class QdrantBase:
|
||||
return data["data"][0]["embedding"]
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to generate embedding via {url}: {e}")
|
||||
return None
|
||||
logger.error(f"Failed to generate embedding via {url}: {e}")
|
||||
raise
|
||||
|
||||
def generate_uuid(self, seed_string: str) -> str:
|
||||
"""
|
||||
@@ -184,6 +187,7 @@ class QdrantBase:
|
||||
self.client.upsert(
|
||||
collection_name=self.collection_name,
|
||||
points=[PointStruct(id=point_id, vector=safe_vector, payload=payload)],
|
||||
wait=True,
|
||||
)
|
||||
|
||||
# ABSOLUTE LOGGING: User requirement for full observability
|
||||
@@ -361,7 +365,7 @@ class UIMemoryDB(QdrantBase):
|
||||
sig = re.sub(r"\s+", " ", sig).strip()
|
||||
|
||||
# 3. Strict truncation for nomic-embed-text context window
|
||||
return sig[:4000]
|
||||
return sig[:2000]
|
||||
|
||||
def _deterministic_id(self, intent: str) -> str:
|
||||
"""
|
||||
@@ -429,8 +433,9 @@ class UIMemoryDB(QdrantBase):
|
||||
if exact_points:
|
||||
eval_result = _evaluate_payload(exact_points[0].payload, score=1.0, point_id=point_id)
|
||||
if eval_result:
|
||||
logger.debug(
|
||||
f"Resolved intent '{intent}' from Qdrant Memory via EXACT ID MATCH! (Confidence: {eval_result['effective_confidence']:.2f})"
|
||||
logger.info(
|
||||
f"🧠 [Memory] Applying learned pattern for '{intent}' (EXACT MATCH, Confidence: {eval_result['effective_confidence']:.2f})",
|
||||
extra={"color": "\x1b[36m"}, # Cyan color
|
||||
)
|
||||
return eval_result["solution"]
|
||||
# If exact match failed evaluation (e.g. decayed), we shouldn't fall back to vector search because it's the exact intent!
|
||||
@@ -459,8 +464,9 @@ class UIMemoryDB(QdrantBase):
|
||||
if results and results[0].score >= similarity_threshold:
|
||||
eval_result = _evaluate_payload(results[0].payload, score=results[0].score, point_id=results[0].id)
|
||||
if eval_result:
|
||||
logger.debug(
|
||||
f"Resolved intent '{intent}' from Qdrant Memory via vector search! (Score: {results[0].score:.3f}, Confidence: {eval_result['effective_confidence']:.2f})"
|
||||
logger.info(
|
||||
f"🧠 [Memory] Applying learned pattern for '{intent}' (VECTOR MATCH, Score: {results[0].score:.3f}, Confidence: {eval_result['effective_confidence']:.2f})",
|
||||
extra={"color": "\x1b[36m"}, # Cyan color
|
||||
)
|
||||
return eval_result["solution"]
|
||||
return None
|
||||
@@ -511,7 +517,10 @@ class UIMemoryDB(QdrantBase):
|
||||
],
|
||||
wait=True,
|
||||
)
|
||||
logger.info(f"Learned pattern for '{intent}' and saved to Qdrant Memory (ID: {point_id[:8]}...).")
|
||||
logger.info(
|
||||
f"📥 [Memory] Learned new pattern for '{intent}' and saved to Qdrant (ID: {point_id[:8]}...)",
|
||||
extra={"color": "\x1b[35m"}, # Magenta color
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Qdrant storage error: {e}")
|
||||
|
||||
@@ -573,7 +582,12 @@ class UIMemoryDB(QdrantBase):
|
||||
payload={"confidence": new_confidence},
|
||||
points=[point_id],
|
||||
)
|
||||
logger.debug(f"Confidence for '{intent}' adjusted to {new_confidence:.2f} (delta: {delta:+.2f}).")
|
||||
color = "\x1b[32m" if delta > 0 else "\x1b[31m" # Green for positive, Red for negative
|
||||
symbol = "📈 [Memory] Positive Reinforcement:" if delta > 0 else "📉 [Memory] Negative Reinforcement:"
|
||||
logger.info(
|
||||
f"{symbol} Confidence for '{intent}' adjusted to {new_confidence:.2f} (delta: {delta:+.2f})",
|
||||
extra={"color": color},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Confidence adjustment error: {e}")
|
||||
|
||||
@@ -1188,6 +1202,25 @@ class ParasocialCRMDB(QdrantBase):
|
||||
log_success=f"🧠 [ParasocialCRM] Updated @{username} into Qdrant. Stage {stage} ({intent_type})",
|
||||
)
|
||||
|
||||
def enrich_lead(self, username: str, data: dict):
|
||||
"""
|
||||
Enriches a lead with scraped data.
|
||||
"""
|
||||
if not self.is_connected:
|
||||
return
|
||||
|
||||
current = self.get_relationship_stage(username)
|
||||
current.update(data)
|
||||
|
||||
vector = self._get_embedding(f"User: {username}")
|
||||
if vector:
|
||||
self.upsert_point(
|
||||
seed_string=f"User_{username}",
|
||||
vector=vector,
|
||||
payload=current,
|
||||
log_success=f"🧠 [ParasocialCRM] Enriched @{username} data.",
|
||||
)
|
||||
|
||||
def log_generated_comment(self, username: str, comment_text: str):
|
||||
"""Phase 10: RAG memory point for specific users."""
|
||||
if not self.is_connected:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from colorama import Fore
|
||||
@@ -331,14 +332,32 @@ class ResonanceEngine:
|
||||
is_comment_node = "comment" in res_id or "textview" in res_id
|
||||
|
||||
# 3. Block accessibility garbage & UI labels
|
||||
# Zero-Maintenance: Only structural patterns. Short strings
|
||||
# (< 5 chars) from UI buttons are blocked by length, not by
|
||||
# translating every possible language.
|
||||
is_ui_junk = (
|
||||
val.lower().startswith("go to")
|
||||
or val.lower().startswith("tap to")
|
||||
or "actions for this post" in val.lower()
|
||||
or len(val.strip()) < 3
|
||||
)
|
||||
|
||||
# Block known English UI action labels.
|
||||
# We intentionally do NOT add German/Spanish/etc translations.
|
||||
# Instead, we rely on the structural `is_comment_node` filter
|
||||
# above + length heuristic to catch non-comment UI elements.
|
||||
blocked_exact = [
|
||||
"reply",
|
||||
"like",
|
||||
"view replies",
|
||||
"see translation",
|
||||
"hide replies",
|
||||
"view all comments",
|
||||
"send",
|
||||
]
|
||||
|
||||
if val and len(val) > 2 and is_comment_node and not is_ui_junk:
|
||||
if val.lower() not in ["reply", "like", "view replies", "see translation", "hide replies"]:
|
||||
if val.lower() not in blocked_exact:
|
||||
raw_comments.append(val)
|
||||
except Exception as e:
|
||||
logger.error(f"🧠 [Comment Learning] Failed to parse XML: {e}")
|
||||
@@ -367,8 +386,8 @@ class ResonanceEngine:
|
||||
"Set 'keep' to false only for clear spam, bots, UI buttons, or blacklist violations.\n"
|
||||
)
|
||||
|
||||
model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b")
|
||||
url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate")
|
||||
model = getattr(configs.args, "ai_condenser_model")
|
||||
url = getattr(configs.args, "ai_condenser_url")
|
||||
|
||||
try:
|
||||
import json
|
||||
@@ -393,7 +412,7 @@ class ResonanceEngine:
|
||||
logger.debug(f"DEBUG CONDENSER RAW: {response_text}")
|
||||
|
||||
# Parse json gracefully
|
||||
if type(response_text) is str:
|
||||
if isinstance(response_text, str):
|
||||
clean_json = response_text.strip()
|
||||
if clean_json.startswith("```json"):
|
||||
clean_json = clean_json[7:]
|
||||
|
||||
@@ -33,6 +33,7 @@ class ScreenTopology:
|
||||
"tap profile tab": ScreenType.OWN_PROFILE,
|
||||
"tap reels tab": ScreenType.REELS_FEED,
|
||||
"tap messages tab": ScreenType.DM_INBOX,
|
||||
"tap story ring avatar": ScreenType.STORY_VIEW,
|
||||
},
|
||||
ScreenType.EXPLORE_GRID: {
|
||||
"tap home tab": ScreenType.HOME_FEED,
|
||||
@@ -57,9 +58,17 @@ class ScreenTopology:
|
||||
ScreenType.FOLLOW_LIST: {
|
||||
"press back": ScreenType.OWN_PROFILE,
|
||||
},
|
||||
ScreenType.STORY_VIEW: {
|
||||
"press back": ScreenType.HOME_FEED,
|
||||
},
|
||||
ScreenType.OTHER_PROFILE: {
|
||||
"press back": ScreenType.HOME_FEED,
|
||||
},
|
||||
ScreenType.POST_DETAIL: {
|
||||
"tap home tab": ScreenType.HOME_FEED,
|
||||
"tap explore tab": ScreenType.EXPLORE_GRID,
|
||||
"tap profile tab": ScreenType.OWN_PROFILE,
|
||||
"tap reels tab": ScreenType.REELS_FEED,
|
||||
},
|
||||
ScreenType.UNKNOWN: {
|
||||
"tap home tab": ScreenType.HOME_FEED,
|
||||
@@ -88,7 +97,9 @@ class ScreenTopology:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def find_route(cls, from_screen: ScreenType, to_screen: ScreenType) -> Optional[List[Tuple[str, ScreenType]]]:
|
||||
def find_route(
|
||||
cls, from_screen: ScreenType, to_screen: ScreenType, avoid_actions: set = None
|
||||
) -> Optional[List[Tuple[str, ScreenType]]]:
|
||||
"""
|
||||
BFS shortest path from from_screen to to_screen.
|
||||
|
||||
@@ -100,6 +111,8 @@ class ScreenTopology:
|
||||
if from_screen == to_screen:
|
||||
return []
|
||||
|
||||
avoid_actions = avoid_actions or set()
|
||||
|
||||
queue: deque = deque()
|
||||
queue.append((from_screen, []))
|
||||
visited = {from_screen}
|
||||
@@ -109,6 +122,9 @@ class ScreenTopology:
|
||||
transitions = cls.TRANSITIONS.get(current, {})
|
||||
|
||||
for action, next_screen in transitions.items():
|
||||
if action in avoid_actions or action.replace(" ", "_") in avoid_actions:
|
||||
continue
|
||||
|
||||
if next_screen == to_screen:
|
||||
return path + [(action, next_screen)]
|
||||
|
||||
|
||||
@@ -277,7 +277,29 @@ class SessionState:
|
||||
|
||||
|
||||
class SessionStateEncoder(JSONEncoder):
|
||||
"""JSON encoder for SessionState that is crash-proof against non-serializable types."""
|
||||
|
||||
_SAFE_TYPES = (str, int, float, bool, type(None))
|
||||
|
||||
@classmethod
|
||||
def _sanitize_value(cls, value):
|
||||
"""Convert any non-JSON-serializable value to a safe string representation."""
|
||||
if isinstance(value, cls._SAFE_TYPES):
|
||||
return value
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
if isinstance(value, dict):
|
||||
return {k: cls._sanitize_value(v) for k, v in value.items()}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [cls._sanitize_value(v) for v in value]
|
||||
# Last resort: stringify unknown objects to prevent json.dump mid-write crashes
|
||||
return str(value)
|
||||
|
||||
def default(self, session_state: SessionState):
|
||||
# Sanitize args dict — never trust raw __dict__, it may contain datetime or other garbage
|
||||
raw_args = session_state.args.__dict__ if hasattr(session_state.args, "__dict__") else {}
|
||||
safe_args = {k: self._sanitize_value(v) for k, v in raw_args.items()}
|
||||
|
||||
return {
|
||||
"id": session_state.id,
|
||||
"total_interactions": sum(session_state.totalInteractions.values()),
|
||||
@@ -291,7 +313,7 @@ class SessionStateEncoder(JSONEncoder):
|
||||
"total_scraped": session_state.totalScraped,
|
||||
"start_time": str(session_state.startTime),
|
||||
"finish_time": str(session_state.finishTime),
|
||||
"args": session_state.args.__dict__,
|
||||
"args": safe_args,
|
||||
"profile": {
|
||||
"posts": session_state.my_posts_count,
|
||||
"followers": session_state.my_followers_count,
|
||||
|
||||
@@ -8,6 +8,11 @@ and learns from every episode — positive AND negative.
|
||||
|
||||
After initial learning, 95%+ of situations are handled from memory
|
||||
alone with ZERO LLM calls. This is "Tesla fleet learning" for bots.
|
||||
|
||||
CRITICAL ARCHITECTURE RULE: Language Agnosticism & Structural Determinism
|
||||
NO hardcoded localized UI strings (e.g. "Report", "OK", "Deny") are permitted.
|
||||
All structural heuristic fallbacks must use O(1) Android `resource-id` bounds
|
||||
to ensure the bot functions flawlessly regardless of the device language.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
@@ -270,7 +275,13 @@ class SituationalAwarenessEngine:
|
||||
if clickable == "true":
|
||||
parts.append("CLICKABLE")
|
||||
if bounds:
|
||||
parts.append(f"bounds={bounds}")
|
||||
nums = [int(n) for n in re.findall(r"\d+", bounds)]
|
||||
if len(nums) == 4:
|
||||
cx = (nums[0] + nums[2]) // 2
|
||||
cy = (nums[1] + nums[3]) // 2
|
||||
parts.append(f"bounds={bounds} center=({cx},{cy})")
|
||||
else:
|
||||
parts.append(f"bounds={bounds}")
|
||||
|
||||
elements.append(" | ".join(parts))
|
||||
|
||||
@@ -285,47 +296,53 @@ class SituationalAwarenessEngine:
|
||||
stable = re.sub(r"Battery \d+ per cent", "Battery NN per cent", stable)
|
||||
return hashlib.sha256(stable.encode()).hexdigest()[:32]
|
||||
|
||||
def _get_model_config(self):
|
||||
"""
|
||||
Get model/URL config from Config() — the SINGLE source of truth.
|
||||
Crashes loudly if config is unavailable (Fail Fast, no silent defaults).
|
||||
"""
|
||||
from GramAddict.core.config import Config
|
||||
|
||||
cfg = Config()
|
||||
args = cfg.args
|
||||
return {
|
||||
"model": getattr(args, "ai_model", None),
|
||||
"url": getattr(args, "ai_model_url", None),
|
||||
"telepathic_model": getattr(args, "ai_telepathic_model", None),
|
||||
"telepathic_url": getattr(args, "ai_telepathic_url", None),
|
||||
}
|
||||
|
||||
def perceive(self, xml_dump: str) -> SituationType:
|
||||
"""
|
||||
Fast structural classification — NO LLM needed for perception.
|
||||
Uses package names + structural markers to classify.
|
||||
Autonomous situation classification — ZERO hardcoded UI element identifiers.
|
||||
|
||||
Flow:
|
||||
1. Empty/invalid XML → FOREIGN_APP
|
||||
2. Hardware check (screen off) → LOCKED_SCREEN
|
||||
3. Package-based detection (app-agnostic, zero maintenance):
|
||||
- Permission controller packages → OBSTACLE_SYSTEM
|
||||
- App package missing → OBSTACLE_FOREIGN_APP
|
||||
4. Qdrant semantic cache → instant recall of learned screen types
|
||||
5. ScreenIdentity structural delegation → if MODAL, return OBSTACLE_MODAL
|
||||
6. LLM classification fallback → autonomous discovery of new obstacle types
|
||||
|
||||
NO hardcoded resource-ids, NO hardcoded button texts, NO localized strings.
|
||||
The bot discovers and learns ALL obstacles autonomously via the LLM + Qdrant loop.
|
||||
"""
|
||||
if not xml_dump or not isinstance(xml_dump, str):
|
||||
return SituationType.OBSTACLE_FOREIGN_APP
|
||||
|
||||
xml_dump.lower()
|
||||
|
||||
blocked_markers = [
|
||||
"try again later",
|
||||
"action blocked",
|
||||
"restrict certain activity",
|
||||
"help us confirm you own",
|
||||
"confirm it's you",
|
||||
"später erneut versuchen",
|
||||
"bestätige, dass du es bist",
|
||||
"handlung blockiert",
|
||||
"eingeschränkt",
|
||||
]
|
||||
|
||||
# Guard: Check if the text matches are relatively isolated (e.g. short strings).
|
||||
# If the string is buried inside a 200-character caption, it's a false positive.
|
||||
# We can regex match text="..." attributes that are less than 60 characters total,
|
||||
# OR just use the compressed string where text is capped at 60 chars anyway.
|
||||
compressed_lower = self._compress_xml(xml_dump).lower()
|
||||
if any(re.search(rf"(?:text|desc)='[^']*?{m}[^']*?'", compressed_lower) for m in blocked_markers):
|
||||
# To be extra safe against false positives, check if there's a dialog/modal container
|
||||
if "dialog" in compressed_lower or "bottom_sheet" in compressed_lower or "alert" in compressed_lower:
|
||||
return SituationType.DANGER_ACTION_BLOCKED
|
||||
|
||||
# ── Hardware Guard: Screen Off / Locked ──
|
||||
if not getattr(self.device.deviceV2, "info", {}).get("screenOn", True):
|
||||
logger.info("📱 [SAE Perceive] Screen is physically OFF.")
|
||||
return SituationType.OBSTACLE_LOCKED_SCREEN
|
||||
|
||||
# ── System Dialog / Permission Detect (Fast Path) ──
|
||||
packages = set(re.findall(r'package=["\']([^"\']+)["\']', xml_dump))
|
||||
# ── System Dialog / Permission Detect (package-based, app-agnostic) ──
|
||||
packages = set(re.findall(r'package=["\'](.[^"\']+)["\']+', xml_dump))
|
||||
app_id = getattr(self.device, "app_id", "com.instagram.android")
|
||||
|
||||
# Permission controller packages are Android system-level — not app-specific.
|
||||
# These will NEVER change with an Instagram update. Completely safe to check.
|
||||
system_dialog_pkgs = {
|
||||
"com.google.android.permissioncontroller",
|
||||
"com.android.permissioncontroller",
|
||||
@@ -335,23 +352,24 @@ class SituationalAwarenessEngine:
|
||||
logger.info("📱 [SAE Perceive] System permission dialog explicitly detected.")
|
||||
return SituationType.OBSTACLE_SYSTEM
|
||||
|
||||
# ── Foreign Environment Detection (package-based) ──
|
||||
# If the main app package is completely absent from the UI hierarchy,
|
||||
# OR if there's a dominant foreign package and no app package, we might have lost the app.
|
||||
|
||||
# If our app is on screen, we trust we are in the app (even if a custom keyboard is open).
|
||||
# We only trigger foreign app classification if our app is completely missing from the screen.
|
||||
is_foreign = False
|
||||
if packages and app_id not in packages:
|
||||
is_foreign = True
|
||||
# ── Foreign Environment Detection (package-based, zero maintenance) ──
|
||||
# If our app package is completely absent from the screen → foreign app.
|
||||
# Package names are Android-level identifiers, NOT Instagram UI elements.
|
||||
is_foreign = bool(packages) and app_id not in packages
|
||||
|
||||
if is_foreign:
|
||||
# We explicitly ask the TelepathicEngine to classify this to avoid writing brittle substring hacks
|
||||
# for Android System UI variations across different device manufacturers.
|
||||
# Any package that isn't our app AND isn't just systemui = foreign
|
||||
dominant_pkgs = packages - {"com.android.systemui"}
|
||||
if dominant_pkgs:
|
||||
logger.info(f"🚨 [SAE Perceive] Foreign package detected: {dominant_pkgs} → OBSTACLE_FOREIGN_APP")
|
||||
return SituationType.OBSTACLE_FOREIGN_APP
|
||||
|
||||
# SystemUI-only edge case: could be lock screen, notification shade, etc.
|
||||
# Use LLM for disambiguation.
|
||||
try:
|
||||
from GramAddict.core.config import Config
|
||||
from GramAddict.core.llm_provider import query_telepathic_llm
|
||||
|
||||
cfg = self._get_model_config()
|
||||
screen_off = not getattr(self.device.deviceV2, "info", {}).get("screenOn", True)
|
||||
|
||||
prompt = (
|
||||
@@ -364,17 +382,9 @@ class SituationalAwarenessEngine:
|
||||
f"XML:\n{self._compress_xml(xml_dump)[:2500]}"
|
||||
)
|
||||
|
||||
args = {}
|
||||
try:
|
||||
args = Config().args
|
||||
except Exception:
|
||||
pass
|
||||
model = getattr(args, "ai_telepathic_model", "qwen3.5:latest")
|
||||
url = getattr(args, "ai_telepathic_url", "http://localhost:11434/api/generate")
|
||||
|
||||
res = query_telepathic_llm(
|
||||
model=model,
|
||||
url=url,
|
||||
model=cfg["model"],
|
||||
url=cfg["url"],
|
||||
system_prompt="Strict JSON classifier.",
|
||||
user_prompt=prompt,
|
||||
use_local_edge=True,
|
||||
@@ -385,83 +395,95 @@ class SituationalAwarenessEngine:
|
||||
situ_str = data.get("situation", "")
|
||||
|
||||
if situ_str == "OBSTACLE_LOCKED_SCREEN":
|
||||
logger.info("🧠 [Smart Perceive] SystemUI definitively classified as: LOCKED_SCREEN.")
|
||||
logger.info("🧠 [Smart Perceive] SystemUI classified as: LOCKED_SCREEN.")
|
||||
return SituationType.OBSTACLE_LOCKED_SCREEN
|
||||
elif situ_str == "OBSTACLE_SYSTEM":
|
||||
logger.info("🧠 [Smart Perceive] SystemUI definitively classified as: SYSTEM_DIALOG.")
|
||||
logger.info("🧠 [Smart Perceive] SystemUI classified as: SYSTEM_DIALOG.")
|
||||
return SituationType.OBSTACLE_SYSTEM
|
||||
else:
|
||||
logger.info("🧠 [Smart Perceive] SystemUI classified as: FOREIGN_APP / NOTIFICATION.")
|
||||
logger.info("🧠 [Smart Perceive] SystemUI classified as: FOREIGN_APP.")
|
||||
return SituationType.OBSTACLE_FOREIGN_APP
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ [Smart Perceive] LLM Classification failed ({e}). Defaulting to FOREIGN_APP.")
|
||||
return SituationType.OBSTACLE_FOREIGN_APP
|
||||
|
||||
# ── Modal/Obstacle Detection (Autonomous LLM + Memory) ──
|
||||
# We explicitly query ScreenMemoryDB. If unknown, we ask the LLM.
|
||||
# This replaces ALL brittle string/ID matching for modals.
|
||||
# ── In-App Obstacle Detection (100% autonomous, ZERO hardcoded UI identifiers) ──
|
||||
# The bot learns ALL obstacle types via the LLM + Qdrant feedback loop.
|
||||
# First encounter: LLM classifies → result is cached in Qdrant.
|
||||
# All subsequent encounters: instant O(1) recall from cache.
|
||||
from GramAddict.core.qdrant_memory import ScreenMemoryDB
|
||||
|
||||
screen_memory = ScreenMemoryDB()
|
||||
|
||||
compressed = self._compress_xml(xml_dump)
|
||||
|
||||
# ── Structural Fast-Check: Content-Creation Overlays ──
|
||||
# These full-screen overlays live INSIDE Instagram's package but block
|
||||
# all normal navigation. They are invisible to the foreign-app detector
|
||||
# and frequently fool the LLM into thinking they are "normal" browsing.
|
||||
# Detecting them structurally is O(1) and requires ZERO LLM calls.
|
||||
creation_flow_markers = (
|
||||
"quick_capture", # Camera / story capture overlay
|
||||
"gallery_cancel_button", # Story gallery "Back to Home" button
|
||||
"creation_flow", # Post creation wizard
|
||||
"reel_camera", # Reel recording interface
|
||||
)
|
||||
|
||||
# Guard: Check against compressed string to ensure these markers ONLY appear
|
||||
# as resource IDs (e.g. "id=quick_capture_...") and not as plain text in
|
||||
# user comments/bios (which would look like "text='... creation_flow ...'")
|
||||
if any(re.search(rf"id=[^\s|]*{marker}", compressed, re.IGNORECASE) for marker in creation_flow_markers):
|
||||
logger.info("🧠 [SAE Perceive] Content-creation overlay detected structurally → OBSTACLE_MODAL")
|
||||
screen_memory.store_screen(compressed, "OBSTACLE_MODAL")
|
||||
return SituationType.OBSTACLE_MODAL
|
||||
# ── Priority 1: Qdrant Semantic Cache (O(1), zero LLM calls) ──
|
||||
cached_type = screen_memory.get_screen_type(compressed)
|
||||
|
||||
if cached_type:
|
||||
if cached_type == "OBSTACLE_MODAL":
|
||||
return SituationType.OBSTACLE_MODAL
|
||||
elif cached_type == "DANGER_ACTION_BLOCKED":
|
||||
return SituationType.DANGER_ACTION_BLOCKED
|
||||
elif cached_type == "NORMAL":
|
||||
return SituationType.NORMAL
|
||||
|
||||
# If not cached, query LLM for autonomous structural classification
|
||||
# ── Priority 2: ScreenIdentity structural delegation ──
|
||||
# ScreenIdentity classifies the screen using its own structural logic.
|
||||
# If it says MODAL → trust it (it uses the same zero-trust approach).
|
||||
from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType
|
||||
|
||||
screen_id = ScreenIdentity(getattr(self.device, "bot_username", ""))
|
||||
screen_result = screen_id.identify(xml_dump)
|
||||
screen_type = screen_result.get("screen_type", ScreenType.UNKNOWN)
|
||||
|
||||
if screen_type == ScreenType.MODAL:
|
||||
logger.info("🧠 [SAE Perceive] ScreenIdentity classified as MODAL → OBSTACLE_MODAL")
|
||||
screen_memory.store_screen(compressed, "OBSTACLE_MODAL")
|
||||
return SituationType.OBSTACLE_MODAL
|
||||
|
||||
if screen_type == ScreenType.FOREIGN_APP:
|
||||
logger.info("🧠 [SAE Perceive] ScreenIdentity classified as FOREIGN_APP → OBSTACLE_FOREIGN_APP")
|
||||
return SituationType.OBSTACLE_FOREIGN_APP
|
||||
|
||||
if screen_type == ScreenType.DANGER_ACTION_BLOCKED:
|
||||
logger.info("🧠 [SAE Perceive] ScreenIdentity classified as DANGER_ACTION_BLOCKED")
|
||||
screen_memory.store_screen(compressed, "DANGER_ACTION_BLOCKED")
|
||||
return SituationType.DANGER_ACTION_BLOCKED
|
||||
|
||||
# If ScreenIdentity positively identified a known screen type (not UNKNOWN),
|
||||
# we trust it as NORMAL — no LLM needed.
|
||||
if screen_type != ScreenType.UNKNOWN:
|
||||
screen_memory.store_screen(compressed, "NORMAL")
|
||||
return SituationType.NORMAL
|
||||
|
||||
# ── Priority 3: LLM autonomous classification (first-encounter learning) ──
|
||||
# This is the ONLY path that reaches the LLM. After classification,
|
||||
# the result is cached in Qdrant — so this screen type is learned forever.
|
||||
try:
|
||||
from GramAddict.core.config import Config
|
||||
from GramAddict.core.llm_provider import query_telepathic_llm
|
||||
|
||||
cfg = self._get_model_config()
|
||||
|
||||
prompt = (
|
||||
"You are a Situation Classifier for a mobile automation agent.\n"
|
||||
"Analyze the given Android UI XML dump. Is there a blocking MODAL, DIALOG, or POPUP "
|
||||
"covering the screen that needs to be dismissed, or is this a NORMAL usable screen?\n"
|
||||
"A 'clean_sheet_container' with standard Instagram feed content is NORMAL.\n"
|
||||
"A survey, rating prompt, 'not now' prompt, or permission dialog is an OBSTACLE_MODAL.\n"
|
||||
"An 'Add to story' screen, camera interface, 'quick_capture' layout, gallery picker, "
|
||||
"or ANY content-creation flow (reel recording, post editor, live mode) is an OBSTACLE_MODAL — "
|
||||
"it blocks normal navigation and must be dismissed.\n"
|
||||
"Respond ONLY with a valid JSON object strictly matching this schema: "
|
||||
'{"situation": "OBSTACLE_MODAL" | "NORMAL"}\n\n'
|
||||
"Analyze the given Android UI XML dump AND screenshot. Classify the screen into one of:\n"
|
||||
"- OBSTACLE_MODAL: Any blocking overlay, dialog, popup, survey, rating prompt, "
|
||||
"browser window, camera/creation flow, or any UI that blocks normal feed browsing.\n"
|
||||
"- DANGER_ACTION_BLOCKED: Instagram's rate-limit or action-block warning "
|
||||
"(e.g. 'Try Again Later', 'Action Blocked', or any restriction/verification screen).\n"
|
||||
"- NORMAL: A standard usable screen (feed, explore, profile, DM, etc.)\n\n"
|
||||
"Respond ONLY with a valid JSON object: "
|
||||
'{"situation": "OBSTACLE_MODAL" | "DANGER_ACTION_BLOCKED" | "NORMAL"}\n\n'
|
||||
f"XML:\n{compressed[:2500]}"
|
||||
)
|
||||
|
||||
args = {}
|
||||
try:
|
||||
args = Config().args
|
||||
except Exception:
|
||||
pass
|
||||
model = getattr(args, "ai_telepathic_model", "qwen3.5:latest")
|
||||
url = getattr(args, "ai_telepathic_url", "http://localhost:11434/api/generate")
|
||||
|
||||
screenshot_b64 = getattr(self.device, "get_screenshot_b64", lambda: None)()
|
||||
res = query_telepathic_llm(
|
||||
model=model, url=url, system_prompt="Strict JSON classifier.", user_prompt=prompt, use_local_edge=True
|
||||
model=cfg["telepathic_model"],
|
||||
url=cfg["telepathic_url"],
|
||||
system_prompt="Strict JSON classifier.",
|
||||
user_prompt=prompt,
|
||||
images_b64=[screenshot_b64] if screenshot_b64 else None,
|
||||
use_local_edge=True,
|
||||
)
|
||||
import json
|
||||
|
||||
@@ -472,6 +494,10 @@ class SituationalAwarenessEngine:
|
||||
logger.info("🧠 [Smart Perceive] Screen classified as: OBSTACLE_MODAL.")
|
||||
screen_memory.store_screen(compressed, "OBSTACLE_MODAL")
|
||||
return SituationType.OBSTACLE_MODAL
|
||||
elif situ_str == "DANGER_ACTION_BLOCKED":
|
||||
logger.info("🧠 [Smart Perceive] Screen classified as: DANGER_ACTION_BLOCKED.")
|
||||
screen_memory.store_screen(compressed, "DANGER_ACTION_BLOCKED")
|
||||
return SituationType.DANGER_ACTION_BLOCKED
|
||||
else:
|
||||
logger.info("🧠 [Smart Perceive] Screen classified as: NORMAL.")
|
||||
screen_memory.store_screen(compressed, "NORMAL")
|
||||
@@ -501,28 +527,27 @@ class SituationalAwarenessEngine:
|
||||
LLM-powered escape planning for situations where structural scan fails.
|
||||
Called ONLY when recall AND structural planning both miss.
|
||||
"""
|
||||
from GramAddict.core.config import Config
|
||||
from GramAddict.core.llm_provider import query_llm
|
||||
from GramAddict.core.llm_provider import query_telepathic_llm
|
||||
|
||||
try:
|
||||
args = Config().args
|
||||
model = getattr(args, "ai_fallback_model", "llama3.2:1b")
|
||||
url = getattr(args, "ai_fallback_url", "http://localhost:11434/api/generate")
|
||||
except Exception:
|
||||
model = "llama3.2:1b"
|
||||
url = "http://localhost:11434/api/generate"
|
||||
cfg = self._get_model_config()
|
||||
model = cfg["telepathic_model"]
|
||||
url = cfg["telepathic_url"]
|
||||
|
||||
system_prompt = (
|
||||
"You are an Android UI navigation agent. Your job is to escape obstacles "
|
||||
"(dialogs, modals, foreign apps, system popups) and return to Instagram. "
|
||||
"Analyze the screen content and return a JSON escape action.\n\n"
|
||||
"Analyze the screen content (Screenshot AND XML) and return a JSON escape action.\n\n"
|
||||
"Rules:\n"
|
||||
"- If you see a dismiss/close/cancel/skip/not now button, click it\n"
|
||||
"- If the Situation type is OBSTACLE_LOCKED_SCREEN, action must be 'unlock'\n"
|
||||
"- If the Situation type is OBSTACLE_FOREIGN_APP, action must be 'kill_foreign_apps'\n"
|
||||
"- If the Situation type is obstacle_locked_screen, action must be 'unlock'\n"
|
||||
"- If the Situation type is obstacle_foreign_app, action must be 'kill_foreign_apps'\n"
|
||||
"- If the Situation type is obstacle_system, you MUST look for the negative action (e.g. deny, block, do not allow, cancel) and click it. \n"
|
||||
" NEVER click the positive action (allow, accept, confirm) on system permissions.\n"
|
||||
" If no negative action button exists, action must be 'back'\n"
|
||||
"- If there is NO obstacle and the screen is a normal Instagram view (false positive), action must be 'false_positive'\n"
|
||||
"- If nothing else works, suggest 'app_start' to force-reopen Instagram\n"
|
||||
"- NEVER click 'OK'/'Confirm'/'Accept' on surveys or prompts\n"
|
||||
"- NEVER click the positive/accept button on surveys or prompts\n"
|
||||
"- When you choose to click, you MUST use the EXACT coordinates provided in `center=(x,y)` for that element in the XML\n"
|
||||
'- Return ONLY valid JSON: {"action": "click"|"back"|"app_start"|"unlock"|"kill_foreign_apps"|"false_positive", "x": N, "y": N, "reason": "..."}'
|
||||
)
|
||||
|
||||
@@ -533,20 +558,31 @@ class SituationalAwarenessEngine:
|
||||
user_prompt += "What action should I take to clear this obstacle and return to Instagram? Return JSON only."
|
||||
|
||||
try:
|
||||
resp = query_llm(
|
||||
screenshot_b64 = getattr(self.device, "get_screenshot_b64", lambda: None)()
|
||||
|
||||
resp = query_telepathic_llm(
|
||||
url=url,
|
||||
model=model,
|
||||
prompt=user_prompt,
|
||||
system=system_prompt,
|
||||
format_json=True,
|
||||
timeout=30,
|
||||
max_tokens=300,
|
||||
user_prompt=user_prompt,
|
||||
system_prompt=system_prompt,
|
||||
images_b64=[screenshot_b64] if screenshot_b64 else None,
|
||||
temperature=0.0,
|
||||
)
|
||||
if resp and "response" in resp:
|
||||
if resp:
|
||||
import json
|
||||
|
||||
data = json.loads(resp["response"])
|
||||
try:
|
||||
data = json.loads(resp)
|
||||
except json.JSONDecodeError:
|
||||
# Try extracting JSON via regex if LLM was chatty
|
||||
import re
|
||||
|
||||
match = re.search(r"\{.*\}", resp, re.DOTALL)
|
||||
if match:
|
||||
data = json.loads(match.group(0))
|
||||
else:
|
||||
raise ValueError(f"Could not parse JSON from: {resp}")
|
||||
|
||||
return EscapeAction(
|
||||
action_type=data.get("action", "back"),
|
||||
x=int(data.get("x", 0)),
|
||||
@@ -658,6 +694,24 @@ class SituationalAwarenessEngine:
|
||||
|
||||
logger.warning(f"🔍 [SAE] Obstacle detected: {situation.value} (attempt {attempt + 1}/{max_attempts})")
|
||||
|
||||
# ── O(1) Fast-Path for Foreign Apps ──
|
||||
if situation == SituationType.OBSTACLE_FOREIGN_APP:
|
||||
logger.warning("⚡ [SAE Fast-Path] Foreign App detected. Bypassing LLM and killing immediately.")
|
||||
action = EscapeAction("kill_foreign_apps", reason="O(1) fast-path to eliminate foreign app")
|
||||
self._execute_escape(action)
|
||||
|
||||
# Check if we recovered
|
||||
post_xml = self.device.dump_hierarchy()
|
||||
if self.perceive(post_xml) == SituationType.NORMAL:
|
||||
logger.info("✅ [SAE Fast-Path] Foreign App cleared successfully!")
|
||||
self._consecutive_failures = 0
|
||||
return True
|
||||
|
||||
# If we didn't recover, log it and let the loop continue
|
||||
logger.warning("⚠️ [SAE Fast-Path] kill_foreign_apps did not return to NORMAL. Retrying...")
|
||||
self._consecutive_failures += 1
|
||||
continue
|
||||
|
||||
# ── COMPRESS for memory lookup ──
|
||||
compressed = self._compress_xml(xml_dump)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from time import sleep
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def ghost_type(device, text: str):
|
||||
def ghost_type(device, text: str, speed: str = "normal"):
|
||||
"""
|
||||
Tesla Stealth Ghost Keyboard.
|
||||
Bypasses UIAutomator virtual IME completely and sends raw Native InputEvents.
|
||||
@@ -48,6 +48,10 @@ def ghost_type(device, text: str):
|
||||
else:
|
||||
_adb_inject_text(device, chunk)
|
||||
|
||||
if speed == "fast":
|
||||
sleep(random.uniform(0.01, 0.05))
|
||||
continue
|
||||
|
||||
# Realistic pause between semantic bursts (humans think while typing)
|
||||
if chunk.endswith((" ", ".", ",", "!", "?")):
|
||||
sleep(random.uniform(0.2, 0.5))
|
||||
|
||||
@@ -53,34 +53,21 @@ class TelepathicEngine:
|
||||
# Core Resolution Engine
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def find_best_node(self, xml_string: str, intent_description: str, device=None, **kwargs) -> Optional[dict]:
|
||||
def find_best_node(
|
||||
self,
|
||||
xml_string: str,
|
||||
intent_description: str,
|
||||
device=None,
|
||||
track: bool = True,
|
||||
exclude_bounds: list[str] = None,
|
||||
**kwargs,
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
Public facade for resolving a node.
|
||||
Translates Android UI bounds into standard GramAddict node dicts.
|
||||
"""
|
||||
logger.debug(f"🧠 [SpatialEngine] Resolving intent: '{intent_description}'")
|
||||
|
||||
# 0. DM Thread Guard: Block profile intents inside DM threads
|
||||
is_dm_thread = "direct_thread_header" in xml_string or "row_thread_composer_edittext" in xml_string
|
||||
if is_dm_thread:
|
||||
profile_keywords = ["profile", "follow", "first image", "grid", "avatar", "story ring", "feed"]
|
||||
if any(k in intent_description.lower() for k in profile_keywords):
|
||||
logger.warning(f"🛡️ [DM Guard] Blocked profile/feed intent '{intent_description}' inside DM thread.")
|
||||
return {"blocked_by_dm_thread": True}
|
||||
|
||||
# 0.5 Comments Disabled Guard
|
||||
if "comment" in intent_description.lower():
|
||||
if "comments are turned off" in xml_string.lower():
|
||||
logger.warning("🛡️ [Comment Guard] Comments are disabled on this post.")
|
||||
return {"skip": True, "semantic": "comments disabled"}
|
||||
|
||||
# 1.25 Grid Fast-Path (Deterministically bypass VLM for first grid item)
|
||||
if "first image in explore grid" in intent_description.lower():
|
||||
nodes_dicts = self._extract_semantic_nodes(xml_string)
|
||||
fast_node = self._grid_fast_path(intent_description, nodes_dicts, kwargs.get("skip_positions"))
|
||||
if fast_node:
|
||||
return fast_node
|
||||
|
||||
# 1. Parse into Spatial Topology
|
||||
root = self._parser.parse(xml_string)
|
||||
if not root:
|
||||
@@ -90,24 +77,66 @@ class TelepathicEngine:
|
||||
# 2. Extract interactable candidates
|
||||
candidates = self._parser.get_clickable_nodes(root)
|
||||
|
||||
if exclude_bounds:
|
||||
filtered_candidates = []
|
||||
for c in candidates:
|
||||
bounds_str = f"[{c.x1},{c.y1}][{c.x2},{c.y2}]"
|
||||
if bounds_str not in exclude_bounds:
|
||||
filtered_candidates.append(c)
|
||||
candidates = filtered_candidates
|
||||
|
||||
# --- Active Keyboard Dismiss Guard ---
|
||||
# If keyboard is open but intent is NOT typing related -> dismiss it!
|
||||
intent_lower = intent_description.lower()
|
||||
if device and self._resolver.has_keyboard_open(candidates):
|
||||
typing_keywords = ["type", "message", "comment", "search", "write"]
|
||||
if not any(k in intent_lower for k in typing_keywords):
|
||||
logger.warning("⌨️ [TelepathicEngine] Keyboard detected during non-typing intent! Auto-dismissing.")
|
||||
import time
|
||||
|
||||
device.back()
|
||||
time.sleep(1.0)
|
||||
# Re-fetch UI state
|
||||
xml_string = device.dump_hierarchy()
|
||||
if xml_string:
|
||||
root = self._parser.parse(xml_string)
|
||||
if root:
|
||||
candidates = self._parser.get_clickable_nodes(root)
|
||||
|
||||
# 3. Resolve intent against candidates
|
||||
best_node = self._resolver.resolve(intent_description, candidates)
|
||||
best_node = self._resolver.resolve(intent_description, candidates, device=device)
|
||||
|
||||
if not best_node:
|
||||
logger.warning(f"No viable nodes found for intent: '{intent_description}'")
|
||||
return None
|
||||
|
||||
# 3.1 BUG 7 Fix: Semantic Guard for 'post media content'
|
||||
intent_lower = intent_description.lower()
|
||||
semantic_str = (
|
||||
(best_node.text or "") + " " + (best_node.content_desc or "") + " " + (best_node.resource_id or "")
|
||||
).lower()
|
||||
if "post media content" in intent_lower:
|
||||
if "follow" in semantic_str.replace("_", " "):
|
||||
logger.warning("🚫 [SpatialEngine] VLM selected a 'Follow' button for 'post media content'. Blocked.")
|
||||
return None
|
||||
|
||||
# 3.5 Following Button Guard
|
||||
if "follow" in intent_description.lower() and "unfollow" not in intent_description.lower():
|
||||
if (
|
||||
"follow" in intent_description.lower()
|
||||
and "unfollow" not in intent_description.lower()
|
||||
and "following" not in intent_description.lower()
|
||||
):
|
||||
semantic = (
|
||||
(best_node.text or "") + " " + (best_node.content_desc or "") + " " + (best_node.resource_id or "")
|
||||
)
|
||||
semantic = semantic.lower()
|
||||
if "following" in semantic or "gefolgt" in semantic or "requested" in semantic or "angefragt" in semantic:
|
||||
# Zero-Maintenance: Strictly structural IDs, no localized strings.
|
||||
if "button_following" in semantic or "profile_header_following" in semantic:
|
||||
return {"skip": True, "semantic": "already_followed"}
|
||||
|
||||
# 4. Track action
|
||||
self._memory.track_click(intent_description, best_node, xml_string)
|
||||
if track:
|
||||
self._memory.track_click(intent_description, best_node, xml_string)
|
||||
|
||||
# Translate to old GramAddict dict format for backward compatibility
|
||||
return self._translate_node(best_node)
|
||||
@@ -122,6 +151,7 @@ class TelepathicEngine:
|
||||
"description": node.content_desc,
|
||||
"id": node.resource_id,
|
||||
"class": node.class_name,
|
||||
"semantic": node.content_desc or node.text or node.resource_id,
|
||||
"original_attribs": node.to_dict(),
|
||||
}
|
||||
|
||||
@@ -144,27 +174,6 @@ class TelepathicEngine:
|
||||
nodes = self._parser.get_clickable_nodes(root)
|
||||
return [self._translate_node(n) for n in nodes]
|
||||
|
||||
def _grid_fast_path(self, intent_description: str, nodes: list, skip_positions: set = None) -> Optional[dict]:
|
||||
if skip_positions is None:
|
||||
skip_positions = set()
|
||||
|
||||
if "first image in explore grid" in intent_description.lower():
|
||||
grid_items = [
|
||||
n
|
||||
for n in nodes
|
||||
if n.get("y", 9999) < 2000
|
||||
and (
|
||||
"grid card layout container" in (n.get("semantic_string", "") or "").lower()
|
||||
or "image button" in (n.get("semantic_string", "") or "").lower()
|
||||
)
|
||||
and (n.get("x", -1), n.get("y", -1)) not in skip_positions
|
||||
]
|
||||
if grid_items:
|
||||
# Sort by y (row) then by x (col)
|
||||
grid_items.sort(key=lambda n: (n.get("y", 9999), n.get("x", 9999)))
|
||||
return grid_items[0]
|
||||
return None
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Action Memory Delegation
|
||||
# ──────────────────────────────────────────────
|
||||
@@ -178,11 +187,11 @@ class TelepathicEngine:
|
||||
def decay_click(self, intent: str = None):
|
||||
self._memory.reject_click(intent) # Alias to reject
|
||||
|
||||
def verify_success(self, intent: str, post_click_xml: str) -> bool:
|
||||
def verify_success(self, intent: str, post_click_xml: str, device=None, confidence: float = 0.0) -> bool:
|
||||
pre_click_xml = ""
|
||||
if self._memory._last_click_context:
|
||||
pre_click_xml = self._memory._last_click_context.get("xml_context", "")
|
||||
return self._memory.verify_success(intent, pre_click_xml, post_click_xml)
|
||||
return self._memory.verify_success(intent, pre_click_xml, post_click_xml, device=device, confidence=confidence)
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Semantic Evaluator Delegation
|
||||
@@ -193,13 +202,19 @@ class TelepathicEngine:
|
||||
if not root:
|
||||
return None
|
||||
|
||||
# Find grid-like visual nodes
|
||||
# Find grid-like visual nodes using structural markers
|
||||
all_nodes = self._parser.get_all_nodes(root)
|
||||
grid_candidates = [
|
||||
n
|
||||
for n in all_nodes
|
||||
if not n.text and ("photo" in n.content_desc.lower() or "video" in n.content_desc.lower() or n.area > 50000)
|
||||
]
|
||||
grid_candidates = []
|
||||
for n in all_nodes:
|
||||
rid = (n.resource_id or "").lower()
|
||||
desc = (n.content_desc or "").lower()
|
||||
|
||||
# Structural anchor for Instagram grid items
|
||||
if "grid_card_layout_container" in rid:
|
||||
grid_candidates.append(n)
|
||||
# Fallback for older versions or profile grids if resource-id is missing
|
||||
elif not n.text and ("photo" in desc or "video" in desc) and (50000 < n.area < 400000):
|
||||
grid_candidates.append(n)
|
||||
|
||||
best = self._evaluator.evaluate_grid_visuals(device, persona_interests, grid_candidates)
|
||||
if best:
|
||||
@@ -238,35 +253,34 @@ class TelepathicEngine:
|
||||
y = node.get("y", 0)
|
||||
semantic = (node.get("semantic_string", "") or "").lower()
|
||||
|
||||
# 1. Navigation Tab Guard (Must be at the bottom)
|
||||
nav_intents = [
|
||||
"tap direct message icon inbox",
|
||||
"tap inbox",
|
||||
"tap heart icon notifications",
|
||||
"tap home tab",
|
||||
"tap explore tab",
|
||||
"tap reels tab",
|
||||
"tap profile tab",
|
||||
"tap messages tab",
|
||||
]
|
||||
is_nav_intent = any(n in intent for n in nav_intents)
|
||||
if is_nav_intent:
|
||||
if y < screen_height * 0.85:
|
||||
return False
|
||||
return True
|
||||
|
||||
# 2. Block non-nav intents from clicking in the nav zone
|
||||
if y >= screen_height * 0.85:
|
||||
# Not a nav intent, but trying to click the nav bar
|
||||
return False
|
||||
|
||||
# 3. Post Username Guard
|
||||
if "post username" in intent:
|
||||
if "story" in semantic and y < screen_height * 0.2:
|
||||
# 1. Post Username Guard
|
||||
if "post username" in intent or "author username" in intent:
|
||||
if "story" in semantic:
|
||||
# E.g. "Your Story" circle at the top
|
||||
return False
|
||||
# Prevent tapping a search list item when looking for a post username
|
||||
if "row search user container" in semantic.replace("_", " "):
|
||||
return False
|
||||
# Prevent tapping bottom tabs
|
||||
if "tab" in semantic and "exclude bottom tabs" in intent:
|
||||
return False
|
||||
return True
|
||||
|
||||
# 3.5 Media Content Guard
|
||||
if "post media content" in intent:
|
||||
# Prevent tapping a search keyword instead of a media post
|
||||
if "row search keyword title" in semantic.replace("_", " "):
|
||||
return False
|
||||
# Prevent tapping bottom tabs
|
||||
if "tab" in semantic and "exclude bottom tabs" in intent:
|
||||
return False
|
||||
|
||||
# 3.6 Post Author Username Header Guard
|
||||
if "post author username header" in intent:
|
||||
# Prevent tapping the follow button when looking for the username
|
||||
if "follow button" in semantic.replace("_", " "):
|
||||
return False
|
||||
|
||||
# 4. Profile Picture/Story Ring Guard
|
||||
if "story ring" in intent or "avatar" in intent:
|
||||
current_user = self._get_current_username()
|
||||
|
||||
@@ -65,8 +65,29 @@ def _run_zero_latency_unfollow_loop(
|
||||
try:
|
||||
xml_dump = device.dump_hierarchy()
|
||||
|
||||
# Smart Unfollow Phase 1: Find user rows instead of just clicking "Following"
|
||||
nodes = telepathic._extract_semantic_nodes(xml_dump, "find user profile rows in list", threshold=0.7)
|
||||
# ── Perimeter Guard: Verify we're still inside Instagram ──
|
||||
if xml_dump:
|
||||
import re
|
||||
|
||||
unfollow_packages = set(re.findall(r'package="([^"]+)"', xml_dump))
|
||||
unfollow_app_id = getattr(device, "app_id", "com.instagram.android")
|
||||
if unfollow_packages and unfollow_app_id not in unfollow_packages:
|
||||
logger.error(
|
||||
f"🚨 [UnfollowLoop] FOREIGN APP DETECTED! Packages: {unfollow_packages}. Aborting loop."
|
||||
)
|
||||
device.press("back")
|
||||
random_sleep(1.0, 1.5)
|
||||
return "CONTEXT_LOST"
|
||||
|
||||
# Autonomously identify user rows via Semantic Extraction
|
||||
telepathic = cognitive_stack.get("telepathic")
|
||||
nodes = []
|
||||
if telepathic:
|
||||
nodes = telepathic._extract_semantic_nodes(
|
||||
xml_dump, "List item containing a user profile image, username, and following/following button"
|
||||
)
|
||||
else:
|
||||
logger.warning("No telepathic engine found, skipping semantic extraction.")
|
||||
|
||||
action_taken = False
|
||||
for node in nodes:
|
||||
@@ -84,7 +105,7 @@ def _run_zero_latency_unfollow_loop(
|
||||
|
||||
# 2. Close Friend Guard
|
||||
profile_text = profile_xml.lower()
|
||||
if "enge freunde" in profile_text or "close friend" in profile_text:
|
||||
if "close friend" in profile_text:
|
||||
logger.info(
|
||||
"💚 [Anti-Friend] Profile is a Close Friend. Skipping unfollow.", extra={"color": Fore.GREEN}
|
||||
)
|
||||
@@ -106,19 +127,23 @@ def _run_zero_latency_unfollow_loop(
|
||||
extra={"color": Fore.YELLOW},
|
||||
)
|
||||
|
||||
# Find 'Following' button on their profile
|
||||
# Find the button that indicates active subscription (Following)
|
||||
following_nodes = telepathic._extract_semantic_nodes(
|
||||
profile_xml, "find 'Following' button", threshold=0.7
|
||||
profile_xml,
|
||||
"find the button indicating active subscription, look for id 'button_following' or 'profile_header_following'",
|
||||
threshold=0.7,
|
||||
)
|
||||
if following_nodes and not following_nodes[0].get("skip"):
|
||||
f_node = following_nodes[0]
|
||||
_humanized_click(device, f_node["x"], f_node["y"])
|
||||
random_sleep(1.0, 2.0)
|
||||
|
||||
# Find 'Unfollow' confirm
|
||||
# Find the confirmation button
|
||||
confirm_xml = device.dump_hierarchy()
|
||||
confirm_nodes = telepathic._extract_semantic_nodes(
|
||||
confirm_xml, "find 'Unfollow' confirmation button", threshold=0.8
|
||||
confirm_xml,
|
||||
"find the confirmation button to stop following, look for id 'follow_sheet_unfollow_row' or red warning text",
|
||||
threshold=0.8,
|
||||
)
|
||||
|
||||
if confirm_nodes and not confirm_nodes[0].get("skip"):
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
from time import sleep
|
||||
|
||||
@@ -95,6 +97,70 @@ def get_value(count, name, default=0):
|
||||
return default
|
||||
|
||||
|
||||
_LEARNED_AD_MARKERS_FILE = os.path.join(os.getcwd(), "learned_ad_markers.json")
|
||||
_LEARNED_AD_MARKERS_CACHE = None
|
||||
|
||||
|
||||
def get_learned_ad_markers() -> set:
|
||||
global _LEARNED_AD_MARKERS_CACHE
|
||||
if _LEARNED_AD_MARKERS_CACHE is not None:
|
||||
return _LEARNED_AD_MARKERS_CACHE
|
||||
|
||||
if os.path.exists(_LEARNED_AD_MARKERS_FILE):
|
||||
try:
|
||||
with open(_LEARNED_AD_MARKERS_FILE, "r") as f:
|
||||
_LEARNED_AD_MARKERS_CACHE = set(json.load(f))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load learned ad markers: {e}")
|
||||
_LEARNED_AD_MARKERS_CACHE = set()
|
||||
else:
|
||||
_LEARNED_AD_MARKERS_CACHE = set()
|
||||
|
||||
return _LEARNED_AD_MARKERS_CACHE
|
||||
|
||||
|
||||
def learn_ad_marker(marker: str, xml_hierarchy: str):
|
||||
global _LEARNED_AD_MARKERS_CACHE
|
||||
if not marker or len(marker) > 30:
|
||||
return
|
||||
|
||||
marker = marker.strip().lower()
|
||||
|
||||
# Structural verification: the VLM-suggested marker MUST exist as an exact node text/desc in the current UI!
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
try:
|
||||
root = ET.fromstring(xml_hierarchy)
|
||||
found_in_ui = False
|
||||
for node in root.iter("node"):
|
||||
text = node.attrib.get("text", "").strip().lower()
|
||||
desc = node.attrib.get("content-desc", "").strip().lower()
|
||||
if text == marker or desc == marker:
|
||||
found_in_ui = True
|
||||
break
|
||||
|
||||
if not found_in_ui:
|
||||
logger.debug(
|
||||
f"🧠 [Autonomous FSD] Rejected hallucinated Ad marker '{marker}' (not found as exact node match in UI)."
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
|
||||
markers = get_learned_ad_markers()
|
||||
if marker not in markers and marker not in {"ad", "sponsored", "advertisement"}:
|
||||
markers.add(marker)
|
||||
logger.info(
|
||||
f"🧠 [Autonomous FSD] Verified and Learned new Ad marker: '{marker}'. Persisting for zero-latency detection.",
|
||||
extra={"color": f"{Style.BRIGHT}{Fore.GREEN}"},
|
||||
)
|
||||
try:
|
||||
with open(_LEARNED_AD_MARKERS_FILE, "w") as f:
|
||||
json.dump(list(markers), f)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save learned ad markers: {e}")
|
||||
|
||||
|
||||
def is_ad(xml_hierarchy: str, cognitive_stack: dict = None) -> bool:
|
||||
"""
|
||||
Checks if the current view contains an advertisement using autonomous learning.
|
||||
@@ -102,7 +168,6 @@ def is_ad(xml_hierarchy: str, cognitive_stack: dict = None) -> bool:
|
||||
If a cognitive_stack is provided, it uses the Telepathic Engine for
|
||||
semantic classification (Zero-Latency vector lookup).
|
||||
"""
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
if cognitive_stack:
|
||||
@@ -123,24 +188,37 @@ def is_ad(xml_hierarchy: str, cognitive_stack: dict = None) -> bool:
|
||||
"com.instagram.android:id/ad_not_interested_button",
|
||||
]
|
||||
|
||||
AD_MARKERS = [r"\b(sponsored|ad|advertisement)\b", r"\b(gesponsert|anzeige|werbung)\b"]
|
||||
# Standalone label patterns: match only when the text/desc IS the ad marker,
|
||||
# not when "ad" appears inside longer phrases like "Create messaging ad"
|
||||
AD_EXACT_LABELS = {"ad", "sponsored", "advertisement"}
|
||||
AD_EXACT_LABELS.update(get_learned_ad_markers())
|
||||
|
||||
try:
|
||||
root = ET.fromstring(xml_hierarchy)
|
||||
|
||||
# Check if we are in a feed (to prevent false positives on profiles with 'Ad Tools' buttons)
|
||||
from GramAddict.core.perception.feed_analysis import FEED_MARKERS
|
||||
|
||||
in_feed = any(marker in xml_hierarchy for marker in FEED_MARKERS)
|
||||
|
||||
for node in root.iter("node"):
|
||||
attrib = node.attrib
|
||||
content_desc = attrib.get("content-desc", "")
|
||||
text = attrib.get("text", "")
|
||||
res_id = attrib.get("resource-id", "")
|
||||
|
||||
# Structural check (Instagram specific)
|
||||
# Structural check (Instagram specific) is always trusted
|
||||
if any(marker_id in res_id for marker_id in AD_RESOURCE_IDS):
|
||||
return True
|
||||
|
||||
# Content check (Legacy)
|
||||
searchable = f"{content_desc} {text}".lower()
|
||||
for pattern in AD_MARKERS:
|
||||
if re.search(pattern, searchable):
|
||||
# Exact label match: only trigger when the entire text/desc
|
||||
# IS an ad marker (e.g. text="Ad", content-desc="Sponsored")
|
||||
# We ONLY trust this if we are actually in a feed, to prevent triggering
|
||||
# on the "Ad Tools" / "Ad" buttons present on business profiles.
|
||||
if in_feed:
|
||||
if text.strip().lower() in AD_EXACT_LABELS:
|
||||
return True
|
||||
if content_desc.strip().lower() in AD_EXACT_LABELS:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
14
README.md
14
README.md
@@ -21,6 +21,7 @@ If Instagram updates its app and moves a button, GramPilot doesn't crash. It fal
|
||||
## ✨ Core Features
|
||||
|
||||
* 🚫 **Zero Limits Configuration**: Forget about configuring "max_likes" or "delays". GramPilot uses a **Dopamine Pacing Engine** to simulate human boredom. If the content isn't interesting, it skips it or ends the session early.
|
||||
* 🎯 **Mission-Driven Navigation**: Say goodbye to abstract goal configurations. Define a `strategy` (like `aggressive_growth` or `nurture_community`) in `config.yml`, and the **Goal Decomposer Engine** automatically orchestrates the optimal routing and task allocation using enabled plugins.
|
||||
* ⚖️ **Active Inference (Shadow Mode)**: The bot continuously predicts the outcome of its clicks. If it lands on a popup instead of a profile, it registers a "Prediction Error", presses back, and dynamically recalibrates without panicking.
|
||||
* ⛩️ **Telepathic Engine**: A strictly tiered resolution cascade (Keyword -> Vectors -> LLM) that ensures 90% of navigation happens at 0-token cost while maintaining fallback AI resilience.
|
||||
* 🧬 **Resonance Oracle**: The bot only interacts with content that matches a pre-defined persona aesthetic, completely bypassing spam or low-quality content.
|
||||
@@ -62,3 +63,16 @@ The engine has undergone a massive stabilization refactor to achieve **100% TDD
|
||||
|
||||
> [!NOTE]
|
||||
> Unlike legacy bots, GramPilot requires zero maintenance. It will automatically re-learn the UI over time using its integrated Qdrant memory vectors.
|
||||
|
||||
---
|
||||
|
||||
## 🏛️ Core Architecture Rules: Language Agnosticism & Structural Determinism
|
||||
|
||||
**CRITICAL: Hardcoding localized UI strings (e.g., "Follow", "Like", "Send", "Report") is STRICTLY FORBIDDEN across the entire codebase.**
|
||||
|
||||
GramPilot operates on a globally language-agnostic plane. If the UI is switched to German, Arabic, or Japanese, the bot must not fail. To achieve this, all perception and navigation logic must adhere to the following strict rules:
|
||||
|
||||
1. **Spatial Geometry over Text:** Use coordinates and boundaries to identify elements. (e.g., The "Send" button in DMs is *always* on the far right `center_x > width * 0.75`).
|
||||
2. **Structural Resource IDs over Descriptions:** Use Android UI resource IDs (`action_sheet_row_text_view`, `button_following`) instead of English `desc` or `text` properties. IDs are not localized and are universally stable.
|
||||
3. **Semantic LLM Prompts:** When querying the Vision-Language Model (VLM), **do not give exact string examples** that assume an English UI. For example, do not say `find the 'Unfollow' button`. Instead, say `find the button to stop following, look for a red warning text or id 'follow_sheet_unfollow_row'`.
|
||||
4. **Zero Magie:** Any text-based matching logic is considered a legacy flaw and will be mercilessly purged. If you find a text match, replace it with a structural O(1) fast-path or spatial guard.
|
||||
|
||||
@@ -9,11 +9,14 @@ from datetime import datetime
|
||||
# Add root project path so we can import internal modules safely
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from GramAddict.core.llm_provider import query_telepathic_llm
|
||||
from GramAddict.core.llm_provider import query_llm, query_telepathic_llm
|
||||
|
||||
BENCHMARKS_FILE = os.path.join(os.path.dirname(__file__), "data/llm_benchmarks.json")
|
||||
SCENARIOS_FILE = os.path.join(os.path.dirname(__file__), "data/benchmark_scenarios.json")
|
||||
|
||||
# Minimum iterations for statistical significance
|
||||
MIN_ITERATIONS = 5
|
||||
|
||||
|
||||
def load_json(path):
|
||||
if os.path.exists(path):
|
||||
@@ -31,35 +34,37 @@ def save_json(path, data):
|
||||
|
||||
|
||||
def normalize_scores(db):
|
||||
"""Normalize relative performance by AVERAGE score per scenario, not raw totals."""
|
||||
if not db.get("models"):
|
||||
return db
|
||||
|
||||
# 1. Find the highest raw score across all models
|
||||
max_raw = 0
|
||||
max_avg = 0
|
||||
leader_model = None
|
||||
|
||||
for name, data in db["models"].items():
|
||||
if data.get("is_unsuitable"):
|
||||
continue
|
||||
|
||||
raw = data.get("raw_score", 0)
|
||||
if raw > max_raw:
|
||||
max_raw = raw
|
||||
scenario_count = data.get("scenario_count", 1)
|
||||
avg = data.get("raw_score", 0) / max(scenario_count, 1)
|
||||
data["avg_score_per_scenario"] = round(avg, 1)
|
||||
|
||||
if avg > max_avg:
|
||||
max_avg = avg
|
||||
leader_model = name
|
||||
elif raw == max_raw and max_raw > 0:
|
||||
# Tie-breaker: Latency
|
||||
elif avg == max_avg and max_avg > 0:
|
||||
current_lat = data.get("latency_ms", 99999)
|
||||
leader_lat = db["models"][leader_model].get("latency_ms", 99999)
|
||||
if current_lat < leader_lat:
|
||||
leader_model = name
|
||||
|
||||
if max_raw == 0:
|
||||
if max_avg == 0:
|
||||
return db
|
||||
|
||||
# 2. Update relative performance
|
||||
for name, data in db["models"].items():
|
||||
raw = data.get("raw_score", 0)
|
||||
data["relative_performance_pct"] = round((raw / max_raw) * 100, 1)
|
||||
scenario_count = data.get("scenario_count", 1)
|
||||
avg = data.get("raw_score", 0) / max(scenario_count, 1)
|
||||
data["relative_performance_pct"] = round((avg / max_avg) * 100, 1)
|
||||
data["is_leader"] = name == leader_model
|
||||
|
||||
return db
|
||||
@@ -75,21 +80,15 @@ def get_installed_ollama_models():
|
||||
models = []
|
||||
for line in output.split("\n")[1:]:
|
||||
if line.strip():
|
||||
# Format: NAME, ID, SIZE, MODIFIED
|
||||
parts = line.split()
|
||||
if len(parts) >= 3:
|
||||
name = parts[0]
|
||||
size = parts[2]
|
||||
|
||||
# 1. Skip if size is '-' (remote/cloud model)
|
||||
if size == "-":
|
||||
continue
|
||||
|
||||
# 2. Skip ':cloud' tagged models explicitly
|
||||
if ":cloud" in name:
|
||||
continue
|
||||
|
||||
# 3. Filter out purely embedding models
|
||||
if any(k in name.lower() for k in ["embed", "minilm", "rerank"]):
|
||||
continue
|
||||
|
||||
@@ -100,7 +99,131 @@ def get_installed_ollama_models():
|
||||
return []
|
||||
|
||||
|
||||
def benchmark_model(model_name: str, url: str, force: bool = False):
|
||||
def _run_telepathic_scenario(scenario, model_name, url, iterations):
|
||||
"""Run a telepathic (JSON element selection) scenario."""
|
||||
system_prompt = (
|
||||
"You identify which UI element to tap based ONLY on a JSON array of parsed Android elements. "
|
||||
'Output ONLY valid JSON: {"index": number, "reason": "brief reason"}'
|
||||
)
|
||||
|
||||
user_prompt = (
|
||||
f"Which element should I tap to: {scenario['task']}\n\n"
|
||||
f"Elements:\n{json.dumps(scenario['nodes'], indent=1)}\n\n"
|
||||
"Rules:\n"
|
||||
"- Pick the SMALLEST, most specific button or icon\n"
|
||||
"- NEVER pick large containers\n"
|
||||
'Return: {"index": number, "reason": "..."}'
|
||||
)
|
||||
|
||||
latencies = []
|
||||
scores = []
|
||||
successes = 0
|
||||
|
||||
for _ in range(iterations):
|
||||
start_time = time.time()
|
||||
try:
|
||||
resp_str = query_telepathic_llm(model_name, url, system_prompt, user_prompt)
|
||||
latency = int((time.time() - start_time) * 1000)
|
||||
latencies.append(latency)
|
||||
except Exception as e:
|
||||
print(f" ❌ API Request failed: {e}")
|
||||
scores.append(0)
|
||||
continue
|
||||
|
||||
raw_points = 0
|
||||
try:
|
||||
clean = resp_str.strip()
|
||||
if clean.startswith("```json"):
|
||||
clean = clean[7:]
|
||||
if clean.endswith("```"):
|
||||
clean = clean[:-3]
|
||||
data = json.loads(clean)
|
||||
|
||||
if "index" in data and "reason" in data:
|
||||
raw_points += 40
|
||||
if data["index"] == scenario["target_index"]:
|
||||
raw_points += 60
|
||||
successes += 1
|
||||
else:
|
||||
print(f" ❌ Wrong index ({data.get('index')}). Target was {scenario['target_index']}.")
|
||||
else:
|
||||
print(" ❌ JSON missing fields.")
|
||||
except Exception:
|
||||
print(" ❌ JSON Parsing failed.")
|
||||
|
||||
scores.append(raw_points)
|
||||
|
||||
return scores, latencies, successes
|
||||
|
||||
|
||||
def _run_brain_scenario(scenario, model_name, url, iterations):
|
||||
"""Run a brain action extraction scenario (format_json=False)."""
|
||||
system_prompt = (
|
||||
f"You are an autonomous Instagram agent. Your goal is: '{scenario['task']}'.\n"
|
||||
f"You are currently on screen: {scenario['screen_type']}.\n"
|
||||
f"Available actions: {scenario['available_actions']}\n"
|
||||
"INSTRUCTIONS: Reply with ONLY the action string. Nothing else."
|
||||
)
|
||||
|
||||
user_prompt = "Choose the next best action."
|
||||
|
||||
latencies = []
|
||||
scores = []
|
||||
successes = 0
|
||||
|
||||
for _ in range(iterations):
|
||||
start_time = time.time()
|
||||
try:
|
||||
# CRITICAL: Use format_json=False — this is the Brain code path
|
||||
ans = query_llm(
|
||||
url=url,
|
||||
model=model_name,
|
||||
prompt=user_prompt,
|
||||
system=system_prompt,
|
||||
format_json=False,
|
||||
timeout=30,
|
||||
temperature=0.0,
|
||||
max_tokens=50,
|
||||
)
|
||||
latency = int((time.time() - start_time) * 1000)
|
||||
latencies.append(latency)
|
||||
except Exception as e:
|
||||
print(f" ❌ API Request failed: {e}")
|
||||
scores.append(0)
|
||||
continue
|
||||
|
||||
raw_points = 0
|
||||
if ans and "response" in ans:
|
||||
response = ans["response"].strip().lower()
|
||||
|
||||
# Points for structural adherence (returned a clean string)
|
||||
if response and response in [a.lower() for a in scenario["available_actions"]]:
|
||||
raw_points += 40
|
||||
|
||||
# Points for correctness
|
||||
if scenario.get("accept_any_valid"):
|
||||
# Any valid action from the list is acceptable
|
||||
raw_points += 60
|
||||
successes += 1
|
||||
elif response == scenario["target_action"].lower():
|
||||
raw_points += 60
|
||||
successes += 1
|
||||
else:
|
||||
print(f" ⚠️ Valid but suboptimal: '{response}' (target: '{scenario['target_action']}')")
|
||||
raw_points += 20 # Partial credit for valid but wrong action
|
||||
else:
|
||||
print(f" ❌ Invalid response: '{response}' not in available actions")
|
||||
else:
|
||||
print(" ❌ Empty or null response from LLM")
|
||||
|
||||
scores.append(raw_points)
|
||||
|
||||
return scores, latencies, successes
|
||||
|
||||
|
||||
def benchmark_model(model_name: str, url: str, force: bool = False, iterations: int = MIN_ITERATIONS):
|
||||
iterations = max(iterations, MIN_ITERATIONS) # Enforce minimum
|
||||
|
||||
db = load_json(BENCHMARKS_FILE) or {"models": {}}
|
||||
scenarios_data = load_json(SCENARIOS_FILE)
|
||||
if not scenarios_data:
|
||||
@@ -113,75 +236,46 @@ def benchmark_model(model_name: str, url: str, force: bool = False):
|
||||
print(f"Typical execution skip for {model_name} (Rel: {pct}%). Use --force.")
|
||||
return
|
||||
|
||||
print(f"\n🚀 [Competitive Benchmarking] Model: {model_name}")
|
||||
print(f"\n🚀 [Competitive Benchmarking] Model: {model_name} ({iterations} iterations)")
|
||||
|
||||
total_raw = 0
|
||||
total_latency = 0
|
||||
results_detail = {}
|
||||
passed_all = True
|
||||
|
||||
system_prompt = (
|
||||
"You identify which UI element to tap based ONLY on a JSON array of parsed Android elements. "
|
||||
'Output ONLY valid JSON: {"index": number, "reason": "brief reason"}'
|
||||
)
|
||||
|
||||
scenarios = scenarios_data["scenarios"]
|
||||
for scenario in scenarios:
|
||||
print(f"--- Running: {scenario['name']} ---")
|
||||
scenario_type = scenario.get("type", "telepathic")
|
||||
print(f"--- [{scenario_type.upper()}] {scenario['name']} ---")
|
||||
|
||||
user_prompt = (
|
||||
f"Which element should I tap to: {scenario['task']}\n\n"
|
||||
f"Elements:\n{json.dumps(scenario['nodes'], indent=1)}\n\n"
|
||||
"Rules:\n"
|
||||
"- Pick the SMALLEST, most specific button or icon\n"
|
||||
"- NEVER pick large containers\n"
|
||||
"Return: {\"index\": number, \"reason\": \"...\"}"
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
try:
|
||||
resp_str = query_telepathic_llm(model_name, url, system_prompt, user_prompt)
|
||||
latency = int((time.time() - start_time) * 1000)
|
||||
total_latency += latency
|
||||
except Exception as e:
|
||||
print(f" ❌ API Request failed for scenario {scenario['id']}: {e}")
|
||||
passed_all = False
|
||||
if scenario_type == "telepathic":
|
||||
scores, latencies, successes = _run_telepathic_scenario(scenario, model_name, url, iterations)
|
||||
elif scenario_type == "brain_action":
|
||||
scores, latencies, successes = _run_brain_scenario(scenario, model_name, url, iterations)
|
||||
else:
|
||||
print(f" ⚠️ Unknown scenario type: {scenario_type}")
|
||||
continue
|
||||
|
||||
raw_points = 0
|
||||
try:
|
||||
clean = resp_str.strip()
|
||||
if clean.startswith("```json"):
|
||||
clean = clean[7:]
|
||||
if clean.endswith("```"):
|
||||
clean = clean[:-3]
|
||||
data = json.loads(clean)
|
||||
avg_score = int(sum(scores) / len(scores)) if scores else 0
|
||||
avg_latency = int(sum(latencies) / len(latencies)) if latencies else 0
|
||||
pass_rate = (successes / iterations) * 100
|
||||
|
||||
# Points for structural adherence
|
||||
if "index" in data and "reason" in data:
|
||||
raw_points += 40
|
||||
|
||||
# Points for correctness
|
||||
if data["index"] == scenario["target_index"]:
|
||||
raw_points += 60
|
||||
print(f" ✅ Correct index ({data['index']}).")
|
||||
else:
|
||||
passed_all = False
|
||||
print(f" ❌ Wrong index ({data['index']}). Target was {scenario['target_index']}.")
|
||||
else:
|
||||
passed_all = False
|
||||
print(" ❌ JSON missing fields.")
|
||||
except Exception:
|
||||
if pass_rate < 100.0:
|
||||
passed_all = False
|
||||
print(" ❌ JSON Parsing failed.")
|
||||
|
||||
results_detail[scenario["id"]] = raw_points
|
||||
total_raw += raw_points
|
||||
print(f" Result: {pass_rate:.0f}% Pass | Avg Score: {avg_score}/100 | Avg Latency: {avg_latency}ms")
|
||||
|
||||
# Consistent format: always an object
|
||||
results_detail[scenario["id"]] = {
|
||||
"avg_score": avg_score,
|
||||
"pass_rate": pass_rate,
|
||||
"latency": avg_latency,
|
||||
}
|
||||
total_raw += avg_score
|
||||
total_latency += avg_latency
|
||||
|
||||
avg_latency = total_latency // len(scenarios) if scenarios else 0
|
||||
print(
|
||||
f"\n📊 {model_name} Result: {'PASS' if passed_all else 'FAIL'} | Score: {total_raw} | Latency: {avg_latency}ms"
|
||||
)
|
||||
print(f"\n📊 {model_name}: {'PASS' if passed_all else 'FAIL'} | Total: {total_raw} | Latency: {avg_latency}ms")
|
||||
|
||||
if model_name not in db["models"]:
|
||||
db["models"][model_name] = {}
|
||||
@@ -189,16 +283,17 @@ def benchmark_model(model_name: str, url: str, force: bool = False):
|
||||
db["models"][model_name].update(
|
||||
{
|
||||
"raw_score": total_raw,
|
||||
"scenario_count": len(scenarios),
|
||||
"telepathic_score": int((total_raw / (len(scenarios) * 100)) * 100) if scenarios else 0,
|
||||
"latency_ms": avg_latency,
|
||||
"last_tested": datetime.utcnow().isoformat() + "Z",
|
||||
"details": results_detail,
|
||||
"passed_all": passed_all,
|
||||
"is_unsuitable": not passed_all,
|
||||
"iterations": iterations,
|
||||
}
|
||||
)
|
||||
|
||||
# Recalculate relative scores across all models
|
||||
db = normalize_scores(db)
|
||||
save_json(BENCHMARKS_FILE, db)
|
||||
|
||||
@@ -212,6 +307,9 @@ if __name__ == "__main__":
|
||||
parser.add_argument("--url", type=str, help="Explicit endpoint URL")
|
||||
parser.add_argument("--force", action="store_true", help="Force re-testing")
|
||||
parser.add_argument("--all-ollama", action="store_true", help="Automatically find and test all local Ollama models")
|
||||
parser.add_argument(
|
||||
"--iterations", type=int, default=MIN_ITERATIONS, help=f"Iterations per scenario (min: {MIN_ITERATIONS})"
|
||||
)
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
@@ -241,5 +339,5 @@ if __name__ == "__main__":
|
||||
sys.exit(1)
|
||||
|
||||
for m, u in set(models_to_test):
|
||||
benchmark_model(m, u, args.force)
|
||||
benchmark_model(m, u, args.force, args.iterations)
|
||||
time.sleep(1)
|
||||
|
||||
@@ -89,6 +89,11 @@ telegram-reports: false # for using telegram-reports you have also to configure
|
||||
interactions-count: 30-40
|
||||
likes-count: 1-2
|
||||
likes-percentage: 100
|
||||
|
||||
plugins:
|
||||
dm_reply:
|
||||
enabled: false # Generates AI replies to unread DMs
|
||||
|
||||
stories-count: 1-2
|
||||
stories-percentage: 30-40
|
||||
carousel-count: 2-3
|
||||
|
||||
@@ -7,7 +7,7 @@ emoji==2.12.1
|
||||
langdetect==1.0.9
|
||||
atomicwrites==1.4.1
|
||||
spintax==1.0.4
|
||||
requests>=2.31.0
|
||||
requests>=2.32.0
|
||||
packaging>=23.0
|
||||
python-dotenv==1.0.1
|
||||
qdrant-client>=1.7.0
|
||||
|
||||
@@ -20,13 +20,32 @@ else
|
||||
filename=$(basename "$file")
|
||||
# Heuristic: Try to find a matching unit test
|
||||
test_file="tests/unit/test_${filename}"
|
||||
core_test_file="tests/core/test_${filename}"
|
||||
if [ -f "$test_file" ]; then
|
||||
TEST_TARGETS="$TEST_TARGETS $test_file"
|
||||
elif [ -f "$core_test_file" ]; then
|
||||
TEST_TARGETS="$TEST_TARGETS $core_test_file"
|
||||
else
|
||||
# If no direct unit test, fallback to running all unit tests to be safe
|
||||
echo "⚠️ No direct unit test found for $file, falling back to all unit tests."
|
||||
TEST_TARGETS="tests/unit"
|
||||
break
|
||||
# Try to find matching e2e tests by searching for each word in the module name
|
||||
module_name="${filename%.py}"
|
||||
e2e_matches=""
|
||||
for word in $(echo "$module_name" | tr '_' '\n'); do
|
||||
if [ ${#word} -ge 4 ]; then # Only search meaningful words (4+ chars)
|
||||
found=$(find tests/e2e -name "test_*${word}*.py" 2>/dev/null | head -3)
|
||||
if [ -n "$found" ]; then
|
||||
e2e_matches="$e2e_matches $found"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
e2e_matches=$(echo "$e2e_matches" | xargs -n1 2>/dev/null | sort -u | head -3 | xargs 2>/dev/null)
|
||||
if [ -n "$e2e_matches" ]; then
|
||||
echo "⚠️ No direct unit test for $file, using matching E2E tests: $e2e_matches"
|
||||
TEST_TARGETS="$TEST_TARGETS $e2e_matches"
|
||||
else
|
||||
echo "⚠️ No direct unit test found for $file, falling back to all unit tests."
|
||||
TEST_TARGETS="tests/unit"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
@@ -41,7 +60,7 @@ if [ -z "$TEST_TARGETS" ]; then
|
||||
fi
|
||||
|
||||
echo "🧪 Running tests on: $TEST_TARGETS"
|
||||
venv/bin/pytest $TEST_TARGETS --cov=GramAddict --cov-report=xml -q
|
||||
PYTHONPATH=. venv/bin/pytest $TEST_TARGETS --cov=GramAddict --cov-report=xml -q
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
|
||||
@@ -18,8 +18,8 @@ logger = logging.getLogger("TestingToolkit")
|
||||
def _save_dump(device, fixture_dir, filename, description):
|
||||
logger.info(f"⏳ Waiting for UI to settle for [{description}]...")
|
||||
time.sleep(3.5) # ensure animations finish
|
||||
xml_data = device.dump_hierarchy()
|
||||
|
||||
xml_data = device.dump_hierarchy()
|
||||
if not xml_data or len(xml_data) < 100:
|
||||
logger.warning(f"⚠️ Received empty or exceptionally small XML dump for {filename}. Is the app open?")
|
||||
|
||||
@@ -28,6 +28,23 @@ def _save_dump(device, fixture_dir, filename, description):
|
||||
f.write(xml_data)
|
||||
logger.info(f"✅ Saved REAL DUMP to {filename} ({len(xml_data)} bytes)")
|
||||
|
||||
# Capture screenshot
|
||||
try:
|
||||
import base64
|
||||
|
||||
screenshot_b64 = device.get_screenshot_b64()
|
||||
if screenshot_b64:
|
||||
screenshot_data = base64.b64decode(screenshot_b64)
|
||||
screenshot_filename = filename.replace(".xml", ".jpg")
|
||||
screenshot_path = os.path.join(fixture_dir, screenshot_filename)
|
||||
with open(screenshot_path, "wb") as f:
|
||||
f.write(screenshot_data)
|
||||
logger.info(f"✅ Saved REAL SCREENSHOT to {screenshot_filename}")
|
||||
else:
|
||||
logger.warning(f"⚠️ Failed to capture screenshot for {filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to capture screenshot: {e}")
|
||||
|
||||
|
||||
def run_interactive_guide(device, fixture_dir):
|
||||
print("\n" + "=" * 60)
|
||||
@@ -79,6 +96,13 @@ def main():
|
||||
|
||||
device_id = args.device
|
||||
|
||||
# Auto-detect config if not provided
|
||||
if not args.config:
|
||||
if os.path.exists("test_config.yml"):
|
||||
args.config = "test_config.yml"
|
||||
elif os.path.exists("config.yml"):
|
||||
args.config = "config.yml"
|
||||
|
||||
# Try to extract device from config if provided
|
||||
if args.config:
|
||||
try:
|
||||
|
||||
107
test_config.yml
107
test_config.yml
@@ -1,107 +0,0 @@
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# 🤖 ANTIGRAVITY ELITE CONFIGURATION (Plugin-Based Architecture)
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
# Dieses Brain ist modular aufgebaut. Jedes Verhalten ist ein autonomes Plugin.
|
||||
# Einstellungen können global oder spezifisch für jedes Plugin definiert werden.
|
||||
# Design-Prinzip: Zero Trust & Fail Fast.
|
||||
|
||||
identity:
|
||||
username: "marisaundmarc"
|
||||
persona: "Travel blogger, landscape photographer, and outdoors enthusiast"
|
||||
vibe: "friendly, authentic, helpful, and appreciative of good art"
|
||||
|
||||
mission:
|
||||
strategy: "aggressive_growth"
|
||||
selectivity_threshold: "high"
|
||||
target_audience: "travel, landscape, nature, mountain photography, wanderlust"
|
||||
blacklist_topics: "onlyfans, nsfw, sale, discount, promo, 18+, giveaway, crypto"
|
||||
|
||||
# ── Core Action Jobs (Wann soll der Bot wo aktiv werden?) ──
|
||||
actions:
|
||||
feed: "5-10" # Anzahl der Posts im Home-Feed pro Session
|
||||
explore: "5-10" # Anzahl der Posts im Explore-Grid
|
||||
# reels: "5-10" # In Entwicklung
|
||||
# stories: "3-5" # In Entwicklung
|
||||
|
||||
# ── Plugin Configuration (Das Herzstück der Verhaltenssteuerung) ──
|
||||
plugins:
|
||||
# 🛡️ Guards & Safety (Filtern, bevor Interaktion passiert)
|
||||
ad_guard:
|
||||
enabled: true
|
||||
|
||||
close_friends_guard:
|
||||
enabled: true # Postings von 'Engen Freunden' ignorieren
|
||||
|
||||
obstacle_guard:
|
||||
enabled: true # Popups, Update-Dialoge etc. wegräumen
|
||||
|
||||
anomaly_handler:
|
||||
enabled: true # Erkennt Blockierungen oder Captchas sofort (Fail Fast)
|
||||
|
||||
# 🧠 Perception & Evaluation (Vorverarbeitung)
|
||||
post_data_extraction:
|
||||
enabled: true # Extrahiert Text, Hashtags und Metadata
|
||||
|
||||
resonance_evaluator:
|
||||
visual_vibe_check_percentage: 100
|
||||
selectivity_threshold: "high"
|
||||
|
||||
# ⚡ Interactions (Die eigentlichen Aktionen)
|
||||
likes:
|
||||
percentage: 100 # Wahrscheinlichkeit pro Post
|
||||
count: "2-3" # Falls im Grid, wie viele?
|
||||
|
||||
comment:
|
||||
percentage: 40
|
||||
dry_run: true # Generiert KI-Kommentare ohne zu posten (Review-Mode)
|
||||
|
||||
follow:
|
||||
percentage: 100
|
||||
|
||||
repost:
|
||||
percentage: 20 # Teilen in die eigene Story
|
||||
|
||||
story_view:
|
||||
percentage: 80
|
||||
count: "1-3" # Wie viele Stories pro User schauen?
|
||||
|
||||
profile_visit:
|
||||
percentage: 100 # Wahrscheinlichkeit, vom Feed ins Profil zu gehen
|
||||
learn_own_profile: true
|
||||
|
||||
grid_like:
|
||||
percentage: 60 # Liked Posts aus dem Profil-Grid des Users
|
||||
count: "1-3"
|
||||
|
||||
# 🎢 Special Behaviors
|
||||
carousel_browsing:
|
||||
percentage: 100 # Erkennt Carousels und swiped durch
|
||||
count: "2-4" # Wie viele Slides pro Post?
|
||||
|
||||
rabbit_hole:
|
||||
percentage: 30 # Geht tiefer in verwandte Profile (Inception-Mode)
|
||||
|
||||
darwin_dwell:
|
||||
percentage: 50 # Simuliert unregelmäßige Lesezeiten (Biometrie)
|
||||
|
||||
# ── Limits & Budget ──
|
||||
limits:
|
||||
daily_budget_hours: 2.5
|
||||
max_comments_per_day: 40
|
||||
total_likes_limit: 300
|
||||
total_follows_limit: 50
|
||||
speed_multiplier: 1.0
|
||||
|
||||
# ── Infrastructure & System ──
|
||||
device: 192.168.1.206:40505
|
||||
app-id: com.instagram.android
|
||||
debug: true
|
||||
blank_start: true
|
||||
|
||||
# ── AI Model Endpoints (Ollama / OpenRouter) ──
|
||||
ai-model: qwen3.5:latest
|
||||
ai-model-url: http://localhost:11434/api/generate
|
||||
ai-telepathic-model: llama3.2-vision
|
||||
ai-telepathic-url: http://localhost:11434/api/generate
|
||||
ai-embedding-model: nomic-embed-text
|
||||
ai-embedding-url: http://localhost:11434/api/embeddings
|
||||
621
test_errors.txt
Normal file
621
test_errors.txt
Normal file
@@ -0,0 +1,621 @@
|
||||
============================= test session starts ==============================
|
||||
platform darwin -- Python 3.11.9, pytest-8.3.5, pluggy-1.5.0 -- /Users/marcmintel/.pyenv/versions/3.11.9/bin/python3
|
||||
cachedir: .pytest_cache
|
||||
hypothesis profile 'default'
|
||||
metadata: {'Python': '3.11.9', 'Platform': 'macOS-26.3.1-arm64-arm-64bit', 'Packages': {'pytest': '8.3.5', 'pluggy': '1.5.0'}, 'Plugins': {'anyio': '4.8.0', 'snapshot': '0.9.0', 'xdist': '3.7.0', 'instafail': '0.5.0', 'allure-pytest': '2.15.0', 'hypothesis': '6.140.2', 'html': '4.1.1', 'json-report': '1.5.0', 'timeout': '2.4.0', 'metadata': '3.1.1', 'md': '0.2.0', 'Faker': '37.8.0', 'clarity': '1.0.1', 'datadir': '1.8.0', 'cov': '6.2.1', 'mock': '3.14.1', 'pytest_httpserver': '1.1.3', 'sugar': '1.1.1', 'benchmark': '5.1.0', 'rerunfailures': '16.0.1'}}
|
||||
benchmark: 5.1.0 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
|
||||
rootdir: /Volumes/Alpha SSD/Coding/bot
|
||||
configfile: pyproject.toml
|
||||
plugins: anyio-4.8.0, snapshot-0.9.0, xdist-3.7.0, instafail-0.5.0, allure-pytest-2.15.0, hypothesis-6.140.2, html-4.1.1, json-report-1.5.0, timeout-2.4.0, metadata-3.1.1, md-0.2.0, Faker-37.8.0, clarity-1.0.1, datadir-1.8.0, cov-6.2.1, mock-3.14.1, pytest_httpserver-1.1.3, sugar-1.1.1, benchmark-5.1.0, rerunfailures-16.0.1
|
||||
collecting ... collected 186 items
|
||||
|
||||
tests/anomalies/test_cognitive_edge_cases.py::TestCognitiveEdgeCases::test_resonance_edge_cases PASSED [ 0%]
|
||||
tests/anomalies/test_cognitive_edge_cases.py::TestCognitiveEdgeCases::test_darwin_edge_cases PASSED [ 1%]
|
||||
tests/anomalies/test_cognitive_edge_cases.py::TestCognitiveEdgeCases::test_growth_brain_edge_cases PASSED [ 1%]
|
||||
tests/anomalies/test_hardware_anomalies_gauss.py::test_gaussian_distribution PASSED [ 2%]
|
||||
tests/anomalies/test_telepathic_guards.py::TestTelepathicGuards::test_strict_story_ring_guard FAILED [ 2%]
|
||||
tests/anomalies/test_telepathic_guards.py::TestTelepathicGuards::test_strict_button_guard FAILED [ 3%]
|
||||
tests/anomalies/test_telepathic_guards.py::TestTelepathicGuards::test_like_semantic_verification PASSED [ 3%]
|
||||
tests/anomalies/test_trap_radome.py::test_zero_point_trap PASSED [ 4%]
|
||||
tests/anomalies/test_trap_radome.py::test_micro_pixel_trap PASSED [ 4%]
|
||||
tests/anomalies/test_trap_radome.py::test_safe_normal_button PASSED [ 5%]
|
||||
tests/anomalies/test_trap_radome.py::test_transparent_interceptor_trap PASSED [ 5%]
|
||||
tests/anomalies/test_trap_radome.py::test_accessibility_trap PASSED [ 6%]
|
||||
tests/e2e/test_e2e_animation_timing.py::test_animation_timing_mocks_purged SKIPPED [ 6%]
|
||||
tests/e2e/test_e2e_dm_engine.py::test_e2e_dm_full_flow_success_real SKIPPED [ 7%]
|
||||
tests/e2e/test_e2e_dm_engine.py::test_e2e_dm_no_messages_real SKIPPED [ 8%]
|
||||
tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_normal_instagram ERROR [ 8%]
|
||||
tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_foreign_app_google ERROR [ 9%]
|
||||
tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_notification_shade ERROR [ 9%]
|
||||
tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_system_permission_dialog ERROR [ 10%]
|
||||
tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_instagram_survey_modal ERROR [ 10%]
|
||||
tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_unknown_modal_interstitial ERROR [ 11%]
|
||||
tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_action_blocked ERROR [ 11%]
|
||||
tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_empty_dump ERROR [ 12%]
|
||||
tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_none_dump ERROR [ 12%]
|
||||
tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_passive_scaffold_as_normal ERROR [ 13%]
|
||||
tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_home_feed_as_normal ERROR [ 13%]
|
||||
tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_explore_grid_as_normal ERROR [ 14%]
|
||||
tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_other_profile_as_normal ERROR [ 15%]
|
||||
tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_post_detail_as_normal ERROR [ 15%]
|
||||
tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_profile_tagged_tab_as_normal ERROR [ 16%]
|
||||
tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_survey_modal_as_obstacle ERROR [ 16%]
|
||||
tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_mystery_interstitial_as_obstacle ERROR [ 17%]
|
||||
tests/e2e/test_engine_perception.py::test_perception_mock_theater_purged SKIPPED [ 17%]
|
||||
tests/e2e/test_goap_loop_prevention.py::test_goap_planner_avoids_infinite_loop_on_masked_edge ERROR [ 18%]
|
||||
tests/e2e/test_goap_loop_prevention.py::test_screen_topology_find_route_avoids_blocked_edges ERROR [ 18%]
|
||||
tests/e2e/test_goap_loop_prevention.py::test_telepathic_engine_finds_following_node_on_profile ERROR [ 19%]
|
||||
tests/e2e/test_goap_loop_prevention.py::test_following_vs_followers_are_both_candidates ERROR [ 19%]
|
||||
tests/e2e/test_goap_loop_prevention.py::test_vlm_prompt_humanizes_content_desc ERROR [ 20%]
|
||||
tests/e2e/test_goap_loop_prevention.py::test_live_vlm_selects_following_not_followers ERROR [ 20%]
|
||||
tests/e2e/test_reel_interactions.py::test_reel_like_button_not_caption ERROR [ 21%]
|
||||
tests/e2e/test_reel_interactions.py::test_reel_follow_button_returns_none_when_absent ERROR [ 22%]
|
||||
tests/e2e/test_reel_interactions.py::test_reel_post_author_selects_username ERROR [ 22%]
|
||||
tests/e2e/test_reel_interactions.py::test_reel_dedup_preserves_like_button ERROR [ 23%]
|
||||
tests/e2e/test_reel_interactions.py::test_reel_caption_with_like_word_is_not_like_button ERROR [ 23%]
|
||||
tests/e2e/test_reel_navigation_guards.py::test_intent_resolver_profile_tab_rejects_author_profile ERROR [ 24%]
|
||||
tests/e2e/test_reel_navigation_guards.py::test_intent_resolver_profile_tab_selects_real_tab ERROR [ 24%]
|
||||
tests/e2e/test_sim_full_lifecycle.py::test_full_lifecycle_sim_purged SKIPPED [ 25%]
|
||||
tests/e2e/test_visual_intent_resolver.py::test_visual_discovery_creates_annotated_screenshot ERROR [ 25%]
|
||||
tests/e2e/test_visual_intent_resolver.py::test_visual_discovery_finds_following_by_seeing ERROR [ 26%]
|
||||
tests/e2e/test_visual_intent_resolver.py::test_resolve_uses_visual_discovery_when_device_available ERROR [ 26%]
|
||||
tests/integration/test_ad_detection.py::test_real_sponsored_reel_flexcode_is_detected PASSED [ 27%]
|
||||
tests/integration/test_ad_detection.py::test_normal_post_not_ad PASSED [ 27%]
|
||||
tests/integration/test_ad_detection.py::test_peugeot_carousel_ad_is_detected PASSED [ 28%]
|
||||
tests/integration/test_core_nav_dm_regression.py::test_core_nav_rejects_generic_action_bar_right PASSED [ 29%]
|
||||
tests/integration/test_false_positive.py::test_real_normal_post_is_not_ad PASSED [ 29%]
|
||||
tests/integration/test_telepathic_hardening.py::test_keyword_nav_threshold FAILED [ 30%]
|
||||
tests/integration/test_telepathic_hardening.py::test_direct_tab_fast_path FAILED [ 30%]
|
||||
tests/integration/test_telepathic_keyword.py::test_keyword_fast_path_no_feed_pollution FAILED [ 31%]
|
||||
tests/repro_reports/test_repro_api_mismatch.py::TestAPIMismatch::test_repro_extract_semantic_nodes_type_error PASSED [ 31%]
|
||||
tests/repro_reports/test_repro_position_rejection.py::TestPositionRejection::test_repro_following_button_rejection_fix FAILED [ 32%]
|
||||
tests/repro_reports/test_repro_reels_tab_hallucination.py::TestReproReelsTabHallucination::test_reels_tab_selection FAILED [ 32%]
|
||||
tests/tdd/test_bezier_gesture.py::TestScrollCurve::test_returns_correct_number_of_points PASSED [ 33%]
|
||||
tests/tdd/test_bezier_gesture.py::TestScrollCurve::test_start_and_end_are_near_requested_positions PASSED [ 33%]
|
||||
tests/tdd/test_bezier_gesture.py::TestScrollCurve::test_path_is_non_linear PASSED [ 34%]
|
||||
tests/tdd/test_bezier_gesture.py::TestScrollCurve::test_right_hander_arcs_right PASSED [ 34%]
|
||||
tests/tdd/test_bezier_gesture.py::TestScrollCurve::test_left_hander_arcs_left PASSED [ 35%]
|
||||
tests/tdd/test_bezier_gesture.py::TestScrollCurve::test_pressure_has_gaussian_peak PASSED [ 36%]
|
||||
tests/tdd/test_bezier_gesture.py::TestScrollCurve::test_pressure_within_valid_range PASSED [ 36%]
|
||||
tests/tdd/test_bezier_gesture.py::TestScrollCurve::test_all_points_have_three_components PASSED [ 37%]
|
||||
tests/tdd/test_bezier_gesture.py::TestTapCurve::test_returns_three_points PASSED [ 37%]
|
||||
tests/tdd/test_bezier_gesture.py::TestTapCurve::test_micro_drift_is_small PASSED [ 38%]
|
||||
tests/tdd/test_bezier_gesture.py::TestTapCurve::test_pressure_sequence_is_down_peak_up PASSED [ 38%]
|
||||
tests/tdd/test_bezier_gesture.py::TestHorizontalSwipeCurve::test_returns_reasonable_point_count PASSED [ 39%]
|
||||
tests/tdd/test_bezier_gesture.py::TestHorizontalSwipeCurve::test_horizontal_distance_is_correct_direction PASSED [ 39%]
|
||||
tests/tdd/test_bezier_gesture.py::TestHorizontalSwipeCurve::test_vertical_arc_exists PASSED [ 40%]
|
||||
tests/tdd/test_bezier_gesture.py::TestSigmoidTiming::test_total_duration_matches PASSED [ 40%]
|
||||
tests/tdd/test_bezier_gesture.py::TestSigmoidTiming::test_edges_are_slower_than_middle PASSED [ 41%]
|
||||
tests/tdd/test_bezier_gesture.py::TestSigmoidTiming::test_single_point_returns_single_interval PASSED [ 41%]
|
||||
tests/tdd/test_bezier_gesture.py::TestSigmoidTiming::test_no_negative_intervals PASSED [ 42%]
|
||||
tests/tdd/test_physics_body.py::TestHandedness::test_right_hander_anchor_is_right PASSED [ 43%]
|
||||
tests/tdd/test_physics_body.py::TestHandedness::test_left_hander_anchor_is_left PASSED [ 43%]
|
||||
tests/tdd/test_physics_body.py::TestHandedness::test_right_hander_scroll_starts_right PASSED [ 44%]
|
||||
tests/tdd/test_physics_body.py::TestHandedness::test_left_hander_scroll_starts_left PASSED [ 44%]
|
||||
tests/tdd/test_physics_body.py::TestThumbArcBias::test_right_hander_arcs_right PASSED [ 45%]
|
||||
tests/tdd/test_physics_body.py::TestThumbArcBias::test_left_hander_arcs_left PASSED [ 45%]
|
||||
tests/tdd/test_physics_body.py::TestSessionDrift::test_drift_is_zero_initially PASSED [ 46%]
|
||||
tests/tdd/test_physics_body.py::TestSessionDrift::test_drift_accumulates_over_many_gestures PASSED [ 46%]
|
||||
tests/tdd/test_physics_body.py::TestSessionDrift::test_drift_is_bounded PASSED [ 47%]
|
||||
tests/tdd/test_physics_body.py::TestStartPositions::test_positions_stay_within_screen_bounds PASSED [ 47%]
|
||||
tests/tdd/test_physics_body.py::TestStartPositions::test_positions_are_not_identical PASSED [ 48%]
|
||||
tests/tdd/test_physics_body.py::TestStartPositions::test_gesture_count_increments PASSED [ 48%]
|
||||
tests/tdd/test_physics_body.py::TestFatigue::test_fatigue_starts_at_zero PASSED [ 49%]
|
||||
tests/tdd/test_physics_body.py::TestFatigue::test_rapid_gestures_increase_fatigue PASSED [ 50%]
|
||||
tests/tdd/test_physics_body.py::TestFatigue::test_idle_period_reduces_fatigue PASSED [ 50%]
|
||||
tests/tdd/test_physics_body.py::TestFatigue::test_fatigue_is_clamped_0_to_1 PASSED [ 51%]
|
||||
tests/tdd/test_physics_body.py::TestTapPosition::test_tap_position_near_target PASSED [ 51%]
|
||||
tests/tdd/test_physics_body.py::TestTapPosition::test_tap_stays_on_screen PASSED [ 52%]
|
||||
tests/tdd/test_physics_body.py::TestPressureAndTouchMajor::test_pressure_baseline_in_range PASSED [ 52%]
|
||||
tests/tdd/test_physics_body.py::TestPressureAndTouchMajor::test_fatigue_increases_pressure PASSED [ 53%]
|
||||
tests/tdd/test_physics_body.py::TestPressureAndTouchMajor::test_touch_major_in_range PASSED [ 53%]
|
||||
tests/tdd/test_physics_body.py::TestPressureAndTouchMajor::test_fatigue_increases_touch_major PASSED [ 54%]
|
||||
tests/tdd/test_physics_body.py::TestSingleton::test_singleton_returns_same_instance PASSED [ 54%]
|
||||
tests/tdd/test_physics_body.py::TestSingleton::test_reset_clears_singleton PASSED [ 55%]
|
||||
tests/tdd/test_semantic_heuristic_match.py::test_semantic_heuristic_match_blank_start PASSED [ 55%]
|
||||
tests/unit/perception/test_intent_resolver.py::test_intent_resolver_finds_bottom_tab FAILED [ 56%]
|
||||
tests/unit/perception/test_intent_resolver.py::test_intent_resolver_finds_button_by_text PASSED [ 56%]
|
||||
tests/unit/perception/test_intent_resolver.py::test_intent_resolver_returns_none_if_no_match PASSED [ 57%]
|
||||
tests/unit/perception/test_spatial_parser.py::TestSpatialParser::test_parses_xml_into_spatial_nodes PASSED [ 58%]
|
||||
tests/unit/perception/test_spatial_parser.py::TestSpatialParser::test_extracts_all_clickable_nodes PASSED [ 58%]
|
||||
tests/unit/perception/test_spatial_parser.py::TestSpatialParser::test_spatial_containment PASSED [ 59%]
|
||||
tests/unit/perception/test_spatial_parser.py::TestSpatialParser::test_spatial_intersection PASSED [ 59%]
|
||||
tests/unit/test_config_plugins.py::test_config_plugin_section PASSED [ 60%]
|
||||
tests/unit/test_config_plugins.py::test_config_plugin_fallback PASSED [ 60%]
|
||||
tests/unit/test_config_plugins.py::test_config_plugin_not_found PASSED [ 61%]
|
||||
tests/unit/test_darwin_engine_comments.py::test_has_comments_true_reel PASSED [ 61%]
|
||||
tests/unit/test_darwin_engine_comments.py::test_has_comments_true_organic PASSED [ 62%]
|
||||
tests/unit/test_darwin_engine_comments.py::test_has_comments_zero_reel PASSED [ 62%]
|
||||
tests/unit/test_darwin_engine_comments.py::test_has_comments_regex_cases PASSED [ 63%]
|
||||
tests/unit/test_dopamine_engine.py::test_dopamine_engine_wants_to_change_feed PASSED [ 63%]
|
||||
tests/unit/test_dopamine_engine.py::test_dopamine_engine_reset_session_clears_boredom PASSED [ 64%]
|
||||
tests/unit/test_dopamine_engine.py::test_dopamine_engine_wants_to_doomscroll PASSED [ 65%]
|
||||
tests/unit/test_dopamine_loop.py::test_feed_switch_resets_boredom PASSED [ 65%]
|
||||
tests/unit/test_dopamine_loop.py::test_session_limit_terminates_session PASSED [ 66%]
|
||||
tests/unit/test_feed_loop_continuation.py::TestFeedLoopContinuation::test_stories_complete_returns_feed_exhausted PASSED [ 66%]
|
||||
tests/unit/test_feed_loop_continuation.py::TestFeedLoopContinuation::test_main_loop_handles_feed_exhausted PASSED [ 67%]
|
||||
tests/unit/test_goap_graph_routing.py::TestGoapGraphRouting::test_planner_routes_to_profile_first_for_following_list PASSED [ 67%]
|
||||
tests/unit/test_goap_graph_routing.py::TestGoapGraphRouting::test_planner_returns_final_action_on_intermediate_screen PASSED [ 68%]
|
||||
tests/unit/test_goap_graph_routing.py::TestGoapGraphRouting::test_planner_detects_goal_already_achieved PASSED [ 68%]
|
||||
tests/unit/test_goap_graph_routing.py::TestGoapGraphRouting::test_planner_routes_explore_to_following_list PASSED [ 69%]
|
||||
tests/unit/test_grid_retry_diversity.py::TestGridRetryDiversity::test_first_call_returns_topmost_leftmost FAILED [ 69%]
|
||||
tests/unit/test_grid_retry_diversity.py::TestGridRetryDiversity::test_retry_skips_failed_position FAILED [ 70%]
|
||||
tests/unit/test_grid_retry_diversity.py::TestGridRetryDiversity::test_skip_multiple_positions FAILED [ 70%]
|
||||
tests/unit/test_grid_retry_diversity.py::TestGridRetryDiversity::test_all_positions_skipped_returns_none FAILED [ 71%]
|
||||
tests/unit/test_is_ad_substring.py::test_is_ad_false_positive_abroad PASSED [ 72%]
|
||||
tests/unit/test_is_ad_substring.py::test_is_ad_true_positive PASSED [ 72%]
|
||||
tests/unit/test_is_ad_substring.py::test_is_ad_true_positive_ad_word PASSED [ 73%]
|
||||
tests/unit/test_nav_intent_classification.py::TestNavIntentClassification::test_dm_intent_is_classified_as_nav_intent PASSED [ 73%]
|
||||
tests/unit/test_nav_intent_classification.py::TestNavIntentClassification::test_inbox_intent_is_classified_as_nav_intent PASSED [ 74%]
|
||||
tests/unit/test_nav_intent_classification.py::TestNavIntentClassification::test_notification_intent_is_classified_as_nav_intent PASSED [ 74%]
|
||||
tests/unit/test_nav_intent_classification.py::TestNavIntentClassification::test_regular_post_intent_still_blocked_in_nav_zone PASSED [ 75%]
|
||||
tests/unit/test_screen_identity_profile.py::test_screen_identity_own_profile_vs_other_profile PASSED [ 75%]
|
||||
tests/unit/test_screen_identity_profile.py::test_screen_identity_other_profile_vs_own_profile PASSED [ 76%]
|
||||
tests/unit/test_screen_topology.py::TestScreenTopologyRouting::test_route_home_to_following_list PASSED [ 76%]
|
||||
tests/unit/test_screen_topology.py::TestScreenTopologyRouting::test_route_already_there PASSED [ 77%]
|
||||
tests/unit/test_screen_topology.py::TestScreenTopologyRouting::test_route_single_hop PASSED [ 77%]
|
||||
tests/unit/test_screen_topology.py::TestScreenTopologyRouting::test_route_reverse_direction PASSED [ 78%]
|
||||
tests/unit/test_screen_topology.py::TestScreenTopologyRouting::test_no_route_from_unreachable PASSED [ 79%]
|
||||
tests/unit/test_screen_topology.py::TestGoalToTargetScreen::test_following_list_goal PASSED [ 79%]
|
||||
tests/unit/test_screen_topology.py::TestGoalToTargetScreen::test_followers_list_goal PASSED [ 80%]
|
||||
tests/unit/test_screen_topology.py::TestGoalToTargetScreen::test_profile_goal PASSED [ 80%]
|
||||
tests/unit/test_screen_topology.py::TestGoalToTargetScreen::test_home_feed_goal PASSED [ 81%]
|
||||
tests/unit/test_screen_topology.py::TestGoalToTargetScreen::test_explore_goal PASSED [ 81%]
|
||||
tests/unit/test_screen_topology.py::TestGoalToTargetScreen::test_messages_goal PASSED [ 82%]
|
||||
tests/unit/test_screen_topology.py::TestGoalToTargetScreen::test_interaction_goal_returns_none PASSED [ 82%]
|
||||
tests/unit/test_screen_topology.py::TestGoalToTargetScreen::test_unknown_goal_returns_none PASSED [ 83%]
|
||||
tests/unit/test_screen_topology.py::TestGetTransitions::test_home_feed_has_profile_tab PASSED [ 83%]
|
||||
tests/unit/test_screen_topology.py::TestGetTransitions::test_own_profile_has_following_list PASSED [ 84%]
|
||||
tests/unit/test_screen_topology.py::TestGetTransitions::test_unknown_screen_returns_empty PASSED [ 84%]
|
||||
tests/unit/test_screen_topology.py::TestScreenNameMap::test_following_list_maps PASSED [ 85%]
|
||||
tests/unit/test_screen_topology.py::TestScreenNameMap::test_home_feed_maps PASSED [ 86%]
|
||||
tests/unit/test_screen_topology.py::TestScreenNameMap::test_stories_feed_maps_to_home PASSED [ 86%]
|
||||
tests/unit/test_screen_topology.py::TestScreenNameMap::test_search_feed_maps_to_explore PASSED [ 87%]
|
||||
tests/unit/test_screen_topology.py::TestScreenNameToGoal::test_following_list PASSED [ 87%]
|
||||
tests/unit/test_screen_topology.py::TestScreenNameToGoal::test_home_feed PASSED [ 88%]
|
||||
tests/unit/test_screen_topology.py::TestScreenNameToGoal::test_explore_feed PASSED [ 88%]
|
||||
tests/unit/test_screen_topology.py::TestScreenNameToGoal::test_stories_feed PASSED [ 89%]
|
||||
tests/unit/test_screen_topology.py::TestScreenNameToGoal::test_unknown_target PASSED [ 89%]
|
||||
tests/unit/test_screen_topology.py::TestExpectedScreenForAction::test_tap_profile_tab_from_home PASSED [ 90%]
|
||||
tests/unit/test_screen_topology.py::TestExpectedScreenForAction::test_tap_following_list_from_profile PASSED [ 90%]
|
||||
tests/unit/test_screen_topology.py::TestExpectedScreenForAction::test_press_back_from_follow_list PASSED [ 91%]
|
||||
tests/unit/test_screen_topology.py::TestExpectedScreenForAction::test_unknown_action_returns_none PASSED [ 91%]
|
||||
tests/unit/test_screen_topology.py::TestExpectedScreenForAction::test_action_not_available_on_screen PASSED [ 92%]
|
||||
tests/unit/test_screen_topology.py::TestIsStructuralAction::test_tap_profile_tab_is_structural PASSED [ 93%]
|
||||
tests/unit/test_screen_topology.py::TestIsStructuralAction::test_tap_following_list_is_structural PASSED [ 93%]
|
||||
tests/unit/test_screen_topology.py::TestIsStructuralAction::test_random_action_is_not_structural PASSED [ 94%]
|
||||
tests/unit/test_screen_topology.py::TestIsStructuralAction::test_action_on_wrong_screen_is_not_structural PASSED [ 94%]
|
||||
tests/unit/test_session_limits_evaluation.py::test_global_session_limit_evaluation ERROR [ 95%]
|
||||
tests/unit/test_structural_guard.py::test_structural_guard_rejects_own_story_for_post_username PASSED [ 95%]
|
||||
tests/unit/test_structural_guard.py::test_structural_guard_accepts_actual_post_username PASSED [ 96%]
|
||||
tests/unit/test_structural_guard.py::test_structural_guard_rejects_own_username_story PASSED [ 96%]
|
||||
tests/unit/test_structural_guard.py::test_structural_reels_first_grid_item_y_coords PASSED [ 97%]
|
||||
tests/unit/test_telepathic_container_filtering.py::test_media_intent_rejects_grid_containers PASSED [ 97%]
|
||||
tests/unit/test_verify_success_reels.py::TestVerifySuccessGridReels::test_reel_view_accepted_as_valid_grid_result PASSED [ 98%]
|
||||
tests/unit/test_verify_success_reels.py::TestVerifySuccessGridReels::test_normal_feed_post_still_accepted PASSED [ 98%]
|
||||
tests/unit/test_verify_success_reels.py::TestVerifySuccessGridReels::test_explore_grid_still_visible_is_failure PASSED [ 99%]
|
||||
tests/unit/test_verify_success_reels.py::TestVerifySuccessGridReels::test_profile_grid_reel_accepted PASSED [100%]
|
||||
|
||||
==================================== ERRORS ====================================
|
||||
______ ERROR at setup of TestSAEPerception.test_perceive_normal_instagram ______
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_____ ERROR at setup of TestSAEPerception.test_perceive_foreign_app_google _____
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_____ ERROR at setup of TestSAEPerception.test_perceive_notification_shade _____
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
__ ERROR at setup of TestSAEPerception.test_perceive_system_permission_dialog __
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
___ ERROR at setup of TestSAEPerception.test_perceive_instagram_survey_modal ___
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_ ERROR at setup of TestSAEPerception.test_perceive_unknown_modal_interstitial _
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_______ ERROR at setup of TestSAEPerception.test_perceive_action_blocked _______
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_________ ERROR at setup of TestSAEPerception.test_perceive_empty_dump _________
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_________ ERROR at setup of TestSAEPerception.test_perceive_none_dump __________
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_ ERROR at setup of TestSAEPerception.test_perceive_passive_scaffold_as_normal _
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_ ERROR at setup of TestSAERealFixturePerception.test_perceive_home_feed_as_normal _
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_ ERROR at setup of TestSAERealFixturePerception.test_perceive_explore_grid_as_normal _
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_ ERROR at setup of TestSAERealFixturePerception.test_perceive_other_profile_as_normal _
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_ ERROR at setup of TestSAERealFixturePerception.test_perceive_post_detail_as_normal _
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_ ERROR at setup of TestSAERealFixturePerception.test_perceive_profile_tagged_tab_as_normal _
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_ ERROR at setup of TestSAERealFixturePerception.test_perceive_survey_modal_as_obstacle _
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_ ERROR at setup of TestSAERealFixturePerception.test_perceive_mystery_interstitial_as_obstacle _
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
___ ERROR at setup of test_goap_planner_avoids_infinite_loop_on_masked_edge ____
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
____ ERROR at setup of test_screen_topology_find_route_avoids_blocked_edges ____
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
___ ERROR at setup of test_telepathic_engine_finds_following_node_on_profile ___
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
______ ERROR at setup of test_following_vs_followers_are_both_candidates _______
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
___________ ERROR at setup of test_vlm_prompt_humanizes_content_desc ___________
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_______ ERROR at setup of test_live_vlm_selects_following_not_followers ________
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_____________ ERROR at setup of test_reel_like_button_not_caption ______________
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
______ ERROR at setup of test_reel_follow_button_returns_none_when_absent ______
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
___________ ERROR at setup of test_reel_post_author_selects_username ___________
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
___________ ERROR at setup of test_reel_dedup_preserves_like_button ____________
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
____ ERROR at setup of test_reel_caption_with_like_word_is_not_like_button _____
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
__ ERROR at setup of test_intent_resolver_profile_tab_rejects_author_profile ___
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_____ ERROR at setup of test_intent_resolver_profile_tab_selects_real_tab ______
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
_____ ERROR at setup of test_visual_discovery_creates_annotated_screenshot _____
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
______ ERROR at setup of test_visual_discovery_finds_following_by_seeing _______
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
__ ERROR at setup of test_resolve_uses_visual_discovery_when_device_available __
|
||||
/Users/marcmintel/.local/lib/python3.11/site-packages/_pytest/config/__init__.py:1694: in getoption
|
||||
val = getattr(self.option, name)
|
||||
E AttributeError: 'Namespace' object has no attribute '--live'
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
tests/e2e/conftest.py:195: in mock_all_delays
|
||||
if request.config.getoption("--live"):
|
||||
E ValueError: no option named '--live'
|
||||
____________ ERROR at setup of test_global_session_limit_evaluation ____________
|
||||
file /Volumes/Alpha SSD/Coding/bot/tests/unit/test_session_limits_evaluation.py, line 1
|
||||
def test_global_session_limit_evaluation(mock_logger):
|
||||
E fixture 'mock_logger' not found
|
||||
> available fixtures: _session_faker, anyio_backend, anyio_backend_name, anyio_backend_options, benchmark, benchmark_weave, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, class_mocker, cov, datadir, doctest_namespace, extra, extras, faker, httpserver, httpserver_ipv4, httpserver_ipv6, httpserver_listen_address, httpserver_ssl_context, include_metadata_in_junit_xml, json_metadata, lazy_datadir, lazy_shared_datadir, make_httpserver, make_httpserver_ipv4, make_httpserver_ipv6, metadata, mocker, module_mocker, monkeypatch, no_cover, original_datadir, package_mocker, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, session_mocker, shared_datadir, snapshot, testrun_uid, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
|
||||
> use 'pytest --fixtures [testpath]' for help on them.
|
||||
|
||||
/Volumes/Alpha SSD/Coding/bot/tests/unit/test_session_limits_evaluation.py:1
|
||||
=================================== FAILURES ===================================
|
||||
______________ TestTelepathicGuards.test_strict_story_ring_guard _______________
|
||||
tests/anomalies/test_telepathic_guards.py:23: in test_strict_story_ring_guard
|
||||
assert self.engine._structural_sanity_check(invalid_story, intent, screen_height) is False
|
||||
E AssertionError: assert True is False
|
||||
E + where True = _structural_sanity_check({'area': 100, 'resource_id': 'row_feed_profile_header', 'y': 800}, 'tap story ring avatar', 2400)
|
||||
E + where _structural_sanity_check = <GramAddict.core.telepathic_engine.TelepathicEngine object at 0x137e50e50>._structural_sanity_check
|
||||
E + where <GramAddict.core.telepathic_engine.TelepathicEngine object at 0x137e50e50> = <tests.anomalies.test_telepathic_guards.TestTelepathicGuards object at 0x137cc5190>.engine
|
||||
________________ TestTelepathicGuards.test_strict_button_guard _________________
|
||||
tests/anomalies/test_telepathic_guards.py:45: in test_strict_button_guard
|
||||
assert self.engine._structural_sanity_check(invalid_prof, intent, screen_height) is False
|
||||
E assert True is False
|
||||
E + where True = _structural_sanity_check({'area': 100, 'resource_id': 'username', 'semantic_string': "Go to cayleighanddavid's profile", 'y': 1000}, 'Heart like button for comment', 2400)
|
||||
E + where _structural_sanity_check = <GramAddict.core.telepathic_engine.TelepathicEngine object at 0x138184dd0>._structural_sanity_check
|
||||
E + where <GramAddict.core.telepathic_engine.TelepathicEngine object at 0x138184dd0> = <tests.anomalies.test_telepathic_guards.TestTelepathicGuards object at 0x137cc58d0>.engine
|
||||
__________________________ test_keyword_nav_threshold __________________________
|
||||
tests/integration/test_telepathic_hardening.py:37: in test_keyword_nav_threshold
|
||||
res = engine._keyword_match_score("tap messages tab", [reels_node])
|
||||
E AttributeError: 'TelepathicEngine' object has no attribute '_keyword_match_score'
|
||||
__________________________ test_direct_tab_fast_path ___________________________
|
||||
tests/integration/test_telepathic_hardening.py:57: in test_direct_tab_fast_path
|
||||
res = engine._core_navigation_fast_path("tap messages tab", [direct_node])
|
||||
E AttributeError: 'TelepathicEngine' object has no attribute '_core_navigation_fast_path'
|
||||
___________________ test_keyword_fast_path_no_feed_pollution ___________________
|
||||
tests/integration/test_telepathic_keyword.py:30: in test_keyword_fast_path_no_feed_pollution
|
||||
result = engine._keyword_match_score("tap home tab", nodes)
|
||||
E AttributeError: 'TelepathicEngine' object has no attribute '_keyword_match_score'
|
||||
_______ TestPositionRejection.test_repro_following_button_rejection_fix ________
|
||||
tests/repro_reports/test_repro_position_rejection.py:34: in test_repro_following_button_rejection_fix
|
||||
self.assertTrue(passed_keyword, "Following button should be allowed for following intent")
|
||||
E AssertionError: False is not true : Following button should be allowed for following intent
|
||||
----------------------------- Captured stdout call -----------------------------
|
||||
|
||||
[DEBUG] Intent: 'tap following list', Passed: False
|
||||
|
||||
[DEBUG] Intent: 'some other intent', Passed: False
|
||||
___________ TestReproReelsTabHallucination.test_reels_tab_selection ____________
|
||||
tests/repro_reports/test_repro_reels_tab_hallucination.py:49: in test_reels_tab_selection
|
||||
self.assertIn("clips tab", result["semantic"].lower(), "Should select the clips_tab")
|
||||
E KeyError: 'semantic'
|
||||
----------------------------- Captured stdout call -----------------------------
|
||||
FIND_BEST_NODE CALLED
|
||||
Target selected: None at (324, 2298)
|
||||
____________________ test_intent_resolver_finds_bottom_tab _____________________
|
||||
tests/unit/perception/test_intent_resolver.py:16: in test_intent_resolver_finds_bottom_tab
|
||||
assert best_match == bottom_tab
|
||||
E AssertionError: assert None == SpatialNode(bounds=(0, 2200, 100, 2300), node_id='', class_name='', text='', content_desc='Explore Tab', resource_id='', clickable=True, scrollable=False, children=[], parent=None)
|
||||
_______ TestGridRetryDiversity.test_first_call_returns_topmost_leftmost ________
|
||||
tests/unit/test_grid_retry_diversity.py:84: in test_first_call_returns_topmost_leftmost
|
||||
result = self.engine._grid_fast_path("first image in explore grid", nodes)
|
||||
E AttributeError: 'TelepathicEngine' object has no attribute '_grid_fast_path'
|
||||
___________ TestGridRetryDiversity.test_retry_skips_failed_position ____________
|
||||
tests/unit/test_grid_retry_diversity.py:92: in test_retry_skips_failed_position
|
||||
result = self.engine._grid_fast_path("first image in explore grid", nodes, skip_positions={(178, 558)})
|
||||
E AttributeError: 'TelepathicEngine' object has no attribute '_grid_fast_path'
|
||||
_____________ TestGridRetryDiversity.test_skip_multiple_positions ______________
|
||||
tests/unit/test_grid_retry_diversity.py:99: in test_skip_multiple_positions
|
||||
result = self.engine._grid_fast_path(
|
||||
E AttributeError: 'TelepathicEngine' object has no attribute '_grid_fast_path'
|
||||
________ TestGridRetryDiversity.test_all_positions_skipped_returns_none ________
|
||||
tests/unit/test_grid_retry_diversity.py:109: in test_all_positions_skipped_returns_none
|
||||
result = self.engine._grid_fast_path("first image in explore grid", nodes, skip_positions=all_positions)
|
||||
E AttributeError: 'TelepathicEngine' object has no attribute '_grid_fast_path'
|
||||
=============================== warnings summary ===============================
|
||||
../../../../Users/marcmintel/.pyenv/versions/3.11.9/lib/python3.11/site-packages/requests/__init__.py:109
|
||||
/Users/marcmintel/.pyenv/versions/3.11.9/lib/python3.11/site-packages/requests/__init__.py:109: RequestsDependencyWarning: urllib3 (2.4.0) or chardet (7.4.3)/charset_normalizer (3.4.2) doesn't match a supported version!
|
||||
warnings.warn(
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
=========================== short test summary info ============================
|
||||
FAILED tests/anomalies/test_telepathic_guards.py::TestTelepathicGuards::test_strict_story_ring_guard
|
||||
FAILED tests/anomalies/test_telepathic_guards.py::TestTelepathicGuards::test_strict_button_guard
|
||||
FAILED tests/integration/test_telepathic_hardening.py::test_keyword_nav_threshold
|
||||
FAILED tests/integration/test_telepathic_hardening.py::test_direct_tab_fast_path
|
||||
FAILED tests/integration/test_telepathic_keyword.py::test_keyword_fast_path_no_feed_pollution
|
||||
FAILED tests/repro_reports/test_repro_position_rejection.py::TestPositionRejection::test_repro_following_button_rejection_fix
|
||||
FAILED tests/repro_reports/test_repro_reels_tab_hallucination.py::TestReproReelsTabHallucination::test_reels_tab_selection
|
||||
FAILED tests/unit/perception/test_intent_resolver.py::test_intent_resolver_finds_bottom_tab
|
||||
FAILED tests/unit/test_grid_retry_diversity.py::TestGridRetryDiversity::test_first_call_returns_topmost_leftmost
|
||||
FAILED tests/unit/test_grid_retry_diversity.py::TestGridRetryDiversity::test_retry_skips_failed_position
|
||||
FAILED tests/unit/test_grid_retry_diversity.py::TestGridRetryDiversity::test_skip_multiple_positions
|
||||
FAILED tests/unit/test_grid_retry_diversity.py::TestGridRetryDiversity::test_all_positions_skipped_returns_none
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_normal_instagram
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_foreign_app_google
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_notification_shade
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_system_permission_dialog
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_instagram_survey_modal
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_unknown_modal_interstitial
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_action_blocked
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_empty_dump
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_none_dump
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAEPerception::test_perceive_passive_scaffold_as_normal
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_home_feed_as_normal
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_explore_grid_as_normal
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_other_profile_as_normal
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_post_detail_as_normal
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_profile_tagged_tab_as_normal
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_survey_modal_as_obstacle
|
||||
ERROR tests/e2e/test_engine_perception.py::TestSAERealFixturePerception::test_perceive_mystery_interstitial_as_obstacle
|
||||
ERROR tests/e2e/test_goap_loop_prevention.py::test_goap_planner_avoids_infinite_loop_on_masked_edge
|
||||
ERROR tests/e2e/test_goap_loop_prevention.py::test_screen_topology_find_route_avoids_blocked_edges
|
||||
ERROR tests/e2e/test_goap_loop_prevention.py::test_telepathic_engine_finds_following_node_on_profile
|
||||
ERROR tests/e2e/test_goap_loop_prevention.py::test_following_vs_followers_are_both_candidates
|
||||
ERROR tests/e2e/test_goap_loop_prevention.py::test_vlm_prompt_humanizes_content_desc
|
||||
ERROR tests/e2e/test_goap_loop_prevention.py::test_live_vlm_selects_following_not_followers
|
||||
ERROR tests/e2e/test_reel_interactions.py::test_reel_like_button_not_caption
|
||||
ERROR tests/e2e/test_reel_interactions.py::test_reel_follow_button_returns_none_when_absent
|
||||
ERROR tests/e2e/test_reel_interactions.py::test_reel_post_author_selects_username
|
||||
ERROR tests/e2e/test_reel_interactions.py::test_reel_dedup_preserves_like_button
|
||||
ERROR tests/e2e/test_reel_interactions.py::test_reel_caption_with_like_word_is_not_like_button
|
||||
ERROR tests/e2e/test_reel_navigation_guards.py::test_intent_resolver_profile_tab_rejects_author_profile
|
||||
ERROR tests/e2e/test_reel_navigation_guards.py::test_intent_resolver_profile_tab_selects_real_tab
|
||||
ERROR tests/e2e/test_visual_intent_resolver.py::test_visual_discovery_creates_annotated_screenshot
|
||||
ERROR tests/e2e/test_visual_intent_resolver.py::test_visual_discovery_finds_following_by_seeing
|
||||
ERROR tests/e2e/test_visual_intent_resolver.py::test_resolve_uses_visual_discovery_when_device_available
|
||||
ERROR tests/unit/test_session_limits_evaluation.py::test_global_session_limit_evaluation
|
||||
======= 12 failed, 135 passed, 5 skipped, 1 warning, 34 errors in 19.92s =======
|
||||
BIN
tests/.DS_Store
vendored
Normal file
BIN
tests/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -1,198 +0,0 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Force mock qdrant_client before importing any core modules that depend on it
|
||||
from GramAddict.core.bot_flow import _extract_post_content, _run_zero_latency_feed_loop
|
||||
|
||||
|
||||
class TestBotFlowEdgeCases:
|
||||
@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
|
||||
def test_extract_post_content_edge_cases(self, mock_get_telepathic):
|
||||
mock_engine = MagicMock()
|
||||
mock_get_telepathic.return_value = mock_engine
|
||||
|
||||
# 1. Empty string / Invalid XML should not crash (mock finds nothing)
|
||||
mock_engine.find_best_node.return_value = None
|
||||
res = _extract_post_content("")
|
||||
assert res.get("username") == ""
|
||||
assert res.get("description") == ""
|
||||
|
||||
# 2. Extract when only username exists
|
||||
# Side effect: first call (author) returns node, second (media) returns None
|
||||
mock_engine.find_best_node.side_effect = [{"original_attribs": {"text": "just_user"}}, None]
|
||||
res = _extract_post_content("<xml/>")
|
||||
assert res.get("username") == "just_user"
|
||||
assert res.get("description") == ""
|
||||
|
||||
# 3. Extract description
|
||||
mock_engine.find_best_node.side_effect = [None, {"original_attribs": {"desc": "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"}}]
|
||||
res = _extract_post_content("<xml/>")
|
||||
assert res.get("description") == "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"
|
||||
|
||||
# 4. Another valid description tag
|
||||
mock_engine.find_best_node.side_effect = [
|
||||
None,
|
||||
{"original_attribs": {"desc": "some desc with more than 10 chars limits"}},
|
||||
]
|
||||
res = _extract_post_content("<xml/>")
|
||||
assert res.get("description") == "some desc with more than 10 chars limits"
|
||||
|
||||
@patch("GramAddict.core.bot_flow.random.random", return_value=0.5)
|
||||
@patch("GramAddict.core.bot_flow.random.uniform", return_value=1.5)
|
||||
@patch("GramAddict.core.bot_flow.sleep")
|
||||
@patch("GramAddict.core.bot_flow._humanized_scroll")
|
||||
@patch("GramAddict.core.utils.is_ad")
|
||||
@patch("GramAddict.core.bot_flow._align_active_post")
|
||||
@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
|
||||
def test_zero_node_recovery(
|
||||
self, mock_get_telepathic, mock_align, mock_ad, mock_scroll, mock_sleep, mock_uniform, mock_random
|
||||
):
|
||||
# Tests the explicit Zero-Node Recovery added previously
|
||||
device = MagicMock()
|
||||
zero_engine = MagicMock()
|
||||
nav_graph = MagicMock()
|
||||
configs = MagicMock()
|
||||
session_state = MagicMock()
|
||||
|
||||
mock_ad.return_value = False
|
||||
mock_align.return_value = False
|
||||
|
||||
cognitive_stack = {
|
||||
"dopamine": MagicMock(),
|
||||
"darwin": MagicMock(),
|
||||
"resonance": MagicMock(),
|
||||
"active_inference": MagicMock(),
|
||||
"growth_brain": MagicMock(),
|
||||
"swarm": MagicMock(),
|
||||
}
|
||||
|
||||
# Dopamine breaks loop after 1st iteration
|
||||
cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
|
||||
cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
|
||||
cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
|
||||
|
||||
# Fake extreme limits => doesn't break limits
|
||||
session_state.check_limit.return_value = [False] * 10
|
||||
|
||||
# Telepathic Engine returns ZERO nodes on extract
|
||||
mock_engine = MagicMock()
|
||||
mock_engine._extract_semantic_nodes.return_value = []
|
||||
mock_get_telepathic.return_value = mock_engine
|
||||
|
||||
device.dump_hierarchy.return_value = "<xml></xml>"
|
||||
|
||||
# Execute the main loop
|
||||
_run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, "HomeFeed", cognitive_stack)
|
||||
|
||||
# It should trigger _humanized_scroll
|
||||
assert mock_scroll.call_count >= 1
|
||||
|
||||
@patch("GramAddict.core.bot_flow.random.random", return_value=0.5)
|
||||
@patch("GramAddict.core.bot_flow.random.uniform", return_value=1.5)
|
||||
@patch("GramAddict.core.bot_flow.sleep")
|
||||
@patch("GramAddict.core.bot_flow._humanized_scroll")
|
||||
@patch("GramAddict.core.bot_flow._extract_post_content")
|
||||
@patch("GramAddict.core.utils.is_ad")
|
||||
@patch("GramAddict.core.bot_flow._align_active_post")
|
||||
@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
|
||||
def test_content_extraction_failed_recovery(
|
||||
self,
|
||||
mock_get_telepathic,
|
||||
mock_align,
|
||||
mock_ad,
|
||||
mock_extract,
|
||||
mock_scroll,
|
||||
mock_sleep,
|
||||
mock_uniform,
|
||||
mock_random,
|
||||
):
|
||||
device = MagicMock()
|
||||
zero_engine = MagicMock()
|
||||
nav_graph = MagicMock()
|
||||
configs = MagicMock()
|
||||
session_state = MagicMock()
|
||||
|
||||
mock_ad.return_value = False
|
||||
mock_align.return_value = False
|
||||
|
||||
cognitive_stack = {"dopamine": MagicMock(), "darwin": MagicMock()}
|
||||
# break after 1 loop
|
||||
cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
|
||||
cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
|
||||
cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
|
||||
session_state.check_limit.return_value = [False] * 10
|
||||
|
||||
# Ensure it HAS feed markers
|
||||
device.dump_hierarchy.return_value = "<xml>row_feed_photo_profile_name</xml>"
|
||||
|
||||
# Ensure interactive_nodes is NOT zero
|
||||
mock_engine = MagicMock()
|
||||
mock_engine._extract_semantic_nodes.return_value = [{"x": 10}]
|
||||
mock_get_telepathic.return_value = mock_engine
|
||||
|
||||
# Make the extraction fail
|
||||
mock_extract.return_value = {"username": "", "description": ""}
|
||||
|
||||
_run_zero_latency_feed_loop(device, zero_engine, nav_graph, configs, session_state, "HomeFeed", cognitive_stack)
|
||||
|
||||
# Should call mock_scroll (Graceful degradation)
|
||||
mock_scroll.assert_called_once()
|
||||
|
||||
@patch("GramAddict.core.bot_flow.sleep")
|
||||
@patch("GramAddict.core.bot_flow._humanized_scroll")
|
||||
@patch("GramAddict.core.utils.is_ad")
|
||||
@patch("GramAddict.core.bot_flow._align_active_post")
|
||||
@patch("GramAddict.core.bot_flow._extract_post_content")
|
||||
@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
|
||||
@patch("GramAddict.core.llm_provider.query_llm")
|
||||
def test_llm_timeout_handled_smoothly(
|
||||
self, mock_query_llm, mock_get_telepathic, mock_extract, mock_align, mock_ad, mock_scroll, mock_sleep
|
||||
):
|
||||
"""
|
||||
TDD Test: Verifies that if qwen3.5:latest times out during comment generation
|
||||
(simulated by query_llm returning None after circuit breaker), the bot_flow
|
||||
catches the empty response and continues gracefully without crashing.
|
||||
"""
|
||||
device = MagicMock()
|
||||
zero_engine = MagicMock()
|
||||
nav_graph = MagicMock()
|
||||
configs = MagicMock()
|
||||
session_state = MagicMock()
|
||||
|
||||
mock_ad.return_value = False
|
||||
mock_align.return_value = False
|
||||
|
||||
# Make the LLM generation completely timeout and return None
|
||||
mock_query_llm.return_value = None
|
||||
|
||||
cognitive_stack = {"dopamine": MagicMock(), "darwin": MagicMock(), "resonance": MagicMock()}
|
||||
# break after 1 loop
|
||||
cognitive_stack["dopamine"].is_app_session_over.side_effect = [False, True]
|
||||
cognitive_stack["dopamine"].wants_to_change_feed.return_value = False
|
||||
cognitive_stack["dopamine"].wants_to_doomscroll.return_value = False
|
||||
|
||||
# Emulate that dopamine WANTS to comment
|
||||
cognitive_stack["dopamine"].get_action_desires.return_value = {"comment": True, "like": False}
|
||||
|
||||
# Avoid MagicMock comparison errors in Resonance Engine
|
||||
cognitive_stack["resonance"].calculate_resonance.return_value = 0.8
|
||||
|
||||
session_state.check_limit.return_value = [False] * 10
|
||||
|
||||
device.dump_hierarchy.return_value = "<xml>row_feed_photo_profile_name</xml>"
|
||||
|
||||
mock_engine = MagicMock()
|
||||
mock_engine._extract_semantic_nodes.return_value = [{"x": 10}]
|
||||
mock_get_telepathic.return_value = mock_engine
|
||||
|
||||
# Valid post content so it proceeds to comment generation
|
||||
mock_extract.return_value = {"username": "test_user", "description": "a long enough description"}
|
||||
|
||||
# Run feed loop - MUST NOT CRASH
|
||||
try:
|
||||
_run_zero_latency_feed_loop(
|
||||
device, zero_engine, nav_graph, configs, session_state, "HomeFeed", cognitive_stack
|
||||
)
|
||||
except Exception as e:
|
||||
pytest.fail(f"Feed loop crashed on LLM timeout with {e}")
|
||||
@@ -1,69 +0,0 @@
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# Mock directory setup
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
FIXTURE_DIR = os.path.join(ROOT_DIR, "fixtures")
|
||||
|
||||
|
||||
class ConfigMock:
|
||||
def __init__(self):
|
||||
self.args = MagicMock()
|
||||
self.args.app_id = "com.instagram.android"
|
||||
|
||||
|
||||
def test_fsd_handles_persistent_survey_modal():
|
||||
"""
|
||||
Simulates a case where the bot gets stuck on a survey modal.
|
||||
The FSD (Full Self Driving) anomaly handler should trigger,
|
||||
detect that 'Back' didn't work, and engage TelepathicEngine
|
||||
to find and tap the 'Not Now' or 'Dismiss' button.
|
||||
"""
|
||||
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop
|
||||
|
||||
device = MagicMock()
|
||||
device.app_id = "com.instagram.android"
|
||||
device._get_current_app.return_value = "com.instagram.android"
|
||||
configs = ConfigMock()
|
||||
|
||||
# Mock the TelepathicEngine singleton behavior entirely
|
||||
mock_telepathic = MagicMock()
|
||||
mock_telepathic.find_best_node.return_value = {"x": 500, "y": 1400, "semantic": "Not Now"}
|
||||
|
||||
dopamine = MagicMock()
|
||||
dopamine.is_app_session_over.side_effect = [False, False, True] # Run twice, then exit
|
||||
dopamine.wants_to_change_feed.return_value = False
|
||||
dopamine.wants_to_doomscroll.return_value = False
|
||||
|
||||
ai = MagicMock()
|
||||
ai.get_sleep_modifier.return_value = 1.0
|
||||
|
||||
cognitive_stack = {
|
||||
"dopamine": dopamine,
|
||||
"growth_brain": None,
|
||||
"active_inference": ai,
|
||||
}
|
||||
|
||||
# Load the mock survey modal UI
|
||||
xml_path = os.path.join(FIXTURE_DIR, "survey_modal.xml")
|
||||
with open(xml_path, "r") as f:
|
||||
alien_xml = f.read()
|
||||
device.dump_hierarchy.return_value = alien_xml
|
||||
|
||||
with (
|
||||
patch("GramAddict.core.bot_flow.sleep"),
|
||||
patch("GramAddict.core.behaviors.obstacle_guard.sleep"),
|
||||
patch("GramAddict.core.bot_flow._humanized_scroll"),
|
||||
patch("GramAddict.core.behaviors.obstacle_guard.TelepathicEngine.get_instance", return_value=mock_telepathic),
|
||||
patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance", return_value=mock_telepathic),
|
||||
):
|
||||
from GramAddict.core.behaviors import PluginRegistry
|
||||
from GramAddict.core.behaviors.obstacle_guard import ObstacleGuardPlugin
|
||||
|
||||
PluginRegistry.get_instance().register(ObstacleGuardPlugin())
|
||||
|
||||
_run_zero_latency_feed_loop(device, None, MagicMock(), configs, MagicMock(), "HomeFeed", cognitive_stack)
|
||||
|
||||
# VERIFICATION:
|
||||
assert mock_telepathic.find_best_node.called
|
||||
assert device.click.called
|
||||
@@ -1,78 +0,0 @@
|
||||
"""
|
||||
TDD Tests for Zero-Hardcode Screen Classification and Situational Awareness
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
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")
|
||||
@@ -1,190 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop, _wait_for_post_loaded
|
||||
from GramAddict.core.perception.feed_analysis import FEED_MARKERS
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DUMPS = {
|
||||
"organic": os.path.join(ROOT_DIR, "fixtures", "organic_post.xml"),
|
||||
"explore": os.path.join(ROOT_DIR, "fixtures", "explore_feed_dump.xml"),
|
||||
}
|
||||
FIXTURE_DIR = os.path.join(ROOT_DIR, "fixtures")
|
||||
|
||||
|
||||
def mutate_xml_to_foreign(xml_content: str) -> str:
|
||||
"""Removes meaningful text content to simulate a language failure or empty state."""
|
||||
import re
|
||||
|
||||
# Strip text and content-desc
|
||||
xml = re.sub(r'text="[^"]*"', 'text=""', xml_content)
|
||||
xml = re.sub(r'content-desc="[^"]*"', 'content-desc=""', xml)
|
||||
return xml
|
||||
|
||||
|
||||
def mutate_xml_remove_feed_markers(xml_content: str) -> str:
|
||||
"""Removes all feed markers to simulate a grid view or random popup."""
|
||||
xml = xml_content
|
||||
for marker in FEED_MARKERS:
|
||||
xml = xml.replace(marker, "some_random_id")
|
||||
return xml
|
||||
|
||||
|
||||
class ConfigMock:
|
||||
def __init__(self):
|
||||
self.args = MagicMock()
|
||||
self.args.interact_percentage = 0
|
||||
self.args.comment_percentage = 0
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_dumps():
|
||||
dumps = {}
|
||||
with open(DUMPS["organic"], "r") as f:
|
||||
dumps["post"] = f.read()
|
||||
# Fake explore grid that lacks ALL feed markers
|
||||
dumps["grid"] = (
|
||||
'<?xml version="1.0"?><hierarchy><node resource-id="com.instagram.android:id/explore_grid_container" /></hierarchy>'
|
||||
)
|
||||
return dumps
|
||||
|
||||
|
||||
def test_slow_loading_post_recovery(test_dumps):
|
||||
"""
|
||||
Test that _wait_for_post_loaded correctly handles a delay where the
|
||||
first few dumps are grids, and only later it becomes a post.
|
||||
"""
|
||||
device = MagicMock()
|
||||
# Simulate: Grid -> Grid -> Error -> Post
|
||||
device.dump_hierarchy.side_effect = [
|
||||
test_dumps["grid"],
|
||||
test_dumps["grid"],
|
||||
Exception("uiautomator2 temp failure"),
|
||||
test_dumps["post"],
|
||||
]
|
||||
|
||||
# We patch sleep to make the test super fast
|
||||
with patch("GramAddict.core.bot_flow.sleep", return_value=None):
|
||||
time.time()
|
||||
success = _wait_for_post_loaded(device, timeout=5)
|
||||
# Should return true when it hits the 4th element
|
||||
assert success is True
|
||||
assert device.dump_hierarchy.call_count == 4
|
||||
|
||||
|
||||
def test_wait_timeout_aborts_gracefully(test_dumps):
|
||||
"""Test what happens if the network is so slow it times out entirely."""
|
||||
device = MagicMock()
|
||||
# Always return grid
|
||||
device.dump_hierarchy.return_value = test_dumps["grid"]
|
||||
|
||||
# Patch time.time to simulate 6 seconds passing immediately
|
||||
# We add sequence padding because python's logger internally uses time.time()
|
||||
with patch("time.time", side_effect=[0, 1, 6, 6, 6, 6, 6, 6, 6, 6]):
|
||||
with patch("GramAddict.core.bot_flow.sleep", return_value=None):
|
||||
success = _wait_for_post_loaded(device, timeout=5)
|
||||
assert success is False
|
||||
|
||||
|
||||
def test_empty_content_extraction_guard(test_dumps):
|
||||
"""
|
||||
Test that if a post is loaded, but it has strange empty text (foreign language or bug),
|
||||
the bot aborts interaction and scrolls instead of judging empty content.
|
||||
"""
|
||||
device = MagicMock()
|
||||
nav_graph = MagicMock()
|
||||
configs = ConfigMock()
|
||||
|
||||
# We create a fake active inference engine to just break the loop after 1 iteration
|
||||
ai = MagicMock()
|
||||
# Dopamine engine controls loop exit
|
||||
dopamine = MagicMock()
|
||||
dopamine.is_app_session_over.side_effect = [False, True] # Run once, then exit
|
||||
dopamine.wants_to_change_feed.return_value = False
|
||||
dopamine.wants_to_doomscroll.return_value = False
|
||||
|
||||
cognitive_stack = {
|
||||
"dopamine": dopamine,
|
||||
"active_inference": ai,
|
||||
"resonance": None,
|
||||
"growth_brain": None,
|
||||
"swarm": None,
|
||||
"darwin": None,
|
||||
}
|
||||
|
||||
# Mutate the post so it has NO text or description
|
||||
broken_xml = mutate_xml_to_foreign(test_dumps["post"])
|
||||
device.dump_hierarchy.return_value = broken_xml
|
||||
|
||||
from GramAddict.core.situational_awareness import SituationType
|
||||
|
||||
with (
|
||||
patch("GramAddict.core.bot_flow._humanized_scroll") as mock_scroll,
|
||||
patch("GramAddict.core.bot_flow.sleep"),
|
||||
patch(
|
||||
"GramAddict.core.situational_awareness.SituationalAwarenessEngine.perceive",
|
||||
return_value=SituationType.NORMAL,
|
||||
),
|
||||
):
|
||||
result = _run_zero_latency_feed_loop(device, None, nav_graph, configs, MagicMock(), "HomeFeed", cognitive_stack)
|
||||
|
||||
# Ensure scroll was called (the recovery mechanism)
|
||||
assert mock_scroll.called
|
||||
# Check that we never called resonance evaluation because we broke early
|
||||
assert not ai.predict_state.called
|
||||
assert result == "FEED_EXHAUSTED"
|
||||
|
||||
|
||||
def test_missing_feed_markers_guard(test_dumps):
|
||||
"""
|
||||
Test that if the UI is completely foreign (e.g., a system popup),
|
||||
the bot detects missing feed markers and scrolls to recover.
|
||||
"""
|
||||
device = MagicMock()
|
||||
configs = ConfigMock()
|
||||
|
||||
dopamine = MagicMock()
|
||||
dopamine.is_app_session_over.side_effect = [False, True]
|
||||
dopamine.wants_to_change_feed.return_value = False
|
||||
dopamine.wants_to_doomscroll.return_value = False
|
||||
|
||||
cognitive_stack = {"dopamine": dopamine, "growth_brain": None, "active_inference": None}
|
||||
|
||||
# Mutate XML to remove all FEED MARKERS
|
||||
alien_xml = mutate_xml_remove_feed_markers(test_dumps["post"])
|
||||
device.dump_hierarchy.return_value = alien_xml
|
||||
|
||||
with patch("GramAddict.core.bot_flow._humanized_scroll"), patch("GramAddict.core.bot_flow.sleep"):
|
||||
_run_zero_latency_feed_loop(device, None, MagicMock(), configs, MagicMock(), "HomeFeed", cognitive_stack)
|
||||
|
||||
|
||||
@patch("GramAddict.core.device_facade.u2")
|
||||
def test_xpath_watcher_initialization(mock_u2):
|
||||
"""
|
||||
Test fixing the critical watcher API bug.
|
||||
Ensures that device facade uses .watcher("name").when(xpath=...)
|
||||
"""
|
||||
mock_d = MagicMock()
|
||||
mock_u2.connect.return_value = mock_d
|
||||
|
||||
# Setup mock chain: deviceV2.watcher("crash_dialog").when(...)
|
||||
mock_watcher = MagicMock()
|
||||
mock_d.watcher.return_value = mock_watcher
|
||||
mock_when = MagicMock()
|
||||
mock_watcher.when.return_value = mock_when
|
||||
|
||||
# Just init the facade
|
||||
from GramAddict.core.device_facade import create_device
|
||||
|
||||
create_device("fake_serial", "com.fake.app", MagicMock())
|
||||
|
||||
# Verify exact API call structure for XPath
|
||||
mock_d.watcher.assert_any_call("crash_dialog")
|
||||
mock_d.watcher.assert_any_call("system_dialog")
|
||||
|
||||
# We can't perfectly assert the chained arguments natively without a bit of inspection,
|
||||
# but we can verify it didn't crash and called start
|
||||
assert mock_d.watcher.start.called
|
||||
@@ -1,50 +0,0 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from GramAddict.core.device_facade import DeviceFacade
|
||||
|
||||
|
||||
def test_adb_retry_recovers_from_transient_error():
|
||||
# Attempt simulated disconnect on dump_hierarchy
|
||||
device_id = "test"
|
||||
app_id = "test"
|
||||
|
||||
with patch("uiautomator2.connect") as mock_connect:
|
||||
mock_device = MagicMock()
|
||||
mock_connect.return_value = mock_device
|
||||
|
||||
facade = DeviceFacade(device_id, app_id, None)
|
||||
|
||||
# Make the first 2 calls fail, the 3rd one pass
|
||||
mock_device.dump_hierarchy.side_effect = [
|
||||
Exception("ConnectError uiautomator2"),
|
||||
Exception("RPC Error"),
|
||||
"<hierarchy></hierarchy>",
|
||||
]
|
||||
|
||||
# Patch sleep to speed up test
|
||||
with patch("GramAddict.core.device_facade.sleep"):
|
||||
res = facade.dump_hierarchy()
|
||||
assert res == "<hierarchy></hierarchy>"
|
||||
assert mock_device.dump_hierarchy.call_count == 3
|
||||
|
||||
|
||||
def test_adb_retry_crashes_gracefully_after_all_retries():
|
||||
# Attempt simulated disconnect on dump_hierarchy
|
||||
device_id = "test"
|
||||
app_id = "test"
|
||||
|
||||
with patch("uiautomator2.connect") as mock_connect:
|
||||
mock_device = MagicMock()
|
||||
mock_connect.return_value = mock_device
|
||||
|
||||
facade = DeviceFacade(device_id, app_id, None)
|
||||
|
||||
# Always fail
|
||||
mock_device.dump_hierarchy.side_effect = Exception("Permanent ConnectError")
|
||||
|
||||
with patch("GramAddict.core.device_facade.sleep"):
|
||||
with pytest.raises(Exception, match="Permanent ConnectError"):
|
||||
facade.dump_hierarchy()
|
||||
assert mock_device.dump_hierarchy.call_count == 3
|
||||
@@ -1,96 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
# Add parent dir to path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
|
||||
class DummyDevice:
|
||||
class DeviceV2:
|
||||
def __init__(self):
|
||||
self.last_click = None
|
||||
|
||||
def click(self, x, y):
|
||||
self.last_click = (x, y)
|
||||
|
||||
def screenshot(self, path=None):
|
||||
return "fake_screenshot"
|
||||
|
||||
def __init__(self):
|
||||
import unittest
|
||||
|
||||
self.deviceV2 = self.DeviceV2()
|
||||
self.app_id = "com.instagram.android"
|
||||
self.args = unittest.mock.MagicMock()
|
||||
self.args.ai_telepathic_model = "qwen2.5:3b"
|
||||
self.args.ai_telepathic_url = "http://localhost:11434/api/generate"
|
||||
|
||||
def _get_current_app(self):
|
||||
return "com.instagram.android"
|
||||
|
||||
def get_info(self):
|
||||
return {"displayHeight": 2400, "displayWidth": 1080}
|
||||
|
||||
def screenshot(self, path=None):
|
||||
return "fake_screenshot"
|
||||
|
||||
|
||||
class TestHumanHesitation(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.telepathic = TelepathicEngine()
|
||||
self.device = DummyDevice()
|
||||
|
||||
def test_discard_dialog_extraction(self):
|
||||
"""
|
||||
Prove that the Telepathic Engine can correctly identify the 'Discard'
|
||||
button inside a synthetic XML dump, ensuring the 'Umentscheidung'
|
||||
abort logic works in the wild.
|
||||
"""
|
||||
# Synthetic Discard Dialog XML
|
||||
synthetic_dump = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" bounds="[0,0][1080,2400]" package="com.instagram.android">
|
||||
<node index="1" class="android.widget.TextView" text="Discard Comment?" bounds="[200,1000][800,1100]" />
|
||||
<node index="2" class="android.widget.Button" text="IGNORE" content-desc="IGNORE" bounds="[200,1200][400,1300]" />
|
||||
<node index="3" class="android.widget.Button" text="Verwerfen" content-desc="Discard or Verwerfen popup button" bounds="[600,1200][800,1300]" resource-id="com.instagram.android:id/button_discard" />
|
||||
</node>
|
||||
</hierarchy>
|
||||
"""
|
||||
|
||||
# Act
|
||||
result = self.telepathic.find_best_node(
|
||||
synthetic_dump,
|
||||
"Discard or Verwerfen popup button to cancel comment",
|
||||
device=self.device,
|
||||
min_confidence=0.5,
|
||||
)
|
||||
|
||||
# Assert (Should hit the [600,1200][800,1300] box, which centers to (700, 1250))
|
||||
self.assertIsNotNone(result, "Telepathic engine failed to find 'Verwerfen'.")
|
||||
self.assertEqual(result["x"], 700)
|
||||
self.assertEqual(result["y"], 1250)
|
||||
|
||||
def test_dm_inbox_tab_resolution(self):
|
||||
"""
|
||||
Verify that teleporting specifically to the Inbox tab (DM button)
|
||||
succeeds if 'Message' describes it.
|
||||
"""
|
||||
synthetic_dump = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="2" text="" id="direct_tab" package="com.instagram.android" content-desc="Direct messages tab button" bounds="[432,2235][648,2361]" resource-id="com.instagram.android:id/direct_tab">
|
||||
<node content-desc=""/>
|
||||
</node>
|
||||
</hierarchy>"""
|
||||
|
||||
# If ID didn't match perfectly, we fall back to description as programmed.
|
||||
# Direct simulation of UI Automator check isn't in scope for this telepathic test,
|
||||
# but we can ensure Telepathic Engine CAN find it if we rely on it.
|
||||
result = self.telepathic.find_best_node(synthetic_dump, "Direct messages tab button", device=self.device)
|
||||
self.assertIsNotNone(result, "Should find the Message tab")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,52 +0,0 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from GramAddict.core.llm_provider import query_llm
|
||||
|
||||
|
||||
def test_query_llm_hallucination_recovery():
|
||||
# Test that when the primary model hallucinates non-JSON, it triggers fallback
|
||||
with patch("requests.post") as mock_post:
|
||||
# 1st call: Primary fails entirely (e.g., Timeout or strange error)
|
||||
mock_response_1 = MagicMock()
|
||||
mock_response_1.status_code = 500
|
||||
mock_response_1.raise_for_status.side_effect = Exception("500 Server Error")
|
||||
|
||||
# 2nd call: Fallback works and returns valid JSON
|
||||
mock_response_2 = MagicMock()
|
||||
mock_response_2.status_code = 200
|
||||
mock_response_2.raise_for_status.return_value = None
|
||||
mock_response_2.json.return_value = {"choices": [{"message": {"content": '{"test": "success"}'}}]}
|
||||
|
||||
mock_post.side_effect = [mock_response_1, mock_response_2]
|
||||
|
||||
# Attempt a query with a primary model
|
||||
res = query_llm(
|
||||
url="http://fake.api/v1/chat/completions",
|
||||
model="primary-model",
|
||||
prompt="Hello",
|
||||
format_json=True,
|
||||
fallback_model="fallback-model",
|
||||
fallback_url="http://fake.api/v1/chat/completions",
|
||||
)
|
||||
|
||||
assert res is not None
|
||||
assert "response" in res
|
||||
assert res["response"] == '{"test": "success"}'
|
||||
assert mock_post.call_count == 2
|
||||
|
||||
|
||||
def test_query_llm_double_hallucination_safe_return():
|
||||
# Test that when both models hallucinate, we return None gracefully
|
||||
with patch("requests.post") as mock_post:
|
||||
# Both models fail
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 500
|
||||
mock_response.raise_for_status.side_effect = Exception("500 Server Error")
|
||||
|
||||
mock_post.side_effect = [mock_response, mock_response]
|
||||
|
||||
res = query_llm(
|
||||
url="http://fake.api/v1/chat/completions", model="primary-model", prompt="Hello", format_json=True
|
||||
)
|
||||
|
||||
assert res is None
|
||||
@@ -1,49 +0,0 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from GramAddict.core.q_nav_graph import QNavGraph
|
||||
|
||||
|
||||
def test_tap_home_tab_recovery_from_homescreen():
|
||||
"""
|
||||
TDD: Reproduce the failure where tap_home_tab fails because the bot is on
|
||||
the Android Homescreen (app.lawnchair), and verify that it recovers
|
||||
via app_start instead of enterring an auto-repair loop.
|
||||
"""
|
||||
# 1. Setup Mock Device
|
||||
mock_device = MagicMock()
|
||||
mock_device.app_id = "com.instagram.android"
|
||||
# Return homescreen package to simulate context loss
|
||||
mock_device._get_current_app.return_value = "app.lawnchair"
|
||||
|
||||
# 2. Mock DeviceV2 responses
|
||||
mock_device.dump_hierarchy.return_value = "<hierarchy />"
|
||||
mock_device.app_start.return_value = True
|
||||
|
||||
# 3. Initialize NavGraph
|
||||
graph = QNavGraph(mock_device)
|
||||
graph.current_state = "ProfileFeed" # Assume stale state
|
||||
|
||||
# 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
|
||||
|
||||
# Simulate Context Guard hitting: return None forever
|
||||
mock_engine.find_best_node.return_value = None
|
||||
|
||||
# 5. Execute
|
||||
# We expect this to return False gracefully after 3 attempts, without infinitely looping
|
||||
success = graph.navigate_to("ExploreFeed", zero_engine=None)
|
||||
|
||||
# 6. Assertion
|
||||
assert not success, "Navigation should fail gracefully when context cannot be recovered"
|
||||
assert mock_device.app_start.called, "Should have force-started the app when context was lost"
|
||||
@@ -1,123 +0,0 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Force mock qdrant_client before importing any core modules that depend on it
|
||||
from GramAddict.core.q_nav_graph import QNavGraph
|
||||
|
||||
|
||||
class TestQNavGraphEdgeCases:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_graph(self):
|
||||
self.device = MagicMock()
|
||||
self.device.app_id = "com.instagram.android"
|
||||
self.device.info = {"screenOn": True}
|
||||
self.device.dump_hierarchy.return_value = '<hierarchy><node package="com.instagram.android" /></hierarchy>'
|
||||
self.device._get_current_app = MagicMock(return_value="com.instagram.android")
|
||||
|
||||
# Prevent Dojo engine instantiation during tests
|
||||
with patch("GramAddict.core.compiler_engine.VLMCompilerEngine"):
|
||||
self.graph = QNavGraph(self.device)
|
||||
|
||||
def test_find_path_edge_cases(self):
|
||||
# 1. Start == End
|
||||
assert self.graph._find_path("HomeFeed", "HomeFeed") == []
|
||||
|
||||
# 2. Start not in nodes
|
||||
assert self.graph._find_path("UnknownState", "HomeFeed") is None
|
||||
|
||||
# 3. Unreachable states
|
||||
self.graph.nodes = {
|
||||
"HomeFeed": {"transitions": {"tap_explore": "ExploreFeed"}},
|
||||
"IsolatedFeed": {"transitions": {}},
|
||||
}
|
||||
assert self.graph._find_path("HomeFeed", "IsolatedFeed") is None
|
||||
|
||||
# 4. Infinite loop protection (A -> B -> A)
|
||||
self.graph.nodes = {"A": {"transitions": {"to_b": "B"}}, "B": {"transitions": {"to_a": "A"}}}
|
||||
assert (
|
||||
self.graph._find_path("A", "C") is None
|
||||
) # Should safely return None without exceeding recursion/loop depth
|
||||
|
||||
# 5. Longest path possible before unreachability is confirmed
|
||||
assert self.graph._find_path("B", "D") is None
|
||||
|
||||
# 6. Diamond shape path
|
||||
self.graph.nodes = {
|
||||
"Start": {"transitions": {"top": "Top", "bottom": "Bottom"}},
|
||||
"Top": {"transitions": {"top_to_end": "End"}},
|
||||
"Bottom": {"transitions": {"bottom_to_end": "End"}},
|
||||
"End": {},
|
||||
}
|
||||
# BFS should find shortest path (len 2)
|
||||
assert len(self.graph._find_path("Start", "End")) == 2
|
||||
|
||||
@patch("GramAddict.core.q_nav_graph.time.sleep", return_value=None)
|
||||
@patch("GramAddict.core.q_nav_graph.random_sleep", return_value=None)
|
||||
@patch("GramAddict.core.situational_awareness.random_sleep", return_value=None)
|
||||
@patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance")
|
||||
def test_execute_transition_edge_cases(self, mock_get_telepathic, mock_sae_sleep, mock_q_rand_sleep, mock_q_sleep):
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
mock_engine = MagicMock(spec=TelepathicEngine)
|
||||
mock_get_telepathic.return_value = mock_engine
|
||||
|
||||
# Case 1: Telepathic engine finds nothing
|
||||
mock_engine.find_best_node.return_value = None
|
||||
|
||||
# If still in Instagram, it returns False
|
||||
self.device._get_current_app.return_value = "com.instagram.android"
|
||||
assert not self.graph._execute_transition("unknown_action", mock_engine)
|
||||
|
||||
# If app is different, it returns "CONTEXT_LOST"
|
||||
self.device._get_current_app.return_value = "com.android.launcher3"
|
||||
assert self.graph._execute_transition("unknown_action", mock_engine) == "CONTEXT_LOST"
|
||||
|
||||
# Case 2: Best node has skip flag
|
||||
mock_engine.find_best_node.return_value = {"skip": True}
|
||||
assert self.graph._execute_transition("already_done_action", mock_engine)
|
||||
|
||||
# Case 3: Proper interaction, but XML doesn't change (verification fail)
|
||||
mock_engine.find_best_node.return_value = {"x": 10, "y": 10, "score": 0.9}
|
||||
same_xml = '<hierarchy><node package="com.instagram.android" class="same" /></hierarchy>'
|
||||
self.device.dump_hierarchy.side_effect = None
|
||||
self.device.dump_hierarchy.return_value = same_xml
|
||||
assert not self.graph._execute_transition("click_action", mock_engine)
|
||||
assert mock_engine.reject_click.call_count == 3
|
||||
|
||||
# Case 4: Proper interaction, XML changes (verification pass)
|
||||
mock_engine.reset_mock()
|
||||
mock_engine.find_best_node.return_value = {"x": 10, "y": 10, "score": 0.9}
|
||||
before_xml = '<hierarchy><node package="com.instagram.android" class="before" /></hierarchy>'
|
||||
after_xml = '<hierarchy><node package="com.instagram.android" class="after" /></hierarchy>'
|
||||
|
||||
initial_clicks = self.device.click.call_count
|
||||
|
||||
def dynamic_xml(*args, **kwargs):
|
||||
return after_xml if self.device.click.call_count > initial_clicks else before_xml
|
||||
|
||||
self.device.dump_hierarchy.side_effect = dynamic_xml
|
||||
# Explicitly ensure verify_success is truthy
|
||||
mock_engine.verify_success.return_value = True
|
||||
|
||||
assert self.graph._execute_transition("click_action", mock_engine)
|
||||
mock_engine.confirm_click.assert_called_once()
|
||||
|
||||
@patch("GramAddict.core.q_nav_graph.time.sleep", return_value=None)
|
||||
@patch("GramAddict.core.q_nav_graph.random_sleep", return_value=None)
|
||||
@patch("GramAddict.core.situational_awareness.random_sleep", return_value=None)
|
||||
@patch("GramAddict.core.dojo_engine.DojoEngine.get_instance")
|
||||
def test_navigate_to_recovery_edge_cases(self, mock_dojo, mock_sae_sleep, mock_q_rand_sleep, mock_q_sleep):
|
||||
# We test the deepest recovery logic: when everything fails
|
||||
|
||||
zero_engine = MagicMock()
|
||||
|
||||
# Mock transitions completely failing
|
||||
with patch.object(self.graph.goap, "navigate_to_screen", return_value=False):
|
||||
# Recovery attempts maxed out
|
||||
assert not self.graph.navigate_to("ExploreFeed", zero_engine, recovery_attempts=3)
|
||||
|
||||
# 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 `navigate_to_screen` always returns False
|
||||
assert not self.graph.navigate_to("ExploreFeed", zero_engine, recovery_attempts=0)
|
||||
@@ -1,59 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
|
||||
|
||||
from GramAddict.core.goap import GoalExecutor, ScreenType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_device():
|
||||
device = MagicMock()
|
||||
# Simulate XML changing but screen type not being the target
|
||||
device.dump_hierarchy.side_effect = ["<xml1/>", "<xml2/>", "<xml2/>"]
|
||||
device.app_id = "com.instagram.android"
|
||||
return device
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_telepathic():
|
||||
with patch("GramAddict.core.telepathic_engine.TelepathicEngine.get_instance") as mock:
|
||||
engine = mock.return_value
|
||||
engine.find_best_node.return_value = {"x": 100, "y": 200, "semantic_string": "mock_node"}
|
||||
yield engine
|
||||
|
||||
|
||||
def test_execution_rejects_wrong_screen(mock_device, mock_telepathic):
|
||||
"""
|
||||
TDD Case: If we intend to go to DMs but land on Reels,
|
||||
TelepathicEngine.confirm_click should NOT be called.
|
||||
"""
|
||||
executor = GoalExecutor(mock_device, "testuser")
|
||||
|
||||
# We mock perceive to return ReelsFeed after the click
|
||||
with patch.object(executor, "perceive") as mock_perceive:
|
||||
# Before click
|
||||
mock_perceive.side_effect = [
|
||||
{"screen_type": ScreenType.HOME_FEED}, # Initial
|
||||
{"screen_type": ScreenType.REELS_FEED}, # After click (WRONG!)
|
||||
]
|
||||
|
||||
# Action that intends to go to DM_INBOX
|
||||
action = "tap messages tab"
|
||||
|
||||
# We need to make sure _execute_action knows the goal is "open messages"
|
||||
# Since _execute_action is usually called from achieve(), we mock that flow
|
||||
|
||||
success = executor._execute_action(action, goal="open messages")
|
||||
|
||||
# Success should be False because we didn't reach the goal
|
||||
# (Or True if we only care about XML change, but that's what we're changing)
|
||||
assert success is False
|
||||
|
||||
# CRITICAL: confirm_click should NOT have been called for 'messages tab'
|
||||
# since we are on Reels.
|
||||
mock_telepathic.confirm_click.assert_not_called()
|
||||
mock_telepathic.reject_click.assert_called_once_with(action)
|
||||
@@ -1,68 +0,0 @@
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
|
||||
class TestTelepathicGuards:
|
||||
def setup_method(self):
|
||||
self.engine = TelepathicEngine()
|
||||
|
||||
def test_strict_story_ring_guard(self):
|
||||
"""
|
||||
TDD: Story rings MUST be physically near the top of the screen (y < 30%).
|
||||
Post profile headers that appear further down must be aggressively blocked
|
||||
when the intent is 'tap story ring avatar'.
|
||||
"""
|
||||
intent = "tap story ring avatar"
|
||||
screen_height = 2400
|
||||
|
||||
# Valid Story Ring (Top of screen, but below status bar)
|
||||
valid_story = {"resource_id": "reel_ring", "y": 300, "area": 100}
|
||||
assert self.engine._structural_sanity_check(valid_story, intent, screen_height) is True
|
||||
|
||||
# Invalid Story Ring (Hallucination: Post profile header in the feed)
|
||||
invalid_story = {"resource_id": "row_feed_profile_header", "y": 800, "area": 100}
|
||||
assert self.engine._structural_sanity_check(invalid_story, intent, screen_height) is False
|
||||
|
||||
def test_strict_button_guard(self):
|
||||
"""
|
||||
TDD: When explicitly looking for a 'button', nodes that declare themselves
|
||||
as profiles (e.g. 'go to profile') must be blocked, to prevent accidental
|
||||
profile visits when clicking 'like'.
|
||||
"""
|
||||
intent = "Heart like button for comment"
|
||||
screen_height = 2400
|
||||
|
||||
# Valid Like Button
|
||||
valid_btn = {"resource_id": "like_button", "semantic_string": "Like", "y": 1000, "area": 100}
|
||||
assert self.engine._structural_sanity_check(valid_btn, intent, screen_height) is True
|
||||
|
||||
# Invalid Profile Link masquerading as a match due to string proximity
|
||||
invalid_prof = {
|
||||
"resource_id": "username",
|
||||
"semantic_string": "Go to cayleighanddavid's profile",
|
||||
"y": 1000,
|
||||
"area": 100,
|
||||
}
|
||||
assert self.engine._structural_sanity_check(invalid_prof, intent, screen_height) is False
|
||||
|
||||
# However, if the intent *is* profile, it should pass
|
||||
intent_prof = "go to profile"
|
||||
assert self.engine._structural_sanity_check(invalid_prof, intent_prof, screen_height) is True
|
||||
|
||||
def test_like_semantic_verification(self):
|
||||
"""
|
||||
TDD: Verify that 'unlike' is treated as a successful 'Like' action,
|
||||
because tapping 'Like' changes the state to 'Unlike' in English Instagram.
|
||||
"""
|
||||
# Testing the specific regex logic inside verify_success
|
||||
import re
|
||||
|
||||
xml_dump_success = '<node class="android.widget.ImageView" content-desc="Unlike" />'
|
||||
|
||||
marker_found = re.search(r"\b(liked|unlike|gefällt mir nicht mehr|gefällt mir am)\b", xml_dump_success.lower())
|
||||
assert marker_found is not None
|
||||
|
||||
xml_dump_fail = '<node class="android.widget.ImageView" content-desc="Like" />'
|
||||
marker_found_fail = re.search(
|
||||
r"\b(liked|unlike|gefällt mir nicht mehr|gefällt mir am)\b", xml_dump_fail.lower()
|
||||
)
|
||||
assert marker_found_fail is None
|
||||
@@ -1,67 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
|
||||
|
||||
from GramAddict.core.q_nav_graph import QNavGraph
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
|
||||
class TestTrapEscape(unittest.TestCase):
|
||||
@patch("GramAddict.core.q_nav_graph.time.sleep", return_value=None)
|
||||
@patch("GramAddict.core.q_nav_graph.random_sleep", return_value=None)
|
||||
@patch("GramAddict.core.situational_awareness.SituationalAwarenessEngine.ensure_clear_screen", return_value=False)
|
||||
def test_trap_guard_autonomous_ai_escape(self, mock_sae_clear, mock_q_rand_sleep, mock_q_sleep):
|
||||
print("Starting TDD: Testing autonomous Trap Escape with semantic bypass...")
|
||||
|
||||
# 1. Setup mocks
|
||||
mock_device = MagicMock()
|
||||
mock_device.app_id = "com.instagram.android"
|
||||
mock_device._get_current_app.return_value = "com.instagram.android"
|
||||
|
||||
trap_xml = "<hierarchy><node resource-id='modal_trap' /></hierarchy>"
|
||||
current_xml = [trap_xml]
|
||||
|
||||
# Dynamic dump that changes after click
|
||||
def dynamic_dump():
|
||||
return current_xml[0]
|
||||
|
||||
def dynamic_click(**kwargs):
|
||||
if kwargs.get("obj") and kwargs["obj"].get("semantic") and "done" in kwargs["obj"].get("semantic").lower():
|
||||
current_xml[0] = "<html><node text='Reels'/><node text='Home'/></html>"
|
||||
|
||||
mock_device.dump_hierarchy.side_effect = dynamic_dump
|
||||
mock_device.click.side_effect = dynamic_click
|
||||
|
||||
nav_graph = QNavGraph(device=mock_device)
|
||||
|
||||
engine = TelepathicEngine.get_instance()
|
||||
engine.confirm_click = MagicMock()
|
||||
engine.reject_click = MagicMock()
|
||||
|
||||
original_find_best_node = engine.find_best_node
|
||||
|
||||
def spy_find_best_node(xml_hierarchy, intent_description, **kwargs):
|
||||
if "tap home tab" in intent_description.lower():
|
||||
return None
|
||||
return original_find_best_node(xml_hierarchy, intent_description, **kwargs)
|
||||
|
||||
engine.find_best_node = spy_find_best_node
|
||||
nav_graph.engine = engine # explicitly enforce
|
||||
|
||||
# 2. Execute transition
|
||||
# Mock engine finds nothing, triggering the final fallback escape
|
||||
nav_graph._execute_transition("tap_home_tab", max_retries=1, mock_semantic_engine=engine)
|
||||
|
||||
# 3. Assertions
|
||||
# The new SAE/nav_graph behavior explicitly presses BACK when 'tap_home_tab' fails after all retries
|
||||
self.assertTrue(mock_device.press.called, "Trap guard did not autonomously press BACK to escape the sub-view!")
|
||||
called_key = mock_device.press.call_args_list[0][0][0]
|
||||
self.assertEqual(called_key, "back")
|
||||
print("TDD SUCCESS: Autonomous Backend fallback confirmed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,227 +0,0 @@
|
||||
"""
|
||||
Chaos Engineering: Network & Dependency Failure Tests.
|
||||
|
||||
Verifies that the bot degrades gracefully when external services
|
||||
(Qdrant, Ollama, OpenRouter) are unavailable, slow, or return errors.
|
||||
|
||||
Tesla's FSD doesn't crash if the map server is unreachable — neither should we.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.chaos import VALID_FEED_XML
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Qdrant Failure Tests
|
||||
# ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.chaos
|
||||
class TestQdrantFailure:
|
||||
"""Bot must survive total Qdrant outage."""
|
||||
|
||||
def test_telepathic_works_without_qdrant(self):
|
||||
"""TelepathicEngine must still resolve nodes via keyword fast-path when Qdrant is down."""
|
||||
with (
|
||||
patch("GramAddict.core.qdrant_memory.QdrantBase.__init__", return_value=None),
|
||||
patch(
|
||||
"GramAddict.core.qdrant_memory.QdrantBase.is_connected",
|
||||
new_callable=lambda: property(lambda self: False),
|
||||
),
|
||||
):
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
TelepathicEngine._instance = None
|
||||
engine = TelepathicEngine.__new__(TelepathicEngine)
|
||||
engine.ui_memory = MagicMock()
|
||||
engine.ui_memory.is_connected = False
|
||||
engine.ui_memory.query_closest = MagicMock(return_value=None)
|
||||
engine.positive_memory = MagicMock()
|
||||
engine.positive_memory.is_connected = False
|
||||
engine.positive_memory.recall = MagicMock(return_value=None)
|
||||
engine._edge_model = None
|
||||
engine._edge_tokenizer = None
|
||||
|
||||
nodes = engine._extract_semantic_nodes(VALID_FEED_XML)
|
||||
# Should still find clickable nodes via structural parsing
|
||||
assert len(nodes) > 0
|
||||
TelepathicEngine._instance = None
|
||||
|
||||
def test_sae_recall_returns_none_without_qdrant(self):
|
||||
"""SAE episodic memory must return None (not crash) when Qdrant is down."""
|
||||
with (
|
||||
patch("GramAddict.core.qdrant_memory.QdrantBase.__init__", return_value=None),
|
||||
patch(
|
||||
"GramAddict.core.qdrant_memory.QdrantBase.is_connected",
|
||||
new_callable=lambda: property(lambda self: False),
|
||||
),
|
||||
):
|
||||
from GramAddict.core.situational_awareness import SituationEpisodeDB
|
||||
|
||||
db = SituationEpisodeDB()
|
||||
db._db = MagicMock()
|
||||
db._db.is_connected = False
|
||||
|
||||
result = db.recall("test_situation_signature")
|
||||
assert result is None
|
||||
|
||||
def test_sae_learn_silently_fails_without_qdrant(self):
|
||||
"""SAE learning must silently skip (not crash) when Qdrant is down."""
|
||||
with (
|
||||
patch("GramAddict.core.qdrant_memory.QdrantBase.__init__", return_value=None),
|
||||
patch(
|
||||
"GramAddict.core.qdrant_memory.QdrantBase.is_connected",
|
||||
new_callable=lambda: property(lambda self: False),
|
||||
),
|
||||
):
|
||||
from GramAddict.core.situational_awareness import EscapeAction, SituationEpisodeDB
|
||||
|
||||
db = SituationEpisodeDB()
|
||||
db._db = MagicMock()
|
||||
db._db.is_connected = False
|
||||
|
||||
action = EscapeAction("back", reason="test")
|
||||
# Must not raise
|
||||
db.learn("test_signature", action, True)
|
||||
|
||||
def test_qdrant_timeout_doesnt_hang_extraction(self):
|
||||
"""If Qdrant queries time out, node extraction must still complete."""
|
||||
import time
|
||||
|
||||
with (
|
||||
patch("GramAddict.core.qdrant_memory.QdrantBase.__init__", return_value=None),
|
||||
patch(
|
||||
"GramAddict.core.qdrant_memory.QdrantBase.is_connected",
|
||||
new_callable=lambda: property(lambda self: False),
|
||||
),
|
||||
):
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
TelepathicEngine._instance = None
|
||||
engine = TelepathicEngine.__new__(TelepathicEngine)
|
||||
engine.ui_memory = MagicMock()
|
||||
engine.ui_memory.is_connected = False
|
||||
engine.ui_memory.query_closest = MagicMock(side_effect=TimeoutError("Qdrant timeout"))
|
||||
engine.positive_memory = MagicMock()
|
||||
engine.positive_memory.is_connected = False
|
||||
engine.positive_memory.recall = MagicMock(side_effect=TimeoutError("Qdrant timeout"))
|
||||
engine._edge_model = None
|
||||
engine._edge_tokenizer = None
|
||||
|
||||
start = time.time()
|
||||
nodes = engine._extract_semantic_nodes(VALID_FEED_XML)
|
||||
elapsed = time.time() - start
|
||||
|
||||
assert elapsed < 5.0
|
||||
assert isinstance(nodes, list)
|
||||
TelepathicEngine._instance = None
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# LLM (Ollama/OpenRouter) Failure Tests
|
||||
# ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.chaos
|
||||
class TestLLMFailure:
|
||||
"""Bot must survive LLM outages."""
|
||||
|
||||
def test_sae_perceive_defaults_to_normal_on_llm_failure(self):
|
||||
"""If LLM classification fails, SAE must default to NORMAL (safe fallback)."""
|
||||
from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType
|
||||
|
||||
SituationalAwarenessEngine.reset()
|
||||
|
||||
device = MagicMock()
|
||||
device.app_id = "com.instagram.android"
|
||||
device.deviceV2 = MagicMock()
|
||||
device.deviceV2.info = {"screenOn": True}
|
||||
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
sae.episodes = MagicMock()
|
||||
sae.episodes.recall = MagicMock(return_value=None)
|
||||
|
||||
with patch("GramAddict.core.qdrant_memory.ScreenMemoryDB") as MockScreenDB:
|
||||
mock_screen_db = MagicMock()
|
||||
mock_screen_db.get_screen_type = MagicMock(return_value=None)
|
||||
MockScreenDB.return_value = mock_screen_db
|
||||
|
||||
with patch("GramAddict.core.llm_provider.query_telepathic_llm", side_effect=ConnectionError("Ollama down")):
|
||||
result = sae.perceive(VALID_FEED_XML)
|
||||
# Must default to NORMAL, not crash
|
||||
assert result == SituationType.NORMAL
|
||||
|
||||
SituationalAwarenessEngine.reset()
|
||||
|
||||
def test_sae_escape_planning_defaults_to_back_on_llm_failure(self):
|
||||
"""If LLM escape planning fails, SAE must default to BACK press."""
|
||||
from GramAddict.core.situational_awareness import SituationalAwarenessEngine, SituationType
|
||||
|
||||
SituationalAwarenessEngine.reset()
|
||||
|
||||
device = MagicMock()
|
||||
device.app_id = "com.instagram.android"
|
||||
device.deviceV2 = MagicMock()
|
||||
device.deviceV2.info = {"screenOn": True}
|
||||
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
|
||||
with patch("GramAddict.core.llm_provider.query_llm", side_effect=ConnectionError("LLM down")):
|
||||
action = sae._plan_escape_via_llm(VALID_FEED_XML, "compressed_sig", SituationType.OBSTACLE_MODAL)
|
||||
assert action.action_type == "back"
|
||||
assert "failed" in action.reason.lower() or "default" in action.reason.lower()
|
||||
|
||||
SituationalAwarenessEngine.reset()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Active Inference Resilience
|
||||
# ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.chaos
|
||||
class TestActiveInferenceChaos:
|
||||
"""Active Inference engine must survive edge cases."""
|
||||
|
||||
def test_evaluate_with_empty_history(self):
|
||||
"""Evaluating without any predictions must return True (no-op)."""
|
||||
from GramAddict.core.active_inference import ActiveInferenceEngine
|
||||
|
||||
ai = ActiveInferenceEngine("test_user")
|
||||
assert ai.evaluate_prediction("<hierarchy/>") is True
|
||||
|
||||
def test_extreme_free_energy_doesnt_overflow(self):
|
||||
"""Repeated errors must not cause float overflow."""
|
||||
from GramAddict.core.active_inference import ActiveInferenceEngine
|
||||
|
||||
ai = ActiveInferenceEngine("test_user")
|
||||
|
||||
for _ in range(1000):
|
||||
ai.predict_state(["nonexistent_element"])
|
||||
ai.evaluate_prediction("<hierarchy><node text='wrong'/></hierarchy>")
|
||||
|
||||
assert ai.free_energy < float("inf")
|
||||
assert ai.free_energy >= 0
|
||||
|
||||
def test_surprise_with_identical_prediction_is_zero(self):
|
||||
"""Perfect prediction (predicted == observed) must produce near-zero surprise."""
|
||||
from GramAddict.core.active_inference import ActiveInferenceEngine
|
||||
|
||||
ai = ActiveInferenceEngine("test_user")
|
||||
ai.free_energy = 0.0
|
||||
|
||||
result = ai.calculate_surprise(1.0, 1.0)
|
||||
assert result < 0.1 # Near-zero free energy
|
||||
|
||||
def test_sleep_modifier_bounds(self):
|
||||
"""Sleep modifier must always be between 1.0 and 5.0."""
|
||||
from GramAddict.core.active_inference import ActiveInferenceEngine
|
||||
|
||||
ai = ActiveInferenceEngine("test_user")
|
||||
|
||||
for policy in ["STABLE", "CAUTIOUS", "DORMANT"]:
|
||||
ai.policy = policy
|
||||
mod = ai.get_sleep_modifier()
|
||||
assert 1.0 <= mod <= 5.0
|
||||
@@ -1,242 +0,0 @@
|
||||
"""
|
||||
Chaos Engineering: XML Corruption Resilience Tests for TelepathicEngine + SAE.
|
||||
|
||||
Verifies that NEITHER engine crashes on any form of corrupted, truncated,
|
||||
adversarial, or garbage XML input. They must degrade gracefully (return None
|
||||
or empty lists) without raising unhandled exceptions.
|
||||
|
||||
These tests are the "crash barrier" of autonomous navigation — ensuring that
|
||||
no matter what Android dumps to us, the bot survives and recovers.
|
||||
"""
|
||||
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.chaos import generate_corrupted_xml
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Telepathic Engine Chaos Tests
|
||||
# ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def telepathic_engine():
|
||||
"""Creates a real TelepathicEngine instance with mocked Qdrant."""
|
||||
with (
|
||||
patch("GramAddict.core.qdrant_memory.QdrantBase.__init__", return_value=None),
|
||||
patch(
|
||||
"GramAddict.core.qdrant_memory.QdrantBase.is_connected", new_callable=lambda: property(lambda self: False)
|
||||
),
|
||||
):
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
TelepathicEngine._instance = None
|
||||
engine = TelepathicEngine.__new__(TelepathicEngine)
|
||||
engine.ui_memory = MagicMock()
|
||||
engine.ui_memory.is_connected = False
|
||||
engine.ui_memory.query_closest = MagicMock(return_value=None)
|
||||
engine.positive_memory = MagicMock()
|
||||
engine.positive_memory.is_connected = False
|
||||
engine.positive_memory.recall = MagicMock(return_value=None)
|
||||
engine._edge_model = None
|
||||
engine._edge_tokenizer = None
|
||||
yield engine
|
||||
TelepathicEngine._instance = None
|
||||
|
||||
|
||||
ALL_CORRUPTION_TYPES = [
|
||||
"EMPTY_STRING",
|
||||
"NONE_VALUE",
|
||||
"TRUNCATED_MID_TAG",
|
||||
"UNICODE_INJECTION",
|
||||
"MASSIVE_DOM_10K_NODES",
|
||||
"ZERO_SIZE_BOUNDS",
|
||||
"NEGATIVE_COORDINATES",
|
||||
"MISSING_CLOSING_TAGS",
|
||||
"RECURSIVE_NESTING_500_DEEP",
|
||||
"NULL_BYTES",
|
||||
"MALFORMED_BOUNDS",
|
||||
"ONLY_WHITESPACE",
|
||||
"HTML_NOT_XML",
|
||||
"BINARY_GARBAGE",
|
||||
"EXTREMELY_LONG_TEXT",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.chaos
|
||||
class TestTelepathicEngineChaos:
|
||||
"""Telepathic Engine must NEVER crash on corrupted XML."""
|
||||
|
||||
@pytest.mark.parametrize("corruption_type", ALL_CORRUPTION_TYPES)
|
||||
def test_extract_semantic_nodes_survives(self, telepathic_engine, corruption_type):
|
||||
"""Engine's XML parser must return empty list on any corruption."""
|
||||
xml = generate_corrupted_xml(corruption_type)
|
||||
|
||||
# Must NOT raise. May return empty list.
|
||||
if xml is None:
|
||||
# None input — directly test defense
|
||||
result = telepathic_engine._extract_semantic_nodes("")
|
||||
else:
|
||||
result = telepathic_engine._extract_semantic_nodes(xml)
|
||||
|
||||
assert isinstance(result, list)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"corruption_type",
|
||||
[
|
||||
"EMPTY_STRING",
|
||||
"NONE_VALUE",
|
||||
"TRUNCATED_MID_TAG",
|
||||
"MISSING_CLOSING_TAGS",
|
||||
"ONLY_WHITESPACE",
|
||||
"HTML_NOT_XML",
|
||||
"BINARY_GARBAGE",
|
||||
],
|
||||
)
|
||||
def test_find_best_node_survives_garbage(self, telepathic_engine, corruption_type):
|
||||
"""find_best_node must return None on garbage XML, never crash."""
|
||||
xml = generate_corrupted_xml(corruption_type)
|
||||
if xml is None:
|
||||
xml = ""
|
||||
|
||||
result = telepathic_engine._find_best_node_inner(xml, "tap like button", min_confidence=0.82)
|
||||
# Must be None or a dict, never an exception
|
||||
assert result is None or isinstance(result, dict)
|
||||
|
||||
def test_unicode_injection_doesnt_corrupt_semantics(self, telepathic_engine):
|
||||
"""Zalgo text in nodes shouldn't crash semantic extraction."""
|
||||
xml = generate_corrupted_xml("UNICODE_INJECTION")
|
||||
nodes = telepathic_engine._extract_semantic_nodes(xml)
|
||||
# Should extract SOME nodes (the XML structure is valid)
|
||||
assert isinstance(nodes, list)
|
||||
# If nodes found, they should have valid coordinates
|
||||
for node in nodes:
|
||||
assert isinstance(node.get("x", 0), int)
|
||||
assert isinstance(node.get("y", 0), int)
|
||||
|
||||
def test_massive_dom_doesnt_hang(self, telepathic_engine):
|
||||
"""10K nodes must be parsed within 5 seconds — no infinite loops."""
|
||||
xml = generate_corrupted_xml("MASSIVE_DOM_10K_NODES")
|
||||
start = time.time()
|
||||
nodes = telepathic_engine._extract_semantic_nodes(xml)
|
||||
elapsed = time.time() - start
|
||||
|
||||
assert elapsed < 5.0, f"Parsing 10K nodes took {elapsed:.2f}s (limit: 5s)"
|
||||
assert isinstance(nodes, list)
|
||||
|
||||
def test_deep_nesting_doesnt_stackoverflow(self, telepathic_engine):
|
||||
"""500 levels of nesting must not cause stack overflow."""
|
||||
xml = generate_corrupted_xml("RECURSIVE_NESTING_500_DEEP")
|
||||
# This would crash Python's default recursion limit (1000) if
|
||||
# we used recursive parsing. ElementTree uses iterative parsing,
|
||||
# so it should survive.
|
||||
nodes = telepathic_engine._extract_semantic_nodes(xml)
|
||||
assert isinstance(nodes, list)
|
||||
|
||||
def test_null_bytes_stripped(self, telepathic_engine):
|
||||
"""Null bytes in text content must not cause parsing failures."""
|
||||
xml = generate_corrupted_xml("NULL_BYTES")
|
||||
nodes = telepathic_engine._extract_semantic_nodes(xml)
|
||||
assert isinstance(nodes, list)
|
||||
# Verify no null bytes leaked into node semantics
|
||||
for node in nodes:
|
||||
assert "\x00" not in node.get("semantic_string", "")
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# SAE (Situational Awareness Engine) Chaos Tests
|
||||
# ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sae_engine():
|
||||
"""Creates a SAE instance with mocked device."""
|
||||
from GramAddict.core.situational_awareness import SituationalAwarenessEngine
|
||||
|
||||
SituationalAwarenessEngine.reset()
|
||||
|
||||
device = MagicMock()
|
||||
device.app_id = "com.instagram.android"
|
||||
device.deviceV2 = MagicMock()
|
||||
device.deviceV2.info = {"screenOn": True}
|
||||
|
||||
engine = SituationalAwarenessEngine(device)
|
||||
|
||||
# Mock the episode DB to avoid Qdrant dependency
|
||||
engine.episodes = MagicMock()
|
||||
engine.episodes.recall = MagicMock(return_value=None)
|
||||
engine.episodes.learn = MagicMock()
|
||||
|
||||
yield engine
|
||||
SituationalAwarenessEngine.reset()
|
||||
|
||||
|
||||
@pytest.mark.chaos
|
||||
class TestSAEChaos:
|
||||
"""SAE perception must be bulletproof against XML corruption."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"corruption_type",
|
||||
[
|
||||
"EMPTY_STRING",
|
||||
"TRUNCATED_MID_TAG",
|
||||
"MISSING_CLOSING_TAGS",
|
||||
"ONLY_WHITESPACE",
|
||||
"HTML_NOT_XML",
|
||||
"BINARY_GARBAGE",
|
||||
],
|
||||
)
|
||||
def test_compress_xml_survives_garbage(self, sae_engine, corruption_type):
|
||||
"""XML compression must never crash, even on garbage."""
|
||||
xml = generate_corrupted_xml(corruption_type)
|
||||
if xml is None:
|
||||
xml = ""
|
||||
|
||||
result = sae_engine._compress_xml(xml)
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0 # Should always return something
|
||||
|
||||
def test_compress_empty_returns_marker(self, sae_engine):
|
||||
"""Empty/None input must return 'EMPTY_SCREEN' sentinel."""
|
||||
assert sae_engine._compress_xml("") == "EMPTY_SCREEN"
|
||||
assert sae_engine._compress_xml(None) == "EMPTY_SCREEN"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"corruption_type",
|
||||
[
|
||||
"EMPTY_STRING",
|
||||
"TRUNCATED_MID_TAG",
|
||||
"BINARY_GARBAGE",
|
||||
"ONLY_WHITESPACE",
|
||||
],
|
||||
)
|
||||
def test_perceive_survives_garbage(self, sae_engine, corruption_type):
|
||||
"""perceive() must return a valid SituationType on any input."""
|
||||
from GramAddict.core.situational_awareness import SituationType
|
||||
|
||||
xml = generate_corrupted_xml(corruption_type)
|
||||
if xml is None:
|
||||
xml = ""
|
||||
|
||||
result = sae_engine.perceive(xml)
|
||||
assert isinstance(result, SituationType)
|
||||
|
||||
def test_compute_situation_hash_is_deterministic(self, sae_engine):
|
||||
"""Same XML must always produce the same hash."""
|
||||
xml = generate_corrupted_xml("UNICODE_INJECTION")
|
||||
compressed = sae_engine._compress_xml(xml)
|
||||
hash1 = sae_engine._compute_situation_hash(compressed)
|
||||
hash2 = sae_engine._compute_situation_hash(compressed)
|
||||
assert hash1 == hash2
|
||||
|
||||
def test_massive_dom_compression_is_bounded(self, sae_engine):
|
||||
"""10K nodes must be compressed to < 3000 chars (the cap)."""
|
||||
xml = generate_corrupted_xml("MASSIVE_DOM_10K_NODES")
|
||||
start = time.time()
|
||||
result = sae_engine._compress_xml(xml)
|
||||
elapsed = time.time() - start
|
||||
|
||||
assert len(result) <= 3000, f"Compressed output is {len(result)} chars (limit: 3000)"
|
||||
assert elapsed < 5.0, f"Compression took {elapsed:.2f}s"
|
||||
@@ -1,183 +1,102 @@
|
||||
import logging
|
||||
import os
|
||||
from unittest.mock import MagicMock
|
||||
"""
|
||||
Root Test Configuration — Global Guards Against Environmental Pollution
|
||||
=======================================================================
|
||||
|
||||
This conftest protects ALL tests from the #1 cause of mass failure:
|
||||
Config() constructor calling argparse.parse_known_args() which reads
|
||||
sys.argv (pytest's arguments) and crashes with SystemExit: 2.
|
||||
|
||||
Every test directory inherits these fixtures automatically.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--live",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="run tests against a live ADB device (disable DeviceFacade mocks)",
|
||||
)
|
||||
|
||||
|
||||
MagicMock.app_id = "com.instagram.android"
|
||||
MagicMock._get_current_app = MagicMock(return_value="com.instagram.android")
|
||||
|
||||
|
||||
class MockArgs:
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
class MockConfigs:
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
|
||||
|
||||
from unittest.mock import MagicMock, create_autospec
|
||||
|
||||
from GramAddict.core.device_facade import DeviceFacade
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
|
||||
def create_mock_device():
|
||||
mock = create_autospec(DeviceFacade, instance=True)
|
||||
mock.app_id = "com.instagram.android"
|
||||
mock.device_id = "test_device"
|
||||
|
||||
mock.info = {"displayWidth": 1080, "displayHeight": 2400}
|
||||
mock.get_info.return_value = {"displayWidth": 1080, "displayHeight": 2400}
|
||||
mock.cm_to_pixels.side_effect = lambda cm: int(cm * 10)
|
||||
mock.shell.return_value = "" # Ensure SendEventInjector detection gets a string
|
||||
import uuid
|
||||
|
||||
mock.dump_hierarchy.side_effect = (
|
||||
lambda: f'<hierarchy><node resource-id="com.instagram.android:id/row_feed_photo_profile_name" bounds="[0,200][1080,260]" text="testuser" /><node resource-id="com.instagram.android:id/row_comment_imageview" bounds="[10,10][20,20]" content-desc="Story" text="following" /><node resource-id="com.instagram.android:id/button_like" bounds="[50,50][60,60]" /><node resource-id="com.instagram.android:id/reel_viewer" /><node sid="{uuid.uuid4()}" /></hierarchy>'
|
||||
)
|
||||
|
||||
return mock
|
||||
|
||||
|
||||
def create_mock_telepathic_engine():
|
||||
mock = create_autospec(TelepathicEngine, instance=True)
|
||||
mock.find_best_node.return_value = {"x": 500, "y": 500, "confidence": 0.9}
|
||||
mock.evaluate_profile_vibe.return_value = {
|
||||
"quality_score": 8,
|
||||
"matches_niche": True,
|
||||
"reason": "Mocked positive vibe",
|
||||
}
|
||||
mock.evaluate_grid_visuals.return_value = {
|
||||
"x": 500,
|
||||
"y": 500,
|
||||
"score": 0.99,
|
||||
"semantic": "Mocked matching grid cell",
|
||||
"source": "vlm_grid",
|
||||
}
|
||||
mock.find_best_node.return_value = {"x": 500, "y": 500, "semantic_string": "dummy node"}
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_logger():
|
||||
return logging.getLogger("test")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device(request):
|
||||
if request.config.getoption("--live"):
|
||||
import os
|
||||
|
||||
import yaml
|
||||
|
||||
from GramAddict.core.device_facade import create_device
|
||||
|
||||
device_id = "emulator-5554"
|
||||
app_id = "com.instagram.android"
|
||||
|
||||
config_path = "test_config.yml"
|
||||
if os.path.exists(config_path):
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f)
|
||||
if config:
|
||||
device_id = config.get("device", device_id)
|
||||
app_id = config.get("app-id", app_id)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Warning: Could not load {config_path}: {e}")
|
||||
|
||||
print(f"🚀 Connecting to live device: {device_id} (App: {app_id})")
|
||||
return create_device(device_id, app_id)
|
||||
return create_mock_device()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_singletons():
|
||||
"""Ensure all core engine singletons are fresh for each test."""
|
||||
from GramAddict.core.behaviors import PluginRegistry
|
||||
from GramAddict.core.goap import GoalExecutor
|
||||
from GramAddict.core.physics.biomechanics import PhysicsBody
|
||||
from GramAddict.core.physics.sendevent_injector import SendEventInjector
|
||||
from GramAddict.core.qdrant_memory import QdrantBase
|
||||
from GramAddict.core.situational_awareness import SituationalAwarenessEngine
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
def _isolate_config_from_argparse(monkeypatch):
|
||||
"""Prevent Config() from reading sys.argv during tests.
|
||||
|
||||
TelepathicEngine.reset()
|
||||
GoalExecutor.reset()
|
||||
SituationalAwarenessEngine.reset()
|
||||
PluginRegistry.reset()
|
||||
PhysicsBody.reset()
|
||||
SendEventInjector.reset()
|
||||
Root cause: Config.__init__ calls self.parse_args() which calls
|
||||
self.parser.parse_known_args(). In pytest, sys.argv contains
|
||||
pytest flags like '--ignore=...' which argparse interprets as
|
||||
Config arguments, causing SystemExit: 2.
|
||||
|
||||
QdrantBase._connection_failed_logged = False
|
||||
|
||||
from GramAddict.core.dojo_engine import DojoEngine
|
||||
|
||||
if hasattr(DojoEngine, "reset"):
|
||||
DojoEngine.reset()
|
||||
else:
|
||||
DojoEngine._instance = None
|
||||
|
||||
# Aggressively wipe on-disk session files to prevent state leakage in tests
|
||||
for f in [
|
||||
"telepathic_memory.json",
|
||||
"telepathic_blacklist.json",
|
||||
"growth_brain_memory.json",
|
||||
"gramaddict_nav_map.json",
|
||||
"l2_channels_cache.json",
|
||||
]:
|
||||
if os.path.exists(f):
|
||||
try:
|
||||
os.remove(f)
|
||||
except Exception:
|
||||
pass
|
||||
yield
|
||||
|
||||
# Post-test cleanup
|
||||
PhysicsBody.reset()
|
||||
SendEventInjector.reset()
|
||||
Fix: Temporarily set sys.argv to a minimal list so argparse
|
||||
doesn't choke on pytest's arguments.
|
||||
"""
|
||||
monkeypatch.setattr(sys, "argv", ["test_runner"])
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def telepathic_mock(monkeypatch, request):
|
||||
if request.config.getoption("--live"):
|
||||
# TelepathicEngine is a singleton, allow it to run natively
|
||||
return None
|
||||
import GramAddict.core.telepathic_engine
|
||||
|
||||
engine = create_mock_telepathic_engine()
|
||||
monkeypatch.setattr(GramAddict.core.telepathic_engine.TelepathicEngine, "get_instance", lambda: engine)
|
||||
return engine
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Pytest Markers Registration
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cognitive_stack():
|
||||
stack = {
|
||||
"dopamine": MagicMock(),
|
||||
"darwin": MagicMock(),
|
||||
"resonance": MagicMock(),
|
||||
"active_inference": MagicMock(),
|
||||
"growth_brain": MagicMock(),
|
||||
"swarm": MagicMock(),
|
||||
"radome": MagicMock(),
|
||||
"nav_graph": MagicMock(),
|
||||
"zero_engine": MagicMock(),
|
||||
"crm": MagicMock(),
|
||||
"telepathic": create_mock_telepathic_engine(),
|
||||
}
|
||||
stack["radome"].sanitize_xml.side_effect = lambda x: x
|
||||
return stack
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers", "live_llm: requires a running local LLM (Ollama)")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# PERMANENT MOCK BAN — Zero-Tolerance Enforcement
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
_BANNED_PATTERNS = (
|
||||
"from unittest.mock",
|
||||
"from unittest import mock",
|
||||
"import unittest.mock",
|
||||
"from mock import",
|
||||
"import mock",
|
||||
"MagicMock(",
|
||||
"MagicMock)",
|
||||
"@patch(",
|
||||
"@patch\n",
|
||||
"patch.object(",
|
||||
)
|
||||
|
||||
|
||||
def pytest_collect_file(parent, file_path):
|
||||
"""Scan every collected .py test file for banned mock imports.
|
||||
|
||||
This runs at COLLECTION TIME — before any test executes.
|
||||
If a banned pattern is found, the file is still collected but
|
||||
every test inside it will be marked as an error via
|
||||
pytest_collection_modifyitems below.
|
||||
"""
|
||||
if file_path.suffix == ".py" and file_path.name.startswith("test_"):
|
||||
try:
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
for pattern in _BANNED_PATTERNS:
|
||||
if pattern in content:
|
||||
# Store the violation on the config for later reporting
|
||||
if not hasattr(parent.config, "_mock_violations"):
|
||||
parent.config._mock_violations = {}
|
||||
parent.config._mock_violations[str(file_path)] = pattern
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
return None # Let pytest's default collector handle the file
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
"""Fail every test from a file that contains banned mock patterns."""
|
||||
violations = getattr(config, "_mock_violations", {})
|
||||
if not violations:
|
||||
return
|
||||
|
||||
for item in items:
|
||||
test_file = str(item.fspath)
|
||||
if test_file in violations:
|
||||
pattern = violations[test_file]
|
||||
item.add_marker(
|
||||
pytest.mark.xfail(
|
||||
reason=(
|
||||
f"🚨 MOCK BAN VIOLATION: File contains '{pattern}'. "
|
||||
f"unittest.mock is permanently banned. "
|
||||
f"Use monkeypatch + real fixtures instead."
|
||||
),
|
||||
strict=True,
|
||||
raises=Exception,
|
||||
)
|
||||
)
|
||||
|
||||
56
tests/core/test_action_memory_fast_path.py
Normal file
56
tests/core/test_action_memory_fast_path.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from GramAddict.core.perception.action_memory import ActionMemory
|
||||
from GramAddict.core.perception.screen_identity import ScreenIdentity, ScreenType
|
||||
|
||||
|
||||
def test_screen_identity_detects_other_profile_with_missing_header_container():
|
||||
"""
|
||||
RED: ScreenIdentity used to misclassify OTHER_PROFILE as UNKNOWN or POST_DETAIL
|
||||
if `profile_header_container` was missing, even though `row_profile_header_imageview`
|
||||
was present.
|
||||
GREEN: We added row_profile_header_imageview and profile_tabs_container.
|
||||
"""
|
||||
identity = ScreenIdentity(bot_username="my_bot")
|
||||
|
||||
# Simulate a profile screen that is missing the main container but has the imageview
|
||||
xml_dump = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy>
|
||||
<node package="com.instagram.android" resource-id="com.instagram.android:id/row_profile_header_imageview" bounds="[0,0][100,100]" clickable="true"/>
|
||||
<node package="com.instagram.android" resource-id="com.instagram.android:id/profile_tabs_container" bounds="[0,200][1080,300]"/>
|
||||
<node package="com.instagram.android" resource-id="com.instagram.android:id/action_bar_title" text="justkay"/>
|
||||
</hierarchy>
|
||||
"""
|
||||
|
||||
result = identity.identify(xml_dump)
|
||||
assert result["screen_type"] == ScreenType.OTHER_PROFILE
|
||||
|
||||
|
||||
def test_action_memory_verifies_profile_navigation_in_o1_without_vlm(monkeypatch):
|
||||
"""
|
||||
RED: ActionMemory.verify_success used to fallback to VLM because navigating to a profile
|
||||
caused a huge delta, but there was no explicit fast-path for 'profile', causing it
|
||||
to hit `confidence < 0.95` and invoke `evaluator._query_vlm`.
|
||||
GREEN: It now structurally verifies `profile_header_container` instantly.
|
||||
"""
|
||||
memory = ActionMemory()
|
||||
|
||||
class DummyDevice:
|
||||
def get_screenshot_b64(self):
|
||||
raise Exception("VLM SHOULD NOT BE CALLED!")
|
||||
|
||||
device = DummyDevice()
|
||||
|
||||
intent = "tap post username"
|
||||
pre_xml = "<hierarchy><node/></hierarchy>"
|
||||
# Post XML contains profile_header_container!
|
||||
post_xml = "<hierarchy><node resource-id='com.instagram.android:id/profile_header_container'/></hierarchy>"
|
||||
|
||||
# If the fast-path works, it will return True instantly and NOT call get_screenshot_b64
|
||||
success = memory.verify_success(
|
||||
intent=intent,
|
||||
pre_click_xml=pre_xml,
|
||||
post_click_xml=post_xml,
|
||||
device=device,
|
||||
confidence=0.0, # low confidence triggers VLM fallback if fast-path is missing
|
||||
)
|
||||
|
||||
assert success is True
|
||||
57
tests/core/test_config.py
Normal file
57
tests/core/test_config.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import io
|
||||
|
||||
from GramAddict.core.config import Config
|
||||
|
||||
|
||||
def test_config_help_format_no_crash():
|
||||
"""
|
||||
Test that calling print_help() on the config parser does not crash.
|
||||
This prevents bugs where a '%' symbol in help strings causes argparse
|
||||
to fail with "ValueError: unsupported format character".
|
||||
"""
|
||||
config = Config()
|
||||
|
||||
# We redirect stdout/stderr so we don't spam the console, but what matters
|
||||
# is that print_help() executes without throwing a ValueError.
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
f = io.StringIO()
|
||||
with redirect_stdout(f):
|
||||
config.parser.print_help()
|
||||
|
||||
output = f.getvalue()
|
||||
assert len(output) > 0, "print_help() should output help text."
|
||||
assert "Wipe all learned navigation" in output, "Expected blank-start help string in output."
|
||||
|
||||
|
||||
def test_parse_args_no_exit_when_config_loaded(monkeypatch):
|
||||
"""
|
||||
Test that if no CLI arguments are provided (sys.argv == ['run.py']),
|
||||
but a config file is loaded, parse_args() should NOT print help and exit.
|
||||
"""
|
||||
import sys
|
||||
|
||||
# Simulate running without arguments
|
||||
monkeypatch.setattr(sys, "argv", ["run.py"])
|
||||
|
||||
config = Config()
|
||||
|
||||
# Simulate that we successfully loaded a config dictionary (e.g. from config.yml)
|
||||
config.config = {"some_setting": "value"}
|
||||
|
||||
help_called = []
|
||||
def mock_print_help(*args, **kwargs):
|
||||
help_called.append(True)
|
||||
|
||||
monkeypatch.setattr(config.parser, "print_help", mock_print_help)
|
||||
|
||||
# If parse_args() calls exit(0), it will raise SystemExit
|
||||
try:
|
||||
config.parse_args()
|
||||
# If we get here, no exit() was called.
|
||||
# Also, print_help should not have been called.
|
||||
assert not help_called, "print_help should not have been called"
|
||||
except SystemExit:
|
||||
import pytest
|
||||
|
||||
pytest.fail("parse_args() should not exit when a config is loaded.")
|
||||
55
tests/core/test_embeddings_truncation.py
Normal file
55
tests/core/test_embeddings_truncation.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from GramAddict.core.qdrant_memory import QdrantBase
|
||||
|
||||
|
||||
@pytest.mark.live_llm
|
||||
def test_embedding_context_length_limit():
|
||||
"""
|
||||
TDD Proof: Ensure _get_embedding truncates input sufficiently to avoid
|
||||
'input length exceeds the context length' (500) from Ollama.
|
||||
"""
|
||||
|
||||
class DummyMemory(QdrantBase):
|
||||
def __init__(self):
|
||||
# Bypass init connection checks for this test
|
||||
self._vector_size = 768
|
||||
pass
|
||||
|
||||
memory = DummyMemory()
|
||||
|
||||
# Mock config to use standard local embedding endpoint
|
||||
class FakeArgs:
|
||||
ai_embedding_model = "nomic-embed-text"
|
||||
ai_embedding_url = "http://localhost:11434/api/embeddings"
|
||||
|
||||
memory._cached_args = FakeArgs()
|
||||
|
||||
# Generate an extremely long string (e.g. 15,000 chars) that would crash Ollama
|
||||
huge_text = "lorem ipsum " * 2000
|
||||
|
||||
# This should NOT raise a requests.exceptions.HTTPError (500)
|
||||
try:
|
||||
vector = memory._get_embedding(huge_text)
|
||||
assert vector is not None, "Vector should not be None for successful API calls"
|
||||
assert len(vector) > 0, "Vector should have dimensions"
|
||||
except requests.exceptions.HTTPError as e:
|
||||
pytest.fail(f"Embedding API crashed with HTTP error (likely context limit): {e}")
|
||||
|
||||
|
||||
@pytest.mark.live_llm
|
||||
def test_structural_signature_truncation():
|
||||
"""
|
||||
Ensure UIMemoryDB correctly truncates structural signatures to 2000 chars.
|
||||
"""
|
||||
from GramAddict.core.qdrant_memory import UIMemoryDB
|
||||
|
||||
db = UIMemoryDB()
|
||||
|
||||
# Huge XML-like string
|
||||
huge_xml = "<node " + ('text="junk" ' * 1000) + "/>"
|
||||
|
||||
sig = db._create_structural_signature(huge_xml)
|
||||
assert len(sig) <= 2000, f"Signature length {len(sig)} exceeds 2000"
|
||||
assert "text=" not in sig, "Structural signature should have removed 'text' attributes"
|
||||
51
tests/core/test_intent_resolver_multilingual.py
Normal file
51
tests/core/test_intent_resolver_multilingual.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from GramAddict.core.perception.intent_resolver import IntentResolver
|
||||
from GramAddict.core.perception.spatial_parser import SpatialNode
|
||||
|
||||
|
||||
def test_semantic_guard_allows_multilingual_follow_button():
|
||||
"""
|
||||
RED: The IntentResolver's Semantic Guard used to hard-filter for EXACT quotes.
|
||||
If the plugin requested "tap 'Follow' button", but the UI was in German ("Abonnieren"),
|
||||
the Semantic Guard would block it, causing the bot to never follow anyone.
|
||||
GREEN: We added multilingual equivalents to the Semantic Guard logic.
|
||||
"""
|
||||
resolver = IntentResolver()
|
||||
|
||||
intent = "tap 'Follow' button"
|
||||
|
||||
# Create a node that represents a German follow button
|
||||
german_node = SpatialNode(
|
||||
resource_id="com.instagram.android:id/profile_header_follow_button",
|
||||
content_desc="Abonnieren",
|
||||
text="Abonnieren",
|
||||
bounds=(0, 0, 100, 100),
|
||||
)
|
||||
|
||||
# Run the resolver without a device (forces semantic/structural resolution, bypasses VLM)
|
||||
result = resolver.resolve(intent, candidates=[german_node], device=None, screen_height=2000)
|
||||
|
||||
assert result is not None
|
||||
assert result.text == "Abonnieren"
|
||||
|
||||
|
||||
def test_semantic_guard_allows_multilingual_following_button():
|
||||
"""
|
||||
Ensures that "tap 'Following' button" resolves correctly for German UIs
|
||||
("Abonniert", "Gefolgt", "Angefragt").
|
||||
"""
|
||||
resolver = IntentResolver()
|
||||
|
||||
intent = "tap 'Following' button"
|
||||
|
||||
# Create a node that represents a German "Following" button
|
||||
german_node = SpatialNode(
|
||||
resource_id="com.instagram.android:id/profile_header_follow_button",
|
||||
content_desc="Abonniert",
|
||||
text="Abonniert",
|
||||
bounds=(0, 0, 100, 100),
|
||||
)
|
||||
|
||||
result = resolver.resolve(intent, candidates=[german_node], device=None, screen_height=2000)
|
||||
|
||||
assert result is not None
|
||||
assert result.text == "Abonniert"
|
||||
147
tests/core/test_zero_maintenance_strings.py
Normal file
147
tests/core/test_zero_maintenance_strings.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
TDD Tests: Zero-Maintenance String Compliance
|
||||
|
||||
These tests enforce that no hardcoded German/localized strings
|
||||
exist in navigation-critical code paths. The bot must rely
|
||||
exclusively on structural resource_id patterns, never on
|
||||
localized UI text that changes with device language.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 1. TOGGLE_INTENT_MARKERS must be language-agnostic
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestToggleIntentMarkersAreLanguageAgnostic:
|
||||
"""Ensure TOGGLE_INTENT_MARKERS contains zero localized strings."""
|
||||
|
||||
GERMAN_STRINGS = [
|
||||
"gefällt",
|
||||
"gefolgt",
|
||||
"abonnieren",
|
||||
"speichern",
|
||||
"gespeichert",
|
||||
"antworten",
|
||||
"kommentar",
|
||||
"beitrag",
|
||||
]
|
||||
|
||||
def test_no_german_strings_in_toggle_markers(self):
|
||||
from GramAddict.core.perception.action_memory import TOGGLE_INTENT_MARKERS
|
||||
|
||||
for intent_key, markers in TOGGLE_INTENT_MARKERS.items():
|
||||
for marker in markers:
|
||||
assert marker.lower() not in [
|
||||
g.lower() for g in self.GERMAN_STRINGS
|
||||
], f"TOGGLE_INTENT_MARKERS['{intent_key}'] contains German string '{marker}'!"
|
||||
|
||||
def test_markers_only_contain_english_or_resource_id_patterns(self):
|
||||
"""All markers must be English words or resource_id fragments."""
|
||||
from GramAddict.core.perception.action_memory import TOGGLE_INTENT_MARKERS
|
||||
|
||||
allowed_pattern = re.compile(r"^[a-z_]+$")
|
||||
for intent_key, markers in TOGGLE_INTENT_MARKERS.items():
|
||||
for marker in markers:
|
||||
assert allowed_pattern.match(
|
||||
marker
|
||||
), f"Marker '{marker}' in '{intent_key}' contains non-ASCII or non-ID characters!"
|
||||
|
||||
def test_intent_match_rejects_reel_message_composer(self):
|
||||
"""The semantic guard must reject reel message composer for 'like' intent."""
|
||||
from GramAddict.core.perception.action_memory import _intent_matches_node
|
||||
|
||||
# This was the exact production failure: VLM picked the message composer
|
||||
semantic = "text: 'Send message', desc: '', id: 'com.instagram.android:id/reel_viewer_message_composer_text'"
|
||||
assert _intent_matches_node("tap like button", semantic) is False
|
||||
|
||||
def test_intent_match_accepts_real_like_button(self):
|
||||
"""The semantic guard must accept a real like button by resource_id."""
|
||||
from GramAddict.core.perception.action_memory import _intent_matches_node
|
||||
|
||||
semantic = "text: '', desc: 'Like', id: 'com.instagram.android:id/row_feed_button_like'"
|
||||
assert _intent_matches_node("tap like button", semantic) is True
|
||||
|
||||
def test_intent_match_accepts_like_button_by_id_only(self):
|
||||
"""Even without text/desc, resource_id containing 'like' is enough."""
|
||||
from GramAddict.core.perception.action_memory import _intent_matches_node
|
||||
|
||||
semantic = "text: '', desc: '', id: 'com.instagram.android:id/row_feed_button_like'"
|
||||
assert _intent_matches_node("tap like button", semantic) is True
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 2. TelepathicEngine Following Guard must be structural
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestTelepathicEngineFollowingGuardIsStructural:
|
||||
"""The 'already followed' guard must not rely on German strings."""
|
||||
|
||||
def test_no_german_in_following_guard_source(self):
|
||||
"""Scan telepathic_engine.py for any German follow-state strings."""
|
||||
import inspect
|
||||
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
|
||||
source = inspect.getsource(TelepathicEngine)
|
||||
german_terms = ["gefolgt", "angefragt", "abonniert", "abonnieren"]
|
||||
for term in german_terms:
|
||||
assert (
|
||||
term not in source
|
||||
), f"TelepathicEngine source contains German string '{term}'!"
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 3. DarwinEngine comment detection must be structural
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestDarwinEngineCommentDetectionIsStructural:
|
||||
"""The _has_comments heuristic must not rely on German strings."""
|
||||
|
||||
def test_no_german_in_has_comments_source(self):
|
||||
import inspect
|
||||
|
||||
from GramAddict.core.darwin_engine import DarwinEngine
|
||||
|
||||
source = inspect.getsource(DarwinEngine._has_comments)
|
||||
german_terms = ["kommentar", "ansehen"]
|
||||
for term in german_terms:
|
||||
assert (
|
||||
term not in source
|
||||
), f"DarwinEngine._has_comments contains German string '{term}'!"
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 4. ResonanceEngine comment filtering must be structural
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestResonanceEngineCommentFilteringIsStructural:
|
||||
"""Comment extraction blocked_exact list must not contain German strings."""
|
||||
|
||||
def test_no_german_in_resonance_source(self):
|
||||
import inspect
|
||||
|
||||
from GramAddict.core.resonance_engine import ResonanceEngine
|
||||
|
||||
source = inspect.getsource(ResonanceEngine.extract_and_learn_comments)
|
||||
german_terms = [
|
||||
"antworten",
|
||||
"gefällt mir",
|
||||
"antworten ansehen",
|
||||
"übersetzung anzeigen",
|
||||
"antworten verbergen",
|
||||
"alle kommentare ansehen",
|
||||
"absenden",
|
||||
"gehe zu",
|
||||
"tippe auf",
|
||||
"aktionen für diesen beitrag",
|
||||
]
|
||||
for term in german_terms:
|
||||
assert (
|
||||
term not in source
|
||||
), f"ResonanceEngine.extract_and_learn_comments contains German '{term}'!"
|
||||
@@ -13,17 +13,25 @@ Design Principles:
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from GramAddict.core import utils
|
||||
from GramAddict.core.session_state import SessionState
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# CLI Options
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--live", action="store_true", default=False, help="Run live tests")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Constants
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
E2E_TEST_TIMEOUT_SECONDS = 60
|
||||
E2E_TEST_TIMEOUT_SECONDS = 300
|
||||
FIXTURES_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fixtures")
|
||||
E2E_FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures")
|
||||
|
||||
@@ -53,40 +61,6 @@ def load_fixture_xml(filename: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# VirtualClock — Per-Test Instance, Never Module-Level
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class VirtualClock:
|
||||
"""Deterministic time simulation for E2E tests.
|
||||
|
||||
Each test gets its own isolated instance via the `virtual_clock` fixture.
|
||||
This eliminates state bleed between tests.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.time = 0.0
|
||||
self.animation_target_time = 0.0
|
||||
|
||||
def sleep(self, seconds):
|
||||
if hasattr(seconds, "__iter__"):
|
||||
return
|
||||
self.time += float(seconds)
|
||||
|
||||
def is_animating(self) -> bool:
|
||||
return self.time < self.animation_target_time
|
||||
|
||||
def start_animation(self, duration: float = 1.5):
|
||||
self.animation_target_time = self.time + duration
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def virtual_clock():
|
||||
"""Provides a fresh, isolated VirtualClock per test."""
|
||||
return VirtualClock()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Global Test Timeout — Prevents Infinite Hangs
|
||||
# ═══════════════════════════════════════════════════════
|
||||
@@ -107,7 +81,7 @@ def e2e_test_timeout():
|
||||
old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
|
||||
signal.alarm(E2E_TEST_TIMEOUT_SECONDS)
|
||||
yield
|
||||
signal.alarm(0)
|
||||
signal.alarm(E2E_TEST_TIMEOUT_SECONDS)
|
||||
signal.signal(signal.SIGALRM, old_handler)
|
||||
|
||||
|
||||
@@ -156,44 +130,32 @@ def iteration_guard():
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Qdrant Mock — Clean, Non-Poisoning
|
||||
# Real Qdrant DB (Isolated Collection)
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def e2e_qdrant_mock(monkeypatch):
|
||||
"""Mock Qdrant without sys.modules poisoning.
|
||||
|
||||
Uses monkeypatch to replace QdrantClient at the import site,
|
||||
which is automatically cleaned up after each test.
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def session_env(tmp_path_factory):
|
||||
"""
|
||||
mock_qdrant = MagicMock()
|
||||
Forces production parity by using real logic with local-only backends.
|
||||
"""
|
||||
# 1. Honest Qdrant: Use real QdrantClient with in-memory storage
|
||||
os.environ["QDRANT_URL"] = ":memory:"
|
||||
|
||||
mock_collection = MagicMock()
|
||||
mock_collection.config.params.vectors.size = 768
|
||||
mock_qdrant.get_collection.return_value = mock_collection
|
||||
# 2. Honest Persistence: Use a temporary directory for accounts
|
||||
accounts_dir = tmp_path_factory.mktemp("accounts")
|
||||
os.environ["GRAMADDICT_ACCOUNTS_DIR"] = str(accounts_dir)
|
||||
os.makedirs(os.path.join(str(accounts_dir), "testuser"), exist_ok=True)
|
||||
|
||||
# Patch at the import site rather than poisoning sys.modules
|
||||
try:
|
||||
import qdrant_client
|
||||
|
||||
monkeypatch.setattr(qdrant_client, "QdrantClient", MagicMock(return_value=mock_qdrant))
|
||||
except ImportError:
|
||||
pass
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def reset_physics_singletons():
|
||||
"""Resets the PhysicsBody and SendEventInjector singletons to prevent state pollution across tests."""
|
||||
from GramAddict.core.physics.biomechanics import PhysicsBody
|
||||
from GramAddict.core.physics.sendevent_injector import SendEventInjector
|
||||
|
||||
# Also patch at the usage site in our code
|
||||
try:
|
||||
import GramAddict.core.qdrant_memory
|
||||
|
||||
monkeypatch.setattr(
|
||||
GramAddict.core.qdrant_memory,
|
||||
"QdrantClient",
|
||||
MagicMock(return_value=mock_qdrant),
|
||||
)
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
return mock_qdrant
|
||||
PhysicsBody._instance = None
|
||||
SendEventInjector.reset()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
@@ -202,144 +164,283 @@ def e2e_qdrant_mock(monkeypatch):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def e2e_device_dump_injector(request):
|
||||
"""Provides a factory to mock device.dump_hierarchy using real XML files."""
|
||||
if request.config.getoption("--live"):
|
||||
return lambda *args, **kwargs: None
|
||||
def make_real_device_with_xml(monkeypatch):
|
||||
"""Provides a factory to create a REAL DeviceFacade but mocked uiautomator2."""
|
||||
|
||||
def _inject_dump(device_mock, xml_filename):
|
||||
real_xml = load_fixture_xml(xml_filename)
|
||||
device_mock.dump_hierarchy.return_value = real_xml
|
||||
return real_xml
|
||||
def _create(xml_content):
|
||||
import GramAddict.core.device_facade as device_facade
|
||||
from GramAddict.core.device_facade import DeviceFacade
|
||||
|
||||
return _inject_dump
|
||||
class MockU2Watcher:
|
||||
def when(self, xpath=None, **kwargs):
|
||||
return self
|
||||
|
||||
def click(self):
|
||||
return self
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
class MockTouch:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
|
||||
def down(self, x, y):
|
||||
self.parent.interaction_log.append({"action": "click", "coords": (x, y)})
|
||||
|
||||
def up(self, x, y):
|
||||
pass
|
||||
|
||||
class MockU2Device:
|
||||
def __init__(self, xml):
|
||||
self.xml = xml
|
||||
self.info = {"sdkInt": 30, "displaySizeDpX": 400, "displayWidth": 1080, "screenOn": True}
|
||||
self.settings = {}
|
||||
self.interaction_log = []
|
||||
self.touch = MockTouch(self)
|
||||
|
||||
def dump_hierarchy(self, compressed=False):
|
||||
if isinstance(self.xml, list):
|
||||
if len(self.xml) > 1:
|
||||
return self.xml.pop(0)
|
||||
elif len(self.xml) == 1:
|
||||
return self.xml[0]
|
||||
return ""
|
||||
return self.xml
|
||||
|
||||
def screenshot(self):
|
||||
from PIL import Image
|
||||
|
||||
return Image.new("RGB", (1, 1), color="black")
|
||||
|
||||
def app_current(self):
|
||||
return {"package": "com.instagram.android"}
|
||||
|
||||
def _validate_hitbox(self, x, y):
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
try:
|
||||
current_xml = self.xml[0] if isinstance(self.xml, list) and len(self.xml) > 0 else self.xml
|
||||
if not current_xml:
|
||||
return
|
||||
root = ET.fromstring(current_xml)
|
||||
for node in root.iter():
|
||||
bounds_str = node.attrib.get("bounds", "")
|
||||
is_actionable = (
|
||||
node.attrib.get("clickable", "false") == "true"
|
||||
or node.attrib.get("long-clickable", "false") == "true"
|
||||
or node.attrib.get("scrollable", "false") == "true"
|
||||
or node.attrib.get("focusable", "false") == "true"
|
||||
)
|
||||
if bounds_str and is_actionable:
|
||||
match = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds_str)
|
||||
if match:
|
||||
left, top, right, bottom = map(int, match.groups())
|
||||
if left <= x <= right and top <= y <= bottom:
|
||||
return
|
||||
raise AssertionError(
|
||||
f"🛑 ZERO-TRUST VIOLATION: Bot clicked ({x}, {y}) but there is NO actionable UI element at these coordinates! "
|
||||
f"The VLM hallucinated or miscalculated bounds."
|
||||
)
|
||||
except ET.ParseError:
|
||||
pass
|
||||
|
||||
def shell(self, cmd):
|
||||
if isinstance(cmd, str) and cmd.startswith("input tap"):
|
||||
parts = cmd.split()
|
||||
try:
|
||||
x, y = int(parts[-2]), int(parts[-1])
|
||||
self._validate_hitbox(x, y)
|
||||
self.interaction_log.append({"action": "click", "coords": (x, y)})
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
elif isinstance(cmd, str) and cmd.startswith("input swipe"):
|
||||
pass # We could log it if needed
|
||||
|
||||
def press(self, key):
|
||||
self.interaction_log.append({"action": "press", "key": key})
|
||||
|
||||
def swipe(self, sx, sy, ex, ey, **kwargs):
|
||||
self.interaction_log.append({"action": "swipe", "start": (sx, sy), "end": (ex, ey)})
|
||||
|
||||
def click(self, x, y):
|
||||
self._validate_hitbox(x, y)
|
||||
self.interaction_log.append({"action": "click", "coords": (x, y)})
|
||||
|
||||
def watcher(self, name):
|
||||
return MockU2Watcher()
|
||||
|
||||
def app_start(self, package_name, use_monkey=False):
|
||||
pass
|
||||
|
||||
def mock_connect(*args, **kwargs):
|
||||
return MockU2Device(xml_content)
|
||||
|
||||
monkeypatch.setattr(device_facade.u2, "connect", mock_connect)
|
||||
|
||||
# Now we instantiate the REAL DeviceFacade!
|
||||
device = DeviceFacade("test_device", "com.instagram.android", None)
|
||||
|
||||
# Expose legacy emulator properties mapping directly to the underlying MockU2Device interaction log
|
||||
# so that existing test assertions keep working seamlessly.
|
||||
type(device).interaction_log = property(lambda self: self.deviceV2.interaction_log)
|
||||
type(device).pressed_keys = property(
|
||||
lambda self: [log["key"] for log in self.deviceV2.interaction_log if log["action"] == "press"]
|
||||
)
|
||||
type(device).clicks = property(
|
||||
lambda self: [log["coords"] for log in self.deviceV2.interaction_log if log["action"] == "click"]
|
||||
)
|
||||
type(device).swipes = property(
|
||||
lambda self: [log["start"] for log in self.deviceV2.interaction_log if log["action"] == "swipe"]
|
||||
)
|
||||
|
||||
return device
|
||||
|
||||
return _create
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dynamic_e2e_dump_injector(monkeypatch, request, virtual_clock):
|
||||
"""State-Machine Injector: Replaces dump_hierarchy dynamically on transitions.
|
||||
def make_real_device_with_image(monkeypatch):
|
||||
"""Provides a factory to create a REAL DeviceFacade but mocked uiautomator2 returning a real image."""
|
||||
|
||||
Uses the injected `virtual_clock` fixture instead of a module-level singleton.
|
||||
Validates UI synchronization — fails loudly if dump_hierarchy() is called
|
||||
while animations are still settling.
|
||||
"""
|
||||
if request.config.getoption("--live"):
|
||||
return lambda *args, **kwargs: None
|
||||
def _create(img_path, xml_content=None):
|
||||
from PIL import Image
|
||||
|
||||
def _inject(device_mock, state_map, initial_xml):
|
||||
from GramAddict.core.q_nav_graph import QNavGraph
|
||||
import GramAddict.core.device_facade as device_facade
|
||||
from GramAddict.core.device_facade import DeviceFacade
|
||||
|
||||
device_mock._xml_history = [load_fixture_xml(initial_xml)]
|
||||
device_mock._current_active_xml = device_mock._xml_history[-1]
|
||||
class MockU2Watcher:
|
||||
def when(self, xpath=None, **kwargs):
|
||||
return self
|
||||
|
||||
import uuid
|
||||
def click(self):
|
||||
return self
|
||||
|
||||
def _dump_hierarchy_hook():
|
||||
if virtual_clock.is_animating():
|
||||
pytest.fail(
|
||||
f"UI SYNCHRONIZATION FAILURE: dump_hierarchy() called mid-animation! "
|
||||
f"Virtual Clock at {virtual_clock.time:.1f}s, "
|
||||
f"UI needs until {virtual_clock.animation_target_time:.1f}s to settle. "
|
||||
f"Add a time.sleep() guard before interacting with the UI after a click.",
|
||||
pytrace=False,
|
||||
)
|
||||
xml = device_mock._current_active_xml
|
||||
if xml and "</hierarchy>" in xml:
|
||||
xml = xml.replace(
|
||||
"</hierarchy>",
|
||||
f'<node sid="{uuid.uuid4()}" /></hierarchy>',
|
||||
)
|
||||
return xml
|
||||
|
||||
device_mock.dump_hierarchy.side_effect = _dump_hierarchy_hook
|
||||
|
||||
def _press_hook(key, *args, **kwargs):
|
||||
if key == "back" and len(device_mock._xml_history) > 1:
|
||||
device_mock._xml_history.pop()
|
||||
device_mock._current_active_xml = device_mock._xml_history[-1]
|
||||
virtual_clock.start_animation()
|
||||
|
||||
device_mock.press.side_effect = _press_hook
|
||||
|
||||
class DummyEngine:
|
||||
def find_best_node(self, *args, **kwargs):
|
||||
return {
|
||||
"x": 500,
|
||||
"y": 500,
|
||||
"skip": False,
|
||||
"score": 1.0,
|
||||
"source": "e2e_mock",
|
||||
}
|
||||
|
||||
def verify_success(self, *args, **kwargs):
|
||||
return True
|
||||
|
||||
def confirm_click(self, *args, **kwargs):
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def reject_click(self, *args, **kwargs):
|
||||
class MockTouch:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
|
||||
def down(self, x, y):
|
||||
self.parent.interaction_log.append({"action": "click", "coords": (x, y)})
|
||||
|
||||
def up(self, x, y):
|
||||
pass
|
||||
|
||||
original_execute = QNavGraph._execute_transition
|
||||
from GramAddict.core.goap import GoalExecutor
|
||||
class MockU2Device:
|
||||
def __init__(self, img, xml):
|
||||
self.img = img
|
||||
self.xml = xml
|
||||
self.info = {"sdkInt": 30, "displaySizeDpX": 400, "displayWidth": 1080, "screenOn": True}
|
||||
self.settings = {}
|
||||
self.interaction_log = []
|
||||
self.touch = MockTouch(self)
|
||||
|
||||
original_goap_execute = GoalExecutor._execute_action
|
||||
def dump_hierarchy(self, compressed=False):
|
||||
if self.xml:
|
||||
if isinstance(self.xml, list):
|
||||
if len(self.xml) > 1:
|
||||
return self.xml.pop(0)
|
||||
elif len(self.xml) == 1:
|
||||
return self.xml[0]
|
||||
return ""
|
||||
return self.xml
|
||||
return ""
|
||||
|
||||
def _mock_execute_transition(nav_self, action, zero_engine=None, max_retries=2):
|
||||
if action == "tap_post_username":
|
||||
return True
|
||||
def screenshot(self):
|
||||
if isinstance(self.img, list):
|
||||
res = self.img.pop(0) if self.img else None
|
||||
if res is None:
|
||||
return Image.new("RGB", (1, 1), color="black")
|
||||
return Image.open(res) if isinstance(res, str) else res
|
||||
if self.img is None:
|
||||
return Image.new("RGB", (1, 1), color="black")
|
||||
return Image.open(self.img) if isinstance(self.img, str) else self.img
|
||||
|
||||
original_click = nav_self.device.click
|
||||
def app_current(self):
|
||||
return {"package": "com.instagram.android"}
|
||||
|
||||
def _click_hook(obj=None, *args, **kwargs):
|
||||
original_click(obj, *args, **kwargs)
|
||||
if action in state_map:
|
||||
new_xml = load_fixture_xml(state_map[action])
|
||||
device_mock._xml_history.append(new_xml)
|
||||
device_mock._current_active_xml = new_xml
|
||||
virtual_clock.start_animation()
|
||||
def _validate_hitbox(self, x, y):
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
nav_self.device.click = _click_hook
|
||||
try:
|
||||
current_xml = self.xml[0] if isinstance(self.xml, list) and len(self.xml) > 0 else self.xml
|
||||
if not current_xml:
|
||||
return
|
||||
root = ET.fromstring(current_xml)
|
||||
for node in root.iter():
|
||||
bounds_str = node.attrib.get("bounds", "")
|
||||
is_actionable = (
|
||||
node.attrib.get("clickable", "false") == "true"
|
||||
or node.attrib.get("long-clickable", "false") == "true"
|
||||
or node.attrib.get("scrollable", "false") == "true"
|
||||
or node.attrib.get("focusable", "false") == "true"
|
||||
)
|
||||
if bounds_str and is_actionable:
|
||||
match = re.match(r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", bounds_str)
|
||||
if match:
|
||||
left, top, right, bottom = map(int, match.groups())
|
||||
if left <= x <= right and top <= y <= bottom:
|
||||
return
|
||||
raise AssertionError(
|
||||
f"🛑 ZERO-TRUST VIOLATION: Bot clicked ({x}, {y}) but there is NO actionable UI element at these coordinates! "
|
||||
f"The VLM hallucinated or miscalculated bounds."
|
||||
)
|
||||
except ET.ParseError:
|
||||
pass
|
||||
|
||||
try:
|
||||
success = original_execute(
|
||||
nav_self,
|
||||
action,
|
||||
mock_semantic_engine=DummyEngine(),
|
||||
max_retries=max_retries,
|
||||
)
|
||||
return success
|
||||
finally:
|
||||
nav_self.device.click = original_click
|
||||
def shell(self, cmd):
|
||||
if isinstance(cmd, str) and cmd.startswith("input tap"):
|
||||
parts = cmd.split()
|
||||
try:
|
||||
x, y = int(parts[-2]), int(parts[-1])
|
||||
self._validate_hitbox(x, y)
|
||||
self.interaction_log.append({"action": "click", "coords": (x, y)})
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
def _mock_execute_action(goap_self, action, goal=None):
|
||||
action_key = action.replace(" ", "_")
|
||||
if action_key == "tap_post_username":
|
||||
return True
|
||||
def press(self, key):
|
||||
self.interaction_log.append({"action": "press", "key": key})
|
||||
|
||||
original_click = goap_self.device.click
|
||||
def swipe(self, sx, sy, ex, ey, **kwargs):
|
||||
self.interaction_log.append({"action": "swipe", "start": (sx, sy), "end": (ex, ey)})
|
||||
|
||||
def _click_hook(obj=None, *args, **kwargs):
|
||||
original_click(obj, *args, **kwargs)
|
||||
lookup_key = action_key if action_key in state_map else action
|
||||
if lookup_key in state_map:
|
||||
new_xml = load_fixture_xml(state_map[lookup_key])
|
||||
device_mock._xml_history.append(new_xml)
|
||||
device_mock._current_active_xml = new_xml
|
||||
virtual_clock.start_animation()
|
||||
def click(self, x, y):
|
||||
self._validate_hitbox(x, y)
|
||||
self.interaction_log.append({"action": "click", "coords": (x, y)})
|
||||
|
||||
goap_self.device.click = _click_hook
|
||||
def watcher(self, name):
|
||||
return MockU2Watcher()
|
||||
|
||||
try:
|
||||
success = original_goap_execute(goap_self, action, goal=goal)
|
||||
return success
|
||||
finally:
|
||||
goap_self.device.click = original_click
|
||||
def app_start(self, package_name, use_monkey=False):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(QNavGraph, "_execute_transition", _mock_execute_transition)
|
||||
monkeypatch.setattr(GoalExecutor, "_execute_action", _mock_execute_action)
|
||||
def mock_connect(*args, **kwargs):
|
||||
return MockU2Device(img_path, xml_content)
|
||||
|
||||
return _inject
|
||||
monkeypatch.setattr(device_facade.u2, "connect", mock_connect)
|
||||
device = DeviceFacade("test_device", "com.instagram.android", None)
|
||||
|
||||
# Expose legacy emulator properties mapping directly to the underlying MockU2Device interaction log
|
||||
# so that existing test assertions keep working seamlessly.
|
||||
type(device).interaction_log = property(lambda self: self.deviceV2.interaction_log)
|
||||
type(device).pressed_keys = property(
|
||||
lambda self: [log["key"] for log in self.deviceV2.interaction_log if log["action"] == "press"]
|
||||
)
|
||||
type(device).clicks = property(
|
||||
lambda self: [log["coords"] for log in self.deviceV2.interaction_log if log["action"] == "click"]
|
||||
)
|
||||
type(device).swipes = property(
|
||||
lambda self: [log["start"] for log in self.deviceV2.interaction_log if log["action"] == "swipe"]
|
||||
)
|
||||
|
||||
return device
|
||||
|
||||
return _create
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
@@ -364,60 +465,7 @@ def _patch_module_delays(monkeypatch, module_path: str, sleep_fn, random_sleep_f
|
||||
monkeypatch.setattr(mod.random, "uniform", lambda a, b: float(a))
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_all_delays(monkeypatch, request, virtual_clock):
|
||||
"""Replaces all humanized hardware delays with VirtualClock advances.
|
||||
|
||||
Uses the per-test `virtual_clock` fixture for complete isolation.
|
||||
"""
|
||||
if request.config.getoption("--live"):
|
||||
return
|
||||
|
||||
def simulate_sleep(seconds):
|
||||
virtual_clock.sleep(seconds)
|
||||
|
||||
def money_sleep(x):
|
||||
return simulate_sleep(x)
|
||||
|
||||
def random_sleep(a=1.0, b=2.0, *args, **kwargs):
|
||||
return simulate_sleep(max(1.5, float(a)))
|
||||
|
||||
monkeypatch.setattr(time, "sleep", money_sleep)
|
||||
monkeypatch.setattr(utils, "random_sleep", random_sleep)
|
||||
monkeypatch.setattr(utils, "sleep", money_sleep)
|
||||
if hasattr(utils, "random"):
|
||||
monkeypatch.setattr(utils.random, "uniform", lambda a, b: float(a))
|
||||
|
||||
# Each module gets its own try-block so a missing attribute in one
|
||||
# doesn't prevent patching the others.
|
||||
_patch_module_delays(monkeypatch, "GramAddict.core.bot_flow", money_sleep, random_sleep)
|
||||
_patch_module_delays(monkeypatch, "GramAddict.core.q_nav_graph", money_sleep, random_sleep)
|
||||
_patch_module_delays(monkeypatch, "GramAddict.core.goap", money_sleep, random_sleep)
|
||||
_patch_module_delays(monkeypatch, "GramAddict.core.device_facade", money_sleep, random_sleep)
|
||||
|
||||
# Standardize DarwinEngine to prevent mockup math errors on session end
|
||||
try:
|
||||
from GramAddict.core.darwin_engine import DarwinEngine
|
||||
|
||||
monkeypatch.setattr(DarwinEngine, "evaluate_session_end", lambda *args, **kwargs: None)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# Identity & Account Guard
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_identity_guard(monkeypatch):
|
||||
import GramAddict.core.bot_flow
|
||||
|
||||
monkeypatch.setattr(
|
||||
GramAddict.core.bot_flow,
|
||||
"verify_and_switch_account",
|
||||
lambda *args, **kwargs: True,
|
||||
)
|
||||
# Note: mock_all_delays removed to favor production 'speed_multiplier' logic.
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
@@ -428,7 +476,6 @@ def mock_identity_guard(monkeypatch):
|
||||
@pytest.fixture
|
||||
def e2e_configs():
|
||||
import argparse
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
args = argparse.Namespace(
|
||||
username="testuser",
|
||||
@@ -450,7 +497,7 @@ def e2e_configs():
|
||||
stories_percentage=100,
|
||||
working_hours=[0.0, 24.0],
|
||||
time_delta_session=0,
|
||||
speed_multiplier=1.0,
|
||||
speed_multiplier=100.0,
|
||||
disable_filters=False,
|
||||
interaction_users_amount="1",
|
||||
scrape_profiles=False,
|
||||
@@ -461,14 +508,32 @@ def e2e_configs():
|
||||
ai_condenser_url="http://localhost",
|
||||
dry_run_comments=False,
|
||||
visual_vibe_check_percentage=0,
|
||||
current_likes_limit=10,
|
||||
current_comments_limit=10,
|
||||
current_follows_limit=10,
|
||||
current_follow_limit=10,
|
||||
current_unfollow_limit=10,
|
||||
current_pm_limit=10,
|
||||
current_scraped_limit=10,
|
||||
current_watch_limit=10,
|
||||
current_success_limit=10,
|
||||
current_total_limit=10,
|
||||
current_crashes_limit=10,
|
||||
max_follows=10,
|
||||
max_pm=10,
|
||||
max_watch=10,
|
||||
max_success=10,
|
||||
max_total=10,
|
||||
max_crashes=10,
|
||||
)
|
||||
|
||||
configs = MagicMock()
|
||||
configs.args = args
|
||||
configs.username = "testuser"
|
||||
from GramAddict.core.config import Config
|
||||
|
||||
def get_plugin_config_mock(plugin_name):
|
||||
mapping = {
|
||||
config = Config(first_run=True)
|
||||
config.args = args
|
||||
config.username = "testuser"
|
||||
config.config = {
|
||||
"plugins": {
|
||||
"likes": {"count": args.likes_count, "percentage": args.likes_percentage},
|
||||
"comment": {
|
||||
"percentage": args.comment_percentage,
|
||||
@@ -485,34 +550,8 @@ def e2e_configs():
|
||||
"count": getattr(args, "carousel_count", "1"),
|
||||
},
|
||||
}
|
||||
return mapping.get(plugin_name, {})
|
||||
|
||||
configs.get_plugin_config.side_effect = get_plugin_config_mock
|
||||
return configs
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# SAE Mock — Scoped Exclusion
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_sae_perceive(request, monkeypatch):
|
||||
"""Mock SAE.perceive for E2E tests EXCEPT the ones actually testing SAE."""
|
||||
if "test_e2e_sae.py" in str(request.node.fspath):
|
||||
return
|
||||
if "test_e2e_real_llm_learning.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,
|
||||
)
|
||||
}
|
||||
return config
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
@@ -566,3 +605,234 @@ def setup_e2e_plugin_registry():
|
||||
plugin_registry.register(RepostPlugin())
|
||||
plugin_registry.register(PostInteractionPlugin())
|
||||
yield plugin_registry
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# E2E Device Stub — The ONLY mock in true E2E tests
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class E2EDeviceStub:
|
||||
"""
|
||||
Replays a sequence of XML dumps simulating the Android device.
|
||||
This is the ONLY thing mocked in E2E tests. Everything else is real.
|
||||
"""
|
||||
|
||||
def __init__(self, xml_sequence):
|
||||
self._xml_sequence = list(xml_sequence)
|
||||
self._dump_index = 0
|
||||
self.pressed_keys = []
|
||||
self.clicks = []
|
||||
self.swipes = []
|
||||
self.app_starts = []
|
||||
self.app_id = "com.instagram.android"
|
||||
self._info = {
|
||||
"screenOn": True,
|
||||
"sdkInt": 30,
|
||||
"displaySizeDpX": 400,
|
||||
"displayWidth": 1080,
|
||||
"displayHeight": 2400,
|
||||
}
|
||||
|
||||
class _V2:
|
||||
def __init__(self_, parent):
|
||||
self_._parent = parent
|
||||
self_.info = parent._info
|
||||
self_.settings = {}
|
||||
|
||||
def dump_hierarchy(self_, compressed=False):
|
||||
return self_._parent.dump_hierarchy()
|
||||
|
||||
def app_current(self_):
|
||||
return {"package": "com.instagram.android"}
|
||||
|
||||
def screenshot(self_):
|
||||
from PIL import Image
|
||||
|
||||
return Image.new("RGB", (1, 1), color="black")
|
||||
|
||||
def shell(self_, cmd):
|
||||
if isinstance(cmd, str) and cmd.startswith("input tap"):
|
||||
parts = cmd.split()
|
||||
try:
|
||||
x, y = int(parts[-2]), int(parts[-1])
|
||||
self_._parent.clicks.append((x, y))
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
self.deviceV2 = _V2(self)
|
||||
|
||||
class _Touch:
|
||||
def __init__(self_, parent):
|
||||
self_._parent = parent
|
||||
|
||||
def down(self_, x=None, y=None, **kwargs):
|
||||
if "obj" in kwargs:
|
||||
obj = kwargs["obj"]
|
||||
if isinstance(obj, dict) and "bounds" in obj:
|
||||
import re
|
||||
|
||||
b = obj["bounds"]
|
||||
if isinstance(b, str):
|
||||
nums = [int(n) for n in re.findall(r"\d+", b)]
|
||||
x, y = (nums[0] + nums[2]) // 2, (nums[1] + nums[3]) // 2
|
||||
else:
|
||||
x, y = (int(b[0]) + int(b[2])) // 2, (int(b[1]) + int(b[3])) // 2
|
||||
else:
|
||||
x, y = (
|
||||
int(getattr(obj, "x", getattr(obj, "x1", 0))),
|
||||
int(getattr(obj, "y", getattr(obj, "y1", 0))),
|
||||
)
|
||||
self_._parent.clicks.append((int(x) if x is not None else 0, int(y) if y is not None else 0))
|
||||
|
||||
def up(self_, x=None, y=None, **kwargs):
|
||||
pass
|
||||
|
||||
self.deviceV2.touch = _Touch(self)
|
||||
|
||||
def dump_hierarchy(self):
|
||||
if self._dump_index < len(self._xml_sequence):
|
||||
xml = self._xml_sequence[self._dump_index]
|
||||
self._dump_index += 1
|
||||
return xml
|
||||
return self._xml_sequence[-1]
|
||||
|
||||
def press(self, key):
|
||||
self.pressed_keys.append(key)
|
||||
|
||||
def click(self, x=None, y=None, **kwargs):
|
||||
if "obj" in kwargs:
|
||||
obj = kwargs["obj"]
|
||||
if isinstance(obj, dict) and "bounds" in obj:
|
||||
import re
|
||||
|
||||
b = obj["bounds"]
|
||||
if isinstance(b, str):
|
||||
nums = [int(n) for n in re.findall(r"\d+", b)]
|
||||
x, y = (nums[0] + nums[2]) // 2, (nums[1] + nums[3]) // 2
|
||||
else:
|
||||
x, y = (int(b[0]) + int(b[2])) // 2, (int(b[1]) + int(b[3])) // 2
|
||||
else:
|
||||
x, y = int(getattr(obj, "x", getattr(obj, "x1", 0))), int(getattr(obj, "y", getattr(obj, "y1", 0)))
|
||||
self.clicks.append((int(x) if x is not None else 0, int(y) if y is not None else 0))
|
||||
|
||||
def swipe(self, sx, sy, ex, ey, **kwargs):
|
||||
self.swipes.append({"start": (sx, sy), "end": (ex, ey)})
|
||||
|
||||
def app_start(self, pkg, use_monkey=False):
|
||||
self.app_starts.append(pkg)
|
||||
|
||||
def app_stop(self, pkg):
|
||||
pass
|
||||
|
||||
def unlock(self):
|
||||
pass
|
||||
|
||||
def shell(self, cmd):
|
||||
pass
|
||||
|
||||
def cm_to_pixels(self, cm):
|
||||
return int(cm * 40)
|
||||
|
||||
def get_screenshot_b64(self):
|
||||
return "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
|
||||
|
||||
def get_info(self):
|
||||
return self._info
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════
|
||||
# E2E Workflow Fixtures — Real cognitive stack, real config
|
||||
# ═══════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def e2e_device():
|
||||
"""Factory to create an E2EDeviceStub from an XML sequence."""
|
||||
|
||||
def _create(xml_sequence):
|
||||
return E2EDeviceStub(xml_sequence)
|
||||
|
||||
return _create
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def e2e_cognitive_stack_factory(e2e_configs):
|
||||
"""Build the REAL cognitive stack exactly like bot_flow.py does."""
|
||||
|
||||
def _create(device, username="testuser"):
|
||||
from GramAddict.core.active_inference import ActiveInferenceEngine
|
||||
from GramAddict.core.darwin_engine import DarwinEngine
|
||||
from GramAddict.core.dopamine_engine import DopamineEngine
|
||||
from GramAddict.core.growth_brain import GrowthBrain
|
||||
from GramAddict.core.interaction import LLMWriter
|
||||
from GramAddict.core.q_nav_graph import QNavGraph
|
||||
from GramAddict.core.qdrant_memory import DMMemoryDB, ParasocialCRMDB
|
||||
from GramAddict.core.resonance_engine import ResonanceEngine
|
||||
from GramAddict.core.sensors.honeypot_radome import HoneypotRadome
|
||||
from GramAddict.core.swarm_protocol import SwarmProtocol
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
from GramAddict.core.zero_latency_engine import ZeroLatencyEngine
|
||||
|
||||
dopamine = DopamineEngine()
|
||||
dopamine.session_limit_seconds = 0.5
|
||||
dopamine.session_start = time.time()
|
||||
|
||||
info = device.get_info() if hasattr(device, "get_info") else {"displayWidth": 1080, "displayHeight": 2400}
|
||||
|
||||
return {
|
||||
"dopamine": dopamine,
|
||||
"active_inference": ActiveInferenceEngine(username),
|
||||
"swarm": SwarmProtocol(username),
|
||||
"resonance": ResonanceEngine(username, persona_interests=[], crm=ParasocialCRMDB()),
|
||||
"growth_brain": GrowthBrain(username, persona_interests=[]),
|
||||
"radome": HoneypotRadome(info.get("displayWidth", 1080), info.get("displayHeight", 2400)),
|
||||
"nav_graph": QNavGraph(device),
|
||||
"zero_engine": ZeroLatencyEngine(device),
|
||||
"telepathic": TelepathicEngine.get_instance(),
|
||||
"darwin": DarwinEngine(username),
|
||||
"crm": ParasocialCRMDB(),
|
||||
"dm_memory": DMMemoryDB(),
|
||||
"writer": LLMWriter(username, [], e2e_configs),
|
||||
}
|
||||
|
||||
return _create
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def e2e_session(e2e_configs):
|
||||
"""Build a real SessionState."""
|
||||
return SessionState(e2e_configs)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def e2e_workflow_ctx(e2e_configs, e2e_cognitive_stack_factory, setup_e2e_plugin_registry):
|
||||
"""
|
||||
Factory that builds a REAL BehaviorContext + runs execute_all().
|
||||
This is the EXACT code path from bot_flow.py:942-971.
|
||||
"""
|
||||
from GramAddict.core.behaviors import BehaviorContext
|
||||
|
||||
def _run(device, context_xml=None):
|
||||
xml = context_xml if context_xml else device.dump_hierarchy()
|
||||
session = SessionState(e2e_configs)
|
||||
|
||||
cognitive_stack = e2e_cognitive_stack_factory(device)
|
||||
|
||||
ctx = BehaviorContext(
|
||||
device=device,
|
||||
configs=e2e_configs,
|
||||
session_state=session,
|
||||
cognitive_stack=cognitive_stack,
|
||||
context_xml=xml,
|
||||
sleep_mod=1.0,
|
||||
post_data={},
|
||||
username="testuser",
|
||||
shared_state={"consecutive_marker_misses": 0, "consecutive_ads": 0},
|
||||
)
|
||||
|
||||
registry = setup_e2e_plugin_registry
|
||||
results = registry.execute_all(ctx)
|
||||
return results, ctx
|
||||
|
||||
return _run
|
||||
|
||||
301
tests/e2e/device_emulator.py
Normal file
301
tests/e2e/device_emulator.py
Normal file
@@ -0,0 +1,301 @@
|
||||
import logging
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from GramAddict.core.device_facade import DeviceFacade
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_bounds(bounds_str):
|
||||
if not bounds_str:
|
||||
return None
|
||||
nums = [int(n) for n in re.findall(r"\d+", bounds_str)]
|
||||
if len(nums) == 4:
|
||||
return nums
|
||||
return None
|
||||
|
||||
|
||||
class InstagramEmulator:
|
||||
"""
|
||||
A 1:1 State-Machine based Android Device Emulator.
|
||||
Instead of connecting to an Appium server, this class simulates Android
|
||||
by transitioning between real XML dumps when the bot taps specific coordinates.
|
||||
"""
|
||||
|
||||
def __init__(self, initial_state, states, transitions, images=None):
|
||||
self.current_state = initial_state
|
||||
self.states = states
|
||||
self.transitions = transitions
|
||||
self.images = images or {}
|
||||
|
||||
self.pressed_keys = []
|
||||
self.clicks = []
|
||||
self.swipes = []
|
||||
self.app_starts = []
|
||||
self.app_id = "com.instagram.android"
|
||||
self._info = {
|
||||
"screenOn": True,
|
||||
"sdkInt": 30,
|
||||
"displaySizeDpX": 400,
|
||||
"displayWidth": 1080,
|
||||
"displayHeight": 2400,
|
||||
}
|
||||
|
||||
# Mocking the sub-interfaces uiautomator2 uses inside GramAddict
|
||||
class _V2:
|
||||
def __init__(self_, parent):
|
||||
self_._parent = parent
|
||||
self_.info = parent._info
|
||||
self_.settings = {}
|
||||
|
||||
def dump_hierarchy(self_, compressed=False):
|
||||
return self_._parent.dump_hierarchy()
|
||||
|
||||
def app_current(self_):
|
||||
return {"package": "com.instagram.android"}
|
||||
|
||||
def app_start(self_, package_name, **kwargs):
|
||||
self_._parent.app_starts.append(package_name)
|
||||
logger.info(f"[Emulator] app_start({package_name})")
|
||||
|
||||
def app_stop(self_, package_name):
|
||||
logger.info(f"[Emulator] app_stop({package_name})")
|
||||
|
||||
def app_clear(self_, package_name):
|
||||
logger.info(f"[Emulator] app_clear({package_name})")
|
||||
|
||||
def window_size(self_):
|
||||
return (1080, 2400)
|
||||
|
||||
def screenshot(self_):
|
||||
from PIL import Image
|
||||
|
||||
img_src = self_._parent.images.get(self_._parent.current_state)
|
||||
if img_src:
|
||||
if isinstance(img_src, str):
|
||||
return Image.open(img_src)
|
||||
return img_src
|
||||
return Image.new("RGB", (1, 1), color="black")
|
||||
|
||||
def shell(self_, cmd):
|
||||
if isinstance(cmd, str) and cmd.startswith("input tap"):
|
||||
parts = cmd.split()
|
||||
try:
|
||||
x, y = int(parts[-2]), int(parts[-1])
|
||||
self_._parent.click(x, y)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
elif isinstance(cmd, str) and cmd.startswith("input swipe"):
|
||||
parts = cmd.split()
|
||||
try:
|
||||
sx, sy, ex, ey = int(parts[2]), int(parts[3]), int(parts[4]), int(parts[5])
|
||||
self_._parent.swipe(sx, sy, ex, ey)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
def swipe(self_, sx, sy, ex, ey, **kwargs):
|
||||
self_._parent.swipe(sx, sy, ex, ey)
|
||||
|
||||
def press(self_, key):
|
||||
self_._parent.press(key)
|
||||
|
||||
def long_click(self_, x, y, duration=1.5):
|
||||
# Emulator doesn't simulate long click logic differently from click yet
|
||||
self_._parent.click(x, y)
|
||||
|
||||
self.deviceV2 = _V2(self)
|
||||
|
||||
class _Touch:
|
||||
def __init__(self_, parent):
|
||||
self_._parent = parent
|
||||
|
||||
def down(self_, x=None, y=None, **kwargs):
|
||||
if "obj" in kwargs:
|
||||
obj = kwargs["obj"]
|
||||
if isinstance(obj, dict) and "bounds" in obj:
|
||||
b = obj["bounds"]
|
||||
if isinstance(b, str):
|
||||
nums = [int(n) for n in re.findall(r"\d+", b)]
|
||||
x, y = (nums[0] + nums[2]) // 2, (nums[1] + nums[3]) // 2
|
||||
else:
|
||||
x, y = (int(b[0]) + int(b[2])) // 2, (int(b[1]) + int(b[3])) // 2
|
||||
else:
|
||||
x, y = (
|
||||
int(getattr(obj, "x", getattr(obj, "x1", 0))),
|
||||
int(getattr(obj, "y", getattr(obj, "y1", 0))),
|
||||
)
|
||||
|
||||
x_val = int(x) if x is not None else 0
|
||||
y_val = int(y) if y is not None else 0
|
||||
self_._parent.click(x_val, y_val)
|
||||
|
||||
def up(self_, x=None, y=None, **kwargs):
|
||||
pass
|
||||
|
||||
self.deviceV2.touch = _Touch(self)
|
||||
|
||||
def dump_hierarchy(self):
|
||||
logger.info(f"[Emulator] Dumping hierarchy for state: {self.current_state}")
|
||||
return self.states[self.current_state]
|
||||
|
||||
def press(self, key):
|
||||
self.pressed_keys.append(key)
|
||||
logger.info(f"[Emulator] Pressed key: {key}")
|
||||
|
||||
# Handle state transitions for hardware keys (like back)
|
||||
state_trans = self.transitions.get(self.current_state, {})
|
||||
for condition, next_state in state_trans.get("press", []):
|
||||
if condition == key:
|
||||
logger.info(f"[Emulator] Transition: {self.current_state} -> {next_state}")
|
||||
self.current_state = next_state
|
||||
break
|
||||
|
||||
def _validate_hitbox(self, x, y, xml_content):
|
||||
import re
|
||||
|
||||
try:
|
||||
cleaned_content = re.sub(r"<\?xml[^>]*\?>", "", xml_content)
|
||||
root = ET.fromstring(f"<wrapper>{cleaned_content}</wrapper>".encode("utf-8"))
|
||||
for node in root.iter():
|
||||
bounds_str = node.attrib.get("bounds", "")
|
||||
is_actionable = (
|
||||
node.attrib.get("clickable", "false") == "true"
|
||||
or node.attrib.get("long-clickable", "false") == "true"
|
||||
or node.attrib.get("scrollable", "false") == "true"
|
||||
or node.attrib.get("focusable", "false") == "true"
|
||||
)
|
||||
if bounds_str and is_actionable:
|
||||
bounds = parse_bounds(bounds_str)
|
||||
if bounds and bounds[0] <= x <= bounds[2] and bounds[1] <= y <= bounds[3]:
|
||||
return
|
||||
raise AssertionError(
|
||||
f"🛑 ZERO-TRUST VIOLATION: Bot clicked ({x}, {y}) but there is NO actionable UI element at these coordinates! "
|
||||
f"The VLM hallucinated or miscalculated bounds."
|
||||
)
|
||||
except ET.ParseError:
|
||||
pass
|
||||
|
||||
def click(self, x=None, y=None, **kwargs):
|
||||
if "obj" in kwargs:
|
||||
obj = kwargs["obj"]
|
||||
if isinstance(obj, dict) and "bounds" in obj:
|
||||
b = obj["bounds"]
|
||||
if isinstance(b, str):
|
||||
nums = [int(n) for n in re.findall(r"\d+", b)] # noqa: F823
|
||||
x, y = (nums[0] + nums[2]) // 2, (nums[1] + nums[3]) // 2
|
||||
else:
|
||||
x, y = (int(b[0]) + int(b[2])) // 2, (int(b[1]) + int(b[3])) // 2
|
||||
else:
|
||||
x, y = int(getattr(obj, "x", getattr(obj, "x1", 0))), int(getattr(obj, "y", getattr(obj, "y1", 0)))
|
||||
|
||||
x_val = int(x) if x is not None else 0
|
||||
y_val = int(y) if y is not None else 0
|
||||
self.clicks.append((x_val, y_val))
|
||||
|
||||
logger.info(f"[Emulator] Clicked at ({x_val}, {y_val})")
|
||||
xml_content = self.states[self.current_state]
|
||||
self._validate_hitbox(x_val, y_val, xml_content)
|
||||
|
||||
# Evaluate state transition
|
||||
xml_content = self.states[self.current_state]
|
||||
try:
|
||||
# We add a fake wrapper in case the XML dump has multiple roots or no root
|
||||
# But we must strip the XML declaration first if present!
|
||||
import re
|
||||
|
||||
cleaned_content = re.sub(r"<\?xml[^>]*\?>", "", xml_content)
|
||||
root = ET.fromstring(f"<wrapper>{cleaned_content}</wrapper>".encode("utf-8"))
|
||||
clicked_node = None
|
||||
|
||||
# Find the deepest node containing the click (smallest area)
|
||||
min_area = float("inf")
|
||||
for node in root.iter("node"):
|
||||
bounds = parse_bounds(node.attrib.get("bounds", ""))
|
||||
if bounds and bounds[0] <= x_val <= bounds[2] and bounds[1] <= y_val <= bounds[3]:
|
||||
area = (bounds[2] - bounds[0]) * (bounds[3] - bounds[1])
|
||||
if area <= min_area:
|
||||
min_area = area
|
||||
clicked_node = node
|
||||
|
||||
if clicked_node is not None:
|
||||
desc = clicked_node.attrib.get("content-desc", "")
|
||||
res_id = clicked_node.attrib.get("resource-id", "")
|
||||
|
||||
# Check transitions
|
||||
state_trans = self.transitions.get(self.current_state, {})
|
||||
for condition, next_state in state_trans.get("clicks", []):
|
||||
if condition.get("desc") and condition["desc"] in desc:
|
||||
logger.info(
|
||||
f"[Emulator] Transition based on desc='{desc}': {self.current_state} -> {next_state}"
|
||||
)
|
||||
self.current_state = next_state
|
||||
break
|
||||
if condition.get("id") and condition["id"] == res_id:
|
||||
logger.info(
|
||||
f"[Emulator] Transition based on id='{res_id}': {self.current_state} -> {next_state}"
|
||||
)
|
||||
self.current_state = next_state
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"[Emulator] Failed to parse XML for transition logic: {e}")
|
||||
logger.error(f"[Emulator] XML prefix: {repr(self.states[self.current_state][:100])}")
|
||||
|
||||
def swipe(self, sx, sy, ex, ey, **kwargs):
|
||||
self.swipes.append({"start": (sx, sy), "end": (ex, ey)})
|
||||
logger.info(f"[Emulator] Swiped from ({sx}, {sy}) to ({ex}, {ey})")
|
||||
# Could implement swipe transitions here (e.g. scroll down loads new feed)
|
||||
|
||||
def app_start(self, pkg, use_monkey=False):
|
||||
self.app_starts.append(pkg)
|
||||
logger.info(f"[Emulator] app_start({pkg})")
|
||||
|
||||
def app_stop(self, pkg):
|
||||
pass
|
||||
|
||||
def unlock(self):
|
||||
pass
|
||||
|
||||
def shell(self, cmd):
|
||||
pass
|
||||
|
||||
def cm_to_pixels(self, cm):
|
||||
return int(cm * 40)
|
||||
|
||||
def get_screenshot_b64(self):
|
||||
return "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
|
||||
|
||||
def get_info(self):
|
||||
return self._info
|
||||
|
||||
|
||||
def create_emulator_facade(initial_state, states, transitions, monkeypatch, images=None):
|
||||
"""
|
||||
Returns a DeviceFacade that is backed by our InstagramEmulator
|
||||
instead of UIAutomator2.
|
||||
"""
|
||||
emulator = InstagramEmulator(initial_state, states, transitions, images=images)
|
||||
|
||||
# We must patch u2.connect to avoid ConnectError during DeviceFacade init
|
||||
from GramAddict.core import device_facade
|
||||
|
||||
class WatcherStub:
|
||||
def __call__(self, name):
|
||||
return self
|
||||
|
||||
def when(self, xpath=None, **kwargs):
|
||||
return self
|
||||
|
||||
def click(self):
|
||||
return self
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
emulator.deviceV2.watcher = WatcherStub()
|
||||
|
||||
monkeypatch.setattr(device_facade.u2, "connect", lambda *args, **kwargs: emulator.deviceV2)
|
||||
|
||||
facade = DeviceFacade("emulator", "com.instagram.android", None)
|
||||
|
||||
return facade, emulator
|
||||
54
tests/e2e/dump_legend.py
Normal file
54
tests/e2e/dump_legend.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from PIL import Image
|
||||
|
||||
from GramAddict.core.perception.intent_resolver import IntentResolver
|
||||
from GramAddict.core.perception.spatial_parser import SpatialParser
|
||||
|
||||
|
||||
def inspect_nodes(base_name):
|
||||
print(f"\n--- INSPECT FOR {base_name} ---")
|
||||
xml_path = f"tests/fixtures/{base_name}.xml"
|
||||
jpg_path = f"tests/fixtures/{base_name}.jpg"
|
||||
|
||||
with open(xml_path, "r", encoding="utf-8") as f:
|
||||
xml = f.read()
|
||||
|
||||
parser = SpatialParser()
|
||||
root = parser.parse(xml)
|
||||
candidates = parser.get_clickable_nodes(root)
|
||||
|
||||
# We want to use the EXACT intent resolver logic
|
||||
resolver = IntentResolver()
|
||||
|
||||
# Let's mock the device using DeviceFacade
|
||||
from GramAddict.core.device_facade import DeviceFacade
|
||||
|
||||
class MockU2Device:
|
||||
def __init__(self, img_path):
|
||||
self.img = Image.open(img_path)
|
||||
self.info = {"sdkInt": 30, "displaySizeDpX": 400, "displayWidth": 1080, "screenOn": True}
|
||||
|
||||
def screenshot(self):
|
||||
return self.img
|
||||
|
||||
device = object.__new__(DeviceFacade)
|
||||
device.device_id = "test_device"
|
||||
device.app_id = "com.instagram.android"
|
||||
device.args = None
|
||||
device.deviceV2 = MockU2Device(jpg_path)
|
||||
b64, box_map = resolver._annotate_screenshot_with_candidates(device, candidates)
|
||||
|
||||
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 node.resource_id:
|
||||
label_parts.append(f"id='{node.resource_id.split('/')[-1]}'")
|
||||
if not label_parts:
|
||||
label_parts.append("(no visible text)")
|
||||
print(f" [{idx}] {', '.join(label_parts)}")
|
||||
|
||||
|
||||
inspect_nodes("comment_sheet")
|
||||
193
tests/e2e/fixtures/carousel_post_dump.xml
Normal file
193
tests/e2e/fixtures/carousel_post_dump.xml
Normal file
@@ -0,0 +1,193 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_launch_animation_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.viewinterop.ViewFactoryHolder" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_contents" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[70,0][1010,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][485,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_content" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][478,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_except_heads_up" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][478,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="17:18" resource-id="com.android.systemui:id/clock" class="android.widget.TextView" package="com.android.systemui" content-desc="17:18" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,59][188,117]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/notification_icon_area" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[188,3][478,173]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/notificationIcons" class="android.view.ViewGroup" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[188,3][478,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="WhatsApp notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[188,3][246,173]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Photos notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[246,3][304,173]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Gotify notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[304,3][362,173]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="WhatsApp notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[362,3][420,173]" drawing-order="4" hint="" display-id="0" />
|
||||
<node index="4" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Android System notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[420,3][478,173]" drawing-order="6" hint="" display-id="0" />
|
||||
<node index="5" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Amazon Photos notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[420,3][478,173]" drawing-order="5" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/cutout_space_view" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[485,3][595,173]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_end_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[595,3][999,173]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_end_side_content" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[782,3][999,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/system_icons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[782,59][999,117]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/statusIcons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[790,59][918,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="Telekom.de, two bars." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[790,59][851,117]" drawing-order="17" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[798,59][843,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[798,72][843,104]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[798,72][843,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[851,59][910,117]" drawing-order="18" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[859,59][902,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[859,72][902,104]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="Wi-Fi signal full." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[859,72][902,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[918,71][988,105]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[918,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/battery" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[918,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="Battery 71 per cent." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[918,71][981,105]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_root" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_parent" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_main" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_listview_parent_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,2361]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/refreshable_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="false" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="android:id/list" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,457]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_profile_header" class="android.view.ViewGroup" package="com.instagram.android" content-desc="silent_moodart posted a carousel in Forest 24 hours ago" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,457]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][128,457]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[29,339][128,438]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/row_feed_photo_profile_imageview" class="android.widget.ImageView" package="com.instagram.android" content-desc="Profile picture of silent_moodart" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[39,349][118,428]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="silent_moodart " resource-id="com.instagram.android:id/row_feed_photo_profile_name" class="android.widget.Button" package="com.instagram.android" content-desc="silent_moodart " checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,320][965,386]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,386][965,457]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="AI info" resource-id="com.instagram.android:id/secondary_label" class="android.widget.Button" package="com.instagram.android" content-desc="AI info" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,386][965,453]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/media_option_button" class="android.widget.ImageView" package="com.instagram.android" content-desc="More actions for this post" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[965,325][1080,452]" drawing-order="4" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text=" Storm Chant · The Day I Stopped Explaining" resource-id="com.instagram.android:id/secondary_label" class="android.widget.Button" package="com.instagram.android" content-desc=" Storm Chant · The Day I Stopped Explaining" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,395][965,457]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,457][1080,1897]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/carousel_media_group" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,457][1080,1897]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_carousel_media_actions" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,457][1080,1897]" drawing-order="11" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/carousel_viewpager" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,457][1080,1897]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/zoomable_view_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,457][1080,1897]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_photo_tags" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,457][1080,1897]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/row_feed_photo_media_tag_hints" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,457][1080,1897]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/carousel_video_media_group" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Photo 1 of 4 by ꧁ Trix ꧂, Liked by kingiberico and others, 142 comments" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,457][1080,1897]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/carousel_video_media_actions" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,457][1080,1897]" drawing-order="9" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/carousel_video_image" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,457][1080,1897]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,457][1080,1897]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/hint_icon_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[503,1158][577,1195]" drawing-order="14" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/indicator_container" class="android.widget.Button" package="com.instagram.android" content-desc="Turn sound on" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[934,1788][1080,1897]" drawing-order="10" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/indicator" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[971,1788][1043,1860]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/slideout_iconview_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[989,1806][1025,1842]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1897][1080,2273]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_view_group_buttons" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1897][1080,2070]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,1949][106,2070]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_button_like" class="android.widget.Button" package="com.instagram.android" content-desc="Like" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,1949][95,2070]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[106,1949][212,2070]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_button_comment" class="android.widget.Button" package="com.instagram.android" content-desc="Comment" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[138,1949][201,2070]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="142" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[212,1949][271,2070]" drawing-order="4" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/reposts_ufi_icon" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[271,1949][432,2070]" drawing-order="5" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[271,1949][388,2070]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[303,1949][377,2070]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="22" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[388,1949][432,2070]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="4" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[448,1949][538,2070]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_button_share" class="android.widget.Button" package="com.instagram.android" content-desc="Send post" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[464,1949][527,2070]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="5" text="" resource-id="" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[449,1897][630,1976]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="6" text="" resource-id="com.instagram.android:id/row_feed_button_save" class="android.widget.Button" package="com.instagram.android" content-desc="Add to Saved" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[959,1949][1075,2059]" drawing-order="7" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2070][1080,2130]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="User avatars" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,2070][92,2130]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="Liked by kingiberico and others" resource-id="" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[108,2078][668,2122]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="kingiberico" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[259,2078][452,2122]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="silent_moodart Between the Quiet...… more" resource-id="" class="com.instagram.ui.widget.textview.IgTextLayoutView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,2141][1080,2190]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="silent_moodart" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,2141][291,2185]" drawing-order="0" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="Between the Quiet..." checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[300,2141][647,2185]" drawing-order="0" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="Between the Quiet..." checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[300,2141][647,2185]" drawing-order="0" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="more" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[680,2141][765,2185]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="3" text="24 hours ago" resource-id="" class="android.widget.TextView" package="com.instagram.android" content-desc="24 hours ago" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2201][1080,2242]" drawing-order="4" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="3" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2273][1080,2361]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_profile_header" class="android.view.ViewGroup" package="com.instagram.android" content-desc="silent_moodart posted a carousel in Fantasy Forest 4 days ago" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2273][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2273][128,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[29,2292][128,2361]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/row_feed_photo_profile_imageview" class="android.widget.ImageView" package="com.instagram.android" content-desc="Profile picture of silent_moodart" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[39,2302][118,2361]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="silent_moodart " resource-id="com.instagram.android:id/row_feed_photo_profile_name" class="android.widget.Button" package="com.instagram.android" content-desc="silent_moodart " checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,2273][965,2339]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,2339][965,2361]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="AI info" resource-id="com.instagram.android:id/secondary_label" class="android.widget.Button" package="com.instagram.android" content-desc="AI info" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,2339][965,2361]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/media_option_button" class="android.widget.ImageView" package="com.instagram.android" content-desc="More actions for this post" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[965,2278][1080,2361]" drawing-order="4" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text=" Vowless · They Want Me Down" resource-id="com.instagram.android:id/secondary_label" class="android.widget.Button" package="com.instagram.android" content-desc=" Vowless · They Want Me Down" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,2351][965,2361]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/action_bar_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,320]" drawing-order="10" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_wrapper" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,320]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,320]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_new_title_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][779,320]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_button_back" class="android.widget.ImageView" package="com.instagram.android" content-desc="Back" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][147,320]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[147,215][365,278]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_title_and_icons" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[147,215][365,278]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="Posts" resource-id="com.instagram.android:id/action_bar_title" class="android.widget.TextView" package="com.instagram.android" content-desc="Posts" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[189,215][333,278]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/follow_button" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="true" visible-to-user="true" bounds="[832,191][1059,301]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="Follow" resource-id="com.instagram.android:id/action_bar_overflow_text" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="true" visible-to-user="true" bounds="[832,191][1028,280]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>
|
||||
1
tests/e2e/fixtures/comment_sheet.xml
Normal file
1
tests/e2e/fixtures/comment_sheet.xml
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?><hierarchy rotation='0'><node class='android.widget.LinearLayout'><node resource-id='com.instagram.android:id/row_comment_textview_comment' text='Nice post!'/><node resource-id='com.instagram.android:id/row_comment_button_like' bounds='[1,2][3,4]'/><node resource-id='com.instagram.android:id/row_comment_textview_reply_button' bounds='[5,6][7,8]'/></node></hierarchy>
|
||||
181
tests/e2e/fixtures/explore_grid_real.xml
Normal file
181
tests/e2e/fixtures/explore_grid_real.xml
Normal file
@@ -0,0 +1,181 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_root" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/swipe_navigation_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_center_right_coordinator_layout" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_right" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/layout_container_main_panel" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_main_wrapper" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_main" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/swipeable_tab_view_pager" class="androidx.viewpager.widget.ViewPager" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/swipeable_nav_view_pager_inner_recycler_view" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="3" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_swipeable" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_recyclerview_parent_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/refreshable_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/recycler_view" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/grid_card_layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Reel by .......... ...... | .................. ...... ........ .......... .... ....'.. at row 1, column 1" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[0,321][356,797]" drawing-order="1" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="com.instagram.android:id/image_button" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[0,321][356,796]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/play_count_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,714][356,797]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/play_count_logo" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[21,740][53,772]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="11,3M" resource-id="com.instagram.android:id/preview_clip_play_count" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[64,735][147,776]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/grid_card_layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="3 photos by Eric Kerr at row 1, column 2" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[362,321][718,797]" drawing-order="2" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="com.instagram.android:id/image_button" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[362,321][718,796]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,321][1080,797]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.TextureView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,321][1080,797]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/play_count_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,714][1080,797]" drawing-order="7" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/play_count_logo" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[745,740][777,772]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="495K" resource-id="com.instagram.android:id/preview_clip_play_count" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,735][866,776]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,803][356,1279]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.TextureView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,803][356,1279]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/play_count_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1196][356,1279]" drawing-order="7" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/play_count_logo" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[21,1222][53,1254]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="12,7M" resource-id="com.instagram.android:id/preview_clip_play_count" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[64,1217][150,1258]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="4" text="" resource-id="com.instagram.android:id/grid_card_layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="6 photos and 2 videos by Leonardo Soares at row 2, column 2" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[362,803][718,1279]" drawing-order="5" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="com.instagram.android:id/image_button" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[362,803][718,1278]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="5" text="" resource-id="com.instagram.android:id/grid_card_layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="14 photos by Don Amatayakul at row 2, column 3" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[724,803][1080,1279]" drawing-order="6" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="com.instagram.android:id/image_button" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[724,803][1080,1278]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="6" text="" resource-id="com.instagram.android:id/grid_card_layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="5 photos by Forest Barkdoll-Weil at row 3, column 1" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[0,1285][356,1761]" drawing-order="7" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="com.instagram.android:id/image_button" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[0,1285][356,1760]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="7" text="" resource-id="com.instagram.android:id/grid_card_layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="20 photos by ROSS LONG | Photography | Ocean | Workshops | Australia at row 3, column 2" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[362,1285][718,1761]" drawing-order="8" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="com.instagram.android:id/image_button" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[362,1285][718,1760]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="8" text="" resource-id="com.instagram.android:id/layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,1285][1080,1761]" drawing-order="9" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/image_preview" class="android.widget.ImageView" package="com.instagram.android" content-desc="Reel by Harrison Miller & Lauren Witherow at row 3, column 3" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,1285][1080,1760]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/play_count_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,1678][1080,1761]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/play_count_logo" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[745,1704][777,1736]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="6,9M" resource-id="com.instagram.android:id/preview_clip_play_count" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,1699][864,1740]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="9" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1767][1080,1977]" drawing-order="10" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_load_more_button" class="android.widget.ViewAnimator" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1767][1080,1977]" drawing-order="1" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[477,1809][603,1935]" drawing-order="3" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/explore_action_bar" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,320]" drawing-order="8" hint="" display-id="0">
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/explore_action_bar_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,173][1048,265]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_search_hints_text_layout" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,173][943,265]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="Search" resource-id="com.instagram.android:id/action_bar_search_edit_text" class="android.widget.EditText" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[32,173][943,265]" drawing-order="1" hint="Search" display-id="0" />
|
||||
<node index="1" text="" resource-id="" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,173][943,265]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/explore_action_bar_right_button_stub" class="android.widget.ImageView" package="com.instagram.android" content-desc="More" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[985,188][1048,251]" drawing-order="3" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/tab_bar_shadow" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2234][1080,2235]" drawing-order="5" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/tab_bar" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2235][1080,2361]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/feed_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Home" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[0,2235][216,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[76,2266][139,2329]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/clips_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Reels" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[216,2235][432,2361]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[292,2266][355,2329]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/direct_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Message" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[432,2235][648,2361]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[508,2266][571,2329]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/search_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Search and explore" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="true" visible-to-user="true" bounds="[648,2235][864,2361]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="true" visible-to-user="true" bounds="[724,2266][787,2329]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="4" text="" resource-id="com.instagram.android:id/profile_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Profile" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[864,2235][1080,2361]" drawing-order="5" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/wrapper" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[864,2235][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[933,2259][1012,2338]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_avatar" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[933,2259][1012,2338]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/led_badge" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[991,2311][1017,2337]" drawing-order="3" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/activity_and_camera_shared_views_main_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="4" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/bottom_sheet_camera_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/modal_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/overlay_layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="5" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_launch_animation_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.viewinterop.ViewFactoryHolder" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_contents" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[70,0][1010,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][485,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_content" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][252,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_except_heads_up" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][252,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="21:23" resource-id="com.android.systemui:id/clock" class="android.widget.TextView" package="com.android.systemui" content-desc="21:23" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,59][194,117]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/notification_icon_area" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[194,3][252,173]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/notificationIcons" class="android.view.ViewGroup" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[194,3][252,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Android System notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[194,3][252,173]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/cutout_space_view" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[485,3][595,173]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_end_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[595,3][999,173]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_end_side_content" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[772,3][999,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/system_icons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[772,59][999,117]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/statusIcons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[780,59][908,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="Telekom.de, three bars." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[780,59][841,117]" drawing-order="17" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,59][833,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,72][833,104]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,72][833,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[841,59][900,117]" drawing-order="18" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,59][892,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,72][892,104]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="Wi-Fi signal full." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,72][892,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/battery" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="Battery charging, 52 percent." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][971,105]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>
|
||||
BIN
tests/e2e/fixtures/home_feed_real.jpg
Normal file
BIN
tests/e2e/fixtures/home_feed_real.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 223 KiB |
265
tests/e2e/fixtures/home_feed_real.xml
Normal file
265
tests/e2e/fixtures/home_feed_real.xml
Normal file
@@ -0,0 +1,265 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_root" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/swipe_navigation_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/layout_container_center_right_coordinator_layout" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_right" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/activity_and_camera_shared_views_main_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="4" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/layout_container_main_panel" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_main_wrapper" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_main" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/swipeable_tab_view_pager" class="androidx.viewpager.widget.ViewPager" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/swipeable_nav_view_pager_inner_recycler_view" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_swipeable" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/sticky_header_list" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/refreshable_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="android:id/list" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/gap_binder_group" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,320]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,665]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reels_tray_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,665]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="reels tray container" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,665]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][294,665]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_view" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][294,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="marisaundmarc's story, 0 of 27, Unseen." checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][294,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][294,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_image_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="marisaundmarc's story, 0 of 27, Unseen." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[35,355][258,578]" drawing-order="7" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/reel_empty_badge" class="android.widget.ImageView" package="com.instagram.android" content-desc="Add to story" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[195,515][273,593]" drawing-order="15" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,603][294,644]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="Your story" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[72,603][223,644]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[294,320][588,665]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_view" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[294,320][588,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="kristyna_sixtova's story, 1 of 27, Unseen." checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[294,320][588,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[294,320][588,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/seen_state" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[310,336][572,598]" drawing-order="11" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/avatar_image_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="kristyna_sixtova's story, 1 of 27, Unseen." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[329,355][552,578]" drawing-order="7" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[294,603][588,644]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="kristyna_sixtova" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[323,603][560,644]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[588,320][882,665]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_view" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[588,320][882,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="cgnmgj's story, 2 of 27, Unseen." checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[588,320][882,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[588,320][882,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/seen_state" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[604,336][866,598]" drawing-order="11" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/avatar_image_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="cgnmgj's story, 2 of 27, Unseen." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[623,355][846,578]" drawing-order="7" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[588,603][882,644]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="cgnmgj" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[680,603][791,644]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[882,320][1080,665]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_view" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[882,320][1080,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="lisa.crmn's story, 3 of 27, Unseen." checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[882,320][1080,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[882,320][1080,603]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/seen_state" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[898,336][1080,598]" drawing-order="11" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/avatar_image_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="lisa.crmn's story, 3 of 27, Unseen." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[917,355][1080,578]" drawing-order="7" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[882,603][1080,644]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="lisa.crmn" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[963,603][1080,644]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,665][1080,802]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_profile_header" class="android.view.ViewGroup" package="com.instagram.android" content-desc="millionlords posted a photo 3 March" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,665][1080,802]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,665][128,802]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_photo_profile_imageview" class="android.widget.ImageView" package="com.instagram.android" content-desc="Profile picture of millionlords" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[39,694][118,773]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="millionlords" resource-id="com.instagram.android:id/row_feed_photo_profile_name" class="android.widget.Button" package="com.instagram.android" content-desc="millionlords" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,665][965,731]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="Ad" resource-id="com.instagram.android:id/secondary_label" class="android.widget.Button" package="com.instagram.android" content-desc="Ad" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,731][965,802]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/media_option_button" class="android.widget.ImageView" package="com.instagram.android" content-desc="More actions for this post" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[965,670][1080,797]" drawing-order="4" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,802][1080,1882]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_photo_imageview" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,802][1080,1882]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_photo_imageview" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,802][1080,1882]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/media_group" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,802][1080,1882]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_photo_imageview" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Sponsored Photo by NinjaTrader, 3 comments" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,802][1080,1882]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,802][1080,1882]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/row_feed_photo_imageview" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,802][1080,1882]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,802][1080,1882]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,802][1080,1882]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/larger_cta_top_buffer" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1814][1080,1882]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="4" text="" resource-id="com.instagram.android:id/cta_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1882][1080,1998]" drawing-order="5" hint="" display-id="0">
|
||||
<node index="0" text="Install now" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="Install now" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1882][1080,1998]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1997][1080,1998]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="5" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1998][1080,2235]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_view_group_buttons" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1998][1080,2119]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,1998][106,2119]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_button_like" class="android.widget.Button" package="com.instagram.android" content-desc="Like" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,1998][95,2119]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[106,1998][212,2119]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_button_comment" class="android.widget.Button" package="com.instagram.android" content-desc="Comment" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[138,1998][201,2119]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="3" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[212,1998][234,2119]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[250,1998][340,2119]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_button_share" class="android.widget.Button" package="com.instagram.android" content-desc="Send post" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[266,1998][329,2119]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="4" text="" resource-id="com.instagram.android:id/row_feed_button_save" class="android.widget.Button" package="com.instagram.android" content-desc="Add to Saved" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[959,1998][1075,2114]" drawing-order="5" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/larger_cta_bottom_buffer" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1998][1080,2025]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2119][1080,2162]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="View likes" resource-id="" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,2119][1048,2162]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="3" text="millionlords Mit NinjaTrader handeln Sie an echten Märkten – nicht gegen Ihren Broker. Sie profitieren von transpar… more" resource-id="" class="com.instagram.ui.widget.textview.IgTextLayoutView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,2173][1080,2235]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="millionlords" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,2173][219,2217]" drawing-order="0" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="Mit NinjaTrader handeln Sie an echten Märkten – nicht gegen Ihren Broker. Sie profitieren von transpar" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[228,2173][1048,2217]" drawing-order="0" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="Mit NinjaTrader handeln Sie an echten Märkten – nicht gegen Ihren Broker. Sie profitieren von transpar" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[228,2173][1048,2217]" drawing-order="0" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="more" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[958,2222][1043,2266]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/action_bar_LinearLayout" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,320]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/main_feed_action_bar" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,320]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_action_buttons" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,320]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_buttons_container_left" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][127,320]" drawing-order="1" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[0,173][127,320]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/action_bar_title_view" class="android.widget.ViewAnimator" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[127,206][953,287]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/text_title_chevron_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[411,206][669,275]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="For you" resource-id="com.instagram.android:id/title_text" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[411,206][619,275]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_chevron" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[637,224][669,256]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/action_bar_buttons_container_right" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[953,173][1080,320]" drawing-order="4" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="com.instagram.android:id/notification" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[953,173][1080,320]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/wrapper" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[985,215][1048,278]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[985,215][1048,278]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[985,215][1048,278]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/tab_bar_shadow" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2234][1080,2235]" drawing-order="5" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/tab_bar" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2235][1080,2361]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/feed_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Home" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="true" visible-to-user="true" bounds="[0,2235][216,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="true" visible-to-user="true" bounds="[76,2266][139,2329]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/clips_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Reels" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[216,2235][432,2361]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[292,2266][355,2329]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/direct_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Message" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[432,2235][648,2361]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[508,2266][571,2329]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/search_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Search and explore" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[648,2235][864,2361]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,2266][787,2329]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="4" text="" resource-id="com.instagram.android:id/profile_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Profile" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[864,2235][1080,2361]" drawing-order="5" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/wrapper" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[864,2235][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[933,2259][1012,2338]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_avatar" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[933,2259][1012,2338]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/led_badge" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[991,2311][1017,2337]" drawing-order="3" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/bottom_sheet_camera_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/modal_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/overlay_layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="5" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_launch_animation_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.viewinterop.ViewFactoryHolder" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_contents" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[70,0][1010,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][485,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_content" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][242,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_except_heads_up" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][242,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="21:11" resource-id="com.android.systemui:id/clock" class="android.widget.TextView" package="com.android.systemui" content-desc="21:11" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,59][184,117]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/notification_icon_area" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[184,3][242,173]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/notificationIcons" class="android.view.ViewGroup" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[184,3][242,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Android System notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[184,3][242,173]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/cutout_space_view" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[485,3][595,173]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_end_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[595,3][999,173]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_end_side_content" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[772,3][999,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/system_icons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[772,59][999,117]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/statusIcons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[780,59][908,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="Telekom.de, three bars." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[780,59][841,117]" drawing-order="17" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,59][833,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,72][833,104]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,72][833,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[841,59][900,117]" drawing-order="18" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,59][892,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,72][892,104]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="Wi-Fi signal full." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,72][892,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/battery" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="Battery charging, 46 percent." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][971,105]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>
|
||||
323
tests/e2e/fixtures/other_profile_real.xml
Normal file
323
tests/e2e/fixtures/other_profile_real.xml
Normal file
@@ -0,0 +1,323 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_root" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/swipe_navigation_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/layout_container_center_right_coordinator_layout" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_right" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/activity_and_camera_shared_views_main_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="4" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/layout_container_main_panel" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_main_wrapper" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_main" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/swipeable_tab_view_pager" class="androidx.viewpager.widget.ViewPager" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/swipeable_nav_view_pager_inner_recycler_view" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_swipeable" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_action_bar" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,320]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_layout" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1059,320]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/left_action_bar_buttons" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][127,320]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.instagram.android" content-desc="Back" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][127,320]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/action_bar_username_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[169,173][805,320]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[169,173][627,320]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="joannehollings" resource-id="com.instagram.android:id/action_bar_title" class="android.widget.TextView" package="com.instagram.android" content-desc="joannehollings" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[169,173][569,320]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/action_bar_username_buttons_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[569,173][627,320]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_title_verified_badge" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[585,225][627,267]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/right_action_bar_buttons" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[805,173][1059,320]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.instagram.android" content-desc="Open notification settings" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[805,173][932,320]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="" class="android.widget.ImageView" package="com.instagram.android" content-desc="Options" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[932,173][1059,320]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/coordinator_root_layout" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,2361]" drawing-order="7" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/tab_appbar" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,1592]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_header_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,1592]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_header_fixed_list" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,1465]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_profile_header" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,1049]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_header_avatar_container_top_left_stub" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,320][273,551]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,320][273,551]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reel_ring" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,320][273,551]" drawing-order="8" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/row_profile_header_imageview_frame_layout" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[61,339][255,533]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_profile_header_imageview" class="android.widget.ImageView" package="com.instagram.android" content-desc="joannehollings's unseen story" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[61,339][255,533]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="Joanne Hollings" resource-id="com.instagram.android:id/profile_header_full_name_above_vanity" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[315,353][592,397]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/profile_header_metrics_full_width" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[315,397][1038,540]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_header_post_count_front_familiar" class="" package="com.instagram.android" content-desc="1.472posts" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[315,397][513,540]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="1.472" resource-id="com.instagram.android:id/profile_header_familiar_post_count_value" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[315,418][416,472]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="posts" resource-id="com.instagram.android:id/profile_header_familiar_post_count_label" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[315,472][411,519]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/profile_header_followers_stacked_familiar" class="" package="com.instagram.android" content-desc="133Kfollowers" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[513,397][784,540]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="133K" resource-id="com.instagram.android:id/profile_header_familiar_followers_value" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[529,418][624,472]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="followers" resource-id="com.instagram.android:id/profile_header_familiar_followers_label" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[529,472][687,519]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/profile_header_following_stacked_familiar" class="" package="com.instagram.android" content-desc="2.572following" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[784,397][1038,540]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="2.572" resource-id="com.instagram.android:id/profile_header_familiar_following_value" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[800,418][911,472]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="following" resource-id="com.instagram.android:id/profile_header_familiar_following_label" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[800,472][957,519]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="4" text="Photographer" resource-id="com.instagram.android:id/profile_header_business_category" class="android.widget.TextView" package="com.instagram.android" content-desc="Photographer" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,551][281,598]" drawing-order="13" hint="" display-id="0" />
|
||||
<node index="5" text="" resource-id="com.instagram.android:id/profile_user_info_compose_view" class="com.facebook.compose.view.MetaComposeView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,598][1038,771]" drawing-order="18" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,598][1038,771]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,598][1038,771]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="Kiwi .... lifestyle and adventure Photographer .. Inquiries ..: hello@joannehollingscrea… more" resource-id="" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,598][1038,771]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="6" text="" resource-id="com.instagram.android:id/profile_links_view" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,771][677,822]" drawing-order="20" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/button_1" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,771][677,822]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/icon_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,775][84,817]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="bio.site/joannehollings and 1 more" resource-id="com.instagram.android:id/text_view" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[95,771][677,822]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="7" text="" resource-id="com.instagram.android:id/banner_row" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,843][1038,907]" drawing-order="22" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,843][1038,907]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,843][365,907]" drawing-order="1" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="com.instagram.android:id/igds_prism_chip_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[63,859][95,891]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="joannehollings" resource-id="com.instagram.android:id/igds_prism_chip_label" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[111,854][333,896]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[386,843][863,907]" drawing-order="2" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="com.instagram.android:id/igds_prism_chip_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[407,859][439,891]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="updates tips and tricks w/ Joanne" resource-id="com.instagram.android:id/igds_prism_chip_label" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[455,854][831,896]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="8" text="" resource-id="com.instagram.android:id/profile_header_follow_context" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,918][1038,1017]" drawing-order="24" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_header_follow_context_facepile" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,929][246,1013]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="Followed by shadow_of_routes, paige_tingey and 142 others" resource-id="com.instagram.android:id/profile_header_follow_context_text" class="android.widget.TextView" package="com.instagram.android" content-desc="Followed by shadow_of_routes, paige_tingey and 142 others" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[267,925][1038,1017]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1049][1080,1133]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_header_actions_top_row" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1049][1080,1133]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_header_user_action_follow_button" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,1049][326,1133]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="Following" resource-id="com.instagram.android:id/profile_header_follow_button" class="android.widget.TextView" package="com.instagram.android" content-desc="Following Joanne Hollings" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[32,1049][326,1133]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[342,1049][636,1133]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/button_container" class="android.widget.Button" package="com.instagram.android" content-desc="Message" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[342,1049][636,1133]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="Message" resource-id="" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[410,1067][568,1114]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[652,1049][946,1133]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/button_container" class="android.widget.Button" package="com.instagram.android" content-desc="Email" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[652,1049][946,1133]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="Email" resource-id="" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[752,1067][845,1114]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/row_profile_header_button_chaining" class="android.widget.Button" package="com.instagram.android" content-desc="@2131979553" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[962,1049][1046,1133]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/chaining_button_image" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[962,1049][1046,1133]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/highlights_tray" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1165][1080,1439]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/highlights_reel_tray_recycler_view" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1165][1080,1439]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[5,1165][249,1439]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_view" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[5,1165][249,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="joannehollings's story, 0 of 0, Seen." checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[5,1165][249,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[5,1165][249,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/seen_state" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[29,1181][225,1377]" drawing-order="11" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/avatar_image_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="joannehollings's highlight story at column 0" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[45,1197][208,1360]" drawing-order="7" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[5,1382][249,1423]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="My gear" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[67,1382][188,1423]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[249,1165][493,1439]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_view" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[249,1165][493,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="joannehollings's story, 1 of 0, Seen." checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[249,1165][493,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[249,1165][493,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/seen_state" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[273,1181][469,1377]" drawing-order="11" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/avatar_image_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="joannehollings's highlight story at column 1" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[289,1197][452,1360]" drawing-order="7" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[249,1382][493,1423]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="Portugal ...." resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[283,1382][459,1423]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[493,1165][737,1439]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_view" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[493,1165][737,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="joannehollings's story, 2 of 0, Seen." checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[493,1165][737,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[493,1165][737,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/seen_state" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[517,1181][713,1377]" drawing-order="11" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/avatar_image_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="joannehollings's highlight story at column 2" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[533,1197][696,1360]" drawing-order="7" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[493,1382][737,1423]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="Ibiza" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[581,1382][650,1423]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[737,1165][981,1439]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_view" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[737,1165][981,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="joannehollings's story, 3 of 0, Seen." checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[737,1165][981,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[737,1165][981,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/seen_state" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[761,1181][957,1377]" drawing-order="11" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/avatar_image_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="joannehollings's highlight story at column 3" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[777,1197][940,1360]" drawing-order="7" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[737,1382][981,1423]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="Washington 25" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[748,1382][970,1423]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="4" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[981,1165][1080,1439]" drawing-order="5" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_view" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[981,1165][1080,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="joannehollings's story, 4 of 0, Seen." checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[981,1165][1080,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/avatar_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[981,1165][1080,1382]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/seen_state" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[1005,1181][1080,1377]" drawing-order="11" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/avatar_image_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="joannehollings's highlight story at column 4" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[1021,1197][1080,1360]" drawing-order="7" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[981,1382][1080,1423]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="Norway" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[1046,1382][1080,1423]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/profile_tabs_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1465][1080,1592]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_tab_layout" class="android.widget.HorizontalScrollView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1465][1080,1591]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1465][1080,1591]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1465][270,1591]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1465][270,1591]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_tab_icon_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="Grid view" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1465][270,1591]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="true" visible-to-user="true" bounds="[270,1465][540,1591]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="true" visible-to-user="true" bounds="[270,1465][540,1591]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_tab_icon_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="Reels" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="true" visible-to-user="true" bounds="[270,1465][540,1591]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[540,1465][810,1591]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[540,1465][810,1591]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_tab_icon_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="Reposted" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[540,1465][810,1591]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[810,1465][1080,1591]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[810,1465][1080,1591]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_tab_icon_view" class="android.widget.ImageView" package="com.instagram.android" content-desc="Photos of you" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[810,1465][1080,1591]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/profile_tabs_bottom_divider" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1591][1080,1592]" drawing-order="5" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/swipe_refresh_animated_progressbar_container_background" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,320][1080,321]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/swipe_refresh_animated_progressbar_container" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[448,320][632,504]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="4" text="" resource-id="" class="android.widget.ScrollView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1592][1080,2361]" drawing-order="5" hint="" display-id="0" />
|
||||
<node index="5" text="" resource-id="com.instagram.android:id/profile_viewpager" class="androidx.viewpager.widget.ViewPager" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1592][1080,2361]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/clips_grid_shimmer_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1592][1080,2361]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/clips_grid_recyclerview" class="android.widget.GridView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1592][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.RelativeLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1598][356,2231]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/preview_clip_thumbnail" class="android.widget.ImageView" package="com.instagram.android" content-desc="Reel by joannehollings. View Count 4,3M. Double tap to play or pause." checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1598][356,2231]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.widget.RelativeLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[362,1598][718,2231]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/preview_clip_thumbnail" class="android.widget.ImageView" package="com.instagram.android" content-desc="Reel by joannehollings. View Count 2M. Double tap to play or pause." checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[362,1598][718,2231]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="" resource-id="" class="android.widget.RelativeLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,1598][1080,2231]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/preview_clip_thumbnail" class="android.widget.ImageView" package="com.instagram.android" content-desc="Reel by joannehollings. View Count 1,9M. Double tap to play or pause." checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,1598][1080,2231]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="3" text="" resource-id="" class="android.widget.RelativeLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2237][356,2361]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/preview_clip_thumbnail" class="android.widget.ImageView" package="com.instagram.android" content-desc="Reel by joannehollings. View Count 0. Double tap to play or pause." checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2237][356,2361]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="4" text="" resource-id="" class="android.widget.RelativeLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[362,2237][718,2361]" drawing-order="5" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/preview_clip_thumbnail" class="android.widget.ImageView" package="com.instagram.android" content-desc="Reel by joannehollings. View Count 51,1K. Double tap to play or pause." checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[362,2237][718,2361]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="5" text="" resource-id="" class="android.widget.RelativeLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,2237][1080,2361]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/preview_clip_thumbnail" class="android.widget.ImageView" package="com.instagram.android" content-desc="Reel by joannehollings. View Count 24,3K. Double tap to play or pause." checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,2237][1080,2361]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/bottom_sheet_camera_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/modal_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/overlay_layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="5" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_launch_animation_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.viewinterop.ViewFactoryHolder" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_contents" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[70,0][1010,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][485,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_content" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][247,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_except_heads_up" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][247,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="21:12" resource-id="com.android.systemui:id/clock" class="android.widget.TextView" package="com.android.systemui" content-desc="21:12" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,59][189,117]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/notification_icon_area" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[189,3][247,173]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/notificationIcons" class="android.view.ViewGroup" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[189,3][247,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Android System notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[189,3][247,173]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/cutout_space_view" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[485,3][595,173]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_end_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[595,3][999,173]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_end_side_content" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[772,3][999,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/system_icons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[772,59][999,117]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/statusIcons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[780,59][908,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="Telekom.de, three bars." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[780,59][841,117]" drawing-order="17" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,59][833,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,72][833,104]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,72][833,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[841,59][900,117]" drawing-order="18" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,59][892,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,72][892,104]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="Wi-Fi signal full." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,72][892,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/battery" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="Battery charging, 46 percent." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][971,105]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>
|
||||
27
tests/e2e/fixtures/post_detail.xml
Normal file
27
tests/e2e/fixtures/post_detail.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" class="android.widget.FrameLayout" package="com.instagram.android">
|
||||
<node index="0" class="android.view.ViewGroup">
|
||||
<!-- Image/Content container -->
|
||||
<node index="0" resource-id="com.instagram.android:id/row_feed_photo_container" class="android.widget.FrameLayout" content-desc="Photo by marisaundmarc: Golden hour in the Dolomites. #nature #mountains" />
|
||||
|
||||
<!-- Caption area -->
|
||||
<node index="1" resource-id="com.instagram.android:id/row_feed_comment_textview_layout" class="android.widget.LinearLayout">
|
||||
<node index="0" resource-id="com.instagram.android:id/row_feed_comment_textview_child_comment_indent" class="android.widget.TextView" text="marisaundmarc Golden hour in the Dolomites. #nature #mountains" />
|
||||
</node>
|
||||
|
||||
<!-- Comments -->
|
||||
<node index="2" class="android.widget.ListView">
|
||||
<node index="0" resource-id="com.instagram.android:id/row_feed_comment_textview_layout" class="android.widget.LinearLayout">
|
||||
<node index="0" resource-id="com.instagram.android:id/row_feed_comment_textview_child_comment_indent" class="android.widget.TextView" text="awesome_traveler Wow, stunning shot! 😍" />
|
||||
</node>
|
||||
<node index="1" resource-id="com.instagram.android:id/row_feed_comment_textview_layout" class="android.widget.LinearLayout">
|
||||
<node index="0" resource-id="com.instagram.android:id/row_feed_comment_textview_child_comment_indent" class="android.widget.TextView" text="nature_lover Can't wait to go back there!" />
|
||||
</node>
|
||||
</node>
|
||||
|
||||
<!-- Bottom comment box (marker for POST_DETAIL) -->
|
||||
<node index="3" resource-id="com.instagram.android:id/layout_post_detail_comment_textview" class="android.widget.TextView" text="Add a comment..." />
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>
|
||||
218
tests/e2e/fixtures/post_detail_real.xml
Normal file
218
tests/e2e/fixtures/post_detail_real.xml
Normal file
@@ -0,0 +1,218 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_root" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/swipe_navigation_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/layout_container_center_right_coordinator_layout" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_right" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/activity_and_camera_shared_views_main_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="4" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/layout_container_main_panel" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_main_wrapper" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_main" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/swipeable_tab_view_pager" class="androidx.viewpager.widget.ViewPager" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/swipeable_nav_view_pager_inner_recycler_view" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_swipeable" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/sticky_header_list" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/refreshable_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="android:id/list" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2235]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,206]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reels_tray_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,206]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.recyclerview.widget.RecyclerView" package="com.instagram.android" content-desc="reels tray container" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,206]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][294,206]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][294,185]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="Your story" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[72,173][223,185]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[294,173][588,206]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[294,173][588,185]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="iri.lajares" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[375,173][508,185]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[588,173][882,206]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[588,173][882,185]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="cgnmgj" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[680,173][791,185]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/outer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[882,173][1080,206]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/title_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[882,173][1080,185]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="lisa.crmn" resource-id="com.instagram.android:id/username" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[963,173][1080,185]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,2000]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/video_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,2000]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/video_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,2000]" drawing-order="1" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="com.instagram.android:id/media_group" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,2000]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.TextureView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,2000]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,2000]" drawing-order="3" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/video_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,2000]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,2000]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,2000]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,2000]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,2000]" drawing-order="4" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1895][1080,2000]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[938,1895][1080,2000]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="Turn sound on" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[975,1895][1043,1963]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/slideout_iconview_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[992,1912][1026,1946]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,534]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/row_feed_profile_header" class="android.view.ViewGroup" package="com.instagram.android" content-desc="joannehollings posted a video 28 seconds ago" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][1080,343]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,206][128,343]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[29,225][128,324]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/row_feed_photo_profile_imageview" class="android.widget.ImageView" package="com.instagram.android" content-desc="Profile picture of joannehollings" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[39,235][118,314]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="joannehollings " resource-id="com.instagram.android:id/row_feed_photo_profile_name" class="android.widget.Button" package="com.instagram.android" content-desc="joannehollings " checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,206][965,272]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text=" stefanlewisfilms · Original audio" resource-id="com.instagram.android:id/secondary_label" class="android.widget.Button" package="com.instagram.android" content-desc=" stefanlewisfilms · Original audio" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[128,272][965,343]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/media_option_button" class="android.widget.ImageView" package="com.instagram.android" content-desc="More actions for this post" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[965,211][1080,338]" drawing-order="4" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2000][1080,2235]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_view_group_buttons" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2000][1080,2121]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,2000][106,2121]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_button_like" class="android.widget.Button" package="com.instagram.android" content-desc="Like" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,2000][95,2121]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[106,2000][212,2121]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_button_comment" class="android.widget.Button" package="com.instagram.android" content-desc="Comment" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[138,2000][201,2121]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/reposts_ufi_icon" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[212,2000][329,2121]" drawing-order="3" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[212,2000][329,2121]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[244,2000][318,2121]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[345,2000][435,2121]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/row_feed_button_share" class="android.widget.Button" package="com.instagram.android" content-desc="Send post" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[361,2000][424,2121]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="4" text="" resource-id="com.instagram.android:id/row_feed_button_save" class="android.widget.Button" package="com.instagram.android" content-desc="Add to Saved" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[958,2000][1074,2110]" drawing-order="5" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2121][1080,2165]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="Liked by tracy_htznn" resource-id="" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,2121][417,2165]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="tracy_htznn" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[183,2121][394,2165]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="joannehollings This and nowhere to be ....… more" resource-id="" class="com.instagram.ui.widget.textview.IgTextLayoutView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,2176][1080,2225]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="joannehollings" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,2176][279,2220]" drawing-order="0" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="This and nowhere to be ...." checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[288,2176][782,2220]" drawing-order="0" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="This and nowhere to be ...." checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[288,2176][782,2220]" drawing-order="0" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="more" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[815,2176][900,2220]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/action_bar_LinearLayout" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,320]" drawing-order="6" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/tab_bar_shadow" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2234][1080,2235]" drawing-order="5" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/tab_bar" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2235][1080,2361]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/feed_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Home" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="true" visible-to-user="true" bounds="[0,2235][216,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="true" visible-to-user="true" bounds="[76,2266][139,2329]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/clips_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Reels" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[216,2235][432,2361]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[292,2266][355,2329]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/direct_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Message" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[432,2235][648,2361]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[508,2266][571,2329]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/search_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Search and explore" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[648,2235][864,2361]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[724,2266][787,2329]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="4" text="" resource-id="com.instagram.android:id/profile_tab" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Profile" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[864,2235][1080,2361]" drawing-order="5" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/wrapper" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[864,2235][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[933,2259][1012,2338]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/tab_avatar" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[933,2259][1012,2338]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/led_badge" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[991,2311][1017,2337]" drawing-order="3" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/bottom_sheet_camera_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/modal_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/overlay_layout_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2361]" drawing-order="5" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_launch_animation_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.viewinterop.ViewFactoryHolder" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_contents" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[70,0][1010,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][485,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_content" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][247,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_except_heads_up" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][247,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="21:12" resource-id="com.android.systemui:id/clock" class="android.widget.TextView" package="com.android.systemui" content-desc="21:12" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,59][189,117]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/notification_icon_area" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[189,3][247,173]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/notificationIcons" class="android.view.ViewGroup" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[189,3][247,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Android System notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[189,3][247,173]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/cutout_space_view" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[485,3][595,173]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_end_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[595,3][999,173]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_end_side_content" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[772,3][999,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/system_icons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[772,59][999,117]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/statusIcons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[780,59][908,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="Telekom.de, three bars." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[780,59][841,117]" drawing-order="17" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,59][833,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,72][833,104]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[788,72][833,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[841,59][900,117]" drawing-order="18" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,59][892,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,72][892,104]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="Wi-Fi signal full." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,72][892,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/battery" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="Battery charging, 46 percent." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[908,71][971,105]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>
|
||||
21
tests/e2e/fixtures/profile_tagged_tab.xml
Normal file
21
tests/e2e/fixtures/profile_tagged_tab.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_tab_layout" class="android.widget.HorizontalScrollView" package="com.instagram.android" selected="false">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" selected="false">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" selected="false" clickable="true">
|
||||
<node index="0" resource-id="com.instagram.android:id/profile_tab_icon_view" content-desc="Grid view" selected="false" clickable="true" bounds="[0,1465][270,1591]"/>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" selected="false" clickable="true">
|
||||
<node index="0" resource-id="com.instagram.android:id/profile_tab_icon_view" content-desc="Reels" selected="false" clickable="true" bounds="[270,1465][540,1591]"/>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" selected="true" clickable="true">
|
||||
<node index="0" resource-id="com.instagram.android:id/profile_tab_icon_view" content-desc="Photos of you" selected="true" clickable="true" bounds="[810,1465][1080,1591]"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" resource-id="com.instagram.android:id/profile_tab_icon_view" content-desc="Grid view" selected="false" clickable="true" bounds="[0,1465][270,1591]"/>
|
||||
<node resource-id="com.instagram.android:id/profile_tab_icon_view" content-desc="Photos of you" selected="true" clickable="true" bounds="[810,1465][1080,1591]"/>
|
||||
<node resource-id="com.instagram.android:id/sticky_header_list" class="android.widget.ListView"/>
|
||||
<!-- Bottom Nav Bar -->
|
||||
<node resource-id="com.instagram.android:id/profile_tab" selected="true" clickable="true" bounds="[864,2281][1080,2424]"/>
|
||||
</hierarchy>
|
||||
149
tests/e2e/fixtures/story_view_full.xml
Normal file
149
tests/e2e/fixtures/story_view_full.xml
Normal file
@@ -0,0 +1,149 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
|
||||
<hierarchy rotation="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/action_bar_root" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_parent" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,2424]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/layout_container_main" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reel_viewer_root" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/view_pager" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reel_viewer_content_layout" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,2361]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,223]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/reel_view_group" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,223][1080,2361]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reel_item_toolbar_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,223][1080,2311]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2111][1080,2143]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2111][1080,2143]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/reel_item_toolbar_footer_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2143][1080,2311]" drawing-order="8" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2143][1080,2311]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/viewer_reel_item_toolbar_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,2143][1080,2311]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/message_composer_container" class="android.widget.Button" package="com.instagram.android" content-desc="Send message or reaction" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[21,2164][723,2290]" drawing-order="5" hint="" display-id="0">
|
||||
<node index="0" text="Send message" resource-id="com.instagram.android:id/composer_text" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[21,2164][356,2290]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/toolbar_buttons_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[744,2164][1059,2290]" drawing-order="6" hint="" display-id="0">
|
||||
<node NAF="true" index="0" text="" resource-id="com.instagram.android:id/viewer_toolbar_browse_button" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[744,2174][849,2279]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/toolbar_like_container" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,2174][954,2279]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/toolbar_like_button" class="android.widget.ImageView" package="com.instagram.android" content-desc="Like Story" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[849,2174][954,2279]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/toolbar_reshare_button" class="android.widget.ImageView" package="com.instagram.android" content-desc="Send story" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[954,2174][1059,2279]" drawing-order="5" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/reel_viewer_media_layout" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,223][1080,2143]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reel_sticker_overlay_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,223][1080,2143]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reel_avatar_accessibility_sticker_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,223][1080,2143]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/reel_viewer_media_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,223][1080,2143]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.TextureView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,223][1080,2143]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/reel_viewer_top_shadow" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,223][1080,486]" drawing-order="4" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="com.instagram.android:id/reel_viewer_header_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,223][1080,411]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reel_viewer_progress_bar" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,244][1080,248]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/reel_viewer_header" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,248][1080,411]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/profile_picture_container" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,272][116,408]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reel_viewer_profile_picture" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,272][116,356]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="Profile picture" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,272][116,356]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reel_viewer_front_avatar" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[32,272][116,356]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/reel_viewer_text_container" class="android.widget.Button" package="com.instagram.android" content-desc="Highlight title Events, story 2 of 25, 29 May 2025" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[116,269][746,411]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[116,269][522,313]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="Events" resource-id="com.instagram.android:id/reel_viewer_title" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[116,269][282,313]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="29 May 2025" resource-id="com.instagram.android:id/reel_viewer_timestamp" class="android.widget.TextView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[282,269][522,313]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/reel_viewer_attribution_frame_layout" class="android.widget.FrameLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[116,313][439,411]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.Button" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[148,313][439,359]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.instagram.android:id/reel_app_attribution_icon" class="android.widget.ImageView" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[148,324][180,356]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="Watch full reel" resource-id="com.instagram.android:id/reel_app_attribution_action_text" class="android.widget.TextView" package="com.instagram.android" content-desc="Watch full reel" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[196,321][407,359]" drawing-order="2" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="2" text="" resource-id="com.instagram.android:id/reel_header_extras_container" class="android.widget.LinearLayout" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[746,248][1080,374]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="Follow" resource-id="com.instagram.android:id/reel_header_unconnected_follow_button_stub" class="android.widget.TextView" package="com.instagram.android" content-desc="Follow Mission Green Energy" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[778,269][975,353]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/header_menu_button" class="android.widget.ImageView" package="com.instagram.android" content-desc="More actions" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[954,248][1080,374]" drawing-order="11" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="4" text="" resource-id="com.instagram.android:id/reel_viewer_bottom_shadow" class="android.view.View" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1880][1080,2143]" drawing-order="11" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.instagram.android:id/reel_volume_indicator_litho" class="android.view.ViewGroup" package="com.instagram.android" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,173][1080,215]" drawing-order="8" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_launch_animation_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="androidx.compose.ui.viewinterop.ViewFactoryHolder" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_contents" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[70,0][1010,173]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][485,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_content" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][434,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_start_side_except_heads_up" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,3][434,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="23:46" resource-id="com.android.systemui:id/clock" class="android.widget.TextView" package="com.android.systemui" content-desc="23:46" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[81,59][202,117]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/notification_icon_area" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[202,3][434,173]" drawing-order="6" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/notificationIcons" class="android.view.ViewGroup" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[202,3][434,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Android System notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[202,3][260,173]" drawing-order="1" hint="" display-id="0" />
|
||||
<node index="1" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Photos notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[260,3][318,173]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Gotify notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[318,3][376,173]" drawing-order="3" hint="" display-id="0" />
|
||||
<node index="3" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="Amazon Photos notification: " checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[376,3][434,173]" drawing-order="4" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="com.android.systemui:id/cutout_space_view" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[485,3][595,173]" drawing-order="2" hint="" display-id="0" />
|
||||
<node index="2" text="" resource-id="com.android.systemui:id/status_bar_end_side_container" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[595,3][999,173]" drawing-order="3" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_end_side_content" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[782,3][999,173]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/system_icons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[782,59][999,117]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/statusIcons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[790,59][918,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="Telekom.de, three bars." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[790,59][851,117]" drawing-order="17" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[798,59][843,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[798,72][843,104]" drawing-order="4" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/mobile_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[798,72][843,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="3" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[851,59][910,117]" drawing-order="18" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_group" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[859,59][902,117]" drawing-order="1" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[859,72][902,104]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/wifi_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="Wi-Fi signal full." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[859,72][902,104]" drawing-order="1" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
<node index="1" text="" resource-id="" class="androidx.compose.ui.platform.ComposeView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[918,71][988,105]" drawing-order="2" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[918,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="com.android.systemui:id/battery" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[918,71][988,105]" drawing-order="0" hint="" display-id="0">
|
||||
<node index="0" text="" resource-id="" class="android.view.View" package="com.android.systemui" content-desc="Battery 48 per cent." checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[918,71][981,105]" drawing-order="0" hint="" display-id="0" />
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
</hierarchy>
|
||||
48
tests/e2e/test_behavior_ad_guard.py
Normal file
48
tests/e2e/test_behavior_ad_guard.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
E2E tests for Ad Guard and anomaly handling.
|
||||
Ensures the system correctly identifies and skips sponsored content.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from GramAddict.core.behaviors import BehaviorContext
|
||||
from GramAddict.core.behaviors.ad_guard import AdGuardPlugin
|
||||
|
||||
|
||||
@pytest.mark.live_llm
|
||||
def test_ad_guard_detects_sponsored_post(make_real_device_with_image, e2e_configs, e2e_cognitive_stack_factory):
|
||||
"""
|
||||
TDD Test: AdGuardPlugin must successfully identify a sponsored post
|
||||
in a real feed using the TelepathicEngine.
|
||||
"""
|
||||
xml_path = "tests/fixtures/home_feed_with_ad.xml"
|
||||
jpg_path = "tests/fixtures/home_feed_with_ad.jpg"
|
||||
|
||||
with open(xml_path, "r", encoding="utf-8") as f:
|
||||
xml = f.read()
|
||||
|
||||
device = make_real_device_with_image(jpg_path, xml)
|
||||
|
||||
from GramAddict.core.session_state import SessionState
|
||||
|
||||
session_state = SessionState(e2e_configs)
|
||||
|
||||
# REAL cognitive stack — all 12 production keys
|
||||
cognitive_stack = e2e_cognitive_stack_factory(device)
|
||||
|
||||
ctx = BehaviorContext(
|
||||
device=device,
|
||||
configs=e2e_configs,
|
||||
session_state=session_state,
|
||||
username="test_ad_user",
|
||||
context_xml=xml,
|
||||
cognitive_stack=cognitive_stack,
|
||||
)
|
||||
|
||||
plugin = AdGuardPlugin()
|
||||
|
||||
result = plugin.execute(ctx)
|
||||
|
||||
# Executed should be True, meaning it triggered and took action (scrolled past the ad)
|
||||
assert result.executed is True, "AdGuardPlugin failed to detect the sponsored post!"
|
||||
assert result.should_skip is True, "AdGuardPlugin executed but did not set should_skip"
|
||||
75
tests/e2e/test_behavior_scrape_profile.py
Normal file
75
tests/e2e/test_behavior_scrape_profile.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
E2E tests for the Scrape Profile behavior.
|
||||
Ensures the VLM can extract Followers, Following, and Bio text accurately
|
||||
from a real profile dump without hardcoded structural guards.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from GramAddict.core.behaviors import BehaviorContext
|
||||
from GramAddict.core.behaviors.scrape_profile import ScrapeProfilePlugin
|
||||
|
||||
|
||||
@pytest.mark.live_llm
|
||||
def test_scrape_profile_extracts_data_correctly(make_real_device_with_image, e2e_configs, e2e_cognitive_stack_factory):
|
||||
"""
|
||||
TDD Test: ScrapeProfilePlugin must use the TelepathicEngine to correctly
|
||||
identify the Follower count, Following count, and Bio text nodes on a real profile.
|
||||
"""
|
||||
xml_path = "tests/fixtures/scraping_profile_dump.xml"
|
||||
jpg_path = "tests/fixtures/scraping_profile_dump.jpg"
|
||||
|
||||
with open(xml_path, "r", encoding="utf-8") as f:
|
||||
xml = f.read()
|
||||
|
||||
device = make_real_device_with_image(jpg_path, xml)
|
||||
|
||||
from GramAddict.core.session_state import SessionState
|
||||
|
||||
# Override only what the test needs — fixture provides all production defaults
|
||||
e2e_configs.args.scrape_profiles = True
|
||||
session_state = SessionState(e2e_configs)
|
||||
|
||||
# REAL cognitive stack — all 12 production keys
|
||||
cognitive_stack = e2e_cognitive_stack_factory(device)
|
||||
|
||||
# Spy wrapper: capture enrichment data for assertions while exercising real CRM
|
||||
crm = cognitive_stack["crm"]
|
||||
_original_enrich = crm.enrich_lead
|
||||
_enriched_data = {}
|
||||
|
||||
def _spy_enrich_lead(username, data):
|
||||
_enriched_data.update(data)
|
||||
_original_enrich(username, data)
|
||||
|
||||
crm.enrich_lead = _spy_enrich_lead
|
||||
|
||||
ctx = BehaviorContext(
|
||||
device=device,
|
||||
configs=e2e_configs,
|
||||
session_state=session_state,
|
||||
username="test_scrape_user",
|
||||
context_xml=xml,
|
||||
cognitive_stack=cognitive_stack,
|
||||
)
|
||||
|
||||
plugin = ScrapeProfilePlugin()
|
||||
|
||||
# Execute the behavior
|
||||
result = plugin.execute(ctx)
|
||||
|
||||
assert result.executed is True, "ScrapeProfilePlugin did not execute successfully"
|
||||
assert len(_enriched_data) > 0, "CRM enrich_lead was not called"
|
||||
|
||||
# Check the scraped data accuracy
|
||||
data = _enriched_data
|
||||
|
||||
assert data["username"] == "test_scrape_user"
|
||||
|
||||
# We don't assert the exact number because we don't know what's in scraping_profile_dump.xml
|
||||
# But it should not be "unknown" if the VLM successfully found the counts.
|
||||
assert data["followers"] != "unknown", "VLM failed to extract Followers count"
|
||||
assert data["following"] != "unknown", "VLM failed to extract Following count"
|
||||
|
||||
# If we look inside scraping_profile_dump.xml, followers might be e.g. "1.5M", following "123".
|
||||
# Just asserting they are extracted is enough to prove the visual discovery works.
|
||||
85
tests/e2e/test_behavior_story_view.py
Normal file
85
tests/e2e/test_behavior_story_view.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
E2E tests for the Story View behavior.
|
||||
Ensures the system correctly identifies and clicks the story ring.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from GramAddict.core.behaviors import BehaviorContext
|
||||
from GramAddict.core.behaviors.story_view import StoryViewPlugin
|
||||
|
||||
|
||||
@pytest.mark.live_llm
|
||||
def test_story_view_clicks_story_ring(make_real_device_with_image, e2e_configs, e2e_cognitive_stack_factory):
|
||||
"""
|
||||
TDD Test: StoryViewPlugin must correctly identify if a story exists
|
||||
and trigger the 'tap story ring avatar' navigation.
|
||||
"""
|
||||
xml_path = "tests/fixtures/home_feed_with_ad.xml"
|
||||
|
||||
with open(xml_path, "r", encoding="utf-8") as f:
|
||||
xml = f.read()
|
||||
|
||||
# The actual image (home_feed_with_ad.jpg) has story rings at the top,
|
||||
# but the XML doesn't contain the specific 'reel_ring' ID that triggers has_story.
|
||||
import re
|
||||
|
||||
# We inject it so the plugin knows there is a story, AND we add a content-desc so the Text VLM easily finds it.
|
||||
xml_before = re.sub(
|
||||
r'resource-id="com\.instagram\.android:id/row_feed_photo_profile_imageview"([^>]+)content-desc="Profile picture of millionlords"',
|
||||
r'resource-id="com.instagram.android:id/reel_ring"\1content-desc="Story ring avatar"',
|
||||
xml,
|
||||
)
|
||||
|
||||
# After tapping the story, the UI should change. We provide story_view_full.xml as the "after" state
|
||||
with open("tests/fixtures/story_view_full.xml", "r", encoding="utf-8") as f:
|
||||
xml_after = f.read()
|
||||
|
||||
# The sequence of dumps for plugin.execute() calling nav_graph.do:
|
||||
# 1. goap.perceive() (xml_before)
|
||||
# 2. goap._execute_action find_node (xml_before)
|
||||
# 3. goap verification post-click (xml_after)
|
||||
# 4. Fallbacks/extras (xml_after, xml_after)
|
||||
device = make_real_device_with_image(
|
||||
"tests/fixtures/home_feed_with_ad.jpg", [xml_before, xml_after, xml_after, xml_after]
|
||||
)
|
||||
|
||||
from GramAddict.core.session_state import SessionState
|
||||
|
||||
# Override only what the test needs — fixture provides all production defaults
|
||||
e2e_configs.args.stories_percentage = 100
|
||||
e2e_configs.args.stories_count = "1"
|
||||
session_state = SessionState(e2e_configs)
|
||||
|
||||
# REAL cognitive stack — all 12 production keys
|
||||
cognitive_stack = e2e_cognitive_stack_factory(device)
|
||||
|
||||
ctx = BehaviorContext(
|
||||
device=device,
|
||||
configs=e2e_configs,
|
||||
session_state=session_state,
|
||||
username="test_story_user",
|
||||
context_xml=xml_before,
|
||||
cognitive_stack=cognitive_stack,
|
||||
)
|
||||
|
||||
plugin = StoryViewPlugin()
|
||||
|
||||
# Execute should return True because it found a reel ring, attempted to navigate to it,
|
||||
# and clicked it. The DeviceFacade records the press.
|
||||
result = plugin.execute(ctx)
|
||||
|
||||
assert result.executed is True, f"StoryViewPlugin failed to execute. Reason: {result.metadata.get('reason')}"
|
||||
|
||||
# We can verify that the device facade recorded a click!
|
||||
assert len(device.deviceV2.interaction_log) > 0, "No interactions were recorded on the device"
|
||||
|
||||
# Specifically, there should be a click from the find_node or a direct bounds tap
|
||||
# We know the VLM should have found the reel ring and clicked it
|
||||
click_found = False
|
||||
for interaction in device.deviceV2.interaction_log:
|
||||
if interaction["action"] == "click":
|
||||
click_found = True
|
||||
break
|
||||
|
||||
assert click_found, f"Device did not record any click action. Log: {device.deviceV2.interaction_log}"
|
||||
99
tests/e2e/test_bot_flow_curiosity.py
Normal file
99
tests/e2e/test_bot_flow_curiosity.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Tests for Bot Flow Curiosity Logic
|
||||
==================================
|
||||
Ensures that the spontaneous CHECK_CURIOSITY feature correctly shifts context
|
||||
and relies on structural safety (navigating to HomeFeed) rather than hallucinating
|
||||
clicks on invalid screens like Profiles.
|
||||
"""
|
||||
|
||||
from GramAddict.core.bot_flow import _run_zero_latency_feed_loop
|
||||
from GramAddict.core.session_state import SessionState
|
||||
from tests.e2e.conftest import load_fixture_xml
|
||||
from tests.e2e.device_emulator import create_emulator_facade
|
||||
|
||||
HOME_FEED_XML = load_fixture_xml("home_feed_real.xml")
|
||||
PROFILE_XML = load_fixture_xml("other_profile_real.xml")
|
||||
|
||||
|
||||
def _build_curiosity_state_machine():
|
||||
states = {
|
||||
"home_feed": HOME_FEED_XML,
|
||||
"other_profile": PROFILE_XML,
|
||||
}
|
||||
|
||||
transitions = {
|
||||
"home_feed": {
|
||||
"clicks": [
|
||||
({"id": "com.instagram.android:id/row_feed_photo_profile_name"}, "other_profile"),
|
||||
],
|
||||
"press": [
|
||||
("back", "home_feed"),
|
||||
],
|
||||
},
|
||||
"other_profile": {
|
||||
"clicks": [
|
||||
({"desc": "Home"}, "home_feed"),
|
||||
({"id": "com.instagram.android:id/feed_tab"}, "home_feed"),
|
||||
],
|
||||
"press": [
|
||||
("back", "home_feed"),
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
return states, transitions
|
||||
|
||||
|
||||
def test_curiosity_navigates_to_homefeed_before_checking(
|
||||
e2e_configs, e2e_cognitive_stack_factory, setup_e2e_plugin_registry, monkeypatch
|
||||
):
|
||||
"""
|
||||
Simulates the feed loop starting on other_profile.
|
||||
When CHECK_CURIOSITY triggers, it must FIRST navigate to HomeFeed before
|
||||
trying to 'tap heart icon notifications'.
|
||||
"""
|
||||
# We do NOT mock production code. No bf.sleep or _humanized_scroll mocking.
|
||||
import random
|
||||
import time
|
||||
|
||||
random.seed(31) # First random.random() is 0.057 < 0.06, triggers CHECK_CURIOSITY
|
||||
|
||||
# 1. Setup emulator starting on other_profile
|
||||
states, transitions = _build_curiosity_state_machine()
|
||||
device, emulator = create_emulator_facade("other_profile", states, transitions, monkeypatch)
|
||||
|
||||
# 2. Setup REAL Config & SessionState
|
||||
configs = e2e_configs
|
||||
|
||||
session_state = SessionState(configs)
|
||||
|
||||
# 4. Setup Cognitive Stack
|
||||
cognitive_stack = e2e_cognitive_stack_factory(device)
|
||||
nav_graph = cognitive_stack["nav_graph"]
|
||||
zero_engine = cognitive_stack["zero_engine"]
|
||||
dopamine = cognitive_stack["dopamine"]
|
||||
growth = cognitive_stack["growth_brain"]
|
||||
|
||||
# Force CHECK_CURIOSITY
|
||||
monkeypatch.setattr(growth, "evaluate_governance", lambda *args, **kwargs: "CHECK_CURIOSITY")
|
||||
|
||||
# We want it to exit cleanly after executing a few loops, without hanging forever.
|
||||
dopamine.session_limit_seconds = 0.1
|
||||
dopamine.session_start = time.time()
|
||||
|
||||
# 5. Run the loop (starting from other_profile)
|
||||
# 5. Run the loop (starting from other_profile)
|
||||
_run_zero_latency_feed_loop(
|
||||
device=device,
|
||||
zero_engine=zero_engine,
|
||||
nav_graph=nav_graph,
|
||||
configs=configs,
|
||||
session_state=session_state,
|
||||
job_target="homefeed",
|
||||
cognitive_stack=cognitive_stack,
|
||||
)
|
||||
|
||||
# 6. Assertions: Must navigate to HomeFeed FIRST
|
||||
assert emulator.current_state == "home_feed", (
|
||||
"Curiosity failed to navigate to HomeFeed before executing! " f"Bot is stuck on {emulator.current_state}"
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user