diff --git a/src/teacher_escalation.py b/src/teacher_escalation.py index 3de0335..56b8ce6 100644 --- a/src/teacher_escalation.py +++ b/src/teacher_escalation.py @@ -327,7 +327,7 @@ def _extract_skill_json(teacher_response: str) -> Optional[Dict[str, Any]]: treated as "teacher declined to write a skill", per the prompt contract. """ - if not teacher_response: + if not isinstance(teacher_response, str) or not teacher_response: return None import json m = re.search(r"```(?:json)?\s*\n(\{[\s\S]*?\})\s*\n```", teacher_response) diff --git a/tests/test_extract_skill_json_nonstring.py b/tests/test_extract_skill_json_nonstring.py new file mode 100644 index 0000000..4a6dc53 --- /dev/null +++ b/tests/test_extract_skill_json_nonstring.py @@ -0,0 +1,19 @@ +"""Regression: _extract_skill_json must tolerate a non-string response. + +The `if not teacher_response` guard only handled falsy values; a truthy +non-string (e.g. a number or list from an unexpected LLM client) reached +`re.search(..., teacher_response)` and raised TypeError. Non-strings now +return None (treated as "no skill"), matching the documented contract. +""" +from src.teacher_escalation import _extract_skill_json + + +def test_non_string_returns_none(): + assert _extract_skill_json(123) is None + assert _extract_skill_json(["x"]) is None + assert _extract_skill_json(None) is None + + +def test_valid_json_block_parsed(): + resp = "sure:\n```json\n{\"name\": \"x\"}\n```\n" + assert _extract_skill_json(resp) == {"name": "x"}