"""Regression tests for issue #92993 — `curator pin` false success + invisible pin. Two stacked defects, each with its own failing assertion: 1. **False success (write-path).** `_cmd_pin` gates on `is_agent_created()` and then prints "pinned" unconditionally, but `set_pinned()` routes through `_mutate(require_curation_eligible=True)`, which silently no-ops when `is_curation_eligible()` is False. A skill can pass the first gate and fail the second; the user sees "pinned" while nothing was written. The CLI must detect the failed write and exit nonzero with a clear error. 2. **Invisible pin (visibility).** `curator status` reads pins from `curated_report()`, which only iterates `list_agent_created_skill_names()` — and that list requires the `created_by: agent` management marker. A skill that IS curation-eligible but carries no marker (pre-provenance record, foreground-created) never appears as a row, so its pin is silently absent from the status output even when successfully written. """ from __future__ import annotations import importlib import io from argparse import Namespace from contextlib import redirect_stdout from pathlib import Path import pytest # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_skill(skills_dir: Path, name: str): d = skills_dir / name d.mkdir(parents=True, exist_ok=True) (d / "SKILL.md").write_text( f"---\nname: {name}\ndescription: x\n---\n", encoding="utf-8", ) return d class _Args: def __init__(self, skill: str): self.skill = skill @pytest.fixture def pin_env(tmp_path, monkeypatch): """Isolated HERMES_HOME with freshly reloaded modules (same pattern as tests/agent/test_curator.py::curator_env but scoped for the CLI layer).""" home = tmp_path / ".hermes" skills = home / "skills" skills.mkdir(parents=True) monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.setenv("HERMES_HOME", str(home)) import hermes_constants importlib.reload(hermes_constants) from tools import skill_usage importlib.reload(skill_usage) # curator module reads config through its own loader; keep defaults. from agent import curator importlib.reload(curator) monkeypatch.setattr(curator, "_load_config", lambda: {}) monkeypatch.setattr(skill_usage, "_prune_builtins_enabled", lambda: False) from hermes_cli import curator as curator_cli importlib.reload(curator_cli) yield { "home": home, "skills": skills, "usage": skill_usage, "cli": curator_cli, } # --------------------------------------------------------------------------- # Test 1 — pin must fail loudly when the write does not land # --------------------------------------------------------------------------- def test_pin_fails_loudly_when_write_does_not_land(pin_env, capsys, monkeypatch): """A skill that passes `is_agent_created()` but fails `is_curation_eligible()` must produce a NONZERO exit and an explanatory error — not a success message over a silent no-write. Real trigger: PROTECTED_BUILTIN_SKILLS blocks by NAME. A user's own skill whose name collides with a protected entry is not in the bundled manifest, so ``is_agent_created()`` says True — but ``is_protected_builtin()`` makes it ineligible, and ``set_pinned()`` silently no-ops through ``_mutate(require_curation_eligible=True)``. The shipped set is currently empty (``plan`` graduated to a built-in command), so the collision is staged with a monkeypatched sentinel.""" env = pin_env name = "sentinel-protected-skill" # collides with the (patched) protected name monkeypatch.setattr(env["usage"], "PROTECTED_BUILTIN_SKILLS", {name}) _make_skill(env["skills"], name) # Sanity: the two gates genuinely disagree for this skill. assert env["usage"].is_agent_created(name) is True assert env["usage"].is_curation_eligible(name) is False cli = env["cli"] buf = io.StringIO() with redirect_stdout(buf): rc = cli._cmd_pin(_Args(name)) out = buf.getvalue() # The bug: rc == 0 with "pinned" printed although nothing was written. assert rc != 0, ( f"_cmd_pin reported success (rc=0, output={out!r}) for a skill whose " "pin write silently did not land — this is the false-success defect." ) # Record on disk must NOT claim a pin. rec = env["usage"].get_record(name) assert not rec.get("pinned"), "usage record claims pinned=true despite failed eligibility" # Error output must explain the refusal, not just fail quietly. assert "pin" in out.lower(), "refusal must mention the pin outcome" # --------------------------------------------------------------------------- # Test 2 — successful pin on eligible-but-unmanaged skill must be VISIBLE # --------------------------------------------------------------------------- def test_pinned_eligible_unmanaged_skill_visible_in_status(pin_env): """A curation-ELIGIBLE skill without the created_by marker (foreground- created / pre-provenance) can be pinned successfully — and once pinned, it MUST surface in `curator status` output. Before the fix it vanishes: curated_report() skips it because list_agent_created_skill_names() requires the management marker.""" env = pin_env usage = env["usage"] cli = env["cli"] _make_skill(env["skills"], "legacy-skill") # No mark_agent_created() call — deliberately unmanaged-but-eligible. buf = io.StringIO() with redirect_stdout(buf): rc = cli._cmd_pin(_Args("legacy-skill")) out = buf.getvalue() # The pin MUST succeed for an eligible skill — unconditional, so a # regression that flips pin to failure fails here instead of silently # skipping the visibility assertions below. assert rc == 0, f"eligible-skill pin must succeed (got rc={rc}): {out.strip()}" rec = usage.get_record("legacy-skill") assert rec.get("pinned") is True, "eligible skill pin wrote nothing despite rc=0" # ...and then status MUST show it. This is the visibility half. buf2 = io.StringIO() with redirect_stdout(buf2): status_rc = cli._cmd_status(Namespace()) assert status_rc == 0 status = buf2.getvalue() assert "legacy-skill" in status and "pinned" in status.lower(), ( "pin landed on disk but the skill is absent from `curator status` " "— the invisible-pin defect." ) # --------------------------------------------------------------------------- # Test 3 — regression guard: normal managed pin still works end to end # --------------------------------------------------------------------------- def test_pin_managed_skill_end_to_end(pin_env): """The happy path must stay green: a properly adopted (managed) skill pins, reports success, and shows in status. Guards against fixing the two defects by breaking legitimate pins.""" env = pin_env usage = env["usage"] cli = env["cli"] _make_skill(env["skills"], "managed-skill") usage.mark_agent_created("managed-skill") buf = io.StringIO() with redirect_stdout(buf): rc = cli._cmd_pin(_Args("managed-skill")) assert rc == 0, "managed skill pin must succeed" assert "pinned" in buf.getvalue().lower() assert usage.get_record("managed-skill").get("pinned") is True buf2 = io.StringIO() with redirect_stdout(buf2): assert cli._cmd_status(Namespace()) == 0 status = buf2.getvalue() assert "managed-skill" in status assert "pinned" in status.lower(), ( "managed+pinned skill missing from the pinned list in status output" )