fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
"""Tests for tools/skills_sync.py — manifest-based skill seeding and updating."""
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
|
|
from tools.skills_sync import (
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
_get_bundled_dir,
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
_read_manifest,
|
|
|
|
|
_write_manifest,
|
|
|
|
|
_discover_bundled_skills,
|
|
|
|
|
_compute_relative_dest,
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
_dir_hash,
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
sync_skills,
|
|
|
|
|
MANIFEST_FILE,
|
|
|
|
|
SKILLS_DIR,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestReadWriteManifest:
|
|
|
|
|
def test_read_missing_manifest(self, tmp_path):
|
2026-03-06 16:13:47 -08:00
|
|
|
with patch(
|
|
|
|
|
"tools.skills_sync.MANIFEST_FILE",
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
tmp_path / "nonexistent",
|
|
|
|
|
):
|
|
|
|
|
result = _read_manifest()
|
2026-03-06 16:13:47 -08:00
|
|
|
assert result == {}
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
def test_write_and_read_roundtrip_v2(self, tmp_path):
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
manifest_file = tmp_path / ".bundled_manifest"
|
2026-03-06 16:13:47 -08:00
|
|
|
entries = {"skill-a": "abc123", "skill-b": "def456", "skill-c": "789012"}
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
|
|
|
|
|
with patch("tools.skills_sync.MANIFEST_FILE", manifest_file):
|
2026-03-06 16:13:47 -08:00
|
|
|
_write_manifest(entries)
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
result = _read_manifest()
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
assert result == entries
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
|
|
|
|
|
def test_write_manifest_sorted(self, tmp_path):
|
|
|
|
|
manifest_file = tmp_path / ".bundled_manifest"
|
2026-03-06 16:13:47 -08:00
|
|
|
entries = {"zebra": "hash1", "alpha": "hash2", "middle": "hash3"}
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
|
|
|
|
|
with patch("tools.skills_sync.MANIFEST_FILE", manifest_file):
|
2026-03-06 16:13:47 -08:00
|
|
|
_write_manifest(entries)
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
|
|
|
|
|
lines = manifest_file.read_text().strip().splitlines()
|
2026-03-06 16:13:47 -08:00
|
|
|
names = [line.split(":")[0] for line in lines]
|
|
|
|
|
assert names == ["alpha", "middle", "zebra"]
|
|
|
|
|
|
|
|
|
|
def test_read_v1_manifest_migration(self, tmp_path):
|
|
|
|
|
"""v1 format (plain names, no hashes) should be read with empty hashes."""
|
|
|
|
|
manifest_file = tmp_path / ".bundled_manifest"
|
|
|
|
|
manifest_file.write_text("skill-a\nskill-b\n")
|
|
|
|
|
|
|
|
|
|
with patch("tools.skills_sync.MANIFEST_FILE", manifest_file):
|
|
|
|
|
result = _read_manifest()
|
|
|
|
|
|
|
|
|
|
assert result == {"skill-a": "", "skill-b": ""}
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
|
|
|
|
|
def test_read_manifest_ignores_blank_lines(self, tmp_path):
|
|
|
|
|
manifest_file = tmp_path / ".bundled_manifest"
|
2026-03-06 16:13:47 -08:00
|
|
|
manifest_file.write_text("skill-a:hash1\n\n \nskill-b:hash2\n")
|
|
|
|
|
|
|
|
|
|
with patch("tools.skills_sync.MANIFEST_FILE", manifest_file):
|
|
|
|
|
result = _read_manifest()
|
|
|
|
|
|
|
|
|
|
assert result == {"skill-a": "hash1", "skill-b": "hash2"}
|
|
|
|
|
|
|
|
|
|
def test_read_manifest_mixed_v1_v2(self, tmp_path):
|
|
|
|
|
"""Manifest with both v1 and v2 lines (shouldn't happen but handle gracefully)."""
|
|
|
|
|
manifest_file = tmp_path / ".bundled_manifest"
|
|
|
|
|
manifest_file.write_text("old-skill\nnew-skill:abc123\n")
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
|
|
|
|
|
with patch("tools.skills_sync.MANIFEST_FILE", manifest_file):
|
|
|
|
|
result = _read_manifest()
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
assert result == {"old-skill": "", "new-skill": "abc123"}
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
|
|
|
|
|
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
class TestDirHash:
|
|
|
|
|
def test_same_content_same_hash(self, tmp_path):
|
|
|
|
|
dir_a = tmp_path / "a"
|
|
|
|
|
dir_b = tmp_path / "b"
|
|
|
|
|
for d in (dir_a, dir_b):
|
|
|
|
|
d.mkdir()
|
|
|
|
|
(d / "SKILL.md").write_text("# Test")
|
|
|
|
|
(d / "main.py").write_text("print(1)")
|
|
|
|
|
assert _dir_hash(dir_a) == _dir_hash(dir_b)
|
|
|
|
|
|
|
|
|
|
def test_different_content_different_hash(self, tmp_path):
|
|
|
|
|
dir_a = tmp_path / "a"
|
|
|
|
|
dir_b = tmp_path / "b"
|
|
|
|
|
dir_a.mkdir()
|
|
|
|
|
dir_b.mkdir()
|
|
|
|
|
(dir_a / "SKILL.md").write_text("# Version 1")
|
|
|
|
|
(dir_b / "SKILL.md").write_text("# Version 2")
|
|
|
|
|
assert _dir_hash(dir_a) != _dir_hash(dir_b)
|
|
|
|
|
|
|
|
|
|
def test_empty_dir(self, tmp_path):
|
|
|
|
|
d = tmp_path / "empty"
|
|
|
|
|
d.mkdir()
|
|
|
|
|
h = _dir_hash(d)
|
|
|
|
|
assert isinstance(h, str) and len(h) == 32
|
|
|
|
|
|
|
|
|
|
def test_nonexistent_dir(self, tmp_path):
|
|
|
|
|
h = _dir_hash(tmp_path / "nope")
|
|
|
|
|
assert isinstance(h, str) # returns hash of empty content
|
|
|
|
|
|
|
|
|
|
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
class TestDiscoverBundledSkills:
|
|
|
|
|
def test_finds_skills_with_skill_md(self, tmp_path):
|
|
|
|
|
(tmp_path / "category" / "skill-a").mkdir(parents=True)
|
|
|
|
|
(tmp_path / "category" / "skill-a" / "SKILL.md").write_text("# Skill A")
|
|
|
|
|
(tmp_path / "skill-b").mkdir()
|
|
|
|
|
(tmp_path / "skill-b" / "SKILL.md").write_text("# Skill B")
|
|
|
|
|
(tmp_path / "not-a-skill").mkdir()
|
|
|
|
|
(tmp_path / "not-a-skill" / "README.md").write_text("Not a skill")
|
|
|
|
|
|
|
|
|
|
skills = _discover_bundled_skills(tmp_path)
|
|
|
|
|
skill_names = {name for name, _ in skills}
|
|
|
|
|
assert "skill-a" in skill_names
|
|
|
|
|
assert "skill-b" in skill_names
|
|
|
|
|
assert "not-a-skill" not in skill_names
|
|
|
|
|
|
|
|
|
|
def test_ignores_git_directories(self, tmp_path):
|
|
|
|
|
(tmp_path / ".git" / "hooks").mkdir(parents=True)
|
|
|
|
|
(tmp_path / ".git" / "hooks" / "SKILL.md").write_text("# Fake")
|
|
|
|
|
skills = _discover_bundled_skills(tmp_path)
|
|
|
|
|
assert len(skills) == 0
|
|
|
|
|
|
|
|
|
|
def test_nonexistent_dir_returns_empty(self, tmp_path):
|
|
|
|
|
skills = _discover_bundled_skills(tmp_path / "nonexistent")
|
|
|
|
|
assert skills == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestComputeRelativeDest:
|
|
|
|
|
def test_preserves_category_structure(self):
|
|
|
|
|
bundled = Path("/repo/skills")
|
|
|
|
|
skill_dir = Path("/repo/skills/mlops/axolotl")
|
|
|
|
|
dest = _compute_relative_dest(skill_dir, bundled)
|
|
|
|
|
assert str(dest).endswith("mlops/axolotl")
|
|
|
|
|
|
|
|
|
|
def test_flat_skill(self):
|
|
|
|
|
bundled = Path("/repo/skills")
|
|
|
|
|
skill_dir = Path("/repo/skills/simple")
|
|
|
|
|
dest = _compute_relative_dest(skill_dir, bundled)
|
|
|
|
|
assert dest.name == "simple"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSyncSkills:
|
|
|
|
|
def _setup_bundled(self, tmp_path):
|
|
|
|
|
"""Create a fake bundled skills directory."""
|
|
|
|
|
bundled = tmp_path / "bundled_skills"
|
|
|
|
|
(bundled / "category" / "new-skill").mkdir(parents=True)
|
|
|
|
|
(bundled / "category" / "new-skill" / "SKILL.md").write_text("# New")
|
|
|
|
|
(bundled / "category" / "new-skill" / "main.py").write_text("print(1)")
|
|
|
|
|
(bundled / "category" / "DESCRIPTION.md").write_text("Category desc")
|
|
|
|
|
(bundled / "old-skill").mkdir()
|
|
|
|
|
(bundled / "old-skill" / "SKILL.md").write_text("# Old")
|
|
|
|
|
return bundled
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
def _patches(self, bundled, skills_dir, manifest_file):
|
|
|
|
|
"""Return context manager stack for patching sync globals."""
|
|
|
|
|
from contextlib import ExitStack
|
|
|
|
|
stack = ExitStack()
|
|
|
|
|
stack.enter_context(patch("tools.skills_sync._get_bundled_dir", return_value=bundled))
|
|
|
|
|
stack.enter_context(patch("tools.skills_sync.SKILLS_DIR", skills_dir))
|
|
|
|
|
stack.enter_context(patch("tools.skills_sync.MANIFEST_FILE", manifest_file))
|
|
|
|
|
return stack
|
|
|
|
|
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
def test_fresh_install_copies_all(self, tmp_path):
|
|
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
result = sync_skills(quiet=True)
|
|
|
|
|
|
|
|
|
|
assert len(result["copied"]) == 2
|
|
|
|
|
assert result["total_bundled"] == 2
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
assert result["updated"] == []
|
2026-03-06 16:13:47 -08:00
|
|
|
assert result["user_modified"] == []
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
assert result["cleaned"] == []
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
assert (skills_dir / "category" / "new-skill" / "SKILL.md").exists()
|
|
|
|
|
assert (skills_dir / "old-skill" / "SKILL.md").exists()
|
|
|
|
|
assert (skills_dir / "category" / "DESCRIPTION.md").exists()
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
def test_fresh_install_records_origin_hashes(self, tmp_path):
|
|
|
|
|
"""After fresh install, manifest should have v2 format with hashes."""
|
|
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
|
|
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
|
|
|
|
sync_skills(quiet=True)
|
|
|
|
|
manifest = _read_manifest()
|
|
|
|
|
|
|
|
|
|
assert "new-skill" in manifest
|
|
|
|
|
assert "old-skill" in manifest
|
|
|
|
|
# Hashes should be non-empty MD5 strings
|
|
|
|
|
assert len(manifest["new-skill"]) == 32
|
|
|
|
|
assert len(manifest["old-skill"]) == 32
|
|
|
|
|
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
def test_user_deleted_skill_not_re_added(self, tmp_path):
|
|
|
|
|
"""Skill in manifest but not on disk = user deleted it. Don't re-add."""
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
skills_dir.mkdir(parents=True)
|
2026-03-06 16:13:47 -08:00
|
|
|
# old-skill is in manifest (v2 format) but NOT on disk
|
|
|
|
|
old_hash = _dir_hash(bundled / "old-skill")
|
|
|
|
|
manifest_file.write_text(f"old-skill:{old_hash}\n")
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
result = sync_skills(quiet=True)
|
|
|
|
|
|
|
|
|
|
assert "new-skill" in result["copied"]
|
|
|
|
|
assert "old-skill" not in result["copied"]
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
assert "old-skill" not in result.get("updated", [])
|
|
|
|
|
assert not (skills_dir / "old-skill").exists()
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
def test_unmodified_skill_gets_updated(self, tmp_path):
|
|
|
|
|
"""Skill in manifest + on disk + user hasn't modified = update from bundled."""
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
# Simulate: user has old version that was synced from an older bundled
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
user_skill = skills_dir / "old-skill"
|
|
|
|
|
user_skill.mkdir(parents=True)
|
2026-03-06 16:13:47 -08:00
|
|
|
(user_skill / "SKILL.md").write_text("# Old v1")
|
|
|
|
|
old_origin_hash = _dir_hash(user_skill)
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
# Record origin hash = hash of what was synced (the old version)
|
|
|
|
|
manifest_file.write_text(f"old-skill:{old_origin_hash}\n")
|
|
|
|
|
|
|
|
|
|
# Now bundled has a newer version ("# Old" != "# Old v1")
|
|
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
result = sync_skills(quiet=True)
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
# Should be updated because user copy matches origin (unmodified)
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
assert "old-skill" in result["updated"]
|
|
|
|
|
assert (user_skill / "SKILL.md").read_text() == "# Old"
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
def test_user_modified_skill_not_overwritten(self, tmp_path):
|
|
|
|
|
"""Skill modified by user should NOT be overwritten even if bundled changed."""
|
|
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
|
|
|
|
|
# Simulate: user had the old version synced, then modified it
|
|
|
|
|
user_skill = skills_dir / "old-skill"
|
|
|
|
|
user_skill.mkdir(parents=True)
|
|
|
|
|
(user_skill / "SKILL.md").write_text("# Old v1")
|
|
|
|
|
old_origin_hash = _dir_hash(user_skill)
|
|
|
|
|
|
|
|
|
|
# Record origin hash from what was originally synced
|
|
|
|
|
manifest_file.write_text(f"old-skill:{old_origin_hash}\n")
|
|
|
|
|
|
|
|
|
|
# User modifies their copy
|
|
|
|
|
(user_skill / "SKILL.md").write_text("# My custom version")
|
|
|
|
|
|
|
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
|
|
|
|
result = sync_skills(quiet=True)
|
|
|
|
|
|
|
|
|
|
# Should NOT update — user modified it
|
|
|
|
|
assert "old-skill" in result["user_modified"]
|
|
|
|
|
assert "old-skill" not in result.get("updated", [])
|
|
|
|
|
assert (user_skill / "SKILL.md").read_text() == "# My custom version"
|
|
|
|
|
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
def test_unchanged_skill_not_updated(self, tmp_path):
|
2026-03-06 16:13:47 -08:00
|
|
|
"""Skill in sync (user == bundled == origin) = no action needed."""
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
# Copy bundled to user dir (simulating perfect sync state)
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
user_skill = skills_dir / "old-skill"
|
|
|
|
|
user_skill.mkdir(parents=True)
|
|
|
|
|
(user_skill / "SKILL.md").write_text("# Old")
|
2026-03-06 16:13:47 -08:00
|
|
|
origin_hash = _dir_hash(user_skill)
|
|
|
|
|
manifest_file.write_text(f"old-skill:{origin_hash}\n")
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
result = sync_skills(quiet=True)
|
|
|
|
|
|
|
|
|
|
assert "old-skill" not in result.get("updated", [])
|
2026-03-06 16:13:47 -08:00
|
|
|
assert "old-skill" not in result.get("user_modified", [])
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
assert result["skipped"] >= 1
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
def test_v1_manifest_migration_sets_baseline(self, tmp_path):
|
|
|
|
|
"""v1 manifest entries (no hash) should set baseline from user's current copy."""
|
|
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
|
|
|
|
|
# Pre-create skill on disk
|
|
|
|
|
user_skill = skills_dir / "old-skill"
|
|
|
|
|
user_skill.mkdir(parents=True)
|
|
|
|
|
(user_skill / "SKILL.md").write_text("# Old modified by user")
|
|
|
|
|
|
|
|
|
|
# v1 manifest (no hashes)
|
|
|
|
|
manifest_file.write_text("old-skill\n")
|
|
|
|
|
|
|
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
|
|
|
|
result = sync_skills(quiet=True)
|
|
|
|
|
# Should skip (migration baseline set), NOT update
|
|
|
|
|
assert "old-skill" not in result.get("updated", [])
|
|
|
|
|
assert "old-skill" not in result.get("user_modified", [])
|
|
|
|
|
|
|
|
|
|
# Now check manifest was upgraded to v2 with user's hash as baseline
|
|
|
|
|
manifest = _read_manifest()
|
|
|
|
|
assert len(manifest["old-skill"]) == 32 # MD5 hash
|
|
|
|
|
|
|
|
|
|
def test_v1_migration_then_bundled_update_detected(self, tmp_path):
|
|
|
|
|
"""After v1 migration, a subsequent sync should detect bundled updates."""
|
|
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
|
|
|
|
|
# User has the SAME content as bundled (in sync)
|
|
|
|
|
user_skill = skills_dir / "old-skill"
|
|
|
|
|
user_skill.mkdir(parents=True)
|
|
|
|
|
(user_skill / "SKILL.md").write_text("# Old")
|
|
|
|
|
|
|
|
|
|
# v1 manifest
|
|
|
|
|
manifest_file.write_text("old-skill\n")
|
|
|
|
|
|
|
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
|
|
|
|
# First sync: migration — sets baseline
|
|
|
|
|
sync_skills(quiet=True)
|
|
|
|
|
|
|
|
|
|
# Now change bundled content
|
|
|
|
|
(bundled / "old-skill" / "SKILL.md").write_text("# Old v2 — improved")
|
|
|
|
|
|
|
|
|
|
# Second sync: should detect bundled changed + user unmodified → update
|
|
|
|
|
result = sync_skills(quiet=True)
|
|
|
|
|
|
|
|
|
|
assert "old-skill" in result["updated"]
|
|
|
|
|
assert (user_skill / "SKILL.md").read_text() == "# Old v2 — improved"
|
|
|
|
|
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
def test_stale_manifest_entries_cleaned(self, tmp_path):
|
|
|
|
|
"""Skills in manifest that no longer exist in bundled dir get cleaned."""
|
|
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
skills_dir.mkdir(parents=True)
|
2026-03-06 16:13:47 -08:00
|
|
|
manifest_file.write_text("old-skill:abc123\nremoved-skill:def456\n")
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
fix: restore all removed bundled skills + fix skills sync system
- Restored 21 skills removed in commits 757d012 and 740dd92:
accelerate, audiocraft, code-review, faiss, flash-attention, gguf,
grpo-rl-training, guidance, llava, nemo-curator, obliteratus, peft,
pytorch-fsdp, pytorch-lightning, simpo, slime, stable-diffusion,
tensorrt-llm, torchtitan, trl-fine-tuning, whisper
- Rewrote sync_skills() with proper update semantics:
* New skills (not in manifest): copied to user dir
* Existing skills (in manifest + on disk): updated via hash comparison
* User-deleted skills (in manifest, not on disk): respected, not re-added
* Stale manifest entries (removed from bundled): cleaned from manifest
- Added sync_skills() to CLI startup (cmd_chat) and gateway startup
(start_gateway) — previously only ran during 'hermes update'
- Updated cmd_update output to show new/updated/cleaned counts
- Rewrote tests: 20 tests covering manifest CRUD, dir hashing, fresh
install, user deletion respect, update detection, stale cleanup, and
name collision handling
75 bundled skills total. 2002 tests pass.
2026-03-06 15:57:12 -08:00
|
|
|
result = sync_skills(quiet=True)
|
|
|
|
|
|
|
|
|
|
assert "removed-skill" in result["cleaned"]
|
|
|
|
|
with patch("tools.skills_sync.MANIFEST_FILE", manifest_file):
|
|
|
|
|
manifest = _read_manifest()
|
|
|
|
|
assert "removed-skill" not in manifest
|
|
|
|
|
|
|
|
|
|
def test_does_not_overwrite_existing_unmanifested_skill(self, tmp_path):
|
|
|
|
|
"""New skill whose name collides with user-created skill = skipped."""
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
|
|
|
|
|
user_skill = skills_dir / "category" / "new-skill"
|
|
|
|
|
user_skill.mkdir(parents=True)
|
|
|
|
|
(user_skill / "SKILL.md").write_text("# User modified")
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
test: add unit tests for 8 untested modules (batch 3) (#191)
* test: add unit tests for 8 untested modules (batch 3)
New test files (143 tests total):
- tools/debug_helpers.py: DebugSession enable/disable, log, save, session info
- tools/skills_guard.py: scan_file, scan_skill, trust levels, install policy, structural checks
- tools/skills_sync.py: manifest read/write, skill discovery, sync logic
- gateway/sticker_cache.py: cache CRUD, sticker injection text builders
- gateway/channel_directory.py: channel resolution, display formatting, session building
- gateway/hooks.py: hook discovery, sync/async emit, wildcard matching
- gateway/mirror.py: session lookup, JSONL append, mirror_to_session
- honcho_integration/client.py: config from env/file, session name resolution, linked workspaces
Also documents a gap in skills_guard: multi-word prompt injection
variants like "ignore all prior instructions" bypass the regex scanner.
* test: strengthen sticker injection tests with exact format assertions
Replace loose "contains" checks with exact output matching for
build_sticker_injection and build_animated_sticker_injection.
Add edge cases: set_name without emoji, empty description, empty emoji.
* test: remove skills_guard gap-documenting test to avoid conflict with fix PR
2026-03-01 16:28:12 +03:00
|
|
|
result = sync_skills(quiet=True)
|
|
|
|
|
|
|
|
|
|
assert (user_skill / "SKILL.md").read_text() == "# User modified"
|
|
|
|
|
|
|
|
|
|
def test_nonexistent_bundled_dir(self, tmp_path):
|
|
|
|
|
with patch("tools.skills_sync._get_bundled_dir", return_value=tmp_path / "nope"):
|
|
|
|
|
result = sync_skills(quiet=True)
|
2026-03-06 16:13:47 -08:00
|
|
|
assert result == {
|
|
|
|
|
"copied": [], "updated": [], "skipped": 0,
|
|
|
|
|
"user_modified": [], "cleaned": [], "total_bundled": 0,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 03:58:32 +03:00
|
|
|
def test_failed_copy_does_not_poison_manifest(self, tmp_path):
|
|
|
|
|
"""If copytree fails, the skill must NOT be added to the manifest.
|
|
|
|
|
|
|
|
|
|
Otherwise the next sync treats it as 'user deleted' and never retries.
|
|
|
|
|
"""
|
|
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
|
|
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
|
|
|
|
# Patch copytree to fail for new-skill
|
|
|
|
|
original_copytree = __import__("shutil").copytree
|
|
|
|
|
|
|
|
|
|
def failing_copytree(src, dst, *a, **kw):
|
|
|
|
|
if "new-skill" in str(src):
|
|
|
|
|
raise OSError("Simulated disk full")
|
|
|
|
|
return original_copytree(src, dst, *a, **kw)
|
|
|
|
|
|
|
|
|
|
with patch("shutil.copytree", side_effect=failing_copytree):
|
|
|
|
|
result = sync_skills(quiet=True)
|
|
|
|
|
|
|
|
|
|
# new-skill should NOT be in copied (it failed)
|
|
|
|
|
assert "new-skill" not in result["copied"]
|
|
|
|
|
|
|
|
|
|
# Critical: new-skill must NOT be in the manifest
|
|
|
|
|
manifest = _read_manifest()
|
|
|
|
|
assert "new-skill" not in manifest, (
|
|
|
|
|
"Failed copy was recorded in manifest — next sync will "
|
|
|
|
|
"treat it as 'user deleted' and never retry"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Now run sync again (copytree works this time) — it should retry
|
|
|
|
|
result2 = sync_skills(quiet=True)
|
|
|
|
|
assert "new-skill" in result2["copied"]
|
|
|
|
|
assert (skills_dir / "category" / "new-skill" / "SKILL.md").exists()
|
|
|
|
|
|
|
|
|
|
def test_failed_update_does_not_destroy_user_copy(self, tmp_path):
|
|
|
|
|
"""If copytree fails during update, the user's existing copy must survive."""
|
|
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
|
|
|
|
|
# Start with old synced version
|
|
|
|
|
user_skill = skills_dir / "old-skill"
|
|
|
|
|
user_skill.mkdir(parents=True)
|
|
|
|
|
(user_skill / "SKILL.md").write_text("# Old v1")
|
|
|
|
|
old_hash = _dir_hash(user_skill)
|
|
|
|
|
manifest_file.write_text(f"old-skill:{old_hash}\n")
|
|
|
|
|
|
|
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
|
|
|
|
# Patch copytree to fail (rmtree succeeds, copytree fails)
|
|
|
|
|
original_copytree = __import__("shutil").copytree
|
|
|
|
|
|
|
|
|
|
def failing_copytree(src, dst, *a, **kw):
|
|
|
|
|
if "old-skill" in str(src):
|
|
|
|
|
raise OSError("Simulated write failure")
|
|
|
|
|
return original_copytree(src, dst, *a, **kw)
|
|
|
|
|
|
|
|
|
|
with patch("shutil.copytree", side_effect=failing_copytree):
|
|
|
|
|
result = sync_skills(quiet=True)
|
|
|
|
|
|
|
|
|
|
# old-skill should NOT be in updated (it failed)
|
|
|
|
|
assert "old-skill" not in result.get("updated", [])
|
|
|
|
|
|
|
|
|
|
# The skill directory should still exist (rmtree destroyed it
|
|
|
|
|
# but copytree failed to replace it — this is data loss)
|
|
|
|
|
assert user_skill.exists(), (
|
|
|
|
|
"Update failure destroyed user's skill copy without replacing it"
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-06 16:13:47 -08:00
|
|
|
def test_update_records_new_origin_hash(self, tmp_path):
|
|
|
|
|
"""After updating a skill, the manifest should record the new bundled hash."""
|
|
|
|
|
bundled = self._setup_bundled(tmp_path)
|
|
|
|
|
skills_dir = tmp_path / "user_skills"
|
|
|
|
|
manifest_file = skills_dir / ".bundled_manifest"
|
|
|
|
|
|
|
|
|
|
# Start with old synced version
|
|
|
|
|
user_skill = skills_dir / "old-skill"
|
|
|
|
|
user_skill.mkdir(parents=True)
|
|
|
|
|
(user_skill / "SKILL.md").write_text("# Old v1")
|
|
|
|
|
old_hash = _dir_hash(user_skill)
|
|
|
|
|
manifest_file.write_text(f"old-skill:{old_hash}\n")
|
|
|
|
|
|
|
|
|
|
with self._patches(bundled, skills_dir, manifest_file):
|
|
|
|
|
sync_skills(quiet=True) # updates to "# Old"
|
|
|
|
|
manifest = _read_manifest()
|
|
|
|
|
|
|
|
|
|
# New origin hash should match the bundled version
|
|
|
|
|
new_bundled_hash = _dir_hash(bundled / "old-skill")
|
|
|
|
|
assert manifest["old-skill"] == new_bundled_hash
|
|
|
|
|
assert manifest["old-skill"] != old_hash
|
feat: nix flake — uv2nix build, NixOS module, persistent container mode (#20)
* feat: nix flake, uv2nix build, dev shell and home manager
* fixed nix run, updated docs for setup
* feat(nix): NixOS module with persistent container mode, managed guards, checks
- Replace homeModules.nix with nixosModules.nix (two deployment modes)
- Mode A (native): hardened systemd service with ProtectSystem=strict
- Mode B (container): persistent Ubuntu container with /nix/store bind-mount,
identity-hash-based recreation, GC root protection, symlink-based updates
- Add HERMES_MANAGED guards blocking CLI config mutation (config set, setup,
gateway install/uninstall) when running under NixOS module
- Add nix/checks.nix with build-time verification (binary, CLI, managed guard)
- Remove container.nix (no Nix-built OCI image; pulls ubuntu:24.04 at runtime)
- Simplify packages.nix (drop fetchFromGitHub submodules, PYTHONPATH wrappers)
- Rewrite docs/nixos-setup.md with full options reference, container
architecture, secrets management, and troubleshooting guide
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update config.py
* feat(nix): add CI workflow and enhanced build checks
- GitHub Actions workflow for nix flake check + build on linux/macOS
- Entry point sync check to catch pyproject.toml drift
- Expanded managed-guard check to cover config edit
- Wrap hermes-acp binary in Nix package
- Fix Path type mismatch in is_managed()
* Update MCP server package name; bundled skills support
* fix reading .env. instead have container user a common mounted .env file
* feat(nix): container entrypoint with privilege drop and sudo provisioning
Container was running as non-root via --user, which broke apt/pip installs
and caused crashes when $HOME didn't exist. Replace --user with a Nix-built
entrypoint script that provisions the hermes user, sudo (NOPASSWD), and
/home/hermes inside the container on first boot, then drops privileges via
setpriv. Writable layer persists so setup only runs once.
Also expands MCP server options to support HTTP transport and sampling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix group and user creation in container mode
* feat(nix): persistent /home/hermes and MESSAGING_CWD in container mode
Container mode now bind-mounts ${stateDir}/home to /home/hermes so the
agent's home directory survives container recreation. Previously it lived
in the writable layer and was lost on image/volume/options changes.
Also passes MESSAGING_CWD to the container so the agent finds its
workspace and documents, matching native mode behavior.
Other changes:
- Extract containerDataDir/containerHomeDir bindings (no more magic strings)
- Fix entrypoint chown to run unconditionally (volume mounts always exist)
- Add schema field to container identity hash for auto-recreation
- Add idempotency test (Scenario G) to config-roundtrip check
* docs: add Nix & NixOS setup guide to docs site
Add comprehensive Nix documentation to the Docusaurus site at
website/docs/getting-started/nix-setup.md, covering nix run/profile
install, NixOS module (native + container modes), declarative settings,
secrets management, MCP servers, managed mode, container architecture,
dev shell, flake checks, and full options reference.
- Register nix-setup in sidebar after installation page
- Add Nix callout tip to installation.md linking to new guide
- Add canonical version pointer in docs/nixos-setup.md
* docs: remove docs/nixos-setup.md, consolidate into website docs
Backfill missing details (restart/restartSec in full example,
gateway.pid, 0750 permissions, docker inspect commands) into
the canonical website/docs/getting-started/nix-setup.md and
delete the old standalone file.
* fix(nix): add compression.protect_last_n and target_ratio to config-keys.json
New keys were added to DEFAULT_CONFIG on main, causing the
config-drift check to fail in CI.
* fix(nix): skip checks on aarch64-darwin (onnxruntime wheel missing)
The full Python venv includes onnxruntime (via faster-whisper/STT)
which lacks a compatible uv2nix wheel on aarch64-darwin. Gate all
checks behind stdenv.hostPlatform.isLinux. The package and devShell
still evaluate on macOS.
* fix(nix): skip flake check and build on macOS CI
onnxruntime (transitive dep via faster-whisper) lacks a compatible
uv2nix wheel on aarch64-darwin. Run full checks and build on Linux
only; macOS CI verifies the flake evaluates without building.
* fix(nix): preserve container writable layer across nixos-rebuild
The container identity hash included the entrypoint's Nix store path,
which changes on every nixpkgs update (due to runtimeShell/stdenv
input-addressing). This caused false-positive identity mismatches,
triggering container recreation and losing the persistent writable layer.
- Use stable symlink (current-entrypoint) like current-package already does
- Remove entrypoint from identity hash (only image/volumes/options matter)
- Add GC root for entrypoint so nix-collect-garbage doesn't break it
- Remove global HERMES_HOME env var from addToSystemPackages (conflicted
with interactive CLI use, service already sets its own)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:08:02 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestGetBundledDir:
|
|
|
|
|
def test_env_var_override(self, tmp_path, monkeypatch):
|
|
|
|
|
"""HERMES_BUNDLED_SKILLS env var overrides the default path resolution."""
|
|
|
|
|
custom_dir = tmp_path / "custom_skills"
|
|
|
|
|
custom_dir.mkdir()
|
|
|
|
|
monkeypatch.setenv("HERMES_BUNDLED_SKILLS", str(custom_dir))
|
|
|
|
|
assert _get_bundled_dir() == custom_dir
|
|
|
|
|
|
|
|
|
|
def test_default_without_env_var(self, monkeypatch):
|
|
|
|
|
"""Without the env var, falls back to relative path from __file__."""
|
|
|
|
|
monkeypatch.delenv("HERMES_BUNDLED_SKILLS", raising=False)
|
|
|
|
|
result = _get_bundled_dir()
|
|
|
|
|
assert result.name == "skills"
|
|
|
|
|
|
|
|
|
|
def test_env_var_empty_string_ignored(self, monkeypatch):
|
|
|
|
|
"""Empty HERMES_BUNDLED_SKILLS should fall back to default."""
|
|
|
|
|
monkeypatch.setenv("HERMES_BUNDLED_SKILLS", "")
|
|
|
|
|
result = _get_bundled_dir()
|
|
|
|
|
assert result.name == "skills"
|