52b77bfcad
Forked from Carlo1911/skill-evolution main. Adds env-var override so a single installed pipeline can target per-profile skills trees (default ~/.hermes/skills, krystie ~/.hermes/profiles/krystie/skills, sami-story ~/.hermes/profiles/sami-story/skills) without monkeypatching HOME or the SKILLS_DIR module constant. - scripts/skill_index.py: add resolve_skills_dir() + SKILLS_DIR_ENV_VAR, wire into scan_skills() with explicit-arg > env-var > default resolution order. Read at call time (matches fetch_sessions.get_state_db_path() convention). - tests/test_skill_index.py: 2 new tests covering nonblank env var and blank fallback (the latter monkeypatches SKILLS_DIR + invokes scan_skills() with no args so the test actually exercises the fallback expression, not just the explicit arg path). - 49/49 conformance tests pass. - Codex grade: A (verified 2026-08-05). Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
97 lines
3.9 KiB
Python
97 lines
3.9 KiB
Python
"""Tests for scripts/skill_index.py's scan of the installed-skills tree."""
|
|
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
|
|
|
|
import skill_index
|
|
|
|
|
|
def _write_skill(root, category, name, description="does a thing"):
|
|
skill_dir = root / category / name
|
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
(skill_dir / "SKILL.md").write_text(
|
|
f"---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n\nBody.\n"
|
|
)
|
|
|
|
|
|
def test_scan_skills_indexes_live_categories(tmp_path):
|
|
_write_skill(tmp_path, "general-skills", "money-admin-messaging")
|
|
_write_skill(tmp_path, "devops", "deploy-helper")
|
|
|
|
found = skill_index.scan_skills(str(tmp_path))
|
|
|
|
assert {s["name"] for s in found} == {"money-admin-messaging", "deploy-helper"}
|
|
assert {s["category"] for s in found} == {"general-skills", "devops"}
|
|
|
|
|
|
def test_scan_skills_skips_dot_directories(tmp_path):
|
|
"""Dot-prefixed dirs under skills/ are internal, not live skill categories.
|
|
|
|
`~/.hermes/skills/` really holds `.archive/` (retired skills),
|
|
`.curator_backups/` (timestamped snapshots) and `.hub/` (lockfiles, audit log,
|
|
quarantine). Indexing them tells the analyzer a retired skill "currently exists",
|
|
and -- because the same name can live in both places -- makes
|
|
optimize_skill.py's duplicate-name check reject the live skill as ambiguous.
|
|
"""
|
|
_write_skill(tmp_path, "general-skills", "money-admin-messaging")
|
|
_write_skill(tmp_path, ".archive", "money-admin-messaging")
|
|
_write_skill(tmp_path, ".curator_backups", "old-thing")
|
|
|
|
found = skill_index.scan_skills(str(tmp_path))
|
|
|
|
assert [s["category"] for s in found] == ["general-skills"]
|
|
assert len(found) == 1, "a retired copy must not shadow or duplicate the live skill"
|
|
|
|
|
|
def test_scan_skills_dot_filter_keeps_names_unambiguous(tmp_path):
|
|
"""The live skill must resolve to exactly one record even with an archived twin."""
|
|
_write_skill(tmp_path, "general-skills", "money-admin-messaging")
|
|
_write_skill(tmp_path, ".archive", "money-admin-messaging")
|
|
|
|
matches = [s for s in skill_index.scan_skills(str(tmp_path))
|
|
if s["name"] == "money-admin-messaging"]
|
|
|
|
assert len(matches) == 1
|
|
assert matches[0]["category"] == "general-skills"
|
|
|
|
|
|
def test_scan_skills_returns_empty_for_missing_dir(tmp_path):
|
|
assert skill_index.scan_skills(str(tmp_path / "nope")) == []
|
|
|
|
|
|
def test_scan_skills_honors_skills_dir_env_var(tmp_path, monkeypatch):
|
|
"""SKILL_EVOLUTION_SKILLS_DIR redirects the scan at call time.
|
|
|
|
Without this, a single installed pipeline can only target one skills tree
|
|
(the hardcoded ~/.hermes/skills default). Per-Hermes-profile layouts
|
|
(~/.hermes/profiles/<name>/skills/) need to override it per run without
|
|
monkeypatching HOME or the module-level SKILLS_DIR constant.
|
|
"""
|
|
live = tmp_path / "live"
|
|
other = tmp_path / "other"
|
|
_write_skill(live, "general-skills", "live-skill")
|
|
_write_skill(other, "general-skills", "other-skill")
|
|
|
|
monkeypatch.setenv(skill_index.SKILLS_DIR_ENV_VAR, str(other))
|
|
found = skill_index.scan_skills()
|
|
|
|
assert {s["name"] for s in found} == {"other-skill"}
|
|
|
|
|
|
def test_scan_skills_env_var_blank_falls_back_to_default(tmp_path, monkeypatch):
|
|
"""A blank/whitespace env var disables the override (matches fetch_sessions'
|
|
get_state_db_path()/get_state_file() convention -- the same read-at-call-time
|
|
fallback behaviour for missing/blank env vars)."""
|
|
_write_skill(tmp_path, "general-skills", "fallback-skill")
|
|
|
|
# Monkeypatch the default to a path that is ONLY reachable via the env-var
|
|
# fallback -- if the blank env var were ever silently winning the override,
|
|
# the test would still pass; we want to confirm the default wins.
|
|
monkeypatch.setattr(skill_index, "SKILLS_DIR", str(tmp_path))
|
|
monkeypatch.setenv(skill_index.SKILLS_DIR_ENV_VAR, " ")
|
|
found = skill_index.scan_skills()
|
|
|
|
assert {s["name"] for s in found} == {"fallback-skill"}
|