Skip to main content
Glama
test_storage.py35.8 kB
"""Tests for JSONL storage layer.""" import asyncio import tempfile import time from pathlib import Path import pytest from mnemex.storage.jsonl_storage import JSONLStorage from mnemex.storage.models import Memory, MemoryMetadata, MemoryStatus, Relation @pytest.fixture def temp_storage(): """Create a temporary JSONL storage for testing.""" with tempfile.TemporaryDirectory() as tmpdir: storage_dir = Path(tmpdir) storage = JSONLStorage(storage_path=storage_dir) storage.connect() yield storage storage.close() def test_storage_init(temp_storage): """Test storage initialization.""" count = temp_storage.count_memories() assert count == 0 def test_save_and_get_memory(temp_storage): """Test saving and retrieving a memory.""" memory = Memory( id="test-123", content="Test memory content", meta=MemoryMetadata(tags=["test"]), ) temp_storage.save_memory(memory) retrieved = temp_storage.get_memory("test-123") assert retrieved is not None assert retrieved.id == "test-123" assert retrieved.content == "Test memory content" assert "test" in retrieved.meta.tags def test_update_memory(temp_storage): """Test updating memory fields.""" memory = Memory( id="test-456", content="Test content", use_count=0, ) temp_storage.save_memory(memory) # Update use count and last_used now = int(time.time()) success = temp_storage.update_memory( memory_id="test-456", last_used=now, use_count=5, ) assert success updated = temp_storage.get_memory("test-456") assert updated is not None assert updated.use_count == 5 assert updated.last_used == now def test_delete_memory(temp_storage): """Test deleting a memory.""" memory = Memory(id="test-789", content="To be deleted") temp_storage.save_memory(memory) assert temp_storage.get_memory("test-789") is not None success = temp_storage.delete_memory("test-789") assert success assert temp_storage.get_memory("test-789") is None def test_list_memories(temp_storage): """Test listing memories with filters.""" # Create some test memories for i in range(5): memory = Memory( id=f"mem-{i}", content=f"Memory {i}", status=MemoryStatus.ACTIVE if i < 3 else MemoryStatus.PROMOTED, ) temp_storage.save_memory(memory) # List all active memories active = temp_storage.list_memories(status=MemoryStatus.ACTIVE) assert len(active) == 3 # List all memories all_mems = temp_storage.list_memories() assert len(all_mems) == 5 def test_search_memories_by_tags(temp_storage): """Test searching memories by tags.""" mem1 = Memory( id="mem-1", content="Python tutorial", meta=MemoryMetadata(tags=["python", "tutorial"]), ) mem2 = Memory( id="mem-2", content="JavaScript guide", meta=MemoryMetadata(tags=["javascript", "guide"]), ) mem3 = Memory( id="mem-3", content="Python guide", meta=MemoryMetadata(tags=["python", "guide"]), ) temp_storage.save_memory(mem1) temp_storage.save_memory(mem2) temp_storage.save_memory(mem3) # Search for python tag results = temp_storage.search_memories(tags=["python"]) assert len(results) == 2 assert all("python" in m.meta.tags for m in results) def test_count_memories(temp_storage): """Test counting memories.""" for i in range(3): memory = Memory(id=f"mem-{i}", content=f"Memory {i}") temp_storage.save_memory(memory) count = temp_storage.count_memories() assert count == 3 count_active = temp_storage.count_memories(status=MemoryStatus.ACTIVE) assert count_active == 3 # ============================================================================ # Initialization edge cases # ============================================================================ def test_init_with_none_storage_path(): """Test initialization with None storage_path uses config default.""" storage = JSONLStorage(storage_path=None) # Should use config's storage_path assert storage.storage_dir is not None assert storage.storage_dir.exists() def test_init_with_string_path(): """Test initialization with string path converts to Path.""" with tempfile.TemporaryDirectory() as tmpdir: storage = JSONLStorage(storage_path=tmpdir) assert isinstance(storage.storage_dir, Path) assert str(storage.storage_dir) == tmpdir def test_storage_path_property_getter_setter(): """Test storage_path property getter and setter.""" with tempfile.TemporaryDirectory() as tmpdir: storage = JSONLStorage() # Test getter original_path = storage.storage_path assert isinstance(original_path, Path) # Test setter with Path new_path = Path(tmpdir) / "new_storage" storage.storage_path = new_path assert storage.storage_path == new_path assert storage.storage_dir == new_path assert new_path.exists() # Test setter with string new_path2 = Path(tmpdir) / "string_storage" storage.storage_path = str(new_path2) assert storage.storage_path == new_path2 assert isinstance(storage.storage_path, Path) # ============================================================================ # Connection and loading # ============================================================================ def test_load_existing_jsonl_with_data(): """Test loading from existing JSONL files with data.""" with tempfile.TemporaryDirectory() as tmpdir: storage_dir = Path(tmpdir) # Create a JSONL file with existing data memories_path = storage_dir / "memories.jsonl" with open(memories_path, "w") as f: mem1 = Memory(id="existing-1", content="Existing memory 1") mem2 = Memory( id="existing-2", content="Existing memory 2", meta=MemoryMetadata(tags=["test", "existing"]), ) f.write(mem1.model_dump_json() + "\n") f.write(mem2.model_dump_json() + "\n") # Load storage storage = JSONLStorage(storage_path=storage_dir) storage.connect() # Verify data was loaded assert storage.count_memories() == 2 loaded1 = storage.get_memory("existing-1") assert loaded1 is not None assert loaded1.content == "Existing memory 1" loaded2 = storage.get_memory("existing-2") assert loaded2 is not None assert "test" in loaded2.meta.tags assert "existing" in loaded2.meta.tags storage.close() def test_load_with_deletion_markers(): """Test loading with deletion markers (_deleted).""" with tempfile.TemporaryDirectory() as tmpdir: storage_dir = Path(tmpdir) memories_path = storage_dir / "memories.jsonl" # Create JSONL with memory and deletion marker with open(memories_path, "w") as f: mem1 = Memory(id="mem-1", content="Memory 1") mem2 = Memory(id="mem-2", content="Memory 2") f.write(mem1.model_dump_json() + "\n") f.write(mem2.model_dump_json() + "\n") # Add deletion marker for mem-1 f.write('{"id": "mem-1", "_deleted": true}\n') # Load storage storage = JSONLStorage(storage_path=storage_dir) storage.connect() # mem-1 should be deleted, mem-2 should exist assert storage.get_memory("mem-1") is None assert storage.get_memory("mem-2") is not None assert storage.count_memories() == 1 assert "mem-1" in storage._deleted_memory_ids storage.close() def test_load_handles_empty_lines(): """Test that empty lines in JSONL are handled correctly.""" with tempfile.TemporaryDirectory() as tmpdir: storage_dir = Path(tmpdir) memories_path = storage_dir / "memories.jsonl" # Create JSONL with empty lines with open(memories_path, "w") as f: mem1 = Memory(id="mem-1", content="Memory 1") f.write(mem1.model_dump_json() + "\n") f.write("\n") # Empty line f.write(" \n") # Whitespace-only line mem2 = Memory(id="mem-2", content="Memory 2") f.write(mem2.model_dump_json() + "\n") # Load storage - should not fail storage = JSONLStorage(storage_path=storage_dir) storage.connect() assert storage.count_memories() == 2 assert storage.get_memory("mem-1") is not None assert storage.get_memory("mem-2") is not None storage.close() def test_tag_index_rebuilding(temp_storage): """Test that tag index is rebuilt on connect.""" # Add memories with tags mem1 = Memory(id="mem-1", content="Memory 1", meta=MemoryMetadata(tags=["python", "test"])) mem2 = Memory(id="mem-2", content="Memory 2", meta=MemoryMetadata(tags=["python", "dev"])) mem3 = Memory(id="mem-3", content="Memory 3", meta=MemoryMetadata(tags=["javascript"])) temp_storage.save_memory(mem1) temp_storage.save_memory(mem2) temp_storage.save_memory(mem3) # Verify tag index was built assert "python" in temp_storage._tag_index assert "test" in temp_storage._tag_index assert "javascript" in temp_storage._tag_index # Check index contents assert "mem-1" in temp_storage._tag_index["python"] assert "mem-2" in temp_storage._tag_index["python"] assert len(temp_storage._tag_index["python"]) == 2 # Search should use tag index results = temp_storage.search_memories(tags=["python"]) assert len(results) == 2 # ============================================================================ # Relations # ============================================================================ def test_create_and_get_relation(temp_storage): """Test creating and retrieving relations.""" # Create some memories mem1 = Memory(id="mem-1", content="Memory 1") mem2 = Memory(id="mem-2", content="Memory 2") temp_storage.save_memory(mem1) temp_storage.save_memory(mem2) # Create a relation relation = Relation( id="rel-1", from_memory_id="mem-1", to_memory_id="mem-2", relation_type="references" ) temp_storage.create_relation(relation) # Get all relations relations = temp_storage.get_all_relations() assert len(relations) == 1 assert relations[0].id == "rel-1" assert relations[0].from_memory_id == "mem-1" assert relations[0].to_memory_id == "mem-2" def test_delete_relation(temp_storage): """Test deleting a relation.""" # Create memories and relation mem1 = Memory(id="mem-1", content="Memory 1") mem2 = Memory(id="mem-2", content="Memory 2") temp_storage.save_memory(mem1) temp_storage.save_memory(mem2) relation = Relation( id="rel-1", from_memory_id="mem-1", to_memory_id="mem-2", relation_type="references" ) temp_storage.create_relation(relation) # Delete relation success = temp_storage.delete_relation("rel-1") assert success assert len(temp_storage.get_all_relations()) == 0 assert "rel-1" in temp_storage._deleted_relation_ids # Try deleting non-existent relation success = temp_storage.delete_relation("non-existent") assert not success def test_get_relations_filtering(temp_storage): """Test filtering relations by source, target, and type.""" # Create memories for i in range(1, 5): temp_storage.save_memory(Memory(id=f"mem-{i}", content=f"Memory {i}")) # Create various relations temp_storage.create_relation( Relation( id="rel-1", from_memory_id="mem-1", to_memory_id="mem-2", relation_type="references" ) ) temp_storage.create_relation( Relation( id="rel-2", from_memory_id="mem-1", to_memory_id="mem-3", relation_type="similar_to" ) ) temp_storage.create_relation( Relation( id="rel-3", from_memory_id="mem-2", to_memory_id="mem-3", relation_type="references" ) ) temp_storage.create_relation( Relation( id="rel-4", from_memory_id="mem-3", to_memory_id="mem-4", relation_type="follows_from" ) ) # Filter by source from_mem1 = temp_storage.get_relations(from_memory_id="mem-1") assert len(from_mem1) == 2 assert all(r.from_memory_id == "mem-1" for r in from_mem1) # Filter by target to_mem3 = temp_storage.get_relations(to_memory_id="mem-3") assert len(to_mem3) == 2 assert all(r.to_memory_id == "mem-3" for r in to_mem3) # Filter by type references = temp_storage.get_relations(relation_type="references") assert len(references) == 2 assert all(r.relation_type == "references" for r in references) # Filter by multiple criteria specific = temp_storage.get_relations(from_memory_id="mem-1", relation_type="references") assert len(specific) == 1 assert specific[0].id == "rel-1" def test_load_relations_with_deletion_markers(): """Test loading relations with deletion markers.""" with tempfile.TemporaryDirectory() as tmpdir: storage_dir = Path(tmpdir) relations_path = storage_dir / "relations.jsonl" # Create JSONL with relations and deletion marker with open(relations_path, "w") as f: rel1 = Relation( id="rel-1", from_memory_id="m1", to_memory_id="m2", relation_type="references" ) rel2 = Relation( id="rel-2", from_memory_id="m2", to_memory_id="m3", relation_type="similar_to" ) f.write(rel1.model_dump_json() + "\n") f.write(rel2.model_dump_json() + "\n") # Add deletion marker for rel-1 f.write('{"id": "rel-1", "_deleted": true}\n') # Load storage storage = JSONLStorage(storage_path=storage_dir) storage.connect() # rel-1 should be deleted, rel-2 should exist all_relations = storage.get_all_relations() assert len(all_relations) == 1 assert all_relations[0].id == "rel-2" assert "rel-1" in storage._deleted_relation_ids storage.close() # ============================================================================ # Enhanced search functionality # ============================================================================ def test_search_with_window_days(temp_storage): """Test search with window_days parameter.""" now = int(time.time()) # Create memories with different last_used times old_mem = Memory(id="old", content="Old memory", last_used=now - 10 * 86400) # 10 days ago recent_mem = Memory( id="recent", content="Recent memory", last_used=now - 2 * 86400 ) # 2 days ago new_mem = Memory(id="new", content="New memory", last_used=now) temp_storage.save_memory(old_mem) temp_storage.save_memory(recent_mem) temp_storage.save_memory(new_mem) # Search within 7 days results = temp_storage.search_memories(window_days=7, limit=10) assert len(results) == 2 result_ids = [m.id for m in results] assert "new" in result_ids assert "recent" in result_ids assert "old" not in result_ids # Search within 1 day results = temp_storage.search_memories(window_days=1, limit=10) assert len(results) == 1 assert results[0].id == "new" def test_search_with_status_none(temp_storage): """Test search with status=None returns all statuses.""" mem1 = Memory(id="m1", content="Active", status=MemoryStatus.ACTIVE) mem2 = Memory(id="m2", content="Promoted", status=MemoryStatus.PROMOTED) mem3 = Memory(id="m3", content="Archived", status=MemoryStatus.ARCHIVED) temp_storage.save_memory(mem1) temp_storage.save_memory(mem2) temp_storage.save_memory(mem3) # Search with status=None should return all results = temp_storage.search_memories(status=None, limit=10) assert len(results) == 3 def test_search_with_limit(temp_storage): """Test search respects limit parameter.""" for i in range(10): mem = Memory(id=f"mem-{i}", content=f"Memory {i}") temp_storage.save_memory(mem) # Test default limit results = temp_storage.search_memories(limit=10) assert len(results) == 10 # Test custom limit results = temp_storage.search_memories(limit=5) assert len(results) == 5 # Test limit larger than available results = temp_storage.search_memories(limit=20) assert len(results) == 10 def test_search_combines_filters(temp_storage): """Test search with multiple filters combined.""" now = int(time.time()) # Create memories with various properties mem1 = Memory( id="m1", content="Python dev", meta=MemoryMetadata(tags=["python", "dev"]), status=MemoryStatus.ACTIVE, last_used=now - 1 * 86400, ) mem2 = Memory( id="m2", content="Python old", meta=MemoryMetadata(tags=["python", "old"]), status=MemoryStatus.ACTIVE, last_used=now - 10 * 86400, ) mem3 = Memory( id="m3", content="JavaScript dev", meta=MemoryMetadata(tags=["javascript", "dev"]), status=MemoryStatus.PROMOTED, last_used=now - 1 * 86400, ) temp_storage.save_memory(mem1) temp_storage.save_memory(mem2) temp_storage.save_memory(mem3) # Search: python tag + active status + within 7 days results = temp_storage.search_memories( tags=["python"], status=MemoryStatus.ACTIVE, window_days=7, limit=10 ) assert len(results) == 1 assert results[0].id == "m1" # ============================================================================ # Knowledge graph # ============================================================================ def test_get_knowledge_graph(temp_storage): """Test get_knowledge_graph method.""" # Create memories with different statuses mem1 = Memory(id="m1", content="Active 1", status=MemoryStatus.ACTIVE, use_count=5) mem2 = Memory(id="m2", content="Active 2", status=MemoryStatus.ACTIVE, use_count=3) mem3 = Memory(id="m3", content="Promoted", status=MemoryStatus.PROMOTED) temp_storage.save_memory(mem1) temp_storage.save_memory(mem2) temp_storage.save_memory(mem3) # Create relations rel1 = Relation(id="r1", from_memory_id="m1", to_memory_id="m2", relation_type="references") rel2 = Relation(id="r2", from_memory_id="m2", to_memory_id="m3", relation_type="promotes_to") temp_storage.create_relation(rel1) temp_storage.create_relation(rel2) # Get knowledge graph filtered by ACTIVE status graph = temp_storage.get_knowledge_graph(status=MemoryStatus.ACTIVE) assert len(graph.memories) == 2 assert len(graph.relations) == 2 # Relations are not filtered assert graph.stats["total_memories"] == 2 assert graph.stats["total_relations"] == 2 assert graph.stats["status_filter"] == "active" assert "avg_score" in graph.stats assert "avg_use_count" in graph.stats assert graph.stats["avg_use_count"] == 4.0 # (5 + 3) / 2 # Get knowledge graph with no status filter graph_all = temp_storage.get_knowledge_graph(status=None) assert len(graph_all.memories) == 3 assert graph_all.stats["status_filter"] == "all" # ============================================================================ # Batch operations # ============================================================================ def test_batch_save_memories(temp_storage): """Test save_memories_batch for efficient batch saving.""" memories = [Memory(id=f"batch-{i}", content=f"Batch memory {i}") for i in range(5)] temp_storage.save_memories_batch(memories) # Verify all were saved assert temp_storage.count_memories() == 5 for i in range(5): mem = temp_storage.get_memory(f"batch-{i}") assert mem is not None assert mem.content == f"Batch memory {i}" def test_batch_save_empty_list(temp_storage): """Test save_memories_batch with empty list.""" temp_storage.save_memories_batch([]) assert temp_storage.count_memories() == 0 def test_batch_delete_memories(temp_storage): """Test deleting multiple memories.""" # Create memories for i in range(5): temp_storage.save_memory(Memory(id=f"del-{i}", content=f"Memory {i}")) assert temp_storage.count_memories() == 5 # Delete multiple for i in [0, 2, 4]: temp_storage.delete_memory(f"del-{i}") assert temp_storage.count_memories() == 2 assert temp_storage.get_memory("del-1") is not None assert temp_storage.get_memory("del-3") is not None assert temp_storage.get_memory("del-0") is None # ============================================================================ # Persistence and compaction # ============================================================================ def test_compact_storage(temp_storage): """Test compact method removes deletion markers and duplicates.""" # Create and delete some memories for i in range(5): temp_storage.save_memory(Memory(id=f"mem-{i}", content=f"Memory {i}")) # Update some memories (creates duplicates in JSONL) temp_storage.update_memory("mem-0", use_count=5) temp_storage.update_memory("mem-1", use_count=3) # Delete some memories temp_storage.delete_memory("mem-2") temp_storage.delete_memory("mem-3") # Count lines in JSONL before compaction with open(temp_storage.memories_path) as f: lines_before = sum(1 for line in f if line.strip()) # Should have: 5 creates + 2 updates + 2 deletions = 9 lines assert lines_before == 9 # Compact stats = temp_storage.compact() assert stats["memories_before"] == 9 assert stats["memories_after"] == 3 # Only 3 memories remain # Count lines after compaction with open(temp_storage.memories_path) as f: lines_after = sum(1 for line in f if line.strip()) assert lines_after == 3 assert len(temp_storage._deleted_memory_ids) == 0 def test_data_persists_across_connections(): """Test that data persists when storage is closed and reopened.""" with tempfile.TemporaryDirectory() as tmpdir: storage_dir = Path(tmpdir) # First session: create and save data storage1 = JSONLStorage(storage_path=storage_dir) storage1.connect() mem = Memory( id="persist-1", content="Persistent memory", meta=MemoryMetadata(tags=["persist", "test"]), ) storage1.save_memory(mem) rel = Relation( id="rel-persist", from_memory_id="persist-1", to_memory_id="persist-1", relation_type="self", ) storage1.create_relation(rel) storage1.close() # Second session: verify data persists storage2 = JSONLStorage(storage_path=storage_dir) storage2.connect() loaded_mem = storage2.get_memory("persist-1") assert loaded_mem is not None assert loaded_mem.content == "Persistent memory" assert "persist" in loaded_mem.meta.tags loaded_rels = storage2.get_all_relations() assert len(loaded_rels) == 1 assert loaded_rels[0].id == "rel-persist" storage2.close() def test_compact_with_relations(temp_storage): """Test compact also works with relations.""" # Create relations for i in range(3): temp_storage.create_relation( Relation( id=f"rel-{i}", from_memory_id=f"m{i}", to_memory_id=f"m{i + 1}", relation_type="test", ) ) # Delete one temp_storage.delete_relation("rel-1") # Compact stats = temp_storage.compact() assert stats["relations_before"] == 4 # 3 creates + 1 delete assert stats["relations_after"] == 2 assert len(temp_storage._deleted_relation_ids) == 0 # ============================================================================ # Error handling # ============================================================================ def test_get_nonexistent_memory(temp_storage): """Test getting a non-existent memory returns None.""" result = temp_storage.get_memory("does-not-exist") assert result is None def test_update_nonexistent_memory(temp_storage): """Test updating a non-existent memory returns False.""" result = temp_storage.update_memory("does-not-exist", use_count=5) assert result is False def test_delete_nonexistent_memory(temp_storage): """Test deleting a non-existent memory returns False.""" result = temp_storage.delete_memory("does-not-exist") assert result is False def test_operations_without_connect(): """Test that operations fail gracefully without connect.""" with tempfile.TemporaryDirectory() as tmpdir: storage = JSONLStorage(storage_path=Path(tmpdir)) # Don't call connect() with pytest.raises(RuntimeError, match="Storage not connected"): storage.save_memory(Memory(id="test", content="Test")) with pytest.raises(RuntimeError, match="Storage not connected"): storage.get_memory("test") with pytest.raises(RuntimeError, match="Storage not connected"): storage.list_memories() with pytest.raises(RuntimeError, match="Storage not connected"): storage.delete_memory("test") # ============================================================================ # Status filtering and list operations # ============================================================================ def test_list_memories_with_different_statuses(temp_storage): """Test list_memories filters correctly by different statuses.""" # Create memories with different statuses temp_storage.save_memory(Memory(id="m1", content="Active 1", status=MemoryStatus.ACTIVE)) temp_storage.save_memory(Memory(id="m2", content="Active 2", status=MemoryStatus.ACTIVE)) temp_storage.save_memory(Memory(id="m3", content="Promoted", status=MemoryStatus.PROMOTED)) temp_storage.save_memory(Memory(id="m4", content="Archived", status=MemoryStatus.ARCHIVED)) # Test filtering active = temp_storage.list_memories(status=MemoryStatus.ACTIVE) assert len(active) == 2 promoted = temp_storage.list_memories(status=MemoryStatus.PROMOTED) assert len(promoted) == 1 assert promoted[0].id == "m3" archived = temp_storage.list_memories(status=MemoryStatus.ARCHIVED) assert len(archived) == 1 assert archived[0].id == "m4" all_mems = temp_storage.list_memories(status=None) assert len(all_mems) == 4 def test_list_memories_with_limit_offset(temp_storage): """Test list_memories pagination with limit and offset.""" # Create 10 memories with different last_used times now = int(time.time()) for i in range(10): mem = Memory( id=f"page-{i}", content=f"Memory {i}", last_used=now - i * 100 ) # Decreasing timestamps temp_storage.save_memory(mem) # Get first page page1 = temp_storage.list_memories(limit=3, offset=0) assert len(page1) == 3 assert page1[0].id == "page-0" # Most recent # Get second page page2 = temp_storage.list_memories(limit=3, offset=3) assert len(page2) == 3 assert page2[0].id == "page-3" # Get with offset only rest = temp_storage.list_memories(offset=7) assert len(rest) == 3 def test_count_memories_by_status(temp_storage): """Test count_memories with status filter.""" temp_storage.save_memory(Memory(id="m1", content="A", status=MemoryStatus.ACTIVE)) temp_storage.save_memory(Memory(id="m2", content="B", status=MemoryStatus.ACTIVE)) temp_storage.save_memory(Memory(id="m3", content="C", status=MemoryStatus.PROMOTED)) assert temp_storage.count_memories(status=MemoryStatus.ACTIVE) == 2 assert temp_storage.count_memories(status=MemoryStatus.PROMOTED) == 1 assert temp_storage.count_memories(status=MemoryStatus.ARCHIVED) == 0 assert temp_storage.count_memories(status=None) == 3 # ============================================================================ # Additional coverage # ============================================================================ def test_context_manager(): """Test using storage as a context manager.""" with tempfile.TemporaryDirectory() as tmpdir: storage_dir = Path(tmpdir) with JSONLStorage(storage_path=storage_dir) as storage: assert storage._connected storage.save_memory(Memory(id="ctx-1", content="Context test")) assert storage.get_memory("ctx-1") is not None # After exiting context, should be closed assert not storage._connected def test_get_all_embeddings(temp_storage): """Test get_all_embeddings returns only active memories with embeddings.""" mem1 = Memory( id="m1", content="With embedding", embed=[0.1, 0.2, 0.3], status=MemoryStatus.ACTIVE ) mem2 = Memory(id="m2", content="No embedding", status=MemoryStatus.ACTIVE) mem3 = Memory( id="m3", content="Promoted with embedding", embed=[0.4, 0.5, 0.6], status=MemoryStatus.PROMOTED, ) temp_storage.save_memory(mem1) temp_storage.save_memory(mem2) temp_storage.save_memory(mem3) embeddings = temp_storage.get_all_embeddings() # Should only get active memory with embedding assert len(embeddings) == 1 assert "m1" in embeddings assert embeddings["m1"] == [0.1, 0.2, 0.3] assert "m2" not in embeddings # No embedding assert "m3" not in embeddings # Not active def test_update_memory_all_fields(temp_storage): """Test update_memory updates all fields correctly.""" now = int(time.time()) mem = Memory(id="update-all", content="Test memory") temp_storage.save_memory(mem) # Update all fields success = temp_storage.update_memory( memory_id="update-all", last_used=now + 100, use_count=10, strength=0.8, status=MemoryStatus.PROMOTED, promoted_at=now + 200, promoted_to="/vault/promoted.md", ) assert success updated = temp_storage.get_memory("update-all") assert updated.last_used == now + 100 assert updated.use_count == 10 assert updated.strength == 0.8 assert updated.status == MemoryStatus.PROMOTED assert updated.promoted_at == now + 200 assert updated.promoted_to == "/vault/promoted.md" def test_connect_multiple_times(temp_storage): """Test that calling connect multiple times is safe.""" # temp_storage is already connected assert temp_storage._connected # Call connect again temp_storage.connect() # Should still be connected and work fine assert temp_storage._connected temp_storage.save_memory(Memory(id="test", content="Test")) assert temp_storage.get_memory("test") is not None # ============================================================================ # Async operations and storage stats # ============================================================================ def test_save_memory_async(temp_storage): """Test async save_memory method.""" async def run_test(): mem = Memory(id="async-1", content="Async memory") await temp_storage.save_memory_async(mem) # Verify it was saved retrieved = temp_storage.get_memory("async-1") assert retrieved is not None assert retrieved.content == "Async memory" asyncio.run(run_test()) def test_get_storage_stats(temp_storage): """Test get_storage_stats method.""" # Create some memories and relations for i in range(5): temp_storage.save_memory(Memory(id=f"stat-{i}", content=f"Memory {i}")) for i in range(3): temp_storage.create_relation( Relation( id=f"rel-stat-{i}", from_memory_id=f"stat-{i}", to_memory_id=f"stat-{i + 1}", relation_type="test", ) ) # Delete one memory and one relation to create compaction potential temp_storage.delete_memory("stat-0") temp_storage.delete_relation("rel-stat-0") # Get stats stats = temp_storage.get_storage_stats() assert "memories" in stats assert "relations" in stats # Check memory stats assert stats["memories"]["active"] == 4 # 5 - 1 deleted assert stats["memories"]["total_lines"] >= 6 # 5 creates + 1 delete assert stats["memories"]["file_size_bytes"] > 0 # Check relation stats assert stats["relations"]["active"] == 2 # 3 - 1 deleted assert stats["relations"]["total_lines"] >= 4 # 3 creates + 1 delete assert stats["relations"]["file_size_bytes"] > 0 def test_get_storage_stats_empty(temp_storage): """Test get_storage_stats on empty storage.""" stats = temp_storage.get_storage_stats() assert stats["memories"]["active"] == 0 assert stats["memories"]["total_lines"] == 0 assert stats["memories"]["file_size_bytes"] == 0 assert stats["relations"]["active"] == 0 assert stats["relations"]["total_lines"] == 0 assert stats["relations"]["file_size_bytes"] == 0 def test_operations_require_connection_all_methods(): """Test that all major operations check for connection.""" with tempfile.TemporaryDirectory() as tmpdir: storage = JSONLStorage(storage_path=Path(tmpdir)) # Don't call connect() # Test all methods that should raise RuntimeError with pytest.raises(RuntimeError, match="Storage not connected"): storage.update_memory("test", use_count=1) with pytest.raises(RuntimeError, match="Storage not connected"): storage.search_memories() with pytest.raises(RuntimeError, match="Storage not connected"): storage.count_memories() with pytest.raises(RuntimeError, match="Storage not connected"): storage.get_all_embeddings() with pytest.raises(RuntimeError, match="Storage not connected"): storage.create_relation( Relation(id="r1", from_memory_id="m1", to_memory_id="m2", relation_type="test") ) with pytest.raises(RuntimeError, match="Storage not connected"): storage.get_relations() with pytest.raises(RuntimeError, match="Storage not connected"): storage.get_all_relations() with pytest.raises(RuntimeError, match="Storage not connected"): storage.delete_relation("test") with pytest.raises(RuntimeError, match="Storage not connected"): storage.get_knowledge_graph() with pytest.raises(RuntimeError, match="Storage not connected"): storage.compact() with pytest.raises(RuntimeError, match="Storage not connected"): storage.get_storage_stats() def test_save_memories_batch_updates_tag_index(temp_storage): """Test that batch save doesn't update tag index (as per implementation).""" memories = [ Memory(id=f"batch-tag-{i}", content=f"Memory {i}", meta=MemoryMetadata(tags=[f"tag{i}"])) for i in range(3) ] temp_storage.save_memories_batch(memories) # Verify all memories were saved assert temp_storage.count_memories() == 3 for i in range(3): mem = temp_storage.get_memory(f"batch-tag-{i}") assert mem is not None

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/mnemexai/mnemex'

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