feat(core): complete P1 hardening phase - ContextGate, Localization Purge, SoM Safety Clamp
This commit is contained in:
65
tests/core/test_context_gate.py
Normal file
65
tests/core/test_context_gate.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
P1-5: ContextGate Interaction Guards
|
||||
|
||||
TDD RED: These tests enforce that:
|
||||
1. Actions are blocked if they are structurally impossible on the current screen.
|
||||
2. 'follow' is blocked on OWN_PROFILE.
|
||||
3. 'comment' is blocked if no comment button is present in the XML.
|
||||
4. 'like' is blocked if no heart/like button is present.
|
||||
"""
|
||||
|
||||
from GramAddict.core.perception.context_gate import ContextGate
|
||||
from GramAddict.core.perception.screen_identity import ScreenType
|
||||
|
||||
|
||||
class TestContextGate:
|
||||
def test_block_follow_on_own_profile(self):
|
||||
"""RED: Following yourself is impossible and usually a sign of navigation drift."""
|
||||
gate = ContextGate()
|
||||
|
||||
# Mock screen state
|
||||
screen = {
|
||||
"screen_type": ScreenType.OWN_PROFILE,
|
||||
"resource_ids": {"profile_tab", "profile_edit_button"},
|
||||
"xml": "<node resource-id='profile_edit_button' />",
|
||||
}
|
||||
|
||||
assert gate.is_allowed("follow", screen) is False, "Should block 'follow' on OWN_PROFILE"
|
||||
|
||||
def test_block_comment_without_button(self):
|
||||
"""RED: Blocking comment if the button isn't there (e.g. on a profile view)."""
|
||||
gate = ContextGate()
|
||||
|
||||
screen = {
|
||||
"screen_type": ScreenType.OTHER_PROFILE,
|
||||
"resource_ids": {"button_follow", "action_bar_overflow_icon"},
|
||||
"xml": "<node resource-id='button_follow' />",
|
||||
}
|
||||
|
||||
# Commenting on a profile page is impossible (must be on post detail or feed)
|
||||
assert gate.is_allowed("comment", screen) is False, "Should block 'comment' if no comment button is present"
|
||||
|
||||
def test_allow_like_on_feed(self):
|
||||
"""GREEN (Target): Allow if the button exists."""
|
||||
gate = ContextGate()
|
||||
|
||||
screen = {
|
||||
"screen_type": ScreenType.HOME_FEED,
|
||||
"resource_ids": {"row_feed_button_like", "row_feed_button_comment"},
|
||||
"xml": "<node resource-id='row_feed_button_like' />",
|
||||
}
|
||||
|
||||
assert gate.is_allowed("like", screen) is True
|
||||
|
||||
def test_block_post_interaction_on_dm_thread(self):
|
||||
"""RED: Prevent accidental likes/comments in DM threads."""
|
||||
gate = ContextGate()
|
||||
|
||||
screen = {
|
||||
"screen_type": ScreenType.DM_THREAD,
|
||||
"resource_ids": {"direct_text_input", "direct_thread_header"},
|
||||
"xml": "<node resource-id='direct_text_input' />",
|
||||
}
|
||||
|
||||
assert gate.is_allowed("like", screen) is False
|
||||
assert gate.is_allowed("comment", screen) is False
|
||||
45
tests/core/test_intent_resolver_hardening.py
Normal file
45
tests/core/test_intent_resolver_hardening.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
P1-7: IntentResolver Box Index Clamping
|
||||
|
||||
TDD RED: These tests enforce that:
|
||||
1. VLM responses with string box indices are correctly parsed as integers.
|
||||
2. Hallucinated indices (OOB) are gracefully rejected without crashing.
|
||||
3. Common VLM hallucination patterns (e.g. {"box": "Box 5"}) are handled.
|
||||
"""
|
||||
|
||||
from GramAddict.core.perception.intent_resolver import IntentResolver
|
||||
from GramAddict.core.perception.spatial_parser import SpatialNode
|
||||
|
||||
|
||||
class TestIntentResolverHardening:
|
||||
def test_parse_box_index_variants(self):
|
||||
"""RED: Resolver should handle various VLM JSON formats and types."""
|
||||
resolver = IntentResolver()
|
||||
|
||||
# Test basic int
|
||||
assert resolver._parse_box_index({"box": 5}) == 5
|
||||
|
||||
# Test string int
|
||||
assert resolver._parse_box_index({"box": "5"}) == 5
|
||||
|
||||
# Test prefixed string
|
||||
assert resolver._parse_box_index({"box": "Box 5"}) == 5
|
||||
|
||||
# Test alternate keys
|
||||
assert resolver._parse_box_index({"selected_index": 3}) == 3
|
||||
assert resolver._parse_box_index({"box_index": "7"}) == 7
|
||||
|
||||
# Test invalid
|
||||
assert resolver._parse_box_index({"box": "none"}) is None
|
||||
assert resolver._parse_box_index({"box": "null"}) is None
|
||||
|
||||
def test_box_clamping_rejection(self):
|
||||
"""RED: Reject indices that are not in the box_map."""
|
||||
resolver = IntentResolver()
|
||||
box_map = {0: SpatialNode(0, 0, 10, 10), 1: SpatialNode(10, 10, 20, 20)}
|
||||
|
||||
# Index 99 is OOB
|
||||
assert resolver._validate_and_get_node(99, box_map) is None
|
||||
|
||||
# Index 1 is valid
|
||||
assert resolver._validate_and_get_node(1, box_map) is not None
|
||||
Reference in New Issue
Block a user