chore: FSD stabilization, strict TDD enforcement, and unlearn mechanism
- implemented self-healing unlearn for Qdrant false positives - centralized testing logic in conftest - documented core rules, ai standards, and goap philosophy - purged old dev scratchpads
This commit is contained in:
0
tests/property/__init__.py
Normal file
0
tests/property/__init__.py
Normal file
208
tests/property/test_property_invariants.py
Normal file
208
tests/property/test_property_invariants.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
Property-Based Tests: Hypothesis-driven invariant verification.
|
||||
|
||||
These tests verify UNIVERSAL PROPERTIES that must hold for ANY input,
|
||||
not just specific examples. Think of them as mathematical proofs of correctness.
|
||||
|
||||
Tesla validates that steering never exceeds max torque for ANY speed —
|
||||
we validate that scroll never exceeds screen bounds for ANY device size.
|
||||
"""
|
||||
import pytest
|
||||
import re
|
||||
from hypothesis import given, strategies as st, settings, assume
|
||||
from tests.chaos import VALID_FEED_XML
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# XML Parsing Properties
|
||||
# ──────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.property
|
||||
class TestXMLParsingProperties:
|
||||
"""Universal properties of the XML extraction pipeline."""
|
||||
|
||||
@given(
|
||||
text=st.text(min_size=0, max_size=200),
|
||||
desc=st.text(min_size=0, max_size=200),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_extracted_nodes_always_have_valid_coordinates(self, text, desc):
|
||||
"""PROPERTY: Any extracted node must have integer x, y >= 0."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# Escape XML special chars
|
||||
safe_text = text.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """).replace("'", "'")
|
||||
safe_desc = desc.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """).replace("'", "'")
|
||||
|
||||
xml = (
|
||||
f'<hierarchy rotation="0">'
|
||||
f'<node index="0" text="{safe_text}" '
|
||||
f'resource-id="com.instagram.android:id/test_button" '
|
||||
f'class="android.widget.Button" '
|
||||
f'package="com.instagram.android" '
|
||||
f'content-desc="{safe_desc}" '
|
||||
f'clickable="true" '
|
||||
f'bounds="[100,200][300,400]" />'
|
||||
f'</hierarchy>'
|
||||
)
|
||||
|
||||
with patch("GramAddict.core.qdrant_memory.QdrantBase.__init__", return_value=None), \
|
||||
patch("GramAddict.core.qdrant_memory.QdrantBase.is_connected", new_callable=lambda: property(lambda self: False)):
|
||||
from GramAddict.core.telepathic_engine import TelepathicEngine
|
||||
TelepathicEngine._instance = None
|
||||
engine = TelepathicEngine.__new__(TelepathicEngine)
|
||||
engine.ui_memory = MagicMock()
|
||||
engine.ui_memory.is_connected = False
|
||||
engine.positive_memory = MagicMock()
|
||||
engine.positive_memory.is_connected = False
|
||||
engine._edge_model = None
|
||||
engine._edge_tokenizer = None
|
||||
|
||||
try:
|
||||
nodes = engine._extract_semantic_nodes(xml)
|
||||
except Exception:
|
||||
# If the generated text breaks XML parsing, that's OK —
|
||||
# the parser should return empty list, not crash
|
||||
nodes = []
|
||||
|
||||
for node in nodes:
|
||||
assert isinstance(node["x"], int)
|
||||
assert isinstance(node["y"], int)
|
||||
assert node["x"] >= 0
|
||||
assert node["y"] >= 0
|
||||
|
||||
TelepathicEngine._instance = None
|
||||
|
||||
@given(
|
||||
left=st.integers(min_value=0, max_value=1080),
|
||||
top=st.integers(min_value=0, max_value=2400),
|
||||
width=st.integers(min_value=1, max_value=500),
|
||||
height=st.integers(min_value=1, max_value=500),
|
||||
)
|
||||
@settings(max_examples=200)
|
||||
def test_center_calculation_always_within_bounds(self, left, top, width, height):
|
||||
"""PROPERTY: Calculated center must lie within the bounding rectangle."""
|
||||
right = min(left + width, 2160)
|
||||
bottom = min(top + height, 3200)
|
||||
|
||||
center_x = (left + right) // 2
|
||||
center_y = (top + bottom) // 2
|
||||
|
||||
assert left <= center_x <= right
|
||||
assert top <= center_y <= bottom
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# SAE Compression Properties
|
||||
# ──────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.property
|
||||
class TestSAECompressionProperties:
|
||||
"""Universal properties of XML compression."""
|
||||
|
||||
@given(
|
||||
n_nodes=st.integers(min_value=0, max_value=200),
|
||||
)
|
||||
@settings(max_examples=30, deadline=None)
|
||||
def test_compression_output_bounded(self, n_nodes):
|
||||
"""PROPERTY: Compressed output must ALWAYS be <= 3000 characters."""
|
||||
from unittest.mock import MagicMock
|
||||
from GramAddict.core.situational_awareness import SituationalAwarenessEngine
|
||||
SituationalAwarenessEngine.reset()
|
||||
|
||||
device = MagicMock()
|
||||
device.deviceV2 = MagicMock()
|
||||
device.deviceV2.info = {"screenOn": True}
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
|
||||
# Generate XML with n_nodes
|
||||
parts = ['<hierarchy rotation="0">']
|
||||
for i in range(n_nodes):
|
||||
parts.append(
|
||||
f'<node index="{i}" text="item_{i}" '
|
||||
f'resource-id="com.instagram.android:id/element_{i}" '
|
||||
f'class="android.widget.TextView" '
|
||||
f'package="com.instagram.android" '
|
||||
f'clickable="true" bounds="[0,{i*50}][100,{i*50+40}]" />'
|
||||
)
|
||||
parts.append('</hierarchy>')
|
||||
xml = "".join(parts)
|
||||
|
||||
result = sae._compress_xml(xml)
|
||||
assert len(result) <= 3000
|
||||
|
||||
SituationalAwarenessEngine.reset()
|
||||
|
||||
@given(
|
||||
text1=st.text(alphabet="abcdefghijklmnopqrstuvwxyz", min_size=5, max_size=50),
|
||||
text2=st.text(alphabet="abcdefghijklmnopqrstuvwxyz", min_size=5, max_size=50),
|
||||
)
|
||||
@settings(max_examples=50)
|
||||
def test_different_inputs_produce_different_hashes(self, text1, text2):
|
||||
"""PROPERTY: Distinct inputs should (almost always) produce distinct hashes."""
|
||||
assume(text1 != text2)
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
from GramAddict.core.situational_awareness import SituationalAwarenessEngine
|
||||
SituationalAwarenessEngine.reset()
|
||||
|
||||
device = MagicMock()
|
||||
device.deviceV2 = MagicMock()
|
||||
device.deviceV2.info = {"screenOn": True}
|
||||
sae = SituationalAwarenessEngine(device)
|
||||
|
||||
hash1 = sae._compute_situation_hash(text1)
|
||||
hash2 = sae._compute_situation_hash(text2)
|
||||
assert hash1 != hash2
|
||||
|
||||
SituationalAwarenessEngine.reset()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Active Inference Properties
|
||||
# ──────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.property
|
||||
class TestActiveInferenceProperties:
|
||||
"""Universal properties of the Active Inference engine."""
|
||||
|
||||
@given(
|
||||
predicted=st.floats(min_value=0.0, max_value=1.0),
|
||||
observed=st.floats(min_value=0.0, max_value=1.0),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_free_energy_always_non_negative(self, predicted, observed):
|
||||
"""PROPERTY: Free energy must NEVER go negative."""
|
||||
from GramAddict.core.active_inference import ActiveInferenceEngine
|
||||
ai = ActiveInferenceEngine("test_user")
|
||||
|
||||
result = ai.calculate_surprise(predicted, observed)
|
||||
assert result >= 0.0
|
||||
|
||||
@given(
|
||||
predicted=st.floats(min_value=0.0, max_value=1.0),
|
||||
observed=st.floats(min_value=0.0, max_value=1.0),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_policy_always_valid(self, predicted, observed):
|
||||
"""PROPERTY: Policy must always be one of the valid states."""
|
||||
from GramAddict.core.active_inference import ActiveInferenceEngine
|
||||
ai = ActiveInferenceEngine("test_user")
|
||||
|
||||
ai.calculate_surprise(predicted, observed)
|
||||
assert ai.policy in ("STABLE", "CAUTIOUS", "DORMANT")
|
||||
|
||||
@given(
|
||||
modifier_count=st.integers(min_value=1, max_value=50),
|
||||
)
|
||||
@settings(max_examples=20)
|
||||
def test_sleep_modifier_always_bounded(self, modifier_count):
|
||||
"""PROPERTY: Sleep modifier must always be in [1.0, 5.0] range."""
|
||||
from GramAddict.core.active_inference import ActiveInferenceEngine
|
||||
ai = ActiveInferenceEngine("test_user")
|
||||
|
||||
for _ in range(modifier_count):
|
||||
ai.calculate_surprise(1.0, 0.0) # Max surprise
|
||||
|
||||
mod = ai.get_sleep_modifier()
|
||||
assert 1.0 <= mod <= 5.0
|
||||
Reference in New Issue
Block a user