mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-06-04 16:10:24 +00:00
dc1e369f89
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)
74 lines
2.9 KiB
Python
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" |