Files
hermes-webui/tests/test_skill_usage.py
T
BonyFish dc1e369f89 fix: remove webui-side writer to avoid conflict with agent
The .usage.json file is owned by hermes-agent (tools/skill_usage.py).
This change removes the webui-side increment logic to avoid:

1. File ownership conflict - both writing to same file
2. Schema mismatch - agent uses ISO strings, webui used floats
3. Concurrency issues - agent uses fcntl locks, webui had no locking
4. Double-counting - agent already increments counters server-side

Changes:
- api/skill_usage.py: keep only read_skill_usage(), remove increment functions
- api/streaming.py: remove skill usage counter hook
- api/routes.py: adapt response to pass through agent's format as-is,
  with defensive coercion for None values and metadata preservation
- tests/test_skill_usage.py: remove increment tests (17→7 cases)
2026-05-27 20:57:32 +08:00

74 lines
2.9 KiB
Python

"""Tests for api/skill_usage.py — .usage.json reader.
Covers:
- read_skill_usage with various .usage.json states
- GET /api/skills/usage route presence (read-only, agent writes the file)
"""
import json
import re
from pathlib import Path
from api.skill_usage import read_skill_usage
_ROUTES = Path(__file__).resolve().parent.parent / "api" / "routes.py"
class TestReadSkillUsage:
def test_read_empty(self, tmp_path):
"""File does not exist -> returns {}."""
assert read_skill_usage(tmp_path) == {}
def test_read_valid(self, tmp_path):
"""Well-formed .usage.json with nested entries is returned as-is."""
data = {
"research-arxiv": {"use_count": 12, "view_count": 5},
"hermes-agent": {"use_count": 8, "view_count": 3},
}
(tmp_path / ".usage.json").write_text(json.dumps(data), encoding="utf-8")
assert read_skill_usage(tmp_path) == data
def test_read_agent_format(self, tmp_path):
"""Agent-side format (ISO timestamps) is accepted."""
data = {
"dev-workflow": {
"use_count": 77,
"view_count": 77,
"last_used_at": "2024-04-05T20:54:38Z",
"state": "active",
},
}
(tmp_path / ".usage.json").write_text(json.dumps(data), encoding="utf-8")
assert read_skill_usage(tmp_path) == data
def test_read_corrupt_json(self, tmp_path):
"""Corrupt JSON returns {} without raising."""
(tmp_path / ".usage.json").write_text("not json", encoding="utf-8")
assert read_skill_usage(tmp_path) == {}
def test_read_wrong_type(self, tmp_path):
"""Non-dict top-level value returns {}."""
(tmp_path / ".usage.json").write_text("42", encoding="utf-8")
assert read_skill_usage(tmp_path) == {}
class TestApiSkillsUsageRoute:
def test_route_handler_present(self):
"""routes.py contains a handler for GET /api/skills/usage."""
src = _ROUTES.read_text(encoding="utf-8")
assert '"/api/skills/usage"' in src, (
"Missing /api/skills/usage route in api/routes.py"
)
assert "read_skill_usage" in src, (
"read_skill_usage import missing in api/routes.py"
)
def test_route_returns_usage_structure(self):
"""The route response shape includes usage/skill_names/total_invocations."""
src = _ROUTES.read_text(encoding="utf-8")
# Find the /api/skills/usage handler block and check for key fields
block_match = re.search(r'if parsed\.path == "/api/skills/usage":.*?(?=\n if parsed|$)', src, re.DOTALL)
assert block_match, "Missing /api/skills/usage handler block"
block = block_match.group()
assert '"usage"' in block and '"skill_names"' in block, "Missing usage or skill_names in response"
assert '"total_invocations"' in block and '"unique_skills_used"' in block, "Missing total_invocations or unique_skills_used"