From 19b6cbac12934429defeff49f3a6a6b87a86ea09 Mon Sep 17 00:00:00 2001 From: Afonso Coutinho Date: Wed, 3 Jun 2026 00:37:05 +0100 Subject: [PATCH] fix: skills CLI summary crashes on a non-string description (#1595) --- scripts/odysseus-skills | 13 ++++++++++- tests/test_skills_cli_preview.py | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tests/test_skills_cli_preview.py diff --git a/scripts/odysseus-skills b/scripts/odysseus-skills index 20a440b..f0fcaf5 100755 --- a/scripts/odysseus-skills +++ b/scripts/odysseus-skills @@ -41,11 +41,22 @@ def _manager() -> SkillsManager: return _mgr +def _preview_text(value, limit: int = 200) -> str: + """Truncated preview of a text field, tolerant of non-string values. + + A skill whose ``description`` is a non-string (e.g. a number from a + hand-edited/legacy store) would crash ``(value or "")[:200]`` with a + TypeError; coerce non-strings to "" instead. + """ + text = value if isinstance(value, str) else "" + return text[:limit] + + def _summary(skill: dict) -> dict: return { "name": skill.get("name", ""), "category": skill.get("category", "general"), - "description": (skill.get("description") or "")[:200], + "description": _preview_text(skill.get("description")), "status": skill.get("status", ""), "uses": skill.get("uses", 0), "last_used": skill.get("last_used") or "", diff --git a/tests/test_skills_cli_preview.py b/tests/test_skills_cli_preview.py new file mode 100644 index 0000000..0bbdb43 --- /dev/null +++ b/tests/test_skills_cli_preview.py @@ -0,0 +1,40 @@ +"""Regression: the skills CLI summary must tolerate a non-string description. + +`_summary` did `(skill.get("description") or "")[:200]`. A non-string +description (e.g. a number from a hand-edited/legacy skill store) is truthy, so +`123[:200]` raised TypeError. `_preview_text` coerces non-strings to "". +""" +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): + mod = types.ModuleType("services.memory.skills") + mod.SkillsManager = MagicMock() + monkeypatch.setitem(sys.modules, "services.memory.skills", mod) + 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_preview_text_ignores_non_string(monkeypatch): + cli = _load_cli(monkeypatch) + assert cli._preview_text(None) == "" + assert cli._preview_text(123) == "" + assert cli._preview_text({"x": 1}) == "" + assert cli._preview_text("y" * 250) == "y" * 200 + + +def test_summary_does_not_crash_on_non_string_description(monkeypatch): + cli = _load_cli(monkeypatch) + out = cli._summary({"name": "n", "description": 123}) + assert out["description"] == ""