Files
Hy-Memory/tests/test_write_embedding_latency_guards.py
2026-10-16 16:26:27 +08:00

1096 lines
35 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Write-path embedding latency guards.
These tests pin two regressions observed in staging:
1. OpenAI SDK retries must not be nested inside HY Memory retries.
2. An unchanged extracted memory must reuse the vector computed for reconcile search.
"""
from __future__ import annotations
import asyncio
import tempfile
import uuid
import pytest
from hy_memory.agent.reconciler import ReconcileOp, ReconcileResult
from hy_memory.agent.llm_provider import LLMProvider
from hy_memory.config import MemoryConfig
from hy_memory.core.embed_service import EmbedService
from hy_memory.models.memory import MemoryNode
from hy_memory.models.memory import MemoryLayer, MemoryStatus
from hy_memory.pipelines.base import WriteRequest
from hy_memory.pipelines.writer import MemoryWriter
@pytest.mark.asyncio
async def test_embed_http_attempts_equal_hy_memory_retry_budget(monkeypatch):
"""max_retries=2 means exactly three HTTP requests, not SDK 3 × outer 3."""
import openai
created_with = []
requests_seen = 0
class _Create:
async def create(self, **kwargs):
nonlocal requests_seen
requests_seen += 1
raise ConnectionError("temporary failure")
class _Embeddings:
def __init__(self):
self.with_raw_response = _Create()
class _FakeAsyncOpenAI:
def __init__(self, **kwargs):
created_with.append(kwargs)
self.embeddings = _Embeddings()
async def close(self):
return None
monkeypatch.setattr(openai, "AsyncOpenAI", _FakeAsyncOpenAI)
config = MemoryConfig.from_dict({
"embedder": {
"provider": "openai",
"model": "test-embedding",
"api_key": "test-key",
"base_url": "http://embedding.test/v1",
"embedding_dims": 3,
"timeout": 1,
"max_retries": 2,
"retry_delay": 0,
}
})
service = EmbedService(config)
with pytest.raises(ConnectionError):
await service.embed("one request budget")
assert requests_seen == 3
assert len(created_with) == 3 # retryable failure evicts the connection pool
assert all(call["max_retries"] == 0 for call in created_with)
@pytest.mark.asyncio
async def test_embed_retries_transient_empty_json_response(monkeypatch):
import json as json_module
import openai
calls = 0
class _Create:
async def create(self, **kwargs):
nonlocal calls
calls += 1
raise json_module.JSONDecodeError("empty", "", 0)
class _FakeClient:
def __init__(self, **kwargs):
self.embeddings = type("_Embeddings", (), {
"with_raw_response": _Create(),
})()
async def close(self):
return None
monkeypatch.setattr(openai, "AsyncOpenAI", _FakeClient)
config = MemoryConfig.from_dict({
"embedder": {
"provider": "openai", "model": "test", "api_key": "test",
"base_url": "http://embedding.test/v1", "embedding_dims": 3,
"timeout": 1, "max_retries": 2, "retry_delay": 0,
}
})
with pytest.raises(json_module.JSONDecodeError):
await EmbedService(config).embed("retry empty response")
assert calls == 3
def test_embedding_cache_evicts_lru_and_accepts_new_entries():
config = MemoryConfig.from_dict({"embedder": {"embedding_dims": 3}})
service = EmbedService(config)
service._embed_config.cache_size = 2
service._cache_put("a", [1.0])
service._cache_put("b", [2.0])
assert service._cache_get("a") == [1.0] # a becomes most recently used
service._cache_put("c", [3.0])
assert list(service._cache) == ["a", "c"]
assert service._cache_get("b") is None
@pytest.mark.asyncio
async def test_full_embedding_cache_still_caches_new_text(monkeypatch):
config = MemoryConfig.from_dict({"embedder": {"embedding_dims": 3}})
service = EmbedService(config)
service._embed_config.cache_size = 2
calls = []
async def _fake_embed(texts):
calls.append(list(texts))
return [[float(len(text)), 0.0, 0.0] for text in texts], [{} for _ in texts]
monkeypatch.setattr(service, "_embed_texts_with_meta", _fake_embed)
await service.embed("old-a")
await service.embed("old-b")
first = await service.embed("new-c")
second = await service.embed("new-c")
assert first == second
assert calls == [["old-a"], ["old-b"], ["new-c"]]
assert service.get_stats()["cache_size"] == 2
@pytest.mark.asyncio
async def test_llm_client_disables_sdk_retries_and_uses_configured_timeout(monkeypatch):
import openai
from unittest.mock import MagicMock
created_with = []
closed = 0
def _fake_client(**kwargs):
nonlocal closed
created_with.append(kwargs)
client = MagicMock()
async def _create(**_kwargs):
response = MagicMock()
response.choices = [MagicMock(
message=MagicMock(content="ok", tool_calls=None),
finish_reason="stop",
)]
response.usage = MagicMock(
prompt_tokens=1, completion_tokens=1, total_tokens=2,
)
response.model = "test-model"
response.id = "resp-1"
return response
client.chat.completions.create = _create
async def _close():
nonlocal closed
closed += 1
client.close = _close
return client
monkeypatch.setattr(openai, "AsyncOpenAI", _fake_client)
config = MemoryConfig.from_dict({
"llm": {
"provider": "openai", "model": "test-model", "api_key": "test",
"timeout": 47, "max_retries": 2,
}
})
provider = LLMProvider(config)
response = await provider.complete("hello", max_tokens=8)
second = await provider.complete("hello again", max_tokens=8)
assert response.content == "ok"
assert second.content == "ok"
assert created_with == [{
"api_key": "test", "base_url": None, "timeout": 47, "max_retries": 0,
}]
assert closed == 0
await provider.close()
assert closed == 1
@pytest.mark.asyncio
async def test_llm_non_retryable_400_stops_after_one_attempt(monkeypatch):
import openai
from unittest.mock import MagicMock
calls = 0
class _BadRequest(Exception):
status_code = 400
def _fake_client(**kwargs):
client = MagicMock()
async def _create(**_kwargs):
nonlocal calls
calls += 1
raise _BadRequest("bad model")
client.chat.completions.create = _create
return client
monkeypatch.setattr(openai, "AsyncOpenAI", _fake_client)
config = MemoryConfig.from_dict({
"llm": {
"provider": "openai", "model": "bad-model", "api_key": "test",
"timeout": 47, "max_retries": 3, "retry_delay": 10,
}
})
with pytest.raises(Exception):
await LLMProvider(config).complete("hello", max_tokens=8)
assert calls == 1
@pytest.mark.asyncio
async def test_llm_reused_client_keeps_per_request_authorization(monkeypatch):
import openai
from unittest.mock import MagicMock
created_with = []
request_headers = []
def _fake_client(**kwargs):
created_with.append(kwargs)
client = MagicMock()
async def _create(**call_kwargs):
request_headers.append(call_kwargs["extra_headers"]["Authorization"])
response = MagicMock()
response.choices = [MagicMock(
message=MagicMock(content="ok", tool_calls=None),
finish_reason="stop",
)]
response.usage = MagicMock(
prompt_tokens=1, completion_tokens=1, total_tokens=2,
)
response.model = "test-model"
response.id = "resp-1"
return response
client.chat.completions.create = _create
client.close = MagicMock()
return client
monkeypatch.setattr(openai, "AsyncOpenAI", _fake_client)
config = MemoryConfig.from_dict({
"llm": {
"provider": "aihub", "model": "test-model", "api_key": "base",
"timeout": 47, "max_retries": 1,
}
})
provider = LLMProvider(config)
tokens = iter(("base?cache_task_id=one", "base?cache_task_id=two"))
monkeypatch.setattr(
provider, "_resolve_endpoint",
lambda: (next(tokens), "http://llm/v1"),
)
await provider.complete("one", max_tokens=8)
await provider.complete("two", max_tokens=8)
assert created_with == [{
"api_key": "base", "base_url": "http://llm/v1",
"timeout": 47, "max_retries": 0,
}]
assert request_headers == [
"Bearer base?cache_task_id=one",
"Bearer base?cache_task_id=two",
]
@pytest.mark.asyncio
async def test_llm_transport_failure_evicts_cached_client(monkeypatch):
import openai
from unittest.mock import MagicMock
created = []
closed = []
def _fake_client(**kwargs):
index = len(created)
client = MagicMock()
async def _create(**_call_kwargs):
if index == 0:
raise ConnectionError("broken pooled connection")
response = MagicMock()
response.choices = [MagicMock(
message=MagicMock(content="ok", tool_calls=None),
finish_reason="stop",
)]
response.usage = MagicMock(
prompt_tokens=1, completion_tokens=1, total_tokens=2,
)
response.model = "test-model"
response.id = "resp-1"
return response
async def _close():
closed.append(index)
client.chat.completions.create = _create
client.close = _close
created.append(client)
return client
monkeypatch.setattr(openai, "AsyncOpenAI", _fake_client)
config = MemoryConfig.from_dict({
"llm": {
"provider": "openai", "model": "test-model", "api_key": "test",
"timeout": 47, "max_retries": 2, "retry_delay": 0,
}
})
provider = LLMProvider(config)
response = await provider.complete("hello", max_tokens=8)
assert response.content == "ok"
assert len(created) == 2
assert closed == [0]
await provider.close()
assert closed == [0, 1]
class _RecordingEmbed:
def __init__(self):
self.batch_calls = []
self.queued_calls = []
async def embed_batch(self, texts):
self.batch_calls.append(list(texts))
return [[0.0, 1.0, 0.0] for _ in texts]
async def embed_queued(self, text, use_cache=True):
self.queued_calls.append(text)
return [0.0, 1.0, 0.0]
class _RecordingStore:
def __init__(self):
self.nodes = []
self.batch_calls = []
self.update_batch_calls = []
async def upsert(self, node: MemoryNode):
self.nodes.append(node)
return node.node_id
async def upsert_batch(self, nodes):
self.batch_calls.append(list(nodes))
self.nodes.extend(nodes)
return [node.node_id for node in nodes]
async def update_payload_batch(self, updates_by_id):
self.update_batch_calls.append(list(updates_by_id))
return [node_id for node_id, _updates in updates_by_id]
async def get_by_id(self, node_id):
return next((n for n in self.nodes if n.node_id == node_id), None)
async def get_embeddings(self, node_ids):
return {}
@pytest.mark.asyncio
async def test_reconcile_is_serialized_per_user_without_candidate_precheck(monkeypatch):
"""Concurrent writes serialize, but every write still runs full reconcile."""
config = MemoryConfig.from_dict({"vector_store": {"embedding_dims": 3}})
writer = MemoryWriter.__new__(MemoryWriter)
writer.config = config
state = {"in_flight": 0, "max_in_flight": 0, "calls": 0}
async def _locked(**kwargs):
state["in_flight"] += 1
state["max_in_flight"] = max(state["max_in_flight"], state["in_flight"])
state["calls"] += 1
assert "skip_candidate_search" not in kwargs
await asyncio.sleep(0.02)
state["in_flight"] -= 1
return [], None, {"add": 0, "supersede": 0, "update": 0, "total": 0}, {
"prompt": 0, "completion": 0, "total": 0,
}
monkeypatch.setattr(writer, "_reconcile_and_store_locked", _locked)
request = WriteRequest(content="raw", user_id="u-race", agent_id="a1")
args = dict(
new_memory_texts=["fact"], new_memories_meta=[{"content": "fact"}],
request=request, vector_store=object(), req_id="req",
)
await asyncio.gather(
writer._reconcile_and_store(**args),
writer._reconcile_and_store(**args),
)
assert state["max_in_flight"] == 1
assert state["calls"] == 2
class _NoopHybridRetriever:
calls = 0
def __init__(self, *args, **kwargs):
pass
async def search_candidates(self, *args, **kwargs):
type(self).calls += 1
return []
async def search_candidates_batch(self, query_texts, *args, **kwargs):
type(self).calls += 1
return [[] for _ in query_texts]
@pytest.mark.asyncio
async def test_reconciler_uses_precomputed_dedup_vectors(monkeypatch):
"""A PMV2-style multi-memory batch must not re-embed texts after dedup."""
from hy_memory.agent.reconciler import MemoryReconciler
from hy_memory.pipelines._retrieval import reconcile_retrieval
monkeypatch.setattr(
reconcile_retrieval, "ReconcileHybridRetriever", _NoopHybridRetriever,
)
_NoopHybridRetriever.calls = 0
config = MemoryConfig.from_dict({"vector_store": {"embedding_dims": 3}})
reconciler = MemoryReconciler(config)
embed = _RecordingEmbed()
texts = ["The user likes tea.", "The user rides a bicycle."]
vectors = {
texts[0]: [1.0, 0.0, 0.0],
texts[1]: [0.0, 1.0, 0.0],
}
result = await reconciler.reconcile(
new_memories=texts,
user_id="u-latency",
agent_id="default_agent",
vector_store=object(),
embed_service=embed,
new_memory_embeddings=vectors,
)
assert result.success is True
assert [op.op for op in result.ops] == ["ADD", "ADD"]
assert result.new_memory_embeddings == vectors
assert embed.batch_calls == []
assert _NoopHybridRetriever.calls == 1
@pytest.mark.asyncio
async def test_writer_reuses_reconcile_embedding_for_unchanged_add():
config = MemoryConfig.from_dict({"vector_store": {"embedding_dims": 3}})
embed = _RecordingEmbed()
store = _RecordingStore()
writer = MemoryWriter(config, embed_service=embed, vector_store=store)
content = "The user likes green tea."
vector = [1.0, 0.0, 0.0]
async def _fake_reconcile(*args, **kwargs):
return ReconcileResult(
ops=[ReconcileOp(op="ADD", content=content, layer="L2_FACT")],
success=True,
new_memory_embeddings={content: vector},
)
writer._reconciler = type("_Reconciler", (), {"reconcile": _fake_reconcile})()
request = WriteRequest(
content="raw",
user_id="u-latency",
agent_id="default_agent",
session_id="s1",
)
stored_ids, error, counts, _tokens = await writer._reconcile_and_store_locked(
new_memory_texts=[content],
new_memories_meta=[{"content": content, "layer": "L2_FACT"}],
request=request,
vector_store=store,
req_id="req-latency",
)
assert error is None
assert len(stored_ids) == 1
assert counts["add"] == 1
assert embed.batch_calls == []
assert embed.queued_calls == []
assert store.nodes[0].embedding == vector
@pytest.mark.asyncio
async def test_writer_embeds_rewritten_reconcile_content_once():
config = MemoryConfig.from_dict({"vector_store": {"embedding_dims": 3}})
embed = _RecordingEmbed()
store = _RecordingStore()
writer = MemoryWriter(config, embed_service=embed, vector_store=store)
extracted = "The user likes tea."
rewritten = "The user likes green tea."
async def _fake_reconcile(*args, **kwargs):
return ReconcileResult(
ops=[ReconcileOp(op="ADD", content=rewritten, layer="L2_FACT")],
success=True,
new_memory_embeddings={extracted: [1.0, 0.0, 0.0]},
)
writer._reconciler = type("_Reconciler", (), {"reconcile": _fake_reconcile})()
request = WriteRequest(
content="raw",
user_id="u-latency",
agent_id="default_agent",
session_id="s1",
)
await writer._reconcile_and_store_locked(
new_memory_texts=[extracted],
new_memories_meta=[{"content": extracted, "layer": "L2_FACT"}],
request=request,
vector_store=store,
req_id="req-latency-rewrite",
)
assert embed.batch_calls == [[rewritten]]
assert embed.queued_calls == []
@pytest.mark.asyncio
async def test_multi_memory_vectors_flow_dedup_to_reconcile_to_store(monkeypatch):
"""The normal PMV2 path computes extracted-memory vectors only once."""
from hy_memory.pipelines._retrieval import reconcile_retrieval
monkeypatch.setattr(
reconcile_retrieval, "ReconcileHybridRetriever", _NoopHybridRetriever,
)
_NoopHybridRetriever.calls = 0
config = MemoryConfig.from_dict({"vector_store": {"embedding_dims": 3}})
class _PipelineEmbed(_RecordingEmbed):
async def embed_queued_with_meta(self, text):
self.queued_calls.append(text)
vector = (
[1.0, 0.0, 0.0]
if text == "The user likes tea."
else [0.0, 1.0, 0.0]
)
return vector, {}
embed = _PipelineEmbed()
store = _RecordingStore()
writer = MemoryWriter(config, embed_service=embed, vector_store=store)
writer._reconciler = __import__(
"hy_memory.agent.reconciler", fromlist=["MemoryReconciler"]
).MemoryReconciler(config)
texts = ["The user likes tea.", "The user rides a bicycle."]
metas = [{"content": text, "layer": "L2_FACT"} for text in texts]
request = WriteRequest(
content="raw", user_id="u-latency", agent_id="a1", session_id="s1",
)
texts, metas, vectors = await writer._dedup_extracted(
texts, metas, request, "req-flow",
)
stored_ids, error, counts, _tokens = await writer._reconcile_and_store_locked(
new_memory_texts=texts,
new_memories_meta=metas,
request=request,
vector_store=store,
req_id="req-flow",
precomputed_embeddings=vectors,
)
assert error is None
assert len(stored_ids) == 2
assert counts["add"] == 2
assert embed.batch_calls == []
assert embed.queued_calls == texts
assert _NoopHybridRetriever.calls == 1
@pytest.mark.asyncio
async def test_default_chroma_identical_second_write_does_not_add(monkeypatch):
"""Every write must search candidates; an exact duplicate produces zero ops."""
from datetime import datetime
from hy_memory.agent.llm_provider import LLMResponse
from hy_memory.agent.reconciler import MemoryReconciler
from hy_memory.data import vector_store_chroma as chroma_module
from hy_memory.data.vector_store_chroma import ChromaVectorStore
from hy_memory.pipelines._retrieval import reconcile_retrieval
async def _direct_vdb_call(func, *args, **kwargs):
return func(*args, **kwargs)
monkeypatch.setattr(chroma_module, "_run_in_vdb_pool", _direct_vdb_call)
class _DenseOnlyRetriever:
calls = 0
def __init__(self, *args, **kwargs):
pass
async def search_candidates_batch(
self, query_texts, query_embeddings, *, vector_store, user_id,
agent_id, layers, **kwargs,
):
type(self).calls += 1
rows = []
for embedding in query_embeddings:
rows.append(await vector_store.search(
query_embedding=embedding,
user_id=user_id,
agent_ids=None,
layers=layers,
limit=20,
score_threshold=0.0,
status_filter=[MemoryStatus.ACTIVE, MemoryStatus.SUPERSEDED],
only_latest=False,
))
return rows
monkeypatch.setattr(
reconcile_retrieval, "ReconcileHybridRetriever", _DenseOnlyRetriever,
)
class _DuplicateSkipLLM:
def __init__(self):
self.calls = []
async def complete(self, prompt, **kwargs):
self.calls.append(prompt)
return LLMResponse(
content="[]", prompt_tokens=1, completion_tokens=1,
tokens_used=2, model="fake", finish_reason="stop",
)
class _FixedEmbed(_RecordingEmbed):
async def embed(self, text, use_cache=True):
return [1.0, 0.0, 0.0]
async def embed_batch(self, texts, use_cache=True):
self.batch_calls.append(list(texts))
return [[1.0, 0.0, 0.0] for _ in texts]
async def embed_queued(self, text, use_cache=True):
self.queued_calls.append(text)
return [1.0, 0.0, 0.0]
with tempfile.TemporaryDirectory(prefix="hy-duplicate-write-") as root:
config = MemoryConfig.from_dict({
"vector_store": {
"provider": "chroma",
"collection_name": f"duplicate_{uuid.uuid4().hex[:8]}",
"persist_directory": root,
"embedding_dims": 3,
},
})
store = ChromaVectorStore(config)
await store.initialize()
embed = _FixedEmbed()
llm = _DuplicateSkipLLM()
writer = MemoryWriter(
config, embed_service=embed, vector_store=store, cache=None,
)
writer._vector_store = store
writer._vector_store_initialized = True
writer._reconciler = MemoryReconciler(config, llm_provider=llm)
writer._reconciler._enable_search_query = False
content = "The user swims every Saturday morning."
async def _write(request_id):
request = WriteRequest(
content=content,
user_id="duplicate-user",
agent_id="default_agent",
session_id=request_id,
request_id=request_id,
memory_at=datetime.now(),
)
return await writer._reconcile_and_store(
new_memory_texts=[content],
new_memories_meta=[{
"content": content, "layer": "L2_FACT", "tags": [],
}],
request=request,
vector_store=store,
req_id=request_id,
)
first_ids, first_error, first_counts, _ = await _write("first")
second_ids, second_error, second_counts, _ = await _write("second")
nodes = await store.list_by_user(
user_id="duplicate-user",
agent_id=None,
layers=[MemoryLayer.L2_FACT],
status_filter=None,
with_vectors=False,
)
assert first_error is None
assert first_counts["add"] == 1
assert len(first_ids) == 1
assert second_error is None
assert second_ids == []
assert second_counts == {
"add": 0, "supersede": 0, "update": 0, "total": 0,
}
assert len(nodes) == 1
assert nodes[0].node_id == first_ids[0]
assert len(llm.calls) == 1
assert first_ids[0] in llm.calls[0]
assert _DenseOnlyRetriever.calls == 2
await store.close()
@pytest.mark.asyncio
async def test_tag_embeddings_are_created_concurrently(monkeypatch):
from hy_memory.pipelines._retrieval import tag_index
monkeypatch.setattr(tag_index.config, "TAG_INDEX_WRITE_ENABLED", True)
in_flight = 0
max_in_flight = 0
class _TagStore:
async def has_tag_embedding(self, user_id, tag):
return False
async def upsert_tag_embedding(self, user_id, tag, embedding):
return True
class _TagEmbed:
def __init__(self):
self.calls = []
async def embed_queued(self, text):
nonlocal in_flight, max_in_flight
self.calls.append(text)
in_flight += 1
max_in_flight = max(max_in_flight, in_flight)
await asyncio.sleep(0.03)
in_flight -= 1
return [1.0, 0.0, 0.0]
embed = _TagEmbed()
ok = await tag_index.ensure_tag_embeddings_for_node(
vector_store=_TagStore(),
embed_service=embed,
user_id="u-tags",
tags=["food", "preferences", "food"],
)
assert ok == 2
assert max_in_flight == 2
assert set(embed.calls) == {"food", "preferences"}
@pytest.mark.asyncio
async def test_tag_embeddings_use_batch_vdb_maintenance_when_supported(monkeypatch):
from hy_memory.pipelines._retrieval import tag_index
monkeypatch.setattr(tag_index.config, "TAG_INDEX_WRITE_ENABLED", True)
class _BatchTagStore:
def __init__(self):
self.existing_calls = []
self.batch_calls = []
async def existing_tag_embeddings(self, user_id, tags):
self.existing_calls.append((user_id, list(tags)))
return ["food"]
async def upsert_tag_embeddings_batch(self, user_id, items):
self.batch_calls.append((user_id, dict(items)))
return len(items)
class _TagEmbed:
def __init__(self):
self.calls = []
async def embed_queued(self, text):
self.calls.append(text)
return [1.0, 0.0, 0.0]
store = _BatchTagStore()
embed = _TagEmbed()
ok = await tag_index.ensure_tag_embeddings_for_node(
vector_store=store,
embed_service=embed,
user_id="u-tags",
tags=["food", "preferences", "travel", "food"],
)
assert ok == 3
assert store.existing_calls == [
("u-tags", ["food", "preferences", "travel"]),
]
assert set(embed.calls) == {"preferences", "travel"}
assert len(store.batch_calls) == 1
assert set(store.batch_calls[0][1]) == {"preferences", "travel"}
@pytest.mark.asyncio
async def test_queued_batch_sends_duplicate_text_once(monkeypatch):
config = MemoryConfig.from_dict({"embedder": {"embedding_dims": 3}})
service = EmbedService(config)
sent = []
async def _fake_embed(texts):
sent.append(list(texts))
return (
[[float(i), 0.0, 0.0] for i, _ in enumerate(texts)],
[{} for _ in texts],
)
monkeypatch.setattr(service, "_embed_texts_with_meta", _fake_embed)
loop = asyncio.get_running_loop()
batch = [
("food", loop.create_future(), loop.time(), 0.0, 1),
("preferences", loop.create_future(), loop.time(), 0.0, 2),
("food", loop.create_future(), loop.time(), 0.0, 3),
]
await service._send_batch(batch)
assert sent == [["food", "preferences"]]
results = [future.result() for _, future, *_ in batch]
assert results[0][0] == results[2][0]
assert all(meta["batch_unique_count"] == 2 for _, meta in results)
@pytest.mark.asyncio
async def test_queue_rearms_when_item_arrives_during_previous_flush(monkeypatch):
"""A request arriving during slow HTTP must not wait for a future enqueue."""
config = MemoryConfig.from_dict({"embedder": {"embedding_dims": 3}})
service = EmbedService(config)
service._embed_config.queue_batch_window_ms = 5
first_started = asyncio.Event()
release_first = asyncio.Event()
batches = []
async def _fake_embed(texts):
batches.append(list(texts))
if len(batches) == 1:
first_started.set()
await release_first.wait()
return [[1.0, 0.0, 0.0] for _ in texts], [{} for _ in texts]
monkeypatch.setattr(service, "_embed_texts_with_meta", _fake_embed)
first = asyncio.create_task(service.embed_queued("first"))
await asyncio.wait_for(first_started.wait(), timeout=0.2)
second = asyncio.create_task(service.embed_queued("second"))
await asyncio.sleep(0.02)
release_first.set()
assert await asyncio.wait_for(first, timeout=0.2) == [1.0, 0.0, 0.0]
assert await asyncio.wait_for(second, timeout=0.2) == [1.0, 0.0, 0.0]
assert batches == [["first"], ["second"]]
@pytest.mark.asyncio
async def test_qdrant_tag_collection_initializes_once_under_concurrency(monkeypatch):
from types import SimpleNamespace
from hy_memory.data import vector_store_qdrant as qdrant_module
from hy_memory.data.vector_store_qdrant import QdrantVectorStore
from hy_memory.pipelines._retrieval import bm25_fastembed
config = MemoryConfig.from_dict({
"vector_store": {"provider": "qdrant", "embedding_dims": 3},
})
store = QdrantVectorStore(config)
created = 0
class _Client:
def get_collections(self):
return SimpleNamespace(collections=[])
def create_collection(self, **kwargs):
nonlocal created
created += 1
def create_payload_index(self, **kwargs):
return None
store._client = _Client()
monkeypatch.setattr(bm25_fastembed, "is_available", lambda: False)
async def _direct(func, *args, **kwargs):
return func(*args, **kwargs)
monkeypatch.setattr(qdrant_module, "_run_in_vdb_pool", _direct)
await asyncio.gather(
store._ensure_tag_index_collection(),
store._ensure_tag_index_collection(),
store._ensure_tag_index_collection(),
)
assert created == 1
@pytest.mark.asyncio
async def test_apply_audit_skips_readback_unless_explicitly_enabled(monkeypatch):
"""Normal writes must not pay one VDB read per persisted memory."""
monkeypatch.setenv("MEMORY_PIPELINE_TRACE_ENABLED", "true")
monkeypatch.setenv("MEMORY_WRITE_APPLY_VERIFY_ENABLED", "false")
writer = MemoryWriter.__new__(MemoryWriter)
emitted = []
class _Store:
reads = 0
async def get_by_id(self, _memory_id):
self.reads += 1
return object()
async def _emit(_request, **kwargs):
emitted.append(kwargs)
writer._emit_pipeline_step = _emit
store = _Store()
await writer._emit_reconcile_apply(
WriteRequest(content="raw", user_id="u1"),
vector_store=store,
op_type="ADD",
memory_id="m1",
layer="l2_fact",
content="fact",
)
assert store.reads == 0
assert emitted[0]["parsed"]["verified"] is None
assert emitted[0]["parsed"]["verification_skipped"] is True
@pytest.mark.asyncio
async def test_apply_audit_keeps_readback_when_explicitly_enabled(monkeypatch):
monkeypatch.setenv("MEMORY_WRITE_APPLY_VERIFY_ENABLED", "true")
writer = MemoryWriter.__new__(MemoryWriter)
emitted = []
class _Store:
reads = 0
async def get_by_id(self, _memory_id):
self.reads += 1
return object()
async def _emit(_request, **kwargs):
emitted.append(kwargs)
writer._emit_pipeline_step = _emit
store = _Store()
await writer._emit_reconcile_apply(
WriteRequest(content="raw", user_id="u1"),
vector_store=store,
op_type="ADD",
memory_id="m1",
layer="l2_fact",
content="fact",
)
assert store.reads == 1
assert emitted[0]["parsed"]["verified"] is True
assert emitted[0]["parsed"]["verification_skipped"] is False
@pytest.mark.asyncio
async def test_add_only_reconcile_uses_one_vector_store_batch(monkeypatch):
monkeypatch.setenv("MEMORY_PIPELINE_TRACE_ENABLED", "false")
config = MemoryConfig.from_dict({"vector_store": {"embedding_dims": 3}})
embed = _RecordingEmbed()
store = _RecordingStore()
writer = MemoryWriter(config, embed_service=embed, vector_store=store)
texts = ["fact one", "fact two", "fact three"]
async def _fake_reconcile(*args, **kwargs):
return ReconcileResult(
ops=[ReconcileOp(op="ADD", content=t, layer="L2_FACT") for t in texts],
success=True,
new_memory_embeddings={t: [1.0, 0.0, 0.0] for t in texts},
)
writer._reconciler = type("_Reconciler", (), {"reconcile": _fake_reconcile})()
request = WriteRequest(content="raw", user_id="u-batch", agent_id="a1")
ids, error, counts, _tokens = await writer._reconcile_and_store_locked(
new_memory_texts=texts,
new_memories_meta=[{"content": t, "layer": "L2_FACT"} for t in texts],
request=request,
vector_store=store,
req_id="req-batch",
precomputed_embeddings={t: [1.0, 0.0, 0.0] for t in texts},
)
assert error is None
assert len(ids) == 3
assert counts == {"add": 3, "supersede": 0, "update": 0, "total": 3}
assert len(store.batch_calls) == 1
assert len(store.batch_calls[0]) == 3
@pytest.mark.asyncio
async def test_qdrant_payload_and_vector_update_use_one_http_batch(monkeypatch):
from hy_memory.data import vector_store_qdrant as qdrant_module
from hy_memory.data.vector_store_qdrant import QdrantVectorStore
config = MemoryConfig.from_dict({
"vector_store": {"provider": "qdrant", "embedding_dims": 3},
})
store = QdrantVectorStore(config)
calls = []
class _Client:
def retrieve(self, **kwargs):
class _Point:
payload = {"content": "original", "tags": []}
return [_Point()]
def batch_update_points(self, **kwargs):
calls.append(("batch", kwargs))
def set_payload(self, **kwargs):
calls.append(("payload", kwargs))
def update_vectors(self, **kwargs):
calls.append(("vector", kwargs))
store._client = _Client()
async def _direct(func, *args, **kwargs):
return func(*args, **kwargs)
monkeypatch.setattr(qdrant_module, "_run_in_vdb_pool", _direct)
ok = await store.update_payload(
"node-1", {"content": "updated", "embedding": [1.0, 0.0, 0.0]},
)
assert ok is True
assert [kind for kind, _kwargs in calls] == ["batch"]
operations = calls[0][1]["update_operations"]
assert len(operations) == 2
@pytest.mark.asyncio
async def test_update_only_reconcile_uses_one_vector_store_batch(monkeypatch):
monkeypatch.setenv("MEMORY_WRITE_APPLY_VERIFY_ENABLED", "false")
config = MemoryConfig.from_dict({"vector_store": {"embedding_dims": 3}})
embed = _RecordingEmbed()
store = _RecordingStore()
writer = MemoryWriter(config, embed_service=embed, vector_store=store)
targets = ["old-1", "old-2", "old-3"]
texts = ["new one", "new two", "new three"]
async def _fake_reconcile(*args, **kwargs):
return ReconcileResult(
ops=[
ReconcileOp(op="UPDATE", memory_id=mid, content=text, layer="L2_FACT")
for mid, text in zip(targets, texts)
],
success=True,
new_memory_embeddings={t: [1.0, 0.0, 0.0] for t in texts},
)
writer._reconciler = type("_Reconciler", (), {"reconcile": _fake_reconcile})()
request = WriteRequest(content="raw", user_id="u-update", agent_id="a1")
ids, error, counts, _tokens = await writer._reconcile_and_store_locked(
new_memory_texts=texts,
new_memories_meta=[{"content": t, "layer": "L2_FACT"} for t in texts],
request=request,
vector_store=store,
req_id="req-update-batch",
precomputed_embeddings={t: [1.0, 0.0, 0.0] for t in texts},
)
assert error is None
assert ids == targets
assert counts == {"add": 0, "supersede": 0, "update": 3, "total": 3}
assert len(store.update_batch_calls) == 1
assert [mid for mid, _updates in store.update_batch_calls[0]] == targets