From eea4f03db8cfd9f55e336c4b9262656df1cb54a5 Mon Sep 17 00:00:00 2001 From: MHJ <84257033+MinhoJJang@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:40:00 +0900 Subject: [PATCH] fix(commands): search skills by keyword in slash autocomplete --- static/commands.js | 7 ++-- tests/test_cli_only_slash_commands.py | 47 +++++++++++++++++++++++++++ tests/test_sprint47.py | 4 ++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/static/commands.js b/static/commands.js index aa3f0486..56b7fcc0 100644 --- a/static/commands.js +++ b/static/commands.js @@ -187,6 +187,7 @@ function getMatchingCommands(prefix){ const matches=COMMANDS.filter(c=>c.name.startsWith(q)).map(c=>({...c,source:'builtin'})); const seen=new Set(matches.map(c=>c.name)); const reserved=_getReservedSlashCommandSlugs(); + const bundleSlugs=new Set(_bundleCommandCache.map(bundle=>bundle.name)); for(const [name, spec] of Object.entries(SLASH_SUBARG_SOURCES)){ if(!name.startsWith(q)||seen.has(name))continue; matches.push({ @@ -228,9 +229,11 @@ function getMatchingCommands(prefix){ } } for(const skill of _skillCommandCache){ - if(!skill.name.startsWith(q)||seen.has(skill.name)||reserved.has(skill.name))continue; + const name=String(skill&&skill.name||'').toLowerCase(); + const description=String(skill&&skill.desc||'').toLowerCase(); + if((!name.includes(q)&&!description.includes(q))||seen.has(name)||reserved.has(name)||bundleSlugs.has(name))continue; matches.push(skill); - seen.add(skill.name); + seen.add(name); } return matches; } diff --git a/tests/test_cli_only_slash_commands.py b/tests/test_cli_only_slash_commands.py index 4f1d9fd4..c01fe737 100644 --- a/tests/test_cli_only_slash_commands.py +++ b/tests/test_cli_only_slash_commands.py @@ -215,6 +215,18 @@ def _run_commands_js(script_body: str) -> dict: {{ name: 'plugin review', description: 'Plugin collisions should stay hidden from slash autocomplete' + }}, + {{ + name: 'hermes-upgrade', + description: 'Safely upgrade and verify a Hermes installation' + }}, + {{ + name: 'maintenance-guide', + description: 'Plan a safe upgrade and rollback path' + }}, + {{ + name: 'reinstall-helper', + description: 'Recover from a failed reinstall' }} ] }}; @@ -352,6 +364,41 @@ def test_cli_only_slugs_reserve_skill_autocomplete_namespace(): assert "use" in result["use_names"] +def test_skill_autocomplete_matches_keyword_in_name_or_description(): + result = _run_commands_js( + """ + await loadSkillCommands(true); + const upgrade = await getSlashAutocompleteMatches('/upgrade'); + const reinstall = await getSlashAutocompleteMatches('/reinstall'); + return { + upgrade_names: upgrade.map(item => item.name), + upgrade_sources: upgrade.map(item => item.source), + reinstall_names: reinstall.map(item => item.name), + reinstall_sources: reinstall.map(item => item.source) + }; + """ + ) + + assert result["upgrade_names"] == ["hermes-upgrade", "maintenance-guide"] + assert result["upgrade_sources"] == ["skill", "skill"] + assert result["reinstall_names"] == ["reinstall-helper"] + assert result["reinstall_sources"] == ["skill"] + + +def test_skill_autocomplete_hides_keyword_match_shadowed_by_bundle(): + result = _run_commands_js( + """ + await loadAgentCommandMetadata(true); + await loadBundleCommands(true); + await loadSkillCommands(true); + const matches = await getSlashAutocompleteMatches('/non-colliding'); + return matches.map(item => ({ name: item.name, source: item.source })); + """ + ) + + assert result == [] + + def test_bundle_collisions_stay_hidden_until_agent_metadata_is_ready(): script = textwrap.dedent( f""" diff --git a/tests/test_sprint47.py b/tests/test_sprint47.py index c3c20a0f..d5060c67 100644 --- a/tests/test_sprint47.py +++ b/tests/test_sprint47.py @@ -63,7 +63,9 @@ def test_skill_mutations_invalidate_slash_skill_caches(): def test_builtin_commands_take_precedence_over_skill_slug_collisions(): assert "_getReservedSlashCommandSlugs" in COMMANDS_JS assert "if(_getReservedSlashCommandSlugs().has(slug)) return null;" in COMMANDS_JS - assert "if(!skill.name.startsWith(q)||seen.has(skill.name)||reserved.has(skill.name))continue;" in COMMANDS_JS + command_matching = _function_body(COMMANDS_JS, "getMatchingCommands") + assert "reserved.has(name)" in command_matching + assert "!name.includes(q)&&!description.includes(q)" in command_matching def test_bundle_entries_merge_before_plain_skill_entries():