diff --git a/reme/steps/index/reindex.py b/reme/steps/index/reindex.py index 6ba44655..acbc9d83 100644 --- a/reme/steps/index/reindex.py +++ b/reme/steps/index/reindex.py @@ -4,6 +4,24 @@ from ..base_step import BaseStep from ...components import R +def _render_reindex(details: dict) -> str: + """Render the store's reindex report as one readable summary line. + + ReMe Studio's settings panel shows ``response.answer`` when it is a string + and otherwise falls back to a generic "index rebuilt" message + (``reme_studio/app/settings-center.tsx``). Returning the report dict as the + answer left that branch unreachable, so the operator never learned what was + actually rebuilt. The report itself stays in ``metadata`` for programmatic + consumers. + """ + scope = str(details.get("scope") or "all") + per_scope = {name: entry for name, entry in details.items() if isinstance(entry, dict)} + if per_scope: + counts = ", ".join(f"{name}={entry.get('indexed', 0)}" for name, entry in per_scope.items()) + return f"Reindexed scope={scope}: {counts}" + return f"Reindexed scope={scope}: {details.get('indexed', 0)} indexed" + + @R.register("reindex_step") class ReindexStep(BaseStep): """Rebuild BM25, embeddings, and/or tags without scanning workspace files.""" @@ -13,7 +31,7 @@ class ReindexStep(BaseStep): scope = str(self.context.get("scope", "all")) details = await self.file_store.reindex(scope) - self.context.response.answer = details + self.context.response.answer = _render_reindex(details) self.context.response.metadata.update(details) self.context.response.metadata["scope"] = scope return self.context.response diff --git a/tests/unit/test_reindex.py b/tests/unit/test_reindex.py index 64bd1a43..b0a350ed 100644 --- a/tests/unit/test_reindex.py +++ b/tests/unit/test_reindex.py @@ -23,6 +23,7 @@ async def test_reindex_step_delegates_scope(scope): store.reindex.assert_awaited_once_with(scope) store.clear.assert_not_called() assert response.metadata == {"indexed": 3, "scope": scope} + assert response.answer == f"Reindexed scope={scope}: 3 indexed" @pytest.mark.asyncio @@ -41,6 +42,33 @@ async def test_reindex_step_delegates_all_once(): store.reindex.assert_awaited_once_with("all") assert response.metadata == details + assert response.answer == "Reindexed scope=all: bm25=3, embedding=3, tag=3" + + +@pytest.mark.asyncio +async def test_reindex_answer_is_a_readable_summary_of_the_report(): + """The answer is a string, so Studio shows it instead of a generic message. + + ReMe Studio only displays ``response.answer`` when it is a string + (``reme_studio/app/settings-center.tsx``); a dict answer silently fell back + to the generic "index rebuilt" text and the operator never saw the counts. + The full report stays in metadata either way. + """ + store = LocalFileStore(name="test_reindex_answer", embedding_store="") + store.reindex = AsyncMock( + return_value={ + "scope": "all", + "bm25": {"scope": "bm25", "indexed": 7}, + "embedding": {"scope": "embedding", "indexed": 5}, + "tag": {"scope": "tag", "indexed": 2}, + }, + ) + + response = await ReindexStep(file_store=store)(RuntimeContext(scope="all")) + + assert isinstance(response.answer, str) + assert response.answer == "Reindexed scope=all: bm25=7, embedding=5, tag=2" + assert response.metadata["bm25"] == {"scope": "bm25", "indexed": 7} def test_reindex_job_schema_exposes_tag_scope():