94b8d5d118
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Waiting to run
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Blocked by required conditions
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Waiting to run
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Blocked by required conditions
Docker / promote-latest (push) Blocked by required conditions
Init Native E2E / init-native (macos-latest, claude) (push) Waiting to run
Init Native E2E / init-native (macos-latest, codex) (push) Waiting to run
Init Native E2E / init-native (macos-latest, copilot) (push) Waiting to run
Install Native E2E / install-native (macos-latest) (push) Waiting to run
Wrap Native E2E / wrap-native (macos-latest) (push) Waiting to run
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / changes (push) Failing after 2s
Deploy Documentation / deploy (push) Failing after 0s
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 1s
Dev Containers / validate-worktree (push) Failing after 1s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Init E2E / docker-init-e2e (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 0s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Merge Conflicts / merge-conflicts (push) Failing after 2s
OpenCode Plugin / typecheck + build + test (push) Failing after 2s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 3s
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
526 lines
16 KiB
Python
526 lines
16 KiB
Python
"""Tests for SQLiteVectorIndex using sqlite-vec.
|
|
|
|
Tests verify:
|
|
- Vector indexing and search
|
|
- True CRUD operations (real deletes)
|
|
- Filtering by user_id, session_id, etc.
|
|
- Persistence across instances
|
|
- Memory stats and bounding
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from headroom.memory.models import Memory
|
|
from headroom.memory.ports import VectorFilter
|
|
|
|
# Check if sqlite-vec is available
|
|
try:
|
|
from headroom.memory.adapters.sqlite_vector import is_sqlite_vec_available
|
|
|
|
SQLITE_VEC_AVAILABLE = is_sqlite_vec_available()
|
|
except ImportError:
|
|
SQLITE_VEC_AVAILABLE = False
|
|
|
|
|
|
@pytest.mark.skipif(not SQLITE_VEC_AVAILABLE, reason="sqlite-vec not available")
|
|
class TestSQLiteVectorIndex:
|
|
"""Tests for SQLiteVectorIndex."""
|
|
|
|
@pytest.fixture
|
|
def index(self):
|
|
"""Create a temporary SQLite vector index."""
|
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
db_path = f.name
|
|
|
|
from headroom.memory.adapters.sqlite_vector import SQLiteVectorIndex
|
|
|
|
index = SQLiteVectorIndex(dimension=384, db_path=db_path)
|
|
yield index
|
|
|
|
if os.path.exists(db_path):
|
|
os.unlink(db_path)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_index_and_search(self, index):
|
|
"""Test basic indexing and search."""
|
|
np.random.seed(42)
|
|
|
|
# Create memories with random embeddings
|
|
memories = []
|
|
for i in range(10):
|
|
embedding = np.random.randn(384).astype(np.float32)
|
|
memory = Memory(
|
|
content=f"Test content {i}",
|
|
user_id="alice",
|
|
embedding=embedding,
|
|
)
|
|
await index.index(memory)
|
|
memories.append(memory)
|
|
|
|
assert index.size == 10
|
|
|
|
# Search with first memory's embedding - should find itself
|
|
filter = VectorFilter(
|
|
query_vector=memories[0].embedding,
|
|
top_k=3,
|
|
user_id="alice",
|
|
)
|
|
results = await index.search(filter)
|
|
|
|
assert len(results) == 3
|
|
assert results[0].memory.id == memories[0].id
|
|
assert results[0].similarity > 0.99 # Should be ~1.0 for exact match
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_true_delete(self, index):
|
|
"""Test that delete actually removes entries."""
|
|
np.random.seed(42)
|
|
|
|
# Add memories
|
|
memories = []
|
|
for i in range(5):
|
|
embedding = np.random.randn(384).astype(np.float32)
|
|
memory = Memory(
|
|
content=f"Content {i}",
|
|
user_id="alice",
|
|
embedding=embedding,
|
|
)
|
|
await index.index(memory)
|
|
memories.append(memory)
|
|
|
|
assert index.size == 5
|
|
|
|
# Delete one
|
|
result = await index.remove(memories[0].id)
|
|
assert result is True
|
|
assert index.size == 4
|
|
|
|
# Search should not find deleted memory
|
|
filter = VectorFilter(
|
|
query_vector=memories[0].embedding,
|
|
top_k=10,
|
|
user_id="alice",
|
|
)
|
|
results = await index.search(filter)
|
|
|
|
result_ids = {r.memory.id for r in results}
|
|
assert memories[0].id not in result_ids
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_embedding(self, index):
|
|
"""Test updating an existing entry."""
|
|
np.random.seed(42)
|
|
|
|
embedding1 = np.random.randn(384).astype(np.float32)
|
|
memory = Memory(
|
|
content="Original content",
|
|
user_id="alice",
|
|
embedding=embedding1,
|
|
)
|
|
await index.index(memory)
|
|
|
|
# Update with new embedding
|
|
embedding2 = np.random.randn(384).astype(np.float32)
|
|
memory.embedding = embedding2
|
|
memory.content = "Updated content"
|
|
await index.index(memory)
|
|
|
|
# Should still be only 1 entry
|
|
assert index.size == 1
|
|
|
|
# Get stored embedding
|
|
stored = await index.get_embedding(memory.id)
|
|
assert stored is not None
|
|
np.testing.assert_array_almost_equal(stored, embedding2)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_filter_by_user(self, index):
|
|
"""Test filtering search results by user_id."""
|
|
np.random.seed(42)
|
|
|
|
# Create memories for different users with similar embeddings
|
|
base_embedding = np.random.randn(384).astype(np.float32)
|
|
|
|
for user in ["alice", "bob", "charlie"]:
|
|
# Slightly perturb embedding for each user
|
|
embedding = base_embedding + np.random.randn(384).astype(np.float32) * 0.1
|
|
memory = Memory(
|
|
content=f"Content for {user}",
|
|
user_id=user,
|
|
embedding=embedding,
|
|
)
|
|
await index.index(memory)
|
|
|
|
# Search filtered by user
|
|
filter = VectorFilter(
|
|
query_vector=base_embedding,
|
|
top_k=10,
|
|
user_id="alice",
|
|
)
|
|
results = await index.search(filter)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].memory.user_id == "alice"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_filter_by_session(self, index):
|
|
"""Test filtering by session_id."""
|
|
np.random.seed(42)
|
|
|
|
embedding = np.random.randn(384).astype(np.float32)
|
|
|
|
# Same user, different sessions
|
|
for session in ["session1", "session2", None]:
|
|
memory = Memory(
|
|
content=f"Content for {session}",
|
|
user_id="alice",
|
|
session_id=session,
|
|
embedding=embedding + np.random.randn(384).astype(np.float32) * 0.01,
|
|
)
|
|
await index.index(memory)
|
|
|
|
filter = VectorFilter(
|
|
query_vector=embedding,
|
|
top_k=10,
|
|
user_id="alice",
|
|
session_id="session1",
|
|
)
|
|
results = await index.search(filter)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].memory.session_id == "session1"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_min_similarity_filter(self, index):
|
|
"""Test minimum similarity threshold."""
|
|
np.random.seed(42)
|
|
|
|
# Create memories with varying similarity to query
|
|
query = np.random.randn(384).astype(np.float32)
|
|
query = query / np.linalg.norm(query) # Normalize
|
|
|
|
# Very similar
|
|
similar = query + np.random.randn(384).astype(np.float32) * 0.1
|
|
similar = similar / np.linalg.norm(similar)
|
|
|
|
# Less similar
|
|
less_similar = np.random.randn(384).astype(np.float32)
|
|
less_similar = less_similar / np.linalg.norm(less_similar)
|
|
|
|
await index.index(Memory(content="Similar", user_id="alice", embedding=similar))
|
|
await index.index(Memory(content="Less similar", user_id="alice", embedding=less_similar))
|
|
|
|
# High threshold should filter out dissimilar
|
|
filter = VectorFilter(
|
|
query_vector=query,
|
|
top_k=10,
|
|
min_similarity=0.8,
|
|
)
|
|
results = await index.search(filter)
|
|
|
|
# Only the similar one should pass
|
|
assert len(results) <= 1
|
|
if len(results) == 1:
|
|
assert results[0].similarity >= 0.8
|
|
|
|
|
|
@pytest.mark.skipif(not SQLITE_VEC_AVAILABLE, reason="sqlite-vec not available")
|
|
class TestSQLiteVectorIndexPersistence:
|
|
"""Tests for persistence across index instances."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_data_persists_across_instances(self):
|
|
"""Test that data survives index restart."""
|
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
db_path = f.name
|
|
|
|
try:
|
|
from headroom.memory.adapters.sqlite_vector import SQLiteVectorIndex
|
|
|
|
# Create index and add data
|
|
index1 = SQLiteVectorIndex(dimension=384, db_path=db_path)
|
|
|
|
np.random.seed(42)
|
|
embedding = np.random.randn(384).astype(np.float32)
|
|
memory = Memory(
|
|
content="Persistent content",
|
|
user_id="alice",
|
|
embedding=embedding,
|
|
)
|
|
await index1.index(memory)
|
|
|
|
memory_id = memory.id
|
|
|
|
# Create new index instance
|
|
index2 = SQLiteVectorIndex(dimension=384, db_path=db_path)
|
|
|
|
assert index2.size == 1
|
|
|
|
# Should find the memory
|
|
filter = VectorFilter(
|
|
query_vector=embedding,
|
|
top_k=1,
|
|
)
|
|
results = await index2.search(filter)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].memory.id == memory_id
|
|
finally:
|
|
if os.path.exists(db_path):
|
|
os.unlink(db_path)
|
|
|
|
|
|
@pytest.mark.skipif(not SQLITE_VEC_AVAILABLE, reason="sqlite-vec not available")
|
|
class TestSQLiteVectorIndexMemoryStats:
|
|
"""Tests for memory statistics."""
|
|
|
|
@pytest.fixture
|
|
def index(self):
|
|
"""Create a temporary SQLite vector index."""
|
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
db_path = f.name
|
|
|
|
from headroom.memory.adapters.sqlite_vector import SQLiteVectorIndex
|
|
|
|
index = SQLiteVectorIndex(dimension=384, db_path=db_path, page_cache_size_kb=4096)
|
|
yield index
|
|
|
|
if os.path.exists(db_path):
|
|
os.unlink(db_path)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_memory_stats(self, index):
|
|
"""Test memory statistics."""
|
|
stats = index.get_memory_stats()
|
|
|
|
assert stats.name == "sqlite_vector_index"
|
|
assert stats.entry_count == 0
|
|
assert stats.budget_bytes == 4096 * 1024 # 4MB cache
|
|
|
|
# Add some entries
|
|
np.random.seed(42)
|
|
for i in range(10):
|
|
embedding = np.random.randn(384).astype(np.float32)
|
|
memory = Memory(
|
|
content=f"Content {i}",
|
|
user_id="alice",
|
|
embedding=embedding,
|
|
)
|
|
await index.index(memory)
|
|
|
|
stats = index.get_memory_stats()
|
|
assert stats.entry_count == 10
|
|
assert stats.size_bytes > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stats(self, index):
|
|
"""Test index statistics."""
|
|
np.random.seed(42)
|
|
|
|
for i in range(5):
|
|
embedding = np.random.randn(384).astype(np.float32)
|
|
memory = Memory(
|
|
content=f"Content {i}",
|
|
user_id="alice" if i < 3 else "bob",
|
|
embedding=embedding,
|
|
)
|
|
await index.index(memory)
|
|
|
|
stats = index.stats()
|
|
|
|
assert stats["size"] == 5
|
|
assert stats["dimension"] == 384
|
|
assert stats["users"] == 2
|
|
assert stats["page_cache_size_kb"] == 4096
|
|
assert stats["db_size_bytes"] > 0
|
|
|
|
|
|
@pytest.mark.skipif(not SQLITE_VEC_AVAILABLE, reason="sqlite-vec not available")
|
|
class TestSQLiteVectorIndexEdgeCases:
|
|
"""Tests for edge cases."""
|
|
|
|
@pytest.fixture
|
|
def index(self):
|
|
"""Create a temporary SQLite vector index."""
|
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
db_path = f.name
|
|
|
|
from headroom.memory.adapters.sqlite_vector import SQLiteVectorIndex
|
|
|
|
index = SQLiteVectorIndex(dimension=384, db_path=db_path)
|
|
yield index
|
|
|
|
if os.path.exists(db_path):
|
|
os.unlink(db_path)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_empty_index(self, index):
|
|
"""Test searching an empty index."""
|
|
np.random.seed(42)
|
|
query = np.random.randn(384).astype(np.float32)
|
|
|
|
filter = VectorFilter(query_vector=query, top_k=10)
|
|
results = await index.search(filter)
|
|
|
|
assert len(results) == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remove_nonexistent(self, index):
|
|
"""Test removing a nonexistent entry."""
|
|
result = await index.remove("nonexistent-id")
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wrong_dimension_raises(self, index):
|
|
"""Test that wrong embedding dimension raises error."""
|
|
wrong_embedding = np.random.randn(128).astype(np.float32) # Wrong dimension
|
|
memory = Memory(
|
|
content="Test",
|
|
user_id="alice",
|
|
embedding=wrong_embedding,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="dimension"):
|
|
await index.index(memory)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_embedding_raises(self, index):
|
|
"""Test that missing embedding raises error."""
|
|
memory = Memory(
|
|
content="Test",
|
|
user_id="alice",
|
|
embedding=None,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="no embedding"):
|
|
await index.index(memory)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clear(self, index):
|
|
"""Test clearing all entries."""
|
|
np.random.seed(42)
|
|
|
|
for i in range(5):
|
|
embedding = np.random.randn(384).astype(np.float32)
|
|
memory = Memory(
|
|
content=f"Content {i}",
|
|
user_id="alice",
|
|
embedding=embedding,
|
|
)
|
|
await index.index(memory)
|
|
|
|
assert index.size == 5
|
|
|
|
index.clear()
|
|
|
|
assert index.size == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_index(self, index):
|
|
"""Test batch indexing."""
|
|
np.random.seed(42)
|
|
|
|
memories = []
|
|
for i in range(10):
|
|
embedding = np.random.randn(384).astype(np.float32)
|
|
memory = Memory(
|
|
content=f"Content {i}",
|
|
user_id="alice",
|
|
embedding=embedding,
|
|
)
|
|
memories.append(memory)
|
|
|
|
# Add one without embedding
|
|
memories.append(Memory(content="No embedding", user_id="alice"))
|
|
|
|
indexed = await index.index_batch(memories)
|
|
|
|
assert indexed == 10
|
|
assert index.size == 10
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_index_uses_single_connection(self, index, monkeypatch):
|
|
"""Test batch indexing reuses a single sqlite-vec connection."""
|
|
np.random.seed(42)
|
|
memories = [
|
|
Memory(
|
|
content=f"Content {i}",
|
|
user_id="alice",
|
|
embedding=np.random.randn(384).astype(np.float32),
|
|
)
|
|
for i in range(10)
|
|
]
|
|
|
|
original_get_conn = index._get_conn
|
|
conn_calls = 0
|
|
|
|
def counting_get_conn():
|
|
nonlocal conn_calls
|
|
conn_calls += 1
|
|
return original_get_conn()
|
|
|
|
monkeypatch.setattr(index, "_get_conn", counting_get_conn)
|
|
|
|
indexed = await index.index_batch(memories)
|
|
|
|
assert indexed == 10
|
|
assert conn_calls == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_remove(self, index):
|
|
"""Test batch removal."""
|
|
np.random.seed(42)
|
|
|
|
memories = []
|
|
for i in range(5):
|
|
embedding = np.random.randn(384).astype(np.float32)
|
|
memory = Memory(
|
|
content=f"Content {i}",
|
|
user_id="alice",
|
|
embedding=embedding,
|
|
)
|
|
await index.index(memory)
|
|
memories.append(memory)
|
|
|
|
# Remove some
|
|
ids_to_remove = [memories[0].id, memories[2].id, "nonexistent"]
|
|
removed = await index.remove_batch(ids_to_remove)
|
|
|
|
assert removed == 2
|
|
assert index.size == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_batch_remove_uses_single_connection(self, index, monkeypatch):
|
|
"""Test batch removal reuses a single sqlite-vec connection."""
|
|
np.random.seed(42)
|
|
memories = []
|
|
for i in range(5):
|
|
memory = Memory(
|
|
content=f"Content {i}",
|
|
user_id="alice",
|
|
embedding=np.random.randn(384).astype(np.float32),
|
|
)
|
|
await index.index(memory)
|
|
memories.append(memory)
|
|
|
|
original_get_conn = index._get_conn
|
|
conn_calls = 0
|
|
|
|
def counting_get_conn():
|
|
nonlocal conn_calls
|
|
conn_calls += 1
|
|
return original_get_conn()
|
|
|
|
monkeypatch.setattr(index, "_get_conn", counting_get_conn)
|
|
|
|
removed = await index.remove_batch([memories[0].id, memories[2].id, "nonexistent"])
|
|
|
|
assert removed == 2
|
|
assert conn_calls == 1
|