Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions hermes_cli/kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -6560,10 +6560,16 @@ def _kanban_worker_skill_available(hermes_home: Optional[str]) -> bool:
omitting the flag only drops the supplementary pattern library.
"""
from pathlib import Path as _Path
from hermes_constants import get_default_hermes_root

# An unset HERMES_HOME means the worker falls back to the default root
# home (``~/.hermes``), which ships the bundled skill.
base = _Path(hermes_home) if hermes_home else (_Path.home() / ".hermes")
# An unset HERMES_HOME means the worker falls back to the platform-native
# default root home (``~/.hermes`` on POSIX, ``%LOCALAPPDATA%\hermes`` on
# native Windows), which ships the bundled skill. Resolve it through the
# same helper the rest of this module uses (see ``kanban_home``) rather than
# hardcoding ``Path.home() / ".hermes"`` — that POSIX-only path is an empty
# husk on native Windows, so the probe would scan the wrong directory and
# silently drop ``--skills kanban-worker``.
base = _Path(hermes_home) if hermes_home else get_default_hermes_root()
skills_root = base / "skills"
if not skills_root.is_dir():
return False
Expand Down
77 changes: 77 additions & 0 deletions tests/hermes_cli/test_kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import pytest

import hermes_constants
from hermes_cli import kanban_db as kb


Expand Down Expand Up @@ -4287,3 +4288,79 @@ def test_bare_connect_does_not_close_on_context_exit(tmp_path):
# Still usable after with-block exit (the leak).
conn.execute("SELECT 1").fetchone()
conn.close() # explicit close to avoid leaking THIS test


# ---------------------------------------------------------------------------
# Worker skill probe — platform-aware default home resolution
# ---------------------------------------------------------------------------


def _make_kanban_worker_skill(home: Path) -> None:
"""Create the bundled kanban-worker SKILL.md under ``<home>/skills/devops/``."""
skill = home / "skills" / "devops" / "kanban-worker" / "SKILL.md"
skill.parent.mkdir(parents=True, exist_ok=True)
skill.write_text("---\nname: kanban-worker\n---\n", encoding="utf-8")


def test_worker_skill_probe_resolves_platform_default_on_windows(tmp_path, monkeypatch):
"""With HERMES_HOME unset on native Windows, the probe resolves the bundled
skill under %LOCALAPPDATA%\\hermes — not the POSIX ~/.hermes husk.

Regression: the None fallback previously hardcoded ``Path.home()/".hermes"``,
which on native Windows points at an empty husk, so the probe returned False
and the dispatcher silently dropped ``--skills kanban-worker``.
"""
local_appdata = tmp_path / "LocalAppData"
user_home = tmp_path / "Home"
user_home.mkdir()
# Skill ships under the platform-native root only.
_make_kanban_worker_skill(local_appdata / "hermes")

monkeypatch.delenv("HERMES_HOME", raising=False)
monkeypatch.setenv("LOCALAPPDATA", str(local_appdata))
monkeypatch.setattr(Path, "home", lambda: user_home)
monkeypatch.setattr(hermes_constants.sys, "platform", "win32")

assert kb._kanban_worker_skill_available(None) is True


def test_worker_skill_probe_ignores_posix_husk_on_windows(tmp_path, monkeypatch):
"""On native Windows the probe must NOT consult ~/.hermes — a leftover skill
in that husk is invisible to a worker that resolves %LOCALAPPDATA%\\hermes."""
local_appdata = tmp_path / "LocalAppData"
# Platform-native root exists with an (empty) skills dir: the probe scans
# here and must find nothing rather than reaching into the husk.
(local_appdata / "hermes" / "skills").mkdir(parents=True)
user_home = tmp_path / "Home"
# Skill present ONLY in the POSIX husk the worker never reads on Windows.
_make_kanban_worker_skill(user_home / ".hermes")

monkeypatch.delenv("HERMES_HOME", raising=False)
monkeypatch.setenv("LOCALAPPDATA", str(local_appdata))
monkeypatch.setattr(Path, "home", lambda: user_home)
monkeypatch.setattr(hermes_constants.sys, "platform", "win32")

assert kb._kanban_worker_skill_available(None) is False


def test_worker_skill_probe_resolves_posix_default_when_home_unset(tmp_path, monkeypatch):
"""On POSIX, the unset-HERMES_HOME fallback still resolves ~/.hermes."""
user_home = tmp_path / "Home"
_make_kanban_worker_skill(user_home / ".hermes")

monkeypatch.delenv("HERMES_HOME", raising=False)
monkeypatch.setattr(Path, "home", lambda: user_home)
monkeypatch.setattr(hermes_constants.sys, "platform", "linux")

assert kb._kanban_worker_skill_available(None) is True


def test_worker_skill_probe_honors_explicit_home(tmp_path, monkeypatch):
"""An explicit hermes_home is scanned directly, independent of platform."""
explicit = tmp_path / "profiles" / "coder"
_make_kanban_worker_skill(explicit)
monkeypatch.setattr(hermes_constants.sys, "platform", "win32")

assert kb._kanban_worker_skill_available(str(explicit)) is True
# A home without the skill resolves False.
assert kb._kanban_worker_skill_available(str(tmp_path / "empty")) is False
Loading