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 (
|
|
|
|
|
_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
|