|
1 | 1 | """Tests for v2 search router endpoints.""" |
2 | 2 |
|
| 3 | +from datetime import datetime, timezone |
| 4 | + |
3 | 5 | import pytest |
4 | 6 | from httpx import AsyncClient |
5 | 7 | from pathlib import Path |
6 | 8 |
|
7 | 9 | from basic_memory.deps.services import get_search_service_v2_external |
8 | 10 | from basic_memory.models import Project |
| 11 | +from basic_memory.repository.search_index_row import SearchIndexRow |
9 | 12 | from basic_memory.repository.semantic_errors import ( |
10 | 13 | SemanticDependenciesMissingError, |
11 | 14 | SemanticSearchDisabledError, |
@@ -428,3 +431,86 @@ async def test_search_has_more_false_on_last_page( |
428 | 431 | assert response.status_code == 200 |
429 | 432 | data = response.json() |
430 | 433 | assert data["has_more"] is False |
| 434 | + |
| 435 | + |
| 436 | +@pytest.mark.asyncio |
| 437 | +async def test_search_result_includes_matched_chunk( |
| 438 | + client: AsyncClient, |
| 439 | + app, |
| 440 | + v2_project_url: str, |
| 441 | +): |
| 442 | + """matched_chunk field appears in search API JSON when set on SearchIndexRow.""" |
| 443 | + now = datetime.now(timezone.utc) |
| 444 | + fake_row = SearchIndexRow( |
| 445 | + project_id=1, |
| 446 | + id=42, |
| 447 | + type="entity", |
| 448 | + file_path="notes/pricing.md", |
| 449 | + created_at=now, |
| 450 | + updated_at=now, |
| 451 | + title="Pricing Notes", |
| 452 | + permalink="notes/pricing", |
| 453 | + content_snippet="# Pricing Notes\n\n- [pricing] Team plan is $9/mo per seat", |
| 454 | + score=0.85, |
| 455 | + matched_chunk_text="- [pricing] Team plan is $9/mo per seat", |
| 456 | + ) |
| 457 | + |
| 458 | + class FakeSearchService: |
| 459 | + async def search(self, *args, **kwargs): |
| 460 | + return [fake_row] |
| 461 | + |
| 462 | + app.dependency_overrides[get_search_service_v2_external] = lambda: FakeSearchService() |
| 463 | + try: |
| 464 | + response = await client.post( |
| 465 | + f"{v2_project_url}/search/", |
| 466 | + json={"search_text": "pricing"}, |
| 467 | + ) |
| 468 | + finally: |
| 469 | + app.dependency_overrides.pop(get_search_service_v2_external, None) |
| 470 | + |
| 471 | + assert response.status_code == 200 |
| 472 | + data = response.json() |
| 473 | + assert len(data["results"]) == 1 |
| 474 | + result = data["results"][0] |
| 475 | + assert result["matched_chunk"] == "- [pricing] Team plan is $9/mo per seat" |
| 476 | + |
| 477 | + |
| 478 | +@pytest.mark.asyncio |
| 479 | +async def test_search_result_omits_matched_chunk_when_none( |
| 480 | + client: AsyncClient, |
| 481 | + app, |
| 482 | + v2_project_url: str, |
| 483 | +): |
| 484 | + """matched_chunk field is null when not set (FTS-only results).""" |
| 485 | + now = datetime.now(timezone.utc) |
| 486 | + fake_row = SearchIndexRow( |
| 487 | + project_id=1, |
| 488 | + id=43, |
| 489 | + type="entity", |
| 490 | + file_path="notes/general.md", |
| 491 | + created_at=now, |
| 492 | + updated_at=now, |
| 493 | + title="General Notes", |
| 494 | + permalink="notes/general", |
| 495 | + content_snippet="# General Notes\n\nSome content here", |
| 496 | + score=0.7, |
| 497 | + ) |
| 498 | + |
| 499 | + class FakeSearchService: |
| 500 | + async def search(self, *args, **kwargs): |
| 501 | + return [fake_row] |
| 502 | + |
| 503 | + app.dependency_overrides[get_search_service_v2_external] = lambda: FakeSearchService() |
| 504 | + try: |
| 505 | + response = await client.post( |
| 506 | + f"{v2_project_url}/search/", |
| 507 | + json={"search_text": "general"}, |
| 508 | + ) |
| 509 | + finally: |
| 510 | + app.dependency_overrides.pop(get_search_service_v2_external, None) |
| 511 | + |
| 512 | + assert response.status_code == 200 |
| 513 | + data = response.json() |
| 514 | + assert len(data["results"]) == 1 |
| 515 | + result = data["results"][0] |
| 516 | + assert result["matched_chunk"] is None |
0 commit comments