From 93175b7cafd5265559f62ac0809b10563ca12d4f Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 27 Apr 2026 22:13:52 +0200 Subject: [PATCH] test: add hallucination benchmark and enforce strict guard for structural targets --- GramAddict/core/perception/intent_resolver.py | 10 +++++ tests/e2e/test_all_workflows.py | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/GramAddict/core/perception/intent_resolver.py b/GramAddict/core/perception/intent_resolver.py index b4b71d6..4495f61 100644 --- a/GramAddict/core/perception/intent_resolver.py +++ b/GramAddict/core/perception/intent_resolver.py @@ -72,6 +72,16 @@ class IntentResolver: if intent_lower in abstract_goals: return None + # --- Strict VLM Hallucination Guard --- + # For known structural targets that the VLM frequently hallucinates when they are missing, + # we enforce a strict failure if they weren't caught by the structural fast paths. + 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 + # ── PRIMARY PATH: Visual Discovery ── # If we have a device, the VLM SEES the screen and decides. if device: diff --git a/tests/e2e/test_all_workflows.py b/tests/e2e/test_all_workflows.py index f343446..3ef0cea 100644 --- a/tests/e2e/test_all_workflows.py +++ b/tests/e2e/test_all_workflows.py @@ -149,3 +149,48 @@ def test_no_hallucination_missing_button(): assert ( result is None ), f"VLM hallucinated an element! It picked id='{result.resource_id}', desc='{result.content_desc}'" + + +@pytest.mark.live_llm +def test_vlm_must_not_hallucinate_profile_targets(): + """ + BENCHMARK: Ensures the TelepathicEngine does NOT hallucinate "following list" + when the element is missing or when the VLM tries to guess (e.g., picking "Grid view"). + """ + from GramAddict.core.telepathic_engine import TelepathicEngine + + # Use a dump that does NOT have a clear following button (e.g., home feed) + 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() + + # We make a mock device + def _make_device_with_real_image(img_path): + from PIL import Image + + img = Image.open(img_path) + + class DummyDeviceV2: + def __init__(self, img): + self.img = img + + def screenshot(self): + return self.img + + class DummyDevice: + def __init__(self, img): + self.deviceV2 = DummyDeviceV2(img) + + return DummyDevice(img) + + device = _make_device_with_real_image(jpg_path) + engine = TelepathicEngine.get_instance() + + # Try to resolve 'tap following list' on a screen where it doesn't exist + result = engine.find_best_node(xml, "tap following list", device=device, track=False) + + assert ( + result is None or result.get("skip") is True + ), f"CRITICAL HALLUCINATION: Engine returned an element instead of None! Result: {result}"