85 lines
3.2 KiB
Python
85 lines
3.2 KiB
Python
"""Tests for GET /api/commands -- exposes hermes-agent COMMAND_REGISTRY."""
|
|
import json
|
|
import urllib.request
|
|
|
|
import pytest
|
|
|
|
from tests.conftest import TEST_BASE, requires_agent_modules
|
|
|
|
|
|
def _get(path):
|
|
"""GET helper -- returns parsed JSON or raises HTTPError."""
|
|
with urllib.request.urlopen(TEST_BASE + path, timeout=10) as r:
|
|
return json.loads(r.read())
|
|
|
|
|
|
@requires_agent_modules
|
|
def test_commands_endpoint_returns_list():
|
|
"""GET /api/commands returns a JSON object with a 'commands' list."""
|
|
body = _get('/api/commands')
|
|
assert 'commands' in body
|
|
assert isinstance(body['commands'], list)
|
|
assert len(body['commands']) > 0
|
|
|
|
|
|
@requires_agent_modules
|
|
def test_commands_endpoint_includes_help():
|
|
"""The 'help' command must always be present (it's not cli_only)."""
|
|
body = _get('/api/commands')
|
|
names = {c['name'] for c in body['commands']}
|
|
assert 'help' in names
|
|
|
|
|
|
@requires_agent_modules
|
|
def test_commands_endpoint_command_shape():
|
|
"""Each command entry has the required fields."""
|
|
body = _get('/api/commands')
|
|
cmd = next(c for c in body['commands'] if c['name'] == 'help')
|
|
required = {
|
|
'name', 'description', 'category', 'aliases',
|
|
'args_hint', 'subcommands', 'cli_only', 'gateway_only',
|
|
}
|
|
assert set(cmd.keys()) >= required
|
|
assert isinstance(cmd['aliases'], list)
|
|
assert isinstance(cmd['subcommands'], list)
|
|
assert isinstance(cmd['cli_only'], bool)
|
|
assert isinstance(cmd['gateway_only'], bool)
|
|
|
|
|
|
@requires_agent_modules
|
|
def test_commands_endpoint_excludes_gateway_only_and_never_expose():
|
|
"""gateway_only commands and the _NEVER_EXPOSE set are filtered out."""
|
|
body = _get('/api/commands')
|
|
names = {c['name'] for c in body['commands']}
|
|
# /sethome, /restart, /update are gateway_only; /commands is in _NEVER_EXPOSE
|
|
for name in ('sethome', 'restart', 'update', 'commands'):
|
|
assert name not in names, f"{name} must be excluded from /api/commands"
|
|
|
|
|
|
@requires_agent_modules
|
|
def test_commands_endpoint_keeps_new_with_reset_alias():
|
|
"""The 'new' command stays exposed and carries its 'reset' alias."""
|
|
body = _get('/api/commands')
|
|
new_cmd = next(c for c in body['commands'] if c['name'] == 'new')
|
|
assert 'reset' in new_cmd['aliases']
|
|
|
|
|
|
def test_list_commands_returns_empty_for_empty_registry():
|
|
"""list_commands(_registry=[]) returns [] -- the same path as when
|
|
hermes_cli is missing (the empty-or-missing case)."""
|
|
from api.commands import list_commands
|
|
assert list_commands(_registry=[]) == []
|
|
|
|
|
|
def test_list_commands_degrades_when_agent_missing(monkeypatch):
|
|
"""If hermes_cli.commands is not importable, list_commands() returns []
|
|
via the ImportError path. Verified by stubbing sys.modules; test cleanup
|
|
is handled by monkeypatch + the fact that we don't reload api.commands."""
|
|
import sys
|
|
monkeypatch.setitem(sys.modules, 'hermes_cli.commands', None)
|
|
# NOTE: we do NOT reload api.commands. The lazy import inside
|
|
# list_commands() will re-attempt the import on each call and hit
|
|
# the stubbed-None module, raising ImportError, taking the fallback path.
|
|
from api.commands import list_commands
|
|
assert list_commands() == []
|