fix: use skill activity in curator status
Treat skill views and edits as activity when curator reports and applies lifecycle transitions, so recently loaded or patched skills are not displayed or transitioned as never used.\n\nAdds regression tests for activity derivation, automatic transitions, and CLI status output.
This commit is contained in:
56
tests/agent/test_curator_activity.py
Normal file
56
tests/agent/test_curator_activity.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Regression tests for curator skill activity timestamps."""
|
||||
|
||||
import importlib
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _write_skill(skills_dir: Path, name: str) -> None:
|
||||
skill_dir = skills_dir / name
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
f"---\nname: {name}\ndescription: test skill\n---\n\n# {name}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def curator_modules(tmp_path, monkeypatch):
|
||||
home = tmp_path / ".hermes"
|
||||
(home / "skills").mkdir(parents=True)
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
|
||||
import tools.skill_usage as skill_usage
|
||||
import agent.curator as curator
|
||||
|
||||
importlib.reload(skill_usage)
|
||||
importlib.reload(curator)
|
||||
return home, skill_usage, curator
|
||||
|
||||
|
||||
def test_recent_view_activity_prevents_false_stale_transition(curator_modules, monkeypatch):
|
||||
home, skill_usage, curator = curator_modules
|
||||
skills_dir = home / "skills"
|
||||
_write_skill(skills_dir, "recently-viewed")
|
||||
|
||||
now = datetime(2026, 4, 30, tzinfo=timezone.utc)
|
||||
created_at = (now - timedelta(days=60)).isoformat()
|
||||
last_viewed_at = (now - timedelta(days=1)).isoformat()
|
||||
skill_usage.save_usage({
|
||||
"recently-viewed": {
|
||||
"created_at": created_at,
|
||||
"last_viewed_at": last_viewed_at,
|
||||
"view_count": 1,
|
||||
"state": "active",
|
||||
}
|
||||
})
|
||||
monkeypatch.setattr(curator, "get_stale_after_days", lambda: 30)
|
||||
monkeypatch.setattr(curator, "get_archive_after_days", lambda: 90)
|
||||
|
||||
counts = curator.apply_automatic_transitions(now=now)
|
||||
|
||||
assert counts["marked_stale"] == 0
|
||||
assert skill_usage.get_record("recently-viewed")["state"] == "active"
|
||||
43
tests/hermes_cli/test_curator_status.py
Normal file
43
tests/hermes_cli/test_curator_status.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Tests for the curator CLI status renderer."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
def test_status_uses_last_activity_not_only_last_used(monkeypatch, capsys):
|
||||
import agent.curator as curator_state
|
||||
import hermes_cli.curator as curator_cli
|
||||
import tools.skill_usage as skill_usage
|
||||
|
||||
monkeypatch.setattr(curator_state, "load_state", lambda: {
|
||||
"paused": False,
|
||||
"last_run_at": None,
|
||||
"last_run_summary": "(none)",
|
||||
"run_count": 0,
|
||||
})
|
||||
monkeypatch.setattr(curator_state, "is_enabled", lambda: True)
|
||||
monkeypatch.setattr(curator_state, "get_interval_hours", lambda: 168)
|
||||
monkeypatch.setattr(curator_state, "get_stale_after_days", lambda: 30)
|
||||
monkeypatch.setattr(curator_state, "get_archive_after_days", lambda: 90)
|
||||
monkeypatch.setattr(skill_usage, "agent_created_report", lambda: [
|
||||
{
|
||||
"name": "recently-viewed",
|
||||
"state": "active",
|
||||
"pinned": False,
|
||||
"use_count": 0,
|
||||
"view_count": 3,
|
||||
"patch_count": 1,
|
||||
"created_at": "2026-01-01T00:00:00+00:00",
|
||||
"last_used_at": None,
|
||||
"last_viewed_at": "2026-04-30T10:00:00+00:00",
|
||||
"last_patched_at": "2026-04-30T11:00:00+00:00",
|
||||
"last_activity_at": "2026-04-30T11:00:00+00:00",
|
||||
"activity_count": 4,
|
||||
}
|
||||
])
|
||||
|
||||
assert curator_cli._cmd_status(SimpleNamespace()) == 0
|
||||
out = capsys.readouterr().out
|
||||
assert "least recently active" in out
|
||||
assert "activity= 4" in out
|
||||
assert "last_activity=never" not in out
|
||||
assert "last_used=never" not in out
|
||||
@@ -401,6 +401,26 @@ def test_agent_created_report_excludes_bundled_and_hub(skills_home):
|
||||
assert "hubbed" not in names
|
||||
|
||||
|
||||
def test_agent_created_report_derives_activity_from_view_and_patch(skills_home, monkeypatch):
|
||||
import tools.skill_usage as skill_usage
|
||||
|
||||
skills_dir = skills_home / "skills"
|
||||
_write_skill(skills_dir, "mine")
|
||||
timestamps = iter([
|
||||
"2026-04-30T10:00:00+00:00",
|
||||
"2026-04-30T11:00:00+00:00",
|
||||
"2026-04-30T12:00:00+00:00",
|
||||
"2026-04-30T13:00:00+00:00",
|
||||
])
|
||||
monkeypatch.setattr(skill_usage, "_now_iso", lambda: next(timestamps))
|
||||
|
||||
skill_usage.bump_view("mine")
|
||||
skill_usage.bump_patch("mine")
|
||||
|
||||
row = next(r for r in skill_usage.agent_created_report() if r["name"] == "mine")
|
||||
assert row["activity_count"] == 2
|
||||
assert row["last_activity_at"] == "2026-04-30T12:00:00+00:00"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provenance guard — telemetry must not leak records for bundled/hub skills
|
||||
|
||||
Reference in New Issue
Block a user