From 544cfb8b064fd4e29b9935afe2b39b02dd327525 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Tue, 14 Apr 2026 09:48:05 +0200 Subject: [PATCH] fix: retain last non-empty result set in search polling loop DSM sends files=[] on the final finished=True poll even when results exist in earlier rounds. Overwriting files each iteration discarded the real results, producing a false "No files found" response. Co-Authored-By: Claude Sonnet 4.6 --- .../tools/filestation.py | 4 ++- tests/test_tools_filestation.py | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/mcp_synology_filestation/tools/filestation.py b/src/mcp_synology_filestation/tools/filestation.py index d3a061f..8ee9639 100644 --- a/src/mcp_synology_filestation/tools/filestation.py +++ b/src/mcp_synology_filestation/tools/filestation.py @@ -287,7 +287,9 @@ def register_filestation( ) return f"Error: {e}" - files = poll_data.get("files", []) + current_files: list[dict] = poll_data.get("files", []) + if current_files: + files = current_files # keep the last non-empty result set finished: bool = poll_data.get("finished", False) if finished: diff --git a/tests/test_tools_filestation.py b/tests/test_tools_filestation.py index 80a53d8..4304b6a 100644 --- a/tests/test_tools_filestation.py +++ b/tests/test_tools_filestation.py @@ -502,6 +502,41 @@ async def test_search_polls_until_finished(config: AppConfig) -> None: assert "1 match(es) found" in result +@pytest.mark.asyncio +async def test_search_empty_final_poll(config: AppConfig) -> None: + """search returns results from an earlier poll when the final finished=True poll is empty. + + DSM can return files=[]] on the finishing poll even when results exist — the tool + must retain the last non-empty result set rather than overwriting with []. + """ + client = MagicMock() + poll_calls = 0 + + async def _request(api, method, **kwargs): + nonlocal poll_calls + if method == "start": + return {"taskid": "t_empty_final"} + if method == "list": + poll_calls += 1 + if poll_calls == 1: + # First poll: results available, not yet finished + return {"files": [_SEARCH_FILE], "finished": False, "total": 1} + # Second poll: finished, but DSM returns empty files + return {"files": [], "finished": True, "total": 1} + return {} + + client.request = AsyncMock(side_effect=_request) + + tools = _make_mcp_and_tools(config, client) + + with patch("asyncio.sleep", new_callable=AsyncMock): + result = await tools["search"](path="/docker", pattern="*.yaml") + + # Must surface the result from the first poll, not treat the empty final as "no results" + assert "1 match(es) found" in result + assert "/docker/app/compose.yaml" in result + + @pytest.mark.asyncio async def test_search_no_results(config: AppConfig) -> None: """search returns a friendly message when no files are found."""