deer-flow/backend/tests/test_memory_search.py
Vanzeren c2002d9fac
feat(memory): add memory tool sets (#4023)
* feat: add memory-as-tool mode alongside existing middleware mode

- Add memory.mode config field (middleware|tool, default middleware)
- Add search_memory_facts() for case-insensitive fact lookup
- Add 4 memory tools: memory_search, memory_add, memory_update, memory_delete
- Wire mode gating in factory.py and lead_agent/agent.py
- 256 memory tests passing, zero regressions

* fix: harden tool-mode memory scoping and docs

* fix: address memory tool mode review feedback

* fix(memory): address tool mode review feedback

* fix: update config_version to 22 in values.yaml
2026-07-11 15:28:16 +08:00

139 lines
5.2 KiB
Python

"""Tests for search_memory_facts function."""
import json
from deerflow.agents.memory.storage import FileMemoryStorage, create_empty_memory
from deerflow.agents.memory.updater import search_memory_facts
def _make_fact(content: str, category: str = "context", confidence: float = 0.7) -> dict:
return {
"id": f"fact_test_{hash(content) & 0xFFFFFFFF:08x}",
"content": content,
"category": category,
"confidence": confidence,
"createdAt": "2026-07-09T00:00:00Z",
"source": "test",
}
class TestSearchMemoryFacts:
"""Tests for search_memory_facts function."""
def test_basic_substring_match(self, tmp_path, monkeypatch):
"""Should find facts containing the query string (case-insensitive)."""
facts = [
_make_fact("User prefers Python", "preference", 0.9),
_make_fact("User works with TypeScript", "context", 0.7),
_make_fact("User lives in Beijing", "personal", 0.8),
]
_setup_memory(tmp_path, monkeypatch, facts)
results = search_memory_facts("python")
assert len(results) == 1
assert results[0]["content"] == "User prefers Python"
def test_case_insensitive(self, tmp_path, monkeypatch):
"""Should match regardless of case."""
facts = [_make_fact("User prefers Python", "preference", 0.9)]
_setup_memory(tmp_path, monkeypatch, facts)
assert len(search_memory_facts("PYTHON")) == 1
assert len(search_memory_facts("python")) == 1
assert len(search_memory_facts("Python")) == 1
def test_category_filter(self, tmp_path, monkeypatch):
"""Should only return facts matching the given category."""
facts = [
_make_fact("Likes dark mode", "preference", 0.8),
_make_fact("Works remotely", "context", 0.7),
_make_fact("Prefers short answers", "preference", 0.6),
]
_setup_memory(tmp_path, monkeypatch, facts)
results = search_memory_facts("prefer", category="preference")
assert len(results) == 1
assert results[0]["content"] == "Prefers short answers"
def test_category_filter_no_match(self, tmp_path, monkeypatch):
"""Should return empty list when category doesn't match."""
facts = [_make_fact("Likes dark mode", "preference", 0.8)]
_setup_memory(tmp_path, monkeypatch, facts)
results = search_memory_facts("dark", category="context")
assert results == []
def test_empty_query_returns_empty(self, tmp_path, monkeypatch):
"""Should return empty list for empty query, not error."""
facts = [_make_fact("Some fact")]
_setup_memory(tmp_path, monkeypatch, facts)
results = search_memory_facts("")
assert results == []
def test_no_match_returns_empty(self, tmp_path, monkeypatch):
"""Should return empty list when nothing matches."""
facts = [_make_fact("User prefers Python")]
_setup_memory(tmp_path, monkeypatch, facts)
results = search_memory_facts("Rust")
assert results == []
def test_sorted_by_confidence_desc(self, tmp_path, monkeypatch):
"""Should return results sorted by confidence descending."""
facts = [
_make_fact("Fact A", confidence=0.3),
_make_fact("Fact B", confidence=0.9),
_make_fact("Fact C", confidence=0.6),
]
_setup_memory(tmp_path, monkeypatch, facts)
results = search_memory_facts("Fact")
assert len(results) == 3
assert results[0]["confidence"] == 0.9
assert results[1]["confidence"] == 0.6
assert results[2]["confidence"] == 0.3
def test_respects_limit(self, tmp_path, monkeypatch):
"""Should return at most `limit` results."""
facts = [_make_fact(f"Fact {i}", confidence=0.5) for i in range(20)]
_setup_memory(tmp_path, monkeypatch, facts)
results = search_memory_facts("Fact", limit=5)
assert len(results) == 5
def test_negative_limit_returns_empty(self, tmp_path, monkeypatch):
"""Should not let negative limits expand the result set via slicing."""
facts = [_make_fact(f"Fact {i}", confidence=0.5) for i in range(3)]
_setup_memory(tmp_path, monkeypatch, facts)
results = search_memory_facts("Fact", limit=-1)
assert results == []
def test_no_facts_returns_empty(self, tmp_path, monkeypatch):
"""Should return empty list when memory has no facts."""
_setup_memory(tmp_path, monkeypatch, [])
results = search_memory_facts("anything")
assert results == []
def _setup_memory(tmp_path, monkeypatch, facts: list[dict]):
"""Set up a FileMemoryStorage with given facts at a temp path."""
memory_file = tmp_path / "memory.json"
memory_data = create_empty_memory()
memory_data["facts"] = facts
memory_file.write_text(json.dumps(memory_data))
storage = FileMemoryStorage()
# Force the storage to use our temp file
monkeypatch.setattr(
"deerflow.agents.memory.updater.get_memory_storage",
lambda: storage,
)
monkeypatch.setattr(
"deerflow.agents.memory.updater.get_memory_data",
lambda agent_name=None, user_id=None: json.loads(memory_file.read_text()),
)