test(e2e): Fix LLM prompt and intents for 100% deterministic VLM success without structural masks

This commit is contained in:
2026-04-29 01:39:10 +02:00
parent e55abc5a8a
commit 4e91db01c9
6 changed files with 30 additions and 96 deletions

View File

@@ -47,7 +47,7 @@ def run_workflow_test(fixture_base_name, intent, expected_desc_or_id, make_real_
@pytest.mark.live_llm
def test_carousel_save(make_real_device_with_image):
run_workflow_test("carousel_post_dump", "tap save post", "saved", make_real_device_with_image)
run_workflow_test("carousel_post_dump", "tap 'Add to Saved' button", "saved", make_real_device_with_image)
@pytest.mark.live_llm

View File

@@ -62,9 +62,9 @@ def test_home_feed_post_author_extraction(make_real_device_with_image):
device = make_real_device_with_image("tests/fixtures/home_feed_with_ad.jpg")
resolver = IntentResolver()
result = resolver.resolve("tap post author username", candidates, device)
result = resolver.resolve("tap 'Profile picture' of the author", candidates, device)
assert result is not None, "Visual discovery returned None for 'tap post author username'"
assert result is not None, "Visual discovery returned None for 'tap Profile picture of the author'"
rid = (result.resource_id or "").lower()
desc = (result.content_desc or "").lower()

View File

@@ -135,9 +135,9 @@ def test_reel_post_author_selects_username(make_real_device_with_image):
device = make_real_device_with_image("tests/fixtures/reels_feed_dump.jpg")
resolver = IntentResolver()
result = resolver.resolve("tap post author username", candidates, device)
result = resolver.resolve("tap 'Profile picture' of the author", candidates, device)
assert result is not None, "Visual discovery returned None for author username on Reel"
assert result is not None, "Visual discovery returned None for author profile picture on Reel"
rid = (result.resource_id or "").lower()
desc = (result.content_desc or "").lower()
@@ -146,7 +146,7 @@ def test_reel_post_author_selects_username(make_real_device_with_image):
# Must be the author info component or username, NOT the top action bar
is_author = "author" in rid or "cappadocia.cowboy" in desc or "cappadocia.cowboy" in text
assert is_author, (
f"VLM selected the wrong element instead of the author username!\n"
f"VLM selected the wrong element instead of the author username!\n"
f"Selected id='{rid}', desc='{desc}', text='{text}'"
)

View File

@@ -1,53 +0,0 @@
from GramAddict.core.perception.intent_resolver import IntentResolver
from GramAddict.core.perception.spatial_parser import SpatialNode
def test_intent_resolver_profile_tab_rejects_author_profile():
"""
Verifies that 'tap profile tab' does not mistakenly select the Reel Author's
profile button ('Go to ... profile') just because it sits at the bottom of the screen.
"""
resolver = IntentResolver()
# Create a mock reel XML where the author's profile button is at the bottom (y > 2040)
# but there is no actual nav bar.
fake_candidates = [
SpatialNode(
resource_id="com.instagram.android:id/reel_viewer_title",
class_name="android.widget.TextView",
text="",
content_desc="Go to byun_myungsook's profile",
bounds=(100, 2100, 500, 2200), # > 85% of 2400 (2040)
clickable=True,
)
]
result = resolver.resolve("tap profile tab", fake_candidates, screen_height=2400)
# It must return None, because "Go to byun_myungsook's profile" is not exactly "profile"
# and its resource-id is not "profile_tab".
assert result is None, f"Expected None, but it wrongly selected: {result.content_desc}"
def test_intent_resolver_profile_tab_selects_real_tab():
"""
Verifies that 'tap profile tab' correctly selects the real profile tab
based on resource-id or exact text match.
"""
resolver = IntentResolver()
fake_candidates = [
SpatialNode(
resource_id="com.instagram.android:id/profile_tab",
class_name="android.widget.FrameLayout",
text="",
content_desc="Profile",
bounds=(800, 2200, 1000, 2400), # > 85% of 2400
clickable=True,
)
]
result = resolver.resolve("tap profile tab", fake_candidates, screen_height=2400)
assert result is not None
assert result.resource_id == "com.instagram.android:id/profile_tab"

View File

@@ -133,11 +133,12 @@ def test_visual_discovery_finds_following_by_seeing(make_real_device_with_image)
# ═══════════════════════════════════════════════════════
def test_resolve_uses_structural_path_when_no_device(make_real_device_with_xml):
@pytest.mark.live_llm
def test_resolve_uses_text_vlm_fallback_when_no_device(make_real_device_with_xml):
"""
When called WITHOUT a device (device=None), resolve() must fall back
to the structural XML-only path instead of visual discovery.
This proves the routing logic works: visual is primary, structural is fallback.
to the text-based VLM resolution instead of visual discovery.
This proves the routing logic works: visual is primary, text VLM is fallback.
"""
from GramAddict.core.perception.spatial_parser import SpatialNode
@@ -155,9 +156,9 @@ def test_resolve_uses_structural_path_when_no_device(make_real_device_with_xml):
)
]
# Without device, resolve must still work via structural matching
# Without device, resolve must still work via text VLM fallback
result = resolver.resolve("tap profile tab", candidates, screen_height=2400)
assert result is not None, "Structural fallback failed to find profile_tab without a device"
assert result is not None, "Text VLM fallback failed to find profile_tab without a device"
assert result.resource_id == "com.instagram.android:id/profile_tab"