feat(core): P0 radical evolution — kill blank_start, complete ContextGate matrix, fix HD Map back transitions
P0-2: Kill blank_start: true in config — the nuclear option that wipes ALL learned knowledge on every run. Replaced with memory_hygiene() selective amnesia that prunes low-confidence entries while preserving high-confidence learned patterns. P0-3: Purge all root-level garbage scripts (scratch.py, test_run.py, profile_dump.xml, resp_dump.json, pytest_output.log, test_output.log, coverage.xml, coverage_e2e.json). Updated .gitignore to prevent reaccumulation. P2-2: Replace piecemeal BANNED_SCREENS/REQUIRED_MARKERS with complete Action Compatibility Matrix (VALID_SCREENS whitelist). Every interaction intent now has an explicit set of valid screens. Covers like, comment, follow, unfollow, save, repost across all 14 screen types. P2-3: Remove non-deterministic 'press back' transitions from HD Map topology for OTHER_PROFILE, POST_DETAIL, and SEARCH_RESULTS. Add tab transitions to POST_DETAIL. P2-4: Replace ScreenMemoryDB nuclear 200-entry wipe with LRU eviction. store_screen() now accepts confidence parameter. TDD: 22 new tests (all GREEN), 240 unit/tdd/integration passed, 0 regressions. E2E: 288 passed (+2 fixed), 6 pre-existing LIE DETECTED failures.
This commit is contained in:
147
tests/tdd/test_context_gate_matrix.py
Normal file
147
tests/tdd/test_context_gate_matrix.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
TDD: ContextGate Action Compatibility Matrix
|
||||
|
||||
Tests that the ContextGate blocks ALL structurally impossible actions,
|
||||
not just the ones that happen to have matching resource-id markers.
|
||||
|
||||
Red-Green-Blue: These tests MUST fail first, then we implement.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from GramAddict.core.perception.context_gate import ContextGate
|
||||
from GramAddict.core.perception.screen_identity import ScreenType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gate():
|
||||
return ContextGate()
|
||||
|
||||
|
||||
def _make_screen(screen_type: ScreenType, resource_ids=None):
|
||||
"""Helper to create a minimal screen_state dict."""
|
||||
return {
|
||||
"screen_type": screen_type,
|
||||
"resource_ids": resource_ids or set(),
|
||||
"available_actions": [],
|
||||
"context": {},
|
||||
}
|
||||
|
||||
|
||||
class TestContextGateCategoricalBans:
|
||||
"""P2-2: Every screen/action combination that is structurally impossible
|
||||
must be categorically banned — not reliant on marker presence."""
|
||||
|
||||
def test_like_blocked_on_story_view(self, gate):
|
||||
"""Cannot like on STORY_VIEW — there's no feed-style like button."""
|
||||
screen = _make_screen(ScreenType.STORY_VIEW)
|
||||
assert gate.is_allowed("like", screen) is False
|
||||
|
||||
def test_comment_blocked_on_story_view(self, gate):
|
||||
"""Cannot comment on STORY_VIEW — reply is a different action."""
|
||||
screen = _make_screen(ScreenType.STORY_VIEW)
|
||||
assert gate.is_allowed("comment", screen) is False
|
||||
|
||||
def test_follow_blocked_on_story_view(self, gate):
|
||||
"""Cannot follow from STORY_VIEW — no follow button exists."""
|
||||
screen = _make_screen(ScreenType.STORY_VIEW)
|
||||
assert gate.is_allowed("follow", screen) is False
|
||||
|
||||
def test_save_blocked_on_story_view(self, gate):
|
||||
"""Cannot save on STORY_VIEW."""
|
||||
screen = _make_screen(ScreenType.STORY_VIEW)
|
||||
assert gate.is_allowed("save", screen) is False
|
||||
|
||||
def test_like_blocked_on_follow_list(self, gate):
|
||||
"""Cannot like on FOLLOW_LIST — it's a list of users, not posts."""
|
||||
screen = _make_screen(ScreenType.FOLLOW_LIST)
|
||||
assert gate.is_allowed("like", screen) is False
|
||||
|
||||
def test_comment_blocked_on_follow_list(self, gate):
|
||||
"""Cannot comment on FOLLOW_LIST."""
|
||||
screen = _make_screen(ScreenType.FOLLOW_LIST)
|
||||
assert gate.is_allowed("comment", screen) is False
|
||||
|
||||
def test_comment_blocked_on_explore_grid(self, gate):
|
||||
"""Cannot comment on EXPLORE_GRID — it's a grid of thumbnails."""
|
||||
screen = _make_screen(ScreenType.EXPLORE_GRID)
|
||||
assert gate.is_allowed("comment", screen) is False
|
||||
|
||||
def test_save_blocked_on_dm_inbox(self, gate):
|
||||
"""Cannot save on DM_INBOX."""
|
||||
screen = _make_screen(ScreenType.DM_INBOX)
|
||||
assert gate.is_allowed("save", screen) is False
|
||||
|
||||
def test_save_blocked_on_dm_thread(self, gate):
|
||||
"""Cannot save on DM_THREAD."""
|
||||
screen = _make_screen(ScreenType.DM_THREAD)
|
||||
assert gate.is_allowed("save", screen) is False
|
||||
|
||||
def test_follow_blocked_on_home_feed(self, gate):
|
||||
"""Cannot follow from HOME_FEED — there's no follow button on feed posts."""
|
||||
screen = _make_screen(ScreenType.HOME_FEED)
|
||||
assert gate.is_allowed("follow", screen) is False
|
||||
|
||||
def test_follow_blocked_on_explore_grid(self, gate):
|
||||
"""Cannot follow from EXPLORE_GRID."""
|
||||
screen = _make_screen(ScreenType.EXPLORE_GRID)
|
||||
assert gate.is_allowed("follow", screen) is False
|
||||
|
||||
def test_repost_blocked_on_follow_list(self, gate):
|
||||
"""Cannot repost from FOLLOW_LIST."""
|
||||
screen = _make_screen(ScreenType.FOLLOW_LIST)
|
||||
assert gate.is_allowed("repost", screen) is False
|
||||
|
||||
|
||||
class TestContextGatePositiveCases:
|
||||
"""Ensure that valid actions are NOT blocked."""
|
||||
|
||||
def test_like_allowed_on_post_detail(self, gate):
|
||||
"""Like IS valid on POST_DETAIL with proper markers."""
|
||||
screen = _make_screen(ScreenType.POST_DETAIL, {"row_feed_button_like", "row_feed_button_comment"})
|
||||
assert gate.is_allowed("like", screen) is True
|
||||
|
||||
def test_comment_allowed_on_post_detail(self, gate):
|
||||
"""Comment IS valid on POST_DETAIL with proper markers."""
|
||||
screen = _make_screen(ScreenType.POST_DETAIL, {"row_feed_button_comment", "row_feed_button_like"})
|
||||
assert gate.is_allowed("comment", screen) is True
|
||||
|
||||
def test_follow_allowed_on_other_profile(self, gate):
|
||||
"""Follow IS valid on OTHER_PROFILE with proper markers."""
|
||||
screen = _make_screen(ScreenType.OTHER_PROFILE, {"profile_header_follow_button"})
|
||||
assert gate.is_allowed("follow", screen) is True
|
||||
|
||||
def test_like_allowed_on_home_feed(self, gate):
|
||||
"""Like IS valid on HOME_FEED with proper markers."""
|
||||
screen = _make_screen(ScreenType.HOME_FEED, {"row_feed_button_like"})
|
||||
assert gate.is_allowed("like", screen) is True
|
||||
|
||||
|
||||
class TestScreenTopologyBackTransitions:
|
||||
"""P2-3: HD Map must NOT claim to know where 'press back' goes.
|
||||
Back is inherently non-deterministic."""
|
||||
|
||||
def test_press_back_has_no_expected_screen(self):
|
||||
"""press back must NEVER return a specific expected screen."""
|
||||
from GramAddict.core.screen_topology import ScreenTopology
|
||||
|
||||
# For every screen that has a 'press back' transition,
|
||||
# we verify it should NOT be in the topology
|
||||
for screen_type, transitions in ScreenTopology.TRANSITIONS.items():
|
||||
if "press back" in transitions:
|
||||
# This test SHOULD fail until we remove press back from TRANSITIONS
|
||||
# for screens where it's non-deterministic
|
||||
if screen_type in (
|
||||
ScreenType.DM_INBOX,
|
||||
ScreenType.FOLLOW_LIST,
|
||||
ScreenType.STORY_VIEW,
|
||||
ScreenType.COMMENTS,
|
||||
):
|
||||
# These are "leaf" screens with a deterministic parent — OK to keep
|
||||
continue
|
||||
# NON-DETERMINISTIC back: OTHER_PROFILE, POST_DETAIL, SEARCH_RESULTS
|
||||
assert False, (
|
||||
f"ScreenTopology claims 'press back' from {screen_type.name} goes to "
|
||||
f"{transitions['press back'].name}, but this is non-deterministic! "
|
||||
f"Remove this transition — it causes GOAP to reject valid navigation states."
|
||||
)
|
||||
98
tests/tdd/test_memory_hygiene.py
Normal file
98
tests/tdd/test_memory_hygiene.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
TDD: Memory Hygiene — Selective Amnesia (NOT Nuclear Wipe)
|
||||
|
||||
Tests that startup performs intelligent memory pruning instead of
|
||||
wiping all learned knowledge on every run.
|
||||
|
||||
Red-Green-Blue: These tests MUST fail first, then we implement.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestMemoryHygiene:
|
||||
"""P0-2: Replace blank_start nuclear wipe with selective memory hygiene."""
|
||||
|
||||
def test_high_confidence_entries_survive_hygiene(self):
|
||||
"""High-confidence learned patterns must survive startup hygiene."""
|
||||
from GramAddict.core.qdrant_memory import ScreenMemoryDB
|
||||
|
||||
db = ScreenMemoryDB()
|
||||
if not db.is_connected:
|
||||
pytest.skip("Qdrant not available")
|
||||
|
||||
# Store a high-confidence entry
|
||||
db.store_screen("sig_high_conf", "HOME_FEED", confidence=0.95)
|
||||
|
||||
# Run hygiene — this should NOT delete the high-confidence entry
|
||||
from GramAddict.core.qdrant_memory import memory_hygiene
|
||||
|
||||
memory_hygiene()
|
||||
|
||||
# Verify it survived
|
||||
result = db.get_screen_type("sig_high_conf", similarity_threshold=0.90)
|
||||
assert result == "HOME_FEED", "High-confidence entry was destroyed by memory_hygiene!"
|
||||
|
||||
def test_low_confidence_entries_are_pruned(self):
|
||||
"""Low-confidence (stale/poisoned) entries must be pruned during hygiene."""
|
||||
from GramAddict.core.qdrant_memory import ScreenMemoryDB
|
||||
|
||||
db = ScreenMemoryDB()
|
||||
if not db.is_connected:
|
||||
pytest.skip("Qdrant not available")
|
||||
|
||||
# Store a low-confidence entry (representing a bad VLM guess)
|
||||
db.store_screen("sig_low_conf", "MODAL", confidence=0.2)
|
||||
|
||||
# Run hygiene — this SHOULD delete the low-confidence entry
|
||||
from GramAddict.core.qdrant_memory import memory_hygiene
|
||||
|
||||
memory_hygiene()
|
||||
|
||||
# Verify it was pruned
|
||||
result = db.get_screen_type("sig_low_conf", similarity_threshold=0.90)
|
||||
assert result is None, "Low-confidence poisoned entry survived hygiene!"
|
||||
|
||||
def test_memory_hygiene_function_exists(self):
|
||||
"""The memory_hygiene function must be importable."""
|
||||
from GramAddict.core.qdrant_memory import memory_hygiene
|
||||
|
||||
assert callable(memory_hygiene)
|
||||
|
||||
def test_blank_start_is_not_default_config(self):
|
||||
"""blank_start must NOT be true in the default config.
|
||||
It should only be available as a --blank-start CLI flag for explicit manual resets."""
|
||||
import yaml
|
||||
|
||||
with open("config.yml", "r") as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
blank_start = config.get("blank_start", False)
|
||||
assert blank_start is False or blank_start is None, (
|
||||
f"blank_start is {blank_start} in config.yml! "
|
||||
"This wipes ALL learned knowledge on every run, negating the entire learning system. "
|
||||
"Remove it or set to false. Use --blank-start CLI flag for explicit resets only."
|
||||
)
|
||||
|
||||
|
||||
class TestScreenMemoryLRU:
|
||||
"""P2-4: Replace nuclear 200-entry wipe with LRU eviction."""
|
||||
|
||||
def test_screen_memory_does_not_nuke_at_200(self):
|
||||
"""ScreenMemoryDB must NOT wipe the entire collection at 200 entries."""
|
||||
from GramAddict.core.qdrant_memory import ScreenMemoryDB
|
||||
|
||||
db = ScreenMemoryDB()
|
||||
if not db.is_connected:
|
||||
pytest.skip("Qdrant not available")
|
||||
|
||||
# Store 201 entries
|
||||
for i in range(201):
|
||||
db.store_screen(f"sig_lru_{i}", "HOME_FEED", confidence=0.9)
|
||||
|
||||
# Verify that the first high-confidence entry still exists
|
||||
# (it should NOT have been nuked)
|
||||
result = db.get_screen_type("sig_lru_0", similarity_threshold=0.90)
|
||||
assert result is not None, (
|
||||
"ScreenMemoryDB nuked all 200+ entries! " "Must use LRU eviction instead of nuclear wipe."
|
||||
)
|
||||
Reference in New Issue
Block a user