diff --git a/tests/mcp/test_tool_recent_activity.py b/tests/mcp/test_tool_recent_activity.py index 9f69dcd82..6ce7acadb 100644 --- a/tests/mcp/test_tool_recent_activity.py +++ b/tests/mcp/test_tool_recent_activity.py @@ -695,3 +695,53 @@ async def test_recent_activity_entity_rows_include_external_id(client, test_grap assert any(re.search(uuid_pattern, line) for line in entity_lines), ( f"entity rows missing external_id: {entity_lines!r}" ) + + +@pytest.mark.asyncio +async def test_recent_activity_never_indexed_says_so( + client, test_project, session_maker, config_home +): + """A never-indexed project must not report an ordinary empty activity feed (#1534).""" + from sqlalchemy import text as sa_text + + from basic_memory import db + + # Trigger: files exist on disk but no index pass has ever completed. + notes_dir = config_home / "notes" + notes_dir.mkdir(exist_ok=True) + (notes_dir / "unindexed-note.md").write_text("# Unindexed Note\n\nNot yet indexed.\n") + async with db.scoped_session(session_maker) as session: + await session.execute( + sa_text("UPDATE project SET last_indexed_at = NULL WHERE id = :id"), + {"id": test_project.id}, + ) + + result = await recent_activity(project=test_project.name, timeframe="7d") + + assert isinstance(result, str) + assert "never been indexed" in result + assert "bm project index" in result + + +@pytest.mark.asyncio +async def test_recent_activity_indexed_empty_keeps_onboarding( + client, test_project, session_maker, config_home +): + """An indexed-but-quiet project keeps the original onboarding copy (#1534).""" + from datetime import datetime + + from sqlalchemy import text as sa_text + + from basic_memory import db + + async with db.scoped_session(session_maker) as session: + await session.execute( + sa_text("UPDATE project SET last_indexed_at = :now WHERE id = :id"), + {"now": datetime.now(), "id": test_project.id}, + ) + + result = await recent_activity(project=test_project.name, timeframe="7d") + + assert isinstance(result, str) + assert "No recent activity" in result + assert "never been indexed" not in result diff --git a/tests/mcp/test_tool_search.py b/tests/mcp/test_tool_search.py index 21bee60a9..1acf62ef1 100644 --- a/tests/mcp/test_tool_search.py +++ b/tests/mcp/test_tool_search.py @@ -2281,3 +2281,112 @@ def test_search_notes_parse_str_list_rejects_non_string_list_elements_in_place() # All-string lists still work correctly. assert parse_str_list(["note", "task"]) == ["note", "task"] assert parse_str_list(["note,task"]) == ["note", "task"] + + +@pytest.mark.asyncio +async def test_search_never_indexed_text_says_so(client, test_project, session_maker, config_home): + """A never-indexed project must not report an ordinary empty result (#1534).""" + from sqlalchemy import text as sa_text + + from basic_memory import db + + # Trigger: files exist on disk but no index pass has ever completed. + notes_dir = config_home / "notes" + notes_dir.mkdir(exist_ok=True) + (notes_dir / "unindexed-note.md").write_text( + "# Unindexed Note\n\nNothing about this file can be found by search.\n" + ) + async with db.scoped_session(session_maker) as session: + await session.execute( + sa_text("UPDATE project SET last_indexed_at = NULL WHERE id = :id"), + {"id": test_project.id}, + ) + + response = await search_notes( + project=test_project.name, + query="unindexed-note-xyzzy", + search_type="text", + output_format="text", + ) + + assert isinstance(response, str) + assert "never been indexed" in response + assert "bm project index" in response + assert "Try broader or different terms" not in response + + +@pytest.mark.asyncio +async def test_search_never_indexed_json_carries_phase( + client, test_project, session_maker, config_home +): + """The structured search path carries index_phase on a never-indexed miss (#1534).""" + from sqlalchemy import text as sa_text + + from basic_memory import db + + notes_dir = config_home / "notes" + notes_dir.mkdir(exist_ok=True) + (notes_dir / "unindexed-note.md").write_text("# Unindexed Note\n\nNo index covers this.\n") + async with db.scoped_session(session_maker) as session: + await session.execute( + sa_text("UPDATE project SET last_indexed_at = NULL WHERE id = :id"), + {"id": test_project.id}, + ) + + response = await search_notes( + project=test_project.name, + query="unindexed-note-xyzzy", + search_type="text", + output_format="json", + ) + + assert isinstance(response, dict) + assert response["results"] == [] + assert response["index_phase"] == "never_indexed" + + +@pytest.mark.asyncio +async def test_search_indexed_miss_keeps_original_copy( + client, test_project, session_maker, config_home +): + """An indexed project with genuinely no match keeps the plain-miss copy (#1534).""" + from datetime import datetime + + from sqlalchemy import text as sa_text + + from basic_memory import db + from basic_memory.mcp.tools import write_note as write_note_tool + + async with db.scoped_session(session_maker) as session: + await session.execute( + sa_text("UPDATE project SET last_indexed_at = :now WHERE id = :id"), + {"now": datetime.now(), "id": test_project.id}, + ) + + created = await write_note_tool( + project=test_project.name, + title="Indexed Note", + directory="notes", + content="# Indexed Note\n\nSearchable content about telescopes.\n", + ) + assert created + + miss = await search_notes( + project=test_project.name, + query="nothing-matches-this-xyzzy", + search_type="text", + output_format="text", + ) + assert isinstance(miss, str) + assert "Try broader or different terms" in miss + assert "never been indexed" not in miss + + hit = await search_notes( + project=test_project.name, + query="telescopes", + search_type="text", + output_format="json", + ) + assert isinstance(hit, dict) + assert len(hit["results"]) > 0 + assert "index_phase" not in hit