fix(sae): purge all hardcoded fallback coords — autonomous structural dismiss

- Removed hardcoded (540,150) fallback in _plan_escape_via_llm
- Added _find_structural_dismiss_target: scans raw XML for clickable
  dismiss/cancel/close buttons and extracts coords from bounds attrs
- LLM repeat detection now falls back to structural scan (not magic numbers)
- Temperature increases progressively when LLM is stubborn (0.1 → 0.7)
- Failure history injected as CRITICAL block into LLM system prompt
- Extended pre-commit test discovery to include tests/tdd/
- TDD: 7 new tests including meta-tests proving zero hardcoded coords
- Full suite: 100/100 passed
This commit is contained in:
2026-05-05 15:30:45 +02:00
parent f8fc8ace8e
commit 8d58c3cf89
3 changed files with 367 additions and 40 deletions

View File

@@ -0,0 +1,206 @@
"""
TDD: Structural Dismiss Target Discovery
Proves that the SAE finds dismiss/close buttons by scanning
the XML structurally — zero hardcoded coordinates.
All coordinates come from bounds attributes in the XML itself.
"""
from GramAddict.core.situational_awareness import (
SituationalAwarenessEngine,
)
class DummyDeviceV2:
def __init__(self):
self.info = {"screenOn": True}
class DummyDevice:
def __init__(self):
self.deviceV2 = DummyDeviceV2()
self.app_id = "com.instagram.android"
self.pressed = []
self.clicks = []
def press(self, key):
self.pressed.append(key)
def click(self, x, y):
self.clicks.append((x, y))
# ── XML Fixtures ──
BOTTOM_SHEET_WITH_CANCEL = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" package="com.instagram.android"
resource-id="com.instagram.android:id/bottom_sheet_container"
class="android.widget.FrameLayout" clickable="false"
bounds="[0,800][1080,2424]">
<node index="0" text="Mute" clickable="true" bounds="[100,900][980,1000]" />
<node index="1" text="Restrict" clickable="true" bounds="[100,1010][980,1110]" />
<node index="2" text="Block" clickable="true" bounds="[100,1120][980,1220]" />
<node index="3" text="Cancel" clickable="true" bounds="[100,1400][980,1500]" />
</node>
</hierarchy>
"""
BOTTOM_SHEET_WITH_CLOSE_DESC = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" package="com.instagram.android"
resource-id="com.instagram.android:id/action_sheet_container"
class="android.widget.FrameLayout" clickable="false"
bounds="[0,600][1080,2424]">
<node index="0" text="" content-desc="Close" clickable="true" bounds="[900,620][980,700]" />
<node index="1" text="Share to..." clickable="true" bounds="[100,800][980,900]" />
<node index="2" text="Copy Link" clickable="true" bounds="[100,910][980,1010]" />
</node>
</hierarchy>
"""
BOTTOM_SHEET_NO_DISMISS = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" package="com.instagram.android"
resource-id="com.instagram.android:id/bottom_sheet_container"
class="android.widget.FrameLayout" clickable="false"
bounds="[0,800][1080,2424]">
<node index="0" text="Mute" clickable="true" bounds="[100,900][980,1000]" />
<node index="1" text="Restrict" clickable="true" bounds="[100,1010][980,1110]" />
<node index="2" text="Block" clickable="true" bounds="[100,1120][980,1220]" />
</node>
</hierarchy>
"""
NOT_NOW_DIALOG = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" package="com.instagram.android"
resource-id="com.instagram.android:id/survey_overlay_container"
class="android.widget.FrameLayout" clickable="false"
bounds="[0,0][1080,2424]">
<node index="0" text="Turn on Notifications?" clickable="false" bounds="[100,800][980,900]" />
<node index="1" text="Turn On" clickable="true" bounds="[100,950][980,1050]" />
<node index="2" text="Not Now" clickable="true" bounds="[100,1060][980,1160]" />
</node>
</hierarchy>
"""
GERMAN_DISMISS_DIALOG = """<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" package="com.instagram.android"
resource-id="com.instagram.android:id/interstitial_container"
class="android.widget.FrameLayout" clickable="false"
bounds="[0,0][1080,2424]">
<node index="0" text="Benachrichtigungen aktivieren?" clickable="false" bounds="[100,800][980,900]" />
<node index="1" text="Aktivieren" clickable="true" bounds="[100,950][980,1050]" />
<node index="2" text="Nicht jetzt" clickable="true" bounds="[100,1060][980,1160]" />
</node>
</hierarchy>
"""
# ── Tests ──
class TestStructuralDismissTarget:
"""Verify that _find_structural_dismiss_target discovers buttons from XML, not hardcoded values."""
def _get_sae(self):
device = DummyDevice()
return SituationalAwarenessEngine.get_instance(device)
def test_finds_cancel_button_from_xml(self):
"""RED: structural scan must find 'Cancel' button with XML-derived coordinates."""
sae = self._get_sae()
result = sae._find_structural_dismiss_target(BOTTOM_SHEET_WITH_CANCEL)
assert result is not None, "Should have found the Cancel button"
assert result.action_type == "click"
# Coordinates must come from bounds="[100,1400][980,1500]" → center (540, 1450)
assert result.x == 540
assert result.y == 1450
assert "cancel" in result.reason.lower()
def test_finds_close_via_content_desc(self):
"""RED: structural scan must find 'Close' button via content-desc attribute."""
sae = self._get_sae()
result = sae._find_structural_dismiss_target(BOTTOM_SHEET_WITH_CLOSE_DESC)
assert result is not None, "Should have found the Close button via content-desc"
assert result.action_type == "click"
# bounds="[900,620][980,700]" → center (940, 660)
assert result.x == 940
assert result.y == 660
assert "close" in result.reason.lower()
def test_returns_none_when_no_dismiss_exists(self):
"""RED: if the XML has no dismiss-type button, return None (don't invent one)."""
sae = self._get_sae()
result = sae._find_structural_dismiss_target(BOTTOM_SHEET_NO_DISMISS)
assert result is None, "Must not invent dismiss targets when none exist in XML"
def test_finds_not_now_button(self):
"""RED: structural scan must find 'Not Now' button."""
sae = self._get_sae()
result = sae._find_structural_dismiss_target(NOT_NOW_DIALOG)
assert result is not None, "Should have found 'Not Now' button"
assert result.action_type == "click"
# bounds="[100,1060][980,1160]" → center (540, 1110)
assert result.x == 540
assert result.y == 1110
def test_finds_german_nicht_jetzt(self):
"""RED: structural scan must find German 'Nicht jetzt' button."""
sae = self._get_sae()
result = sae._find_structural_dismiss_target(GERMAN_DISMISS_DIALOG)
assert result is not None, "Should have found 'Nicht jetzt' button"
assert result.action_type == "click"
# bounds="[100,1060][980,1160]" → center (540, 1110)
assert result.x == 540
assert result.y == 1110
def test_no_hardcoded_coordinates_in_method(self):
"""
META-TEST: Prove that _find_structural_dismiss_target contains
zero hardcoded pixel values. All coordinates must come from XML parsing.
"""
import inspect
source = inspect.getsource(SituationalAwarenessEngine._find_structural_dismiss_target)
# Should NOT contain any hardcoded coordinate patterns like x=540, y=150, etc.
# The only numbers allowed are in the regex patterns, not in EscapeAction constructors
import re
# Find all EscapeAction instantiations with literal x= or y= values
hardcoded_coords = re.findall(r"EscapeAction\([^)]*(?:x=\d+|y=\d+)", source)
assert len(hardcoded_coords) == 0, (
f"Found hardcoded coordinates in _find_structural_dismiss_target: {hardcoded_coords}. "
"All coordinates must come from XML bounds parsing."
)
class TestLLMFallbackNoHardcoding:
"""Verify that _plan_escape_via_llm does NOT use hardcoded fallback coordinates."""
def test_no_hardcoded_coordinates_in_llm_planner(self):
"""
META-TEST: Prove that _plan_escape_via_llm contains zero hardcoded
pixel coordinates as fallback values. When the LLM repeats, it must
delegate to _find_structural_dismiss_target (which reads from XML).
"""
import inspect
import re
source = inspect.getsource(SituationalAwarenessEngine._plan_escape_via_llm)
# Find all EscapeAction("click", x=<number>, y=<number>) with hardcoded coords
hardcoded = re.findall(r'EscapeAction\(\s*"click"\s*,\s*x=\d+\s*,\s*y=\d+', source)
assert len(hardcoded) == 0, (
f"Found hardcoded click coordinates in _plan_escape_via_llm: {hardcoded}. "
"The bot must discover coordinates autonomously from the XML."
)