fix(learning_graph): guard non-dict metadata so /journey can't crash
parse_frontmatter's malformed-YAML fallback stores every value as a string,
so a skill's `metadata` can be a str. `_category`/`_related` chained
`.get("metadata", {}).get("hermes", {})` and blew up with `'str' object has
no attribute 'get'`, taking down `build_learning_graph()` (and thus /journey
and `hermes journey`) whenever any installed skill had bad frontmatter.
Extract a `_hermes_meta()` helper that returns the nested dict only when it
really is one. Fixes the whole class, not just the two call sites.
This commit is contained in:
parent
76a468e513
commit
ec319e4e3e
2 changed files with 34 additions and 2 deletions
|
|
@ -48,8 +48,16 @@ def _frontmatter(text: str) -> dict[str, Any]:
|
|||
return {}
|
||||
|
||||
|
||||
def _hermes_meta(fm: dict[str, Any]) -> dict[str, Any]:
|
||||
"""``metadata.hermes`` as a dict, tolerant of the string-valued frontmatter
|
||||
that ``parse_frontmatter``'s malformed-YAML fallback produces."""
|
||||
meta = fm.get("metadata")
|
||||
hermes = meta.get("hermes") if isinstance(meta, dict) else None
|
||||
return hermes if isinstance(hermes, dict) else {}
|
||||
|
||||
|
||||
def _related(fm: dict[str, Any]) -> list[str]:
|
||||
raw = fm.get("related_skills") or (fm.get("metadata", {}).get("hermes", {}) or {}).get("related_skills")
|
||||
raw = fm.get("related_skills") or _hermes_meta(fm).get("related_skills")
|
||||
if isinstance(raw, list):
|
||||
return [str(r).strip() for r in raw if str(r).strip()]
|
||||
if isinstance(raw, str):
|
||||
|
|
@ -58,7 +66,7 @@ def _related(fm: dict[str, Any]) -> list[str]:
|
|||
|
||||
|
||||
def _category(fm: dict[str, Any], skill_md: Path) -> str:
|
||||
cat = fm.get("category") or (fm.get("metadata", {}).get("hermes", {}) or {}).get("category")
|
||||
cat = fm.get("category") or _hermes_meta(fm).get("category")
|
||||
if cat:
|
||||
return str(cat)
|
||||
# …/skills/<category>/<skill>/SKILL.md
|
||||
|
|
|
|||
|
|
@ -88,6 +88,30 @@ def test_memory_is_cards_split_on_separator(tmp_path):
|
|||
assert any(n["kind"] == "memory" for n in graph["nodes"])
|
||||
|
||||
|
||||
def test_malformed_frontmatter_metadata_does_not_crash(tmp_path):
|
||||
"""``parse_frontmatter``'s malformed-YAML fallback stores every value as a
|
||||
string, so ``metadata`` can be a str. The graph must tolerate that instead
|
||||
of crashing on chained ``.get()`` (the /journey base-CLI crash)."""
|
||||
skill_dir = tmp_path / "skills" / "misc" / "bad-skill"
|
||||
skill_dir.mkdir(parents=True)
|
||||
# The unterminated quote makes yaml_load raise → fallback → metadata is a str.
|
||||
skill_dir.joinpath("SKILL.md").write_text(
|
||||
'---\nname: bad-skill\nmetadata: not-a-dict\ndescription: "oops\n---\n# Bad\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
node = learning_graph.build_skill_nodes([("profile", tmp_path / "skills")])["bad-skill"]
|
||||
|
||||
assert node.category == "misc" # directory fallback, not a crash
|
||||
assert node.related == []
|
||||
|
||||
|
||||
def test_hermes_meta_tolerates_non_dict():
|
||||
assert learning_graph._hermes_meta({"metadata": "junk"}) == {}
|
||||
assert learning_graph._hermes_meta({"metadata": {"hermes": "junk"}}) == {}
|
||||
assert learning_graph._hermes_meta({"metadata": {"hermes": {"category": "x"}}}) == {"category": "x"}
|
||||
|
||||
|
||||
def test_full_payload_shape_and_edge_integrity(tmp_path):
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue