From 7bb7664fae6674ab8b623c010e5773087b82b8b5 Mon Sep 17 00:00:00 2001 From: Drew Cain Date: Wed, 10 Jun 2026 14:05:02 -0500 Subject: [PATCH] fix(core): use Observation.permalink in build_context to match the search index (#946) Closes #929 Signed-off-by: phernandez --- src/basic_memory/services/context_service.py | 9 +++-- tests/services/test_context_service.py | 41 ++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/basic_memory/services/context_service.py b/src/basic_memory/services/context_service.py index 678af883..a27b595f 100644 --- a/src/basic_memory/services/context_service.py +++ b/src/basic_memory/services/context_service.py @@ -245,9 +245,12 @@ class ContextService: type="observation", id=obs.id, title=f"{obs.category}: {obs.content[:50]}...", - permalink=generate_permalink( - f"{primary_item.permalink}/observations/{obs.category}/{obs.content}" - ), + # Observation.permalink is the single definition of the + # synthetic permalink format (200-char truncation plus + # content digest); rebuilding it inline diverged from the + # search index for long observations (#929). The parent + # entity is eager-loaded by ObservationRepository. + permalink=obs.permalink, file_path=primary_item.file_path, content=obs.content, category=obs.category, diff --git a/tests/services/test_context_service.py b/tests/services/test_context_service.py index 0cb4d5d7..b56f40f2 100644 --- a/tests/services/test_context_service.py +++ b/tests/services/test_context_service.py @@ -212,6 +212,47 @@ async def test_build_context_with_observations(context_service, test_graph): assert "Root note" in note_observation.content +@pytest.mark.asyncio +async def test_build_context_observation_permalinks_match_search_index( + context_service, search_service, entity_service +): + """Regression test for #929: observation permalinks must match the search index. + + build_context used to rebuild the synthetic observation permalink inline, + without the 200-char truncation (#446) or the content digest (#931) that + Observation.permalink applies, so for long observations it returned + permalinks the search index doesn't contain. + """ + from basic_memory.schemas.base import Entity as EntitySchema + from basic_memory.schemas.search import SearchQuery + + long_observation = "x" * 210 + " LONG_OBS_MARKER" + entity, _ = await entity_service.create_or_update_entity( + EntitySchema( + title="Long Obs Entity", + note_type="test", + directory="test", + content=f"# Long Obs Entity\n- [note] {long_observation}\n", + ) + ) + await search_service.index_entity(entity) + + url = memory_url.validate_strings(f"memory://{entity.permalink}") + context_result = await context_service.build_context(url, include_observations=True) + assert len(context_result.results) == 1 + context_item = context_result.results[0] + assert len(context_item.observations) == 1 + obs_row = context_item.observations[0] + + # The model property is the single definition of the permalink format + assert obs_row.permalink == entity.observations[0].permalink + + # The search index row for this observation carries the same permalink + index_rows = await search_service.search(SearchQuery(text="LONG_OBS_MARKER")) + obs_permalinks = [r.permalink for r in index_rows if r.type == SearchItemType.OBSERVATION.value] + assert obs_permalinks == [obs_row.permalink] + + @pytest.mark.asyncio async def test_build_context_not_found(context_service): """Test handling non-existent permalinks."""