Spaces:
Runtime error
Runtime error
| """ | |
| Tests for Step 10 β Episodic Memory. | |
| Covers: | |
| β’ _feature_vector(): bounded [0,1], correct dimensionality | |
| β’ _cosine_similarity(): identical vectors β 1.0; orthogonal β 0.0; zero β 0.0 | |
| β’ EpisodicMemory.recall(): L1 miss returns []; L1 hit returns entry | |
| β’ EpisodicMemory.memorize(): grade A/B stored; grade C/D/F not stored | |
| β’ Recall threshold: similarity β₯ 0.85 β recalled; < 0.85 β not recalled | |
| β’ L2 Supabase failure is silent (L1-only fallback) | |
| β’ format_memory_exemplar(): correct format | |
| β’ Empty memory β recall returns [] | |
| β’ memorize() on first run ever β no crash | |
| """ | |
| from __future__ import annotations | |
| import math | |
| from unittest.mock import patch | |
| import pytest | |
| from agents.memory import ( | |
| EpisodicMemory, | |
| MemoryEntry, | |
| _feature_vector, | |
| _cosine_similarity, | |
| _RECALL_THRESHOLD, | |
| _CACHEABLE_GRADES, | |
| format_memory_exemplar, | |
| ) | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _profile(num_rows: int = 500, num_classes: int = 3, | |
| avg_word_count: float = 50.0, noise: float = 0.0, | |
| quality: float = 1.0) -> dict: | |
| return { | |
| "num_rows": num_rows, | |
| "num_classes": num_classes, | |
| "avg_word_count": avg_word_count, | |
| "label_noise_estimate": noise, | |
| "text_quality_score": quality, | |
| "label_distribution": {f"cls_{i}": 100 for i in range(num_classes)}, | |
| } | |
| def _entry(grade: str = "B", f1: float = 0.85, task_type: str = "text_classification") -> MemoryEntry: | |
| fv = _feature_vector(_profile()) | |
| return MemoryEntry( | |
| feature_vector=fv, | |
| task_type=task_type, | |
| model_recipe={"base_model": "bert-base-uncased", "learning_rate": 2e-4}, | |
| eval_grade=grade, | |
| eval_f1=f1, | |
| ) | |
| # ββ Feature vector ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestFeatureVector: | |
| def test_length_is_5(self): | |
| fv = _feature_vector(_profile()) | |
| assert len(fv) == 5 | |
| def test_all_values_bounded_0_1(self): | |
| fv = _feature_vector(_profile(num_rows=1_000_000, num_classes=100, avg_word_count=9999)) | |
| for v in fv: | |
| assert 0.0 <= v <= 1.0, f"Feature out of range: {v}" | |
| def test_zero_rows_no_crash(self): | |
| fv = _feature_vector(_profile(num_rows=0)) | |
| assert isinstance(fv, list) | |
| assert len(fv) == 5 | |
| def test_large_dataset_has_larger_first_feature(self): | |
| fv_small = _feature_vector(_profile(num_rows=100)) | |
| fv_large = _feature_vector(_profile(num_rows=10_000)) | |
| assert fv_large[0] > fv_small[0] | |
| def test_more_classes_has_larger_second_feature(self): | |
| fv_2 = _feature_vector(_profile(num_classes=2)) | |
| fv_20 = _feature_vector(_profile(num_classes=20)) | |
| assert fv_20[1] > fv_2[1] | |
| # ββ Cosine similarity βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestCosineSimilarity: | |
| def test_identical_vectors_is_one(self): | |
| v = [0.5, 0.3, 0.8, 0.1, 0.9] | |
| assert _cosine_similarity(v, v) == pytest.approx(1.0, abs=1e-9) | |
| def test_orthogonal_vectors_is_zero(self): | |
| a = [1.0, 0.0, 0.0] | |
| b = [0.0, 1.0, 0.0] | |
| assert _cosine_similarity(a, b) == pytest.approx(0.0, abs=1e-9) | |
| def test_zero_vector_is_zero(self): | |
| a = [0.0, 0.0, 0.0] | |
| b = [1.0, 1.0, 1.0] | |
| assert _cosine_similarity(a, b) == pytest.approx(0.0) | |
| def test_similar_vectors_high_score(self): | |
| a = [0.5, 0.4, 0.6, 0.1, 0.9] | |
| b = [0.5, 0.4, 0.6, 0.1, 0.9] | |
| assert _cosine_similarity(a, b) > 0.99 | |
| def test_different_vectors_lower_score(self): | |
| a = [1.0, 0.0, 0.0, 0.0, 0.0] | |
| b = [0.0, 0.0, 0.0, 0.0, 1.0] | |
| sim = _cosine_similarity(a, b) | |
| assert sim < 0.5 | |
| # ββ EpisodicMemory ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestEpisodicMemory: | |
| def setup_method(self): | |
| self.mem = EpisodicMemory() | |
| def test_empty_memory_returns_empty_list(self): | |
| result = self.mem.recall(_profile(), "text_classification") | |
| assert result == [] | |
| def test_grade_a_is_stored(self): | |
| p = _profile() | |
| self.mem.memorize(p, "text_classification", {}, "A", 0.92) | |
| assert self.mem.size() == 1 | |
| def test_grade_b_is_stored(self): | |
| p = _profile() | |
| self.mem.memorize(p, "text_classification", {}, "B", 0.85) | |
| assert self.mem.size() == 1 | |
| def test_grade_c_not_stored(self): | |
| p = _profile() | |
| self.mem.memorize(p, "text_classification", {}, "C", 0.72) | |
| assert self.mem.size() == 0 | |
| def test_grade_d_not_stored(self): | |
| self.mem.memorize(_profile(), "text_classification", {}, "D", 0.50) | |
| assert self.mem.size() == 0 | |
| def test_grade_f_not_stored(self): | |
| self.mem.memorize(_profile(), "text_classification", {}, "F", 0.30) | |
| assert self.mem.size() == 0 | |
| def test_identical_profile_recalled(self): | |
| p = _profile(num_rows=500, num_classes=3, avg_word_count=50) | |
| recipe = {"base_model": "bert-base-uncased", "learning_rate": 2e-4} | |
| self.mem.memorize(p, "text_classification", recipe, "A", 0.91) | |
| results = self.mem.recall(p, "text_classification") | |
| assert len(results) == 1 | |
| assert results[0].eval_grade == "A" | |
| assert results[0].model_recipe == recipe | |
| def test_very_different_profile_not_recalled(self): | |
| """Large 100-class dataset vs tiny 2-class dataset β below threshold.""" | |
| p_stored = _profile(num_rows=10_000, num_classes=50, avg_word_count=200) | |
| p_query = _profile(num_rows=50, num_classes=2, avg_word_count=10) | |
| self.mem.memorize(p_stored, "text_classification", {}, "A", 0.90) | |
| results = self.mem.recall(p_query, "text_classification") | |
| assert results == [] | |
| def test_different_task_type_not_recalled(self): | |
| p = _profile() | |
| self.mem.memorize(p, "text_classification", {}, "A", 0.90) | |
| results = self.mem.recall(p, "token_classification") | |
| assert results == [] | |
| def test_top_k_limits_results(self): | |
| p = _profile() | |
| for i in range(5): | |
| self.mem.memorize(p, "text_classification", {}, "A", 0.80 + i * 0.02) | |
| results = self.mem.recall(p, "text_classification", top_k=2) | |
| assert len(results) <= 2 | |
| def test_results_sorted_by_f1_desc(self): | |
| p = _profile() | |
| self.mem.memorize(p, "text_classification", {"lr": 1}, "B", 0.80) | |
| self.mem.memorize(p, "text_classification", {"lr": 2}, "A", 0.92) | |
| self.mem.memorize(p, "text_classification", {"lr": 3}, "A", 0.88) | |
| results = self.mem.recall(p, "text_classification") | |
| f1_scores = [e.eval_f1 for e in results] | |
| assert f1_scores == sorted(f1_scores, reverse=True) | |
| def test_clear_empties_l1(self): | |
| self.mem.memorize(_profile(), "text_classification", {}, "A", 0.9) | |
| self.mem.clear() | |
| assert self.mem.size() == 0 | |
| def test_supabase_failure_silent_on_recall(self): | |
| """L2 errors must not propagate to the caller.""" | |
| with patch("agents.memory._l2_recall", side_effect=Exception("DB error")): | |
| result = self.mem.recall(_profile(), "text_classification") | |
| assert result == [] | |
| def test_supabase_failure_silent_on_memorize(self): | |
| """L2 write errors must not propagate.""" | |
| with patch("agents.memory._l2_memorize", side_effect=Exception("DB error")): | |
| self.mem.memorize(_profile(), "text_classification", {}, "A", 0.90) | |
| assert self.mem.size() == 1 # L1 write succeeded | |
| # ββ format_memory_exemplar ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestFormatMemoryExemplar: | |
| def test_contains_grade(self): | |
| entry = _entry(grade="A", f1=0.91) | |
| out = format_memory_exemplar(entry) | |
| assert "A" in out | |
| def test_contains_f1(self): | |
| entry = _entry(f1=0.876) | |
| out = format_memory_exemplar(entry) | |
| assert "0.876" in out | |
| def test_contains_base_model(self): | |
| entry = _entry() | |
| out = format_memory_exemplar(entry) | |
| assert "bert-base-uncased" in out | |
| def test_is_string(self): | |
| out = format_memory_exemplar(_entry()) | |
| assert isinstance(out, str) | |
| assert len(out) > 20 | |