personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

prompts: hard-error on malformed frontmatter

Replaces the silent debug-log swallow of frontmatter parse errors in
_load_prompt_metadata and _load_raw_templates with a named
PromptMetadataError(ValueError). The new exception carries the offending
file path and chains the underlying parse error via `from exc`.

Surfaced by req_mpnix3zn: two cogitate talents had Python True in JSON
frontmatter and silently dropped from get_talent_configs(type='cogitate'),
masking the registry regression for several rounds.

Out of scope: _load_identity_vars, secondary frontmatter.load calls in
think/talent.py and think/talent_cli.py, and load_prompt's substitution
try/except — all per the lode scope doc.

Refs req_mpnix3zn, req_tttxra67.

+101 -3
+90
tests/test_prompt_metadata.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Tests for prompt metadata parsing failures.""" 5 + 6 + import json 7 + 8 + import pytest 9 + 10 + import think.prompts as prompts 11 + import think.talent as talent 12 + from think.prompts import PromptMetadataError, _load_prompt_metadata 13 + 14 + 15 + def test_load_prompt_metadata_returns_expected_fields(tmp_path): 16 + md_path = tmp_path / "valid.md" 17 + md_path.write_text( 18 + '{\n "title": "Valid Prompt",\n "schedule": "daily"\n}\n\nBody text\n', 19 + encoding="utf-8", 20 + ) 21 + 22 + info = _load_prompt_metadata(md_path) 23 + 24 + assert info["path"] == str(md_path) 25 + assert info["mtime"] == int(md_path.stat().st_mtime) 26 + assert info["title"] == "Valid Prompt" 27 + assert info["schedule"] == "daily" 28 + assert info["color"] == "#6c757d" 29 + 30 + 31 + def test_load_prompt_metadata_raises_prompt_metadata_error_for_bad_json(tmp_path): 32 + md_path = tmp_path / "invalid.md" 33 + md_path.write_text( 34 + '{\n "title": "Invalid Prompt",\n "disabled": True\n}\n\nBody text\n', 35 + encoding="utf-8", 36 + ) 37 + 38 + with pytest.raises(PromptMetadataError) as excinfo: 39 + _load_prompt_metadata(md_path) 40 + 41 + exc = excinfo.value 42 + assert exc.path == md_path 43 + assert str(md_path) in str(exc) 44 + assert isinstance(exc.__cause__, json.JSONDecodeError) 45 + 46 + 47 + def test_load_raw_templates_raises_prompt_metadata_error_for_bad_template( 48 + tmp_path, monkeypatch 49 + ): 50 + templates_dir = tmp_path / "templates" 51 + templates_dir.mkdir() 52 + bad_template = templates_dir / "broken.md" 53 + bad_template.write_text( 54 + '{\n "title": "Broken Template",\n "disabled": True\n}\n\nBody text\n', 55 + encoding="utf-8", 56 + ) 57 + 58 + monkeypatch.setattr(prompts, "TEMPLATES_DIR", templates_dir) 59 + monkeypatch.setattr(prompts, "_templates_cache", None) 60 + 61 + with pytest.raises(PromptMetadataError) as excinfo: 62 + prompts._load_raw_templates() 63 + 64 + exc = excinfo.value 65 + assert exc.path == bad_template 66 + assert str(bad_template) in str(exc) 67 + assert isinstance(exc.__cause__, json.JSONDecodeError) 68 + 69 + 70 + def test_get_talent_configs_propagates_prompt_metadata_error(tmp_path, monkeypatch): 71 + talent_dir = tmp_path / "talent" 72 + talent_dir.mkdir() 73 + apps_dir = tmp_path / "apps" 74 + apps_dir.mkdir() 75 + broken_prompt = talent_dir / "broken.md" 76 + broken_prompt.write_text( 77 + '{\n "title": "Broken Prompt",\n "disabled": True\n}\n\nBody text\n', 78 + encoding="utf-8", 79 + ) 80 + 81 + monkeypatch.setattr(talent, "TALENT_DIR", talent_dir) 82 + monkeypatch.setattr(talent, "APPS_DIR", apps_dir) 83 + 84 + with pytest.raises(PromptMetadataError) as excinfo: 85 + talent.get_talent_configs(include_disabled=True) 86 + 87 + exc = excinfo.value 88 + assert exc.path == broken_prompt 89 + assert str(broken_prompt) in str(exc) 90 + assert isinstance(exc.__cause__, json.JSONDecodeError)
+11 -3
think/prompts.py
··· 66 66 ) 67 67 _templates_cache[var_name] = post.content.strip() 68 68 except Exception as exc: 69 - logging.debug("Failed to load template %s: %s", md_path, exc) 69 + raise PromptMetadataError(md_path) from exc 70 70 71 71 return _templates_cache 72 72 ··· 214 214 def __init__(self, path: Path) -> None: 215 215 self.path = path 216 216 super().__init__(f"Prompt file not found: {path}") 217 + 218 + 219 + class PromptMetadataError(ValueError): 220 + """Raised when prompt frontmatter cannot be parsed.""" 221 + 222 + def __init__(self, path: Path) -> None: 223 + self.path = path 224 + super().__init__(f"Failed to parse frontmatter from {path}") 217 225 218 226 219 227 def _flatten_identity_to_template_vars(identity: dict[str, Any]) -> dict[str, str]: ··· 391 399 ) 392 400 if post.metadata: 393 401 info.update(post.metadata) 394 - except Exception as exc: # pragma: no cover - metadata optional 395 - logging.debug("Error reading frontmatter from %s: %s", md_path, exc) 402 + except Exception as exc: 403 + raise PromptMetadataError(md_path) from exc 396 404 397 405 # Apply default color if not specified 398 406 if "color" not in info: