refactor(spotify): convert to built-in bundled plugin under plugins/spotify (#15174)

Moves the Spotify integration from tools/ into plugins/spotify/,
matching the existing pattern established by plugins/image_gen/ for
third-party service integrations.

Why:
- tools/ should be reserved for foundational capabilities (terminal,
  read_file, web_search, etc.). tools/providers/ was a one-off
  directory created solely for spotify_client.py.
- plugins/ is already the home for image_gen backends, memory
  providers, context engines, and standalone hook-based plugins.
  Spotify is a third-party service integration and belongs alongside
  those, not in tools/.
- Future service integrations (eventually: Deezer, Apple Music, etc.)
  now have a pattern to copy.

Changes:
- tools/spotify_tool.py → plugins/spotify/tools.py (handlers + schemas)
- tools/providers/spotify_client.py → plugins/spotify/client.py
- tools/providers/ removed (was only used for Spotify)
- New plugins/spotify/__init__.py with register(ctx) calling
  ctx.register_tool() × 7. The handler/check_fn wiring is unchanged.
- New plugins/spotify/plugin.yaml (kind: backend, bundled, auto-load).
- tests/tools/test_spotify_client.py: import paths updated.

tools_config fix — _DEFAULT_OFF_TOOLSETS now wins over plugin auto-enable:
- _get_platform_tools() previously auto-enabled unknown plugin
  toolsets for new platforms. That was fine for image_gen (which has
  no toolset of its own) but bad for Spotify, which explicitly
  requires opt-in (don't ship 7 tool schemas to users who don't use
  it). Added a check: if a plugin toolset is in _DEFAULT_OFF_TOOLSETS,
  it stays off until the user picks it in 'hermes tools'.

Pre-existing test bug fix:
- tests/hermes_cli/test_plugins.py::test_list_returns_sorted
  asserted names were sorted, but list_plugins() sorts by key
  (path-derived, e.g. image_gen/openai). With only image_gen plugins
  bundled, name and key order happened to agree. Adding plugins/spotify
  broke that coincidence (spotify sorts between openai-codex and xai
  by name but after xai by key). Updated test to assert key order,
  which is what the code actually documents.

Validation:
- scripts/run_tests.sh tests/hermes_cli/test_plugins.py \
    tests/hermes_cli/test_tools_config.py \
    tests/hermes_cli/test_spotify_auth.py \
    tests/tools/test_spotify_client.py \
    tests/tools/test_registry.py
  → 143 passed
- E2E plugin load: 'spotify' appears in loaded plugins, all 7 tools
  register into the spotify toolset, check_fn gating intact.
This commit is contained in:
Teknium 2026-04-24 07:06:11 -07:00 committed by GitHub
parent e5d41f05d4
commit 8d12fb1e6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 96 additions and 19 deletions

View file

@ -635,7 +635,7 @@ class TestPluginManagerList:
assert mgr.list_plugins() == []
def test_list_returns_sorted(self, tmp_path, monkeypatch):
"""list_plugins() returns results sorted by name."""
"""list_plugins() returns results sorted by key."""
plugins_dir = tmp_path / "hermes_test" / "plugins"
_make_plugin_dir(plugins_dir, "zulu")
_make_plugin_dir(plugins_dir, "alpha")
@ -645,8 +645,10 @@ class TestPluginManagerList:
mgr.discover_and_load()
listing = mgr.list_plugins()
names = [p["name"] for p in listing]
assert names == sorted(names)
# list_plugins sorts by key (path-derived, e.g. ``image_gen/openai``),
# not by display name, so that category plugins group together.
keys = [p["key"] for p in listing]
assert keys == sorted(keys)
def test_list_with_plugins(self, tmp_path, monkeypatch):
"""list_plugins() returns info dicts for each discovered plugin."""