From 83f602e6d1ab63fc01d20abc277e0e9ce91e19a8 Mon Sep 17 00:00:00 2001 From: red person Date: Wed, 3 Jun 2026 08:11:38 +0300 Subject: [PATCH] Skip invalid skills CLI rows (#1553) --- scripts/odysseus-skills | 14 +++++++++----- tests/test_skills_cli_rows.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 tests/test_skills_cli_rows.py diff --git a/scripts/odysseus-skills b/scripts/odysseus-skills index f0fcaf5..c2cee7f 100755 --- a/scripts/odysseus-skills +++ b/scripts/odysseus-skills @@ -52,6 +52,10 @@ def _preview_text(value, limit: int = 200) -> str: return text[:limit] +def _skill_entries(skills): + return [s for s in skills or [] if isinstance(s, dict)] + + def _summary(skill: dict) -> dict: return { "name": skill.get("name", ""), @@ -65,7 +69,7 @@ def _summary(skill: dict) -> dict: def cmd_list(args): - out = _manager().load_all() + out = _skill_entries(_manager().load_all()) if args.category: out = [s for s in out if (s.get("category") or "general") == args.category] out.sort(key=lambda s: (-int(s.get("uses") or 0), s.get("name", ""))) @@ -73,7 +77,7 @@ def cmd_list(args): def cmd_show(args): - for s in _manager().load_all(): + for s in _skill_entries(_manager().load_all()): if s.get("name") == args.name: emit(s, args) return @@ -82,7 +86,7 @@ def cmd_show(args): def cmd_categories(args): counts: dict[str, int] = {} - for s in _manager().load_all(): + for s in _skill_entries(_manager().load_all()): c = s.get("category") or "general" counts[c] = counts.get(c, 0) + 1 emit([{"category": c, "count": n} for c, n in sorted(counts.items())], args) @@ -91,7 +95,7 @@ def cmd_categories(args): def cmd_delete(args): # Locate the skill's directory and rm -rf it. skills_root = Path(_DATA_DIR) / "skills" - for s in _manager().load_all(): + for s in _skill_entries(_manager().load_all()): if s.get("name") != args.name: continue cat = s.get("category") or "general" @@ -105,7 +109,7 @@ def cmd_delete(args): def cmd_export(args): - for s in _manager().load_all(): + for s in _skill_entries(_manager().load_all()): if s.get("name") != args.name: continue cat = s.get("category") or "general" diff --git a/tests/test_skills_cli_rows.py b/tests/test_skills_cli_rows.py new file mode 100644 index 0000000..5438b46 --- /dev/null +++ b/tests/test_skills_cli_rows.py @@ -0,0 +1,31 @@ +import importlib.machinery +import importlib.util +import sys +import types +from pathlib import Path +from unittest.mock import MagicMock + + +ROOT = Path(__file__).resolve().parents[1] + + +def _load_cli(monkeypatch): + svc = types.ModuleType("services.memory.skills") + svc.SkillsManager = MagicMock() + monkeypatch.setitem(sys.modules, "services.memory.skills", svc) + path = ROOT / "scripts" / "odysseus-skills" + loader = importlib.machinery.SourceFileLoader("odysseus_skills_cli", str(path)) + spec = importlib.util.spec_from_loader(loader.name, loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +def test_skill_entries_skips_invalid_rows(monkeypatch): + cli = _load_cli(monkeypatch) + + assert cli._skill_entries([ + {"name": "deploy", "category": "ops"}, + "bad-row", + None, + ]) == [{"name": "deploy", "category": "ops"}]