diff --git a/pyflowlauncher/launcher.py b/pyflowlauncher/launcher.py index 13cd2ab..c7b4254 100644 --- a/pyflowlauncher/launcher.py +++ b/pyflowlauncher/launcher.py @@ -118,7 +118,9 @@ async def run(self, dispatch: Callable[[str, list], Awaitable[Any]]) -> None: # The host sends ("query", [query, Settings.Inner]). if len(params) > 1 and isinstance(params[1], dict): self._settings = params[1] - params = [params[0].get('search') or params[0].get('trimmedQuery', '')] + # Query.search excludes the action keyword; trimmedQuery/rawQuery + # include it, so they must never be used as the search string. + params = [params[0].get('search') or ''] elif method != 'context_menu' and len(params) == 1 and isinstance(params[0], list): # Actions: the host calls RPC.InvokeAsync(method, argument: Parameters), # which wraps the whole Parameters list as one positional argument diff --git a/tests/integration/test_v2_protocol.py b/tests/integration/test_v2_protocol.py index bbc6056..5a4369e 100644 --- a/tests/integration/test_v2_protocol.py +++ b/tests/integration/test_v2_protocol.py @@ -129,6 +129,25 @@ def test_empty_search_string(self): ]) assert query_response(responses, 1)['result']['result'][0]['Title'] == 'Hello, World!' + def test_action_keyword_not_leaked_into_search(self): + """Typing only the action keyword sends {'search': '', 'trimmedQuery': 'kw'}; + the handler must receive '' — not fall back to the keyword-bearing trimmedQuery.""" + received = [] + plugin = Plugin(launcher=FlowLauncherV2()) + + @plugin.on_method + def query(q: str): + received.append(q) + yield Result(title="ok") + + run(plugin, [ + {'id': 1, 'method': 'query', + 'params': [{'search': '', 'trimmedQuery': 'kw', 'rawQuery': 'kw', + 'actionKeyword': 'kw'}, {}]}, + {'id': 2, 'method': 'close', 'params': []}, + ]) + assert received == [''] + class TestV2Lifecycle: