Skip to main content
Glama
test_tools_memory_management.py62.3 kB
""" Tests for tools related to memory lifecycle management. This test suite covers the following tools: - `save_memory` - `open_memories` - `touch_memory` - `gc` - `promote_memory` """ import time from unittest.mock import MagicMock, patch import pytest from cortexgraph.storage.models import Memory, MemoryMetadata, MemoryStatus, Relation from cortexgraph.tools.gc import gc from cortexgraph.tools.open_memories import open_memories from cortexgraph.tools.promote import promote_memory from cortexgraph.tools.save import save_memory from cortexgraph.tools.touch import touch_memory from tests.conftest import make_test_uuid class TestSaveMemory: """Test suite for save_memory tool.""" def test_save_basic_memory(self, mock_config_preprocessor, temp_storage): """Test saving a basic memory with just content.""" # Uses mock_config_preprocessor fixture (disables preprocessing) result = save_memory(content="This is a test memory") assert result["success"] is True assert "memory_id" in result assert "Memory saved with ID:" in result["message"] assert result["has_embedding"] is False # Verify memory was actually saved memory = temp_storage.get_memory(result["memory_id"]) assert memory is not None assert memory.content == "This is a test memory" assert memory.use_count == 0 assert memory.entities == [] assert memory.meta.tags == [] def test_save_memory_with_tags(self, temp_storage): """Test saving memory with tags.""" result = save_memory(content="Tagged memory", tags=["python", "testing", "cortexgraph"]) assert result["success"] is True memory = temp_storage.get_memory(result["memory_id"]) assert memory.meta.tags == ["python", "testing", "cortexgraph"] def test_save_memory_with_entities(self, temp_storage): """Test saving memory with entities.""" result = save_memory(content="Memory about Claude", entities=["Claude", "Anthropic", "AI"]) assert result["success"] is True memory = temp_storage.get_memory(result["memory_id"]) assert memory.entities == ["Claude", "Anthropic", "AI"] def test_save_memory_with_source_and_context(self, temp_storage): """Test saving memory with source and context.""" result = save_memory( content="Memory with metadata", source="user-input", context="During code review session", ) assert result["success"] is True memory = temp_storage.get_memory(result["memory_id"]) assert memory.meta.source == "user-input" assert memory.meta.context == "During code review session" def test_save_memory_with_custom_metadata(self, temp_storage): """Test saving memory with custom metadata.""" custom_meta = {"priority": "high", "project": "cortexgraph"} result = save_memory(content="Memory with custom meta", meta=custom_meta) assert result["success"] is True memory = temp_storage.get_memory(result["memory_id"]) assert memory.meta.extra == custom_meta def test_save_memory_all_fields(self, temp_storage): """Test saving memory with all optional fields.""" result = save_memory( content="Complete memory", tags=["tag1", "tag2"], entities=["Entity1"], source="test-source", context="test-context", meta={"key": "value"}, ) assert result["success"] is True memory = temp_storage.get_memory(result["memory_id"]) assert memory.content == "Complete memory" assert memory.meta.tags == ["tag1", "tag2"] assert memory.entities == ["Entity1"] assert memory.meta.source == "test-source" assert memory.meta.context == "test-context" assert memory.meta.extra == {"key": "value"} def test_save_memory_timestamps(self, temp_storage): """Test that timestamps are set correctly.""" before = int(time.time()) result = save_memory(content="Timestamp test") after = int(time.time()) memory = temp_storage.get_memory(result["memory_id"]) assert before <= memory.created_at <= after assert before <= memory.last_used <= after assert memory.created_at == memory.last_used def test_save_memory_unique_ids(self, temp_storage): """Test that each memory gets a unique ID.""" result1 = save_memory(content="Memory 1") result2 = save_memory(content="Memory 2") result3 = save_memory(content="Memory 3") assert result1["memory_id"] != result2["memory_id"] assert result2["memory_id"] != result3["memory_id"] assert result1["memory_id"] != result3["memory_id"] # Validation tests def test_save_empty_content_fails(self): """Test that empty content fails validation.""" with pytest.raises(ValueError, match="content.*empty"): save_memory(content="") def test_save_content_too_long_fails(self): """Test that content exceeding max length fails.""" long_content = "x" * 50001 # MAX_CONTENT_LENGTH is 50000 with pytest.raises(ValueError, match="content.*exceeds maximum"): save_memory(content=long_content) def test_save_too_many_tags_fails(self): """Test that too many tags fails validation.""" too_many_tags = [f"tag{i}" for i in range(51)] # MAX_TAGS_COUNT is 50 with pytest.raises(ValueError, match="tags.*exceeds maximum"): save_memory(content="Test", tags=too_many_tags) def test_save_tag_too_long_fails(self): """Test that tags exceeding max length fail validation.""" long_tag = "x" * 101 # Tags are limited to 100 chars with pytest.raises(ValueError, match="tag.*exceeds maximum"): save_memory(content="Test", tags=[long_tag]) def test_save_invalid_tag_characters_sanitized(self, temp_storage): """Test that tags with invalid characters are auto-sanitized (MCP-friendly).""" result = save_memory(content="Test", tags=["invalid tag!"]) assert result["success"] is True # Verify memory was saved with sanitized tag ("invalid tag!" -> "invalid_tag") memory = temp_storage.get_memory(result["memory_id"]) assert "invalid_tag" in memory.meta.tags def test_save_too_many_entities_fails(self): """Test that too many entities fails validation.""" too_many_entities = [f"entity{i}" for i in range(101)] # MAX_ENTITIES_COUNT is 100 with pytest.raises(ValueError, match="entities.*exceeds maximum"): save_memory(content="Test", entities=too_many_entities) def test_save_source_too_long_fails(self): """Test that source exceeding max length fails.""" long_source = "x" * 501 # Source max is 500 with pytest.raises(ValueError, match="source.*exceeds maximum"): save_memory(content="Test", source=long_source) def test_save_context_too_long_fails(self): """Test that context exceeding max length fails.""" long_context = "x" * 1001 # Context max is 1000 with pytest.raises(ValueError, match="context.*exceeds maximum"): save_memory(content="Test", context=long_context) # Edge cases def test_save_memory_with_none_tags(self, temp_storage): """Test that None tags are converted to empty list.""" result = save_memory(content="Test", tags=None) memory = temp_storage.get_memory(result["memory_id"]) assert memory.meta.tags == [] def test_save_memory_with_empty_tags(self, temp_storage): """Test saving with empty tags list.""" result = save_memory(content="Test", tags=[]) memory = temp_storage.get_memory(result["memory_id"]) assert memory.meta.tags == [] def test_save_memory_with_none_entities(self, mock_config_preprocessor, temp_storage): """Test that None entities are converted to empty list.""" # Uses mock_config_preprocessor fixture (disables preprocessing) result = save_memory(content="Test", entities=None) memory = temp_storage.get_memory(result["memory_id"]) assert memory.entities == [] def test_save_memory_with_unicode_content(self, temp_storage): """Test saving memory with Unicode characters.""" content = "Unicode test: 你好 🎉 café" result = save_memory(content=content) memory = temp_storage.get_memory(result["memory_id"]) assert memory.content == content def test_save_memory_with_special_characters(self, temp_storage): """Test saving memory with special characters.""" content = "Special chars: <tag> & \"quotes\" 'apostrophe' \\backslash" result = save_memory(content=content) memory = temp_storage.get_memory(result["memory_id"]) assert memory.content == content # Secret detection tests (when enabled) @patch("cortexgraph.tools.save.detect_secrets") def test_save_warns_about_secrets_when_detected( self, mock_detect, mock_config_preprocessor, temp_storage, caplog, ): """Test that secret detection warns but still saves memory.""" from cortexgraph.security.secrets import SecretMatch # Enable secret detection for this test mock_config_preprocessor.detect_secrets = True # Mock a high-confidence secret to trigger the warning without patching should_warn_about_secrets mock_detect.return_value = [ SecretMatch(secret_type="openai_key", position=0, context="...") ] result = save_memory(content="API key: sk-xxx123") # Memory should still be saved assert result["success"] is True # But warning should be logged assert "Secrets detected" in caplog.text # Embedding tests def test_save_memory_with_embeddings_enabled( self, mock_config_embeddings, mock_embeddings_save, temp_storage ): """Test that embeddings are generated when enabled.""" # Uses mock_config_embeddings + mock_embeddings_save fixtures result = save_memory(content="Test embedding") assert result["has_embedding"] is True memory = temp_storage.get_memory(result["memory_id"]) assert memory.embed == [0.1, 0.2, 0.3] def test_save_memory_with_embeddings_disabled(self, mock_config_preprocessor, temp_storage): """Test that embeddings are not generated when disabled.""" # Uses mock_config_preprocessor fixture (embeddings disabled by default) result = save_memory(content="Test no embedding") assert result["has_embedding"] is False memory = temp_storage.get_memory(result["memory_id"]) assert memory.embed is None @patch("cortexgraph.tools.save.SENTENCE_TRANSFORMERS_AVAILABLE", True) @patch("cortexgraph.tools.save._SentenceTransformer") def test_save_memory_embedding_import_error( self, mock_transformer, mock_config_embeddings, temp_storage ): """Test that import error in embedding generation is handled gracefully.""" # Clear model cache to ensure this test gets a fresh model load attempt from cortexgraph.tools import save save._model_cache.clear() # Uses mock_config_embeddings fixture (enables embeddings) # Patch transformer to raise ImportError mock_transformer.side_effect = ImportError("No model found") result = save_memory(content="Test") # Should still save without embedding assert result["success"] is True assert result["has_embedding"] is False class TestOpenMemories: """Test suite for open_memories tool.""" def test_open_single_memory(self, temp_storage): """Test retrieving a single memory by ID.""" mem_id = make_test_uuid("test-123") mem = Memory(id=mem_id, content="Test memory", use_count=5, entities=["python"]) temp_storage.save_memory(mem) result = open_memories(memory_ids=mem_id) assert result["success"] is True assert result["count"] == 1 assert len(result["memories"]) == 1 assert result["memories"][0]["id"] == mem_id assert result["memories"][0]["content"] == "Test memory" assert result["not_found"] == [] def test_open_multiple_memories(self, temp_storage): """Test retrieving multiple memories at once.""" ids = [make_test_uuid(f"mem-{i}") for i in range(3)] for i, mem_id in enumerate(ids): mem = Memory(id=mem_id, content=f"Memory {i}", use_count=1) temp_storage.save_memory(mem) result = open_memories(memory_ids=ids) assert result["success"] is True assert result["count"] == 3 assert len(result["memories"]) == 3 assert result["not_found"] == [] def test_open_memory_includes_all_fields(self, temp_storage): """Test that result includes all expected memory fields.""" now = int(time.time()) mem_id = make_test_uuid("full-mem") mem = Memory( id=mem_id, content="Full memory", entities=["entity1", "entity2"], use_count=10, strength=1.5, created_at=now, last_used=now, ) mem.meta.tags = ["tag1", "tag2"] mem.meta.source = "test" mem.meta.context = "test context" temp_storage.save_memory(mem) result = open_memories(memory_ids=mem_id) assert result["success"] is True memory = result["memories"][0] assert memory["id"] == mem_id assert memory["content"] == "Full memory" assert memory["entities"] == ["entity1", "entity2"] assert memory["tags"] == ["tag1", "tag2"] assert memory["source"] == "test" assert memory["context"] == "test context" assert memory["created_at"] == now assert memory["last_used"] == now assert memory["use_count"] == 10 assert memory["strength"] == 1.5 assert memory["status"] == "active" def test_open_memory_with_scores(self, temp_storage): """Test including decay scores in results.""" mem_id = make_test_uuid("scored-mem") mem = Memory(id=mem_id, content="Test", use_count=5) temp_storage.save_memory(mem) result = open_memories(memory_ids=mem_id, include_scores=True) assert result["success"] is True memory = result["memories"][0] assert "score" in memory assert "age_days" in memory assert isinstance(memory["score"], float) assert isinstance(memory["age_days"], float) assert memory["score"] >= 0 def test_open_memory_without_scores(self, temp_storage): """Test excluding scores from results.""" mem_id = make_test_uuid("no-score") mem = Memory(id=mem_id, content="Test", use_count=1) temp_storage.save_memory(mem) result = open_memories(memory_ids=mem_id, include_scores=False) assert result["success"] is True memory = result["memories"][0] assert "score" not in memory assert "age_days" not in memory def test_open_memory_with_relations(self, temp_storage): """Test including relations in results.""" mem1_id = make_test_uuid("mem-1") mem2_id = make_test_uuid("mem-2") mem3_id = make_test_uuid("mem-3") mem1 = Memory(id=mem1_id, content="Memory 1", use_count=1) mem2 = Memory(id=mem2_id, content="Memory 2", use_count=1) mem3 = Memory(id=mem3_id, content="Memory 3", use_count=1) temp_storage.save_memory(mem1) temp_storage.save_memory(mem2) temp_storage.save_memory(mem3) # Create relations rel1 = Relation( id=make_test_uuid("rel-1"), from_memory_id=mem1_id, to_memory_id=mem2_id, relation_type="related", strength=0.8, created_at=int(time.time()), ) rel2 = Relation( id=make_test_uuid("rel-2"), from_memory_id=mem3_id, to_memory_id=mem1_id, relation_type="causes", strength=0.6, created_at=int(time.time()), ) temp_storage.create_relation(rel1) temp_storage.create_relation(rel2) result = open_memories(memory_ids=mem1_id, include_relations=True) assert result["success"] is True memory = result["memories"][0] assert "relations" in memory assert "outgoing" in memory["relations"] assert "incoming" in memory["relations"] assert len(memory["relations"]["outgoing"]) == 1 assert len(memory["relations"]["incoming"]) == 1 assert memory["relations"]["outgoing"][0]["to"] == mem2_id assert memory["relations"]["incoming"][0]["from"] == mem3_id def test_open_memory_without_relations(self, temp_storage): """Test excluding relations from results.""" mem_id = make_test_uuid("no-rel") mem = Memory(id=mem_id, content="Test", use_count=1) temp_storage.save_memory(mem) result = open_memories(memory_ids=mem_id, include_relations=False) assert result["success"] is True memory = result["memories"][0] assert "relations" not in memory def test_open_memory_not_found(self, temp_storage): """Test retrieving non-existent memory.""" nonexistent_id = make_test_uuid("nonexistent") result = open_memories(memory_ids=nonexistent_id) assert result["success"] is True assert result["count"] == 0 assert len(result["memories"]) == 0 assert nonexistent_id in result["not_found"] def test_open_memories_partial_not_found(self, temp_storage): """Test retrieving mix of existing and non-existent memories.""" existing_id = make_test_uuid("exists") nonexistent_id = make_test_uuid("missing") mem = Memory(id=existing_id, content="Exists", use_count=1) temp_storage.save_memory(mem) result = open_memories(memory_ids=[existing_id, nonexistent_id]) assert result["success"] is True assert result["count"] == 1 assert len(result["memories"]) == 1 assert result["memories"][0]["id"] == existing_id assert nonexistent_id in result["not_found"] assert existing_id not in result["not_found"] def test_open_memories_promoted_memory(self, temp_storage): """Test retrieving promoted memory.""" mem_id = make_test_uuid("promoted") mem = Memory( id=mem_id, content="Promoted memory", use_count=1, status=MemoryStatus.PROMOTED, promoted_at=int(time.time()), promoted_to="/vault/promoted.md", ) temp_storage.save_memory(mem) result = open_memories(memory_ids=mem_id) assert result["success"] is True memory = result["memories"][0] assert memory["status"] == "promoted" assert memory["promoted_at"] is not None assert memory["promoted_to"] == "/vault/promoted.md" # Validation tests def test_open_invalid_uuid_fails(self): """Test that invalid UUID fails validation.""" with pytest.raises(ValueError, match="memory_ids"): open_memories(memory_ids="not-a-uuid") def test_open_invalid_uuid_in_list_fails(self): """Test that invalid UUID in list fails validation.""" valid_id = make_test_uuid("valid") with pytest.raises(ValueError, match="memory_ids"): open_memories(memory_ids=[valid_id, "not-a-uuid"]) def test_open_too_many_ids_fails(self): """Test that exceeding max list length fails.""" # Generate 101 IDs (max is 100) too_many_ids = [make_test_uuid(f"mem-{i}") for i in range(101)] with pytest.raises(ValueError, match="memory_ids"): open_memories(memory_ids=too_many_ids) def test_open_invalid_type_fails(self): """Test that invalid memory_ids type fails.""" with pytest.raises(ValueError, match="memory_ids must be a string or list"): open_memories(memory_ids=123) # type: ignore # Edge cases def test_open_empty_list(self, temp_storage): """Test with empty list of IDs.""" result = open_memories(memory_ids=[]) assert result["success"] is True assert result["count"] == 0 assert result["memories"] == [] assert result["not_found"] == [] def test_open_memory_no_relations(self, temp_storage): """Test memory with no relations when include_relations=True.""" mem_id = make_test_uuid("isolated") mem = Memory(id=mem_id, content="Isolated memory", use_count=1) temp_storage.save_memory(mem) result = open_memories(memory_ids=mem_id, include_relations=True) assert result["success"] is True memory = result["memories"][0] assert "relations" in memory assert memory["relations"]["outgoing"] == [] assert memory["relations"]["incoming"] == [] def test_open_memory_age_calculation(self, temp_storage): """Test age_days calculation.""" now = int(time.time()) three_days_ago = now - (3 * 86400) mem_id = make_test_uuid("old") mem = Memory(id=mem_id, content="Old memory", use_count=1, created_at=three_days_ago) temp_storage.save_memory(mem) result = open_memories(memory_ids=mem_id, include_scores=True) assert result["success"] is True memory = result["memories"][0] assert "age_days" in memory # Should be approximately 3 days old assert 2.9 <= memory["age_days"] <= 3.1 def test_open_memory_score_rounded(self, temp_storage): """Test that scores are rounded to 4 decimal places.""" mem_id = make_test_uuid("rounded") mem = Memory(id=mem_id, content="Test", use_count=1) temp_storage.save_memory(mem) result = open_memories(memory_ids=mem_id, include_scores=True) assert result["success"] is True memory = result["memories"][0] # Score should have at most 4 decimal places score_str = str(memory["score"]) if "." in score_str: decimals = len(score_str.split(".")[1]) assert decimals <= 4 def test_open_memory_relation_strength_rounded(self, temp_storage): """Test that relation strengths are rounded to 4 decimal places.""" mem1_id = make_test_uuid("mem-1") mem2_id = make_test_uuid("mem-2") mem1 = Memory(id=mem1_id, content="M1", use_count=1) mem2 = Memory(id=mem2_id, content="M2", use_count=1) temp_storage.save_memory(mem1) temp_storage.save_memory(mem2) rel = Relation( id=make_test_uuid("rel"), from_memory_id=mem1_id, to_memory_id=mem2_id, relation_type="related", strength=0.123456789, # Many decimal places created_at=int(time.time()), ) temp_storage.create_relation(rel) result = open_memories(memory_ids=mem1_id, include_relations=True) assert result["success"] is True memory = result["memories"][0] strength = memory["relations"]["outgoing"][0]["strength"] # Strength should have at most 4 decimal places strength_str = str(strength) if "." in strength_str: decimals = len(strength_str.split(".")[1]) assert decimals <= 4 class TestTouchMemory: """Test suite for touch_memory tool.""" def test_touch_basic_reinforcement(self, temp_storage): """Test basic memory reinforcement without strength boost.""" test_id = make_test_uuid("test-123") # Create memory with old timestamp to ensure touch updates it old_time = int(time.time()) - 10 # 10 seconds ago mem = Memory( id=test_id, content="Test memory", use_count=0, strength=1.0, last_used=old_time ) temp_storage.save_memory(mem) # Get the saved memory to check its timestamp saved_mem = temp_storage.get_memory(test_id) original_last_used = saved_mem.last_used result = touch_memory(memory_id=test_id, boost_strength=False) assert result["success"] is True assert result["memory_id"] == test_id assert result["use_count"] == 1 assert result["strength"] == pytest.approx(1.0) # Verify memory was updated updated = temp_storage.get_memory(test_id) assert updated.use_count == 1 assert updated.last_used > original_last_used def test_touch_increments_use_count(self, temp_storage): """Test that touch increments use_count correctly.""" test_id = make_test_uuid("test-456") mem = Memory(id=test_id, content="Test", use_count=5) temp_storage.save_memory(mem) result = touch_memory(memory_id=test_id) assert result["success"] is True assert result["use_count"] == 6 updated = temp_storage.get_memory(test_id) assert updated.use_count == 6 def test_touch_with_strength_boost(self, temp_storage): """Test touching with strength boost.""" test_id = make_test_uuid("test-789") mem = Memory(id=test_id, content="Test", strength=1.0) temp_storage.save_memory(mem) result = touch_memory(memory_id=test_id, boost_strength=True) assert result["success"] is True assert result["strength"] == pytest.approx(1.1) updated = temp_storage.get_memory(test_id) assert updated.strength == pytest.approx(1.1) def test_touch_strength_capped_at_2(self, temp_storage): """Test that strength is capped at 2.0.""" test_id = make_test_uuid("test-cap") mem = Memory(id=test_id, content="Test", strength=1.95) temp_storage.save_memory(mem) # First boost: 1.95 + 0.1 = 2.05, capped to 2.0 result = touch_memory(memory_id=test_id, boost_strength=True) assert result["success"] is True assert result["strength"] == pytest.approx(2.0) # Second boost: should stay at 2.0 result2 = touch_memory(memory_id=test_id, boost_strength=True) assert result2["success"] is True assert result2["strength"] == pytest.approx(2.0) updated = temp_storage.get_memory(test_id) assert updated.strength == pytest.approx(2.0) def test_touch_multiple_times(self, temp_storage): """Test touching same memory multiple times.""" test_id = make_test_uuid("test-multi") mem = Memory(id=test_id, content="Test", use_count=0) temp_storage.save_memory(mem) for i in range(1, 6): result = touch_memory(memory_id=test_id) assert result["success"] is True assert result["use_count"] == i updated = temp_storage.get_memory(test_id) assert updated.use_count == 5 def test_touch_improves_score(self, temp_storage): """Test that touching improves the decay score.""" now = int(time.time()) old_time = now - (7 * 86400) # 7 days ago test_id = make_test_uuid("test-score") mem = Memory( id=test_id, content="Old memory", use_count=1, last_used=old_time, created_at=old_time, ) temp_storage.save_memory(mem) result = touch_memory(memory_id=test_id) assert result["success"] is True assert result["new_score"] > result["old_score"] def test_touch_memory_not_found(self, temp_storage): """Test touching non-existent memory.""" result = touch_memory(memory_id="00000000-0000-0000-0000-000000000000") assert result["success"] is False assert "not found" in result["message"].lower() def test_touch_result_format(self, temp_storage): """Test that touch result has correct format.""" test_id = make_test_uuid("test-format") mem = Memory(id=test_id, content="Test") temp_storage.save_memory(mem) result = touch_memory(memory_id=test_id) assert result["success"] is True assert "memory_id" in result assert "old_score" in result assert "new_score" in result assert "use_count" in result assert "strength" in result assert "message" in result def test_touch_updates_last_used_timestamp(self, temp_storage): """Test that last_used timestamp is updated.""" before = int(time.time()) test_id = make_test_uuid("test-time") mem = Memory(id=test_id, content="Test", last_used=before - 1000) temp_storage.save_memory(mem) time.sleep(0.1) result = touch_memory(memory_id=test_id) after = int(time.time()) assert result["success"] is True updated = temp_storage.get_memory(test_id) assert before <= updated.last_used <= after def test_touch_with_tags_preserves_metadata(self, temp_storage): """Test that touching preserves memory metadata.""" test_id = make_test_uuid("test-meta") mem = Memory( id=test_id, content="Test with metadata", meta=MemoryMetadata( tags=["important", "project"], source="user-input", context="Testing", ), entities=["TestEntity"], ) temp_storage.save_memory(mem) result = touch_memory(memory_id=test_id, boost_strength=True) assert result["success"] is True updated = temp_storage.get_memory(test_id) assert updated.meta.tags == ["important", "project"] assert updated.meta.source == "user-input" assert updated.meta.context == "Testing" assert updated.entities == ["TestEntity"] # Validation tests def test_touch_invalid_uuid_fails(self): """Test that invalid UUID fails validation.""" with pytest.raises(ValueError, match="memory_id.*valid UUID"): touch_memory(memory_id="not-a-uuid") def test_touch_empty_string_fails(self): """Test that empty string fails validation.""" with pytest.raises(ValueError, match="memory_id"): touch_memory(memory_id="") # Edge cases def test_touch_with_default_boost_strength(self, temp_storage): """Test that boost_strength defaults to False.""" test_id = make_test_uuid("test-default") mem = Memory(id=test_id, content="Test", strength=1.0) temp_storage.save_memory(mem) # Call without boost_strength parameter result = touch_memory(memory_id=test_id) assert result["success"] is True assert result["strength"] == pytest.approx(1.0) def test_touch_at_max_strength(self, temp_storage): """Test touching memory already at max strength.""" test_id = make_test_uuid("test-max") mem = Memory(id=test_id, content="Test", strength=2.0) temp_storage.save_memory(mem) result = touch_memory(memory_id=test_id, boost_strength=True) assert result["success"] is True assert result["strength"] == pytest.approx(2.0) def test_touch_incremental_strength_boosts(self, temp_storage): """Test multiple incremental strength boosts.""" test_id = make_test_uuid("test-incr") mem = Memory(id=test_id, content="Test", strength=1.0) temp_storage.save_memory(mem) expected_strengths = [1.1, 1.2, 1.3, 1.4, 1.5] for expected in expected_strengths: result = touch_memory(memory_id=test_id, boost_strength=True) assert result["success"] is True assert result["strength"] == pytest.approx(expected, rel=0.01) def test_touch_with_high_use_count(self, temp_storage): """Test touching memory with high use count.""" test_id = make_test_uuid("test-high") mem = Memory(id=test_id, content="Test", use_count=999) temp_storage.save_memory(mem) result = touch_memory(memory_id=test_id) assert result["success"] is True assert result["use_count"] == 1000 def test_touch_recently_created_memory(self, temp_storage): """Test touching memory that was just created.""" test_id = make_test_uuid("test-new") mem = Memory(id=test_id, content="Just created") temp_storage.save_memory(mem) result = touch_memory(memory_id=test_id) assert result["success"] is True assert result["use_count"] == 1 def test_touch_preserves_content(self, temp_storage): """Test that touching doesn't modify memory content.""" original_content = "This content should not change" test_id = make_test_uuid("test-content") mem = Memory(id=test_id, content=original_content) temp_storage.save_memory(mem) touch_memory(memory_id=test_id, boost_strength=True) updated = temp_storage.get_memory(test_id) assert updated.content == original_content def test_touch_score_message(self, temp_storage): """Test that message includes score change.""" test_id = make_test_uuid("test-msg") mem = Memory(id=test_id, content="Test") temp_storage.save_memory(mem) result = touch_memory(memory_id=test_id) assert result["success"] is True assert "reinforced" in result["message"].lower() assert "->" in result["message"] class TestGarbageCollection: """Test suite for gc tool.""" def test_gc_dry_run_mode(self, temp_storage): """Test dry run mode doesn't actually delete memories.""" now = int(time.time()) old_time = now - (30 * 86400) # 30 days ago # Create old, low-scoring memory old_mem = Memory( id="mem-old", content="Old memory", use_count=0, last_used=old_time, created_at=old_time, strength=1.0, ) temp_storage.save_memory(old_mem) result = gc(dry_run=True) assert result["success"] is True assert result["dry_run"] is True # Memory should still exist assert temp_storage.get_memory("mem-old") is not None def test_gc_actually_removes_memories(self, temp_storage): """Test that gc with dry_run=False actually removes memories.""" now = int(time.time()) old_time = now - (30 * 86400) old_mem = Memory( id="mem-remove", content="To be removed", use_count=0, last_used=old_time, created_at=old_time, ) temp_storage.save_memory(old_mem) result = gc(dry_run=False) assert result["success"] is True assert result["dry_run"] is False # Memory should be deleted assert temp_storage.get_memory("mem-remove") is None def test_gc_archive_mode(self, temp_storage): """Test archiving memories instead of deleting.""" now = int(time.time()) old_time = now - (30 * 86400) old_mem = Memory( id="mem-archive", content="To be archived", use_count=0, last_used=old_time, created_at=old_time, ) temp_storage.save_memory(old_mem) result = gc(dry_run=False, archive_instead=True) assert result["success"] is True assert result["archived_count"] >= 1 assert result["removed_count"] == 0 # Memory should exist but be archived archived = temp_storage.get_memory("mem-archive") assert archived is not None assert archived.status == MemoryStatus.ARCHIVED def test_gc_keeps_recent_memories(self, temp_storage): """Test that recent memories are not garbage collected.""" now = int(time.time()) recent_mem = Memory( id="mem-recent", content="Recent memory", use_count=5, last_used=now, created_at=now ) temp_storage.save_memory(recent_mem) result = gc(dry_run=False) assert result["success"] is True # Recent memory should still exist assert temp_storage.get_memory("mem-recent") is not None def test_gc_with_limit(self, temp_storage): """Test limiting number of memories to collect.""" now = int(time.time()) old_time = now - (30 * 86400) # Create multiple old memories for i in range(10): mem = Memory( id=f"mem-{i}", content=f"Old memory {i}", use_count=0, last_used=old_time, created_at=old_time, ) temp_storage.save_memory(mem) result = gc(dry_run=True, limit=3) assert result["success"] is True assert result["total_affected"] == 3 def test_gc_no_memories_to_collect(self, temp_storage): """Test gc when no memories need collection.""" now = int(time.time()) # Create only fresh, high-scoring memories for i in range(3): mem = Memory( id=f"mem-{i}", content=f"Fresh memory {i}", use_count=10, last_used=now, strength=1.5, ) temp_storage.save_memory(mem) result = gc(dry_run=False) assert result["success"] is True assert result["removed_count"] == 0 assert result["archived_count"] == 0 assert result["total_affected"] == 0 def test_gc_result_format(self, temp_storage): """Test that gc result has correct format.""" result = gc(dry_run=True) assert result["success"] is True assert "dry_run" in result assert "removed_count" in result assert "archived_count" in result assert "freed_score_sum" in result assert "memory_ids" in result assert "total_affected" in result assert "message" in result def test_gc_freed_score_sum(self, temp_storage): """Test that freed_score_sum is calculated.""" now = int(time.time()) old_time = now - (30 * 86400) old_mem = Memory( id="mem-score", content="Old", use_count=0, last_used=old_time, created_at=old_time ) temp_storage.save_memory(old_mem) result = gc(dry_run=True) assert result["success"] is True if result["total_affected"] > 0: assert result["freed_score_sum"] >= 0 def test_gc_memory_ids_limited_in_result(self, temp_storage): """Test that memory_ids in result is limited to 10.""" now = int(time.time()) old_time = now - (30 * 86400) # Create 15 old memories for i in range(15): mem = Memory( id=f"mem-{i:02d}", content=f"Old {i}", use_count=0, last_used=old_time, created_at=old_time, ) temp_storage.save_memory(mem) result = gc(dry_run=True) assert result["success"] is True # Result should show max 10 IDs even if more affected assert len(result["memory_ids"]) <= 10 def test_gc_sorts_by_lowest_score_first(self, temp_storage): """Test that gc removes lowest-scoring memories first.""" now = int(time.time()) # Create memories with different ages and track their IDs mem_ids = [] for i in range(5): days_old = 30 + (i * 5) old_time = now - (days_old * 86400) mem_id = make_test_uuid(f"mem-{i}") mem = Memory( id=mem_id, content=f"Memory {i}", use_count=1, # Set to 1 so memories have non-zero scores last_used=old_time, created_at=old_time, ) temp_storage.save_memory(mem) mem_ids.append(mem_id) result = gc(dry_run=False, limit=2) assert result["success"] is True assert result["total_affected"] == 2 # Verify that the two oldest memories (mem-4 and mem-3) were removed assert temp_storage.get_memory(mem_ids[4]) is None assert temp_storage.get_memory(mem_ids[3]) is None # Verify that the others still exist assert temp_storage.get_memory(mem_ids[2]) is not None assert temp_storage.get_memory(mem_ids[1]) is not None assert temp_storage.get_memory(mem_ids[0]) is not None def test_gc_message_includes_threshold(self, temp_storage): """Test that message includes forget threshold.""" result = gc(dry_run=True) assert result["success"] is True assert "threshold" in result["message"].lower() def test_gc_dry_run_shows_would_remove(self, temp_storage): """Test that dry run message says 'Would remove'.""" result = gc(dry_run=True) assert result["success"] is True assert "would remove" in result["message"].lower() def test_gc_actual_run_shows_removed(self, temp_storage): """Test that actual run message says 'Removed'.""" result = gc(dry_run=False) assert result["success"] is True assert result["message"].startswith("Removed") # Validation tests def test_gc_invalid_limit_fails(self): """Test that invalid limit values fail.""" with pytest.raises(ValueError, match="limit"): gc(limit=0) with pytest.raises(ValueError, match="limit"): gc(limit=10001) with pytest.raises(ValueError, match="limit"): gc(limit=-1) # Edge cases def test_gc_default_parameters(self, temp_storage): """Test that gc defaults to dry_run=True.""" now = int(time.time()) old_time = now - (30 * 86400) old_mem = Memory( id="mem-default", content="Old", use_count=0, last_used=old_time, created_at=old_time ) temp_storage.save_memory(old_mem) # Call without parameters result = gc() assert result["success"] is True assert result["dry_run"] is True # Memory should still exist (dry run) assert temp_storage.get_memory("mem-default") is not None def test_gc_with_none_limit(self, temp_storage): """Test that None limit processes all eligible memories.""" now = int(time.time()) old_time = now - (30 * 86400) for i in range(5): mem = Memory( id=f"mem-{i}", content=f"Old {i}", use_count=0, last_used=old_time, created_at=old_time, ) temp_storage.save_memory(mem) result = gc(dry_run=True, limit=None) assert result["success"] is True # Should process all eligible memories if result["total_affected"] > 0: assert result["total_affected"] >= 5 def test_gc_empty_database(self, temp_storage): """Test gc on empty database.""" result = gc(dry_run=False) assert result["success"] is True assert result["removed_count"] == 0 assert result["total_affected"] == 0 def test_gc_preserves_promoted_memories(self, temp_storage): """Test that promoted memories are not collected.""" now = int(time.time()) old_time = now - (30 * 86400) promoted_mem = Memory( id="mem-promoted", content="Promoted memory", use_count=0, last_used=old_time, created_at=old_time, status=MemoryStatus.PROMOTED, ) temp_storage.save_memory(promoted_mem) result = gc(dry_run=False) assert result["success"] is True # Promoted memory should not be collected assert temp_storage.get_memory("mem-promoted") is not None def test_gc_preserves_archived_memories(self, temp_storage): """Test that already archived memories are not re-collected.""" now = int(time.time()) old_time = now - (30 * 86400) archived_mem = Memory( id="mem-archived", content="Already archived", use_count=0, last_used=old_time, created_at=old_time, status=MemoryStatus.ARCHIVED, ) temp_storage.save_memory(archived_mem) result = gc(dry_run=False) assert result["success"] is True # Already archived memory should still exist assert temp_storage.get_memory("mem-archived") is not None def test_gc_dry_run_archive_mode(self, temp_storage): """Test dry run with archive_instead flag.""" now = int(time.time()) old_time = now - (30 * 86400) old_mem = Memory( id="mem-dry-archive", content="Old", use_count=0, last_used=old_time, created_at=old_time, ) temp_storage.save_memory(old_mem) result = gc(dry_run=True, archive_instead=True) assert result["success"] is True assert result["dry_run"] is True # Memory should still exist and be active (dry run) mem = temp_storage.get_memory("mem-dry-archive") assert mem is not None assert mem.status == MemoryStatus.ACTIVE def test_gc_mixed_score_memories(self, temp_storage): """Test gc with mix of high and low scoring memories.""" now = int(time.time()) # High scoring (recent) high_mem = Memory(id="mem-high", content="High score", use_count=10, last_used=now) # Low scoring (old) low_mem = Memory( id="mem-low", content="Low score", use_count=0, last_used=now - (30 * 86400), created_at=now - (30 * 86400), ) temp_storage.save_memory(high_mem) temp_storage.save_memory(low_mem) result = gc(dry_run=False) assert result["success"] is True # High scoring should remain assert temp_storage.get_memory("mem-high") is not None class TestPromoteMemory: """Test suite for promote_memory tool.""" def test_promote_requires_memory_id_or_auto_detect(self): """Test that either memory_id or auto_detect must be provided.""" result = promote_memory() assert result["success"] is False assert "must specify" in result["message"].lower() def test_promote_memory_not_found(self, temp_storage): """Test promoting non-existent memory.""" result = promote_memory(memory_id="00000000-0000-0000-0000-000000000000") assert result["success"] is False assert "not found" in result["message"].lower() def test_promote_already_promoted_memory(self, temp_storage): """Test that already promoted memory returns error.""" test_id = make_test_uuid("mem-promoted") mem = Memory( id=test_id, content="Already promoted", status=MemoryStatus.PROMOTED, promoted_to="/vault/memory.md", ) temp_storage.save_memory(mem) result = promote_memory(memory_id=test_id) assert result["success"] is False assert "already promoted" in result["message"].lower() assert "promoted_to" in result def test_promote_memory_not_meeting_criteria(self, temp_storage): """Test that low-scoring memory cannot be promoted without force. Note: With use_count+1 formula, even use_count=0 gives score of 1.0 when fresh. Need old memory (20 days) to decay below promotion threshold (0.65). """ now = int(time.time()) twenty_days_ago = now - (20 * 86400) test_id = make_test_uuid("mem-low") low_score_mem = Memory( id=test_id, content="Low score memory", use_count=0, # With +1 formula: (0+1)^0.6 = 1.0, but will decay last_used=twenty_days_ago, # Old enough to decay below 0.65 created_at=twenty_days_ago, strength=1.0, ) temp_storage.save_memory(low_score_mem) result = promote_memory(memory_id=test_id) assert result["success"] is False assert "does not meet" in result["message"].lower() assert "score" in result @patch("cortexgraph.tools.promote.BasicMemoryIntegration") def test_promote_with_force_flag(self, mock_integration, temp_storage): """Test that force flag bypasses criteria check.""" now = int(time.time()) mock_integration_instance = MagicMock() mock_integration_instance.promote_to_obsidian.return_value = { "success": True, "path": "/vault/forced.md", } mock_integration.return_value = mock_integration_instance test_id = make_test_uuid("mem-force") low_score_mem = Memory( id=test_id, content="Forced promotion", use_count=0, last_used=now, created_at=now ) temp_storage.save_memory(low_score_mem) result = promote_memory(memory_id=test_id, force=True, dry_run=False) assert result["success"] is True assert result["promoted_count"] >= 1 @patch("cortexgraph.tools.promote.BasicMemoryIntegration") def test_promote_high_scoring_memory(self, mock_integration, temp_storage): """Test promoting a high-scoring memory.""" now = int(time.time()) mock_integration_instance = MagicMock() mock_integration_instance.promote_to_obsidian.return_value = { "success": True, "path": "/vault/high.md", } mock_integration.return_value = mock_integration_instance test_id = make_test_uuid("mem-high") high_score_mem = Memory( id=test_id, content="High value memory", use_count=10, last_used=now, created_at=now - (14 * 86400), # 14 days old strength=1.5, ) temp_storage.save_memory(high_score_mem) result = promote_memory(memory_id=test_id, dry_run=False) assert result["success"] is True assert result["promoted_count"] == 1 assert result["promoted_ids"] == [test_id] def test_promote_dry_run_mode(self, temp_storage): """Test dry run mode doesn't actually promote.""" now = int(time.time()) test_id = make_test_uuid("mem-dry") high_mem = Memory( id=test_id, content="Test dry run", use_count=10, last_used=now, created_at=now - (14 * 86400), strength=1.5, ) temp_storage.save_memory(high_mem) result = promote_memory(memory_id=test_id, dry_run=True, force=True) assert result["success"] is True assert result["dry_run"] is True assert result["promoted_count"] == 0 # Memory should still be active mem = temp_storage.get_memory(test_id) assert mem.status == MemoryStatus.ACTIVE def test_promote_auto_detect_no_candidates(self, temp_storage): """Test auto-detect when no memories meet criteria. Note: With use_count+1 formula, fresh memories have score=1.0. Need old memories (20 days) to decay below promotion threshold. """ now = int(time.time()) twenty_days_ago = now - (20 * 86400) # Create only low-scoring memories (old enough to decay below 0.65) for i in range(3): test_id = make_test_uuid(f"mem-{i}") mem = Memory( id=test_id, content=f"Low score {i}", use_count=0, last_used=twenty_days_ago, # Old enough to decay created_at=twenty_days_ago, ) temp_storage.save_memory(mem) result = promote_memory(auto_detect=True, dry_run=True) assert result["success"] is True assert result["candidates_found"] == 0 @patch("cortexgraph.tools.promote.BasicMemoryIntegration") def test_promote_auto_detect_finds_candidates(self, mock_integration, temp_storage): """Test auto-detect finds high-value memories.""" now = int(time.time()) mock_integration_instance = MagicMock() mock_integration_instance.promote_to_obsidian.return_value = { "success": True, "path": "/vault/auto.md", } mock_integration.return_value = mock_integration_instance # Create high-value memory test_id = make_test_uuid("mem-auto") high_mem = Memory( id=test_id, content="High value", use_count=10, last_used=now, created_at=now - (14 * 86400), strength=1.5, ) temp_storage.save_memory(high_mem) result = promote_memory(auto_detect=True, dry_run=True) assert result["success"] is True # May find candidates if criteria met assert "candidates_found" in result def test_promote_result_format(self, temp_storage): """Test that promote result has correct format.""" result = promote_memory(auto_detect=True, dry_run=True) assert result["success"] is True assert "dry_run" in result assert "candidates_found" in result assert "promoted_count" in result assert "promoted_ids" in result assert "candidates" in result assert "message" in result def test_promote_candidate_preview_format(self, temp_storage): """Test that candidate previews have correct format.""" now = int(time.time()) test_id = make_test_uuid("mem-preview") high_mem = Memory( id=test_id, content="Test preview format" * 10, # Long content use_count=10, last_used=now, created_at=now - (14 * 86400), strength=1.5, ) temp_storage.save_memory(high_mem) result = promote_memory(auto_detect=True, dry_run=True) assert result["success"] is True if result["candidates_found"] > 0: candidate = result["candidates"][0] assert "id" in candidate assert "content_preview" in candidate assert "reason" in candidate assert "score" in candidate assert "use_count" in candidate assert "age_days" in candidate # Content should be truncated to 100 chars assert len(candidate["content_preview"]) <= 100 def test_promote_candidates_limited_to_10(self, temp_storage): """Test that candidate list is limited to 10.""" now = int(time.time()) # Create many high-value memories for i in range(15): test_id = make_test_uuid(f"mem-{i:02d}") mem = Memory( id=test_id, content=f"High value {i}", use_count=10, last_used=now, created_at=now - (14 * 86400), strength=1.5, ) temp_storage.save_memory(mem) result = promote_memory(auto_detect=True, dry_run=True) assert result["success"] is True # Candidate preview list should be limited to 10 assert len(result["candidates"]) <= 10 @patch("cortexgraph.tools.promote.BasicMemoryIntegration") def test_promote_updates_memory_status(self, mock_integration, temp_storage): """Test that promotion updates memory status.""" now = int(time.time()) mock_integration_instance = MagicMock() mock_integration_instance.promote_to_obsidian.return_value = { "success": True, "path": "/vault/promoted.md", } mock_integration.return_value = mock_integration_instance test_id = make_test_uuid("mem-status") mem = Memory( id=test_id, content="Test status update", use_count=10, last_used=now, created_at=now - (14 * 86400), strength=1.5, ) temp_storage.save_memory(mem) result = promote_memory(memory_id=test_id, dry_run=False, force=True) if result["success"] and result["promoted_count"] > 0: updated = temp_storage.get_memory(test_id) assert updated.status == MemoryStatus.PROMOTED assert updated.promoted_at is not None assert updated.promoted_to is not None # Validation tests def test_promote_invalid_uuid_fails(self): """Test that invalid UUID fails.""" with pytest.raises(ValueError, match="memory_id.*valid UUID"): promote_memory(memory_id="not-a-uuid") def test_promote_invalid_target_fails(self): """Test that invalid target fails.""" with pytest.raises(ValueError, match="target"): promote_memory(auto_detect=True, target="invalid-target") def test_promote_empty_string_uuid_fails(self): """Test that empty string UUID fails.""" with pytest.raises(ValueError, match="memory_id"): promote_memory(memory_id="") # Edge cases def test_promote_with_default_parameters(self, temp_storage): """Test default parameter values.""" now = int(time.time()) test_id = make_test_uuid("mem-defaults") mem = Memory( id=test_id, content="Test defaults", use_count=10, last_used=now, created_at=now - (14 * 86400), strength=1.5, ) temp_storage.save_memory(mem) # Should fail because neither memory_id nor auto_detect provided result = promote_memory() assert result["success"] is False def test_promote_obsidian_target(self, temp_storage): """Test that obsidian is valid target.""" result = promote_memory(auto_detect=True, dry_run=True, target="obsidian") assert result["success"] is True def test_promote_message_dry_run(self, temp_storage): """Test that dry run message says 'Would promote'.""" result = promote_memory(auto_detect=True, dry_run=True) assert result["success"] is True assert "would promote" in result["message"].lower() @patch("cortexgraph.tools.promote.BasicMemoryIntegration") def test_promote_message_actual_run(self, mock_integration, temp_storage): """Test that actual run message says 'Promoted'.""" mock_integration_instance = MagicMock() mock_integration_instance.promote_to_obsidian.return_value = { "success": True, "path": "/vault/test.md", } mock_integration.return_value = mock_integration_instance result = promote_memory(auto_detect=True, dry_run=False) assert result["success"] is True assert result["message"].startswith("Promoted") def test_promote_empty_database(self, temp_storage): """Test promotion on empty database.""" result = promote_memory(auto_detect=True, dry_run=True) assert result["success"] is True assert result["candidates_found"] == 0 assert result["promoted_count"] == 0 @patch("cortexgraph.tools.promote.BasicMemoryIntegration") def test_promote_integration_failure(self, mock_integration, temp_storage): """Test handling of integration failure.""" now = int(time.time()) mock_integration_instance = MagicMock() mock_integration_instance.promote_to_obsidian.return_value = { "success": False, "error": "Write failed", } mock_integration.return_value = mock_integration_instance test_id = make_test_uuid("mem-fail") mem = Memory( id=test_id, content="Test failure", use_count=10, last_used=now, created_at=now - (14 * 86400), strength=1.5, ) temp_storage.save_memory(mem) result = promote_memory(memory_id=test_id, dry_run=False, force=True) # Should still succeed but with 0 promoted assert result["success"] is True assert result["promoted_count"] == 0 def test_promote_candidates_sorted_by_score(self, temp_storage): """Test that candidates are sorted by score descending.""" now = int(time.time()) # Create memories with different scores for i in range(3): test_id = make_test_uuid(f"mem-{i}") mem = Memory( id=test_id, content=f"Memory {i}", use_count=i * 5, last_used=now, created_at=now - (14 * 86400), strength=1.0 + (i * 0.2), ) temp_storage.save_memory(mem) result = promote_memory(auto_detect=True, dry_run=True) assert result["success"] is True if result["candidates_found"] > 1: scores = [c["score"] for c in result["candidates"]] assert scores == sorted(scores, reverse=True) def test_promote_preserves_memory_content(self, temp_storage): """Test that promotion doesn't modify content.""" now = int(time.time()) original_content = "This content should not change" test_id = make_test_uuid("mem-content") mem = Memory( id=test_id, content=original_content, use_count=10, last_used=now, created_at=now - (14 * 86400), strength=1.5, ) temp_storage.save_memory(mem) promote_memory(memory_id=test_id, dry_run=True, force=True) updated = temp_storage.get_memory(test_id) assert updated.content == original_content

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/prefrontalsys/mnemex'

If you have feedback or need assistance with the MCP directory API, please join our Discord server