mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## DescriptionKeep graph-expanded local-memory results consistent with the current-only contract already applied by vector search.Closes #2209## Type of Change- [x] Bug fix (non-breaking change that fixes an issue)- [ ] New feature (non-breaking change that adds functionality)- [ ] Breaking change (fix or feature that would cause existing functionality to change)- [ ] Documentation update- [ ] Performance improvement- [ ] Code refactoring (no functional changes)## Changes Made- Reject graph-expanded memories whose `valid_until` is set.- Reject graph-expanded memories whose `superseded_by` is set.- Add focused coverage for active, expired, and superseded related memories.## Testing- [x] Unit tests pass (`pytest`)- [x] Linting passes (`ruff check .`)- [ ] Type checking passes (`mypy headroom`)- [x] New tests added for new functionality- [ ] Manual testing performed### Test Output```text$ uv run --with pytest --with pytest-asyncio --with numpy pytest tests/test_memory/test_local_backend_search.py -q3 passed$ uv run --with ruff ruff check headroom/memory/backends/local.py tests/test_memory/test_local_backend_search.pyAll checks passed!$ uv run --with ruff ruff format --check headroom/memory/backends/local.py tests/test_memory/test_local_backend_search.py2 files already formatted```## Real Behavior Proof- Environment: Python 3.13, synthetic in-memory test doubles- Exact command / steps: run the focused test file above- Observed result: active graph-linked memory is returned; records with `valid_until` or `superseded_by` are excluded- Not tested: full repository suite, external vector/graph implementations## Review Readiness- [x] I have performed a self-review- [x] This PR is ready for human review## Checklist- [x] My code follows the project's style guidelines- [x] I have performed a self-review of my code- [ ] I have commented my code, particularly in hard-to-understand areas- [ ] I have made corresponding changes to the documentation- [ ] My changes generate no new warnings- [x] I have added tests that prove my fix is effective or that my feature works- [x] New and existing unit tests pass locally with my changes- [ ] I have updated the CHANGELOG.md if applicable## Screenshots (if applicable)N/A## Additional NotesDocumentation and changelog changes are not needed for this narrow internal behavior fix. The existing temporal-history APIs remain unchanged. Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
76 lines
2.4 KiB
Python
76 lines
2.4 KiB
Python
"""Focused tests for LocalBackend search result filtering."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from headroom.memory.backends.local import LocalBackend
|
|
from headroom.memory.models import Memory
|
|
|
|
|
|
def _backend_with_related_memory(related_memory: Memory) -> LocalBackend:
|
|
seed = Memory(
|
|
id="seed-memory",
|
|
content="Alice manages Project X",
|
|
user_id="alice",
|
|
entity_refs=["Project X"],
|
|
)
|
|
vector_result = SimpleNamespace(memory=seed, similarity=0.9)
|
|
|
|
backend = LocalBackend()
|
|
backend._initialized = True
|
|
backend._hierarchical_memory = SimpleNamespace(
|
|
search=AsyncMock(return_value=[vector_result]),
|
|
get=AsyncMock(return_value=related_memory),
|
|
)
|
|
backend._graph = SimpleNamespace(
|
|
get_entity_by_name=AsyncMock(return_value=SimpleNamespace(id="project-x")),
|
|
query_subgraph=AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
entities=[SimpleNamespace(metadata={"source_memory_id": related_memory.id})],
|
|
relationships=[],
|
|
)
|
|
),
|
|
)
|
|
return backend
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_expansion_includes_active_related_memory() -> None:
|
|
related = Memory(
|
|
id="related-memory",
|
|
content="Project X uses Python",
|
|
user_id="alice",
|
|
entity_refs=["Project X", "Python"],
|
|
)
|
|
backend = _backend_with_related_memory(related)
|
|
|
|
results = await backend.search_memories("Alice's work", "alice", include_related=True)
|
|
|
|
assert [result.memory.id for result in results] == ["seed-memory", "related-memory"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("inactive_field", ["valid_until", "superseded_by"])
|
|
async def test_graph_expansion_excludes_inactive_related_memory(
|
|
inactive_field: str,
|
|
) -> None:
|
|
related = Memory(
|
|
id="related-memory",
|
|
content="Outdated Project X detail",
|
|
user_id="alice",
|
|
entity_refs=["Project X"],
|
|
)
|
|
if inactive_field == "valid_until":
|
|
related.valid_until = datetime.now(timezone.utc).replace(tzinfo=None)
|
|
else:
|
|
related.superseded_by = "replacement-memory"
|
|
backend = _backend_with_related_memory(related)
|
|
|
|
results = await backend.search_memories("Alice's work", "alice", include_related=True)
|
|
|
|
assert [result.memory.id for result in results] == ["seed-memory"]
|