diff --git a/manifests/context-packs/skill-triggers@1.json b/manifests/context-packs/skill-triggers@1.json index f77ba09c6..e15d4e2a7 100644 --- a/manifests/context-packs/skill-triggers@1.json +++ b/manifests/context-packs/skill-triggers@1.json @@ -10,7 +10,7 @@ "id": "hand-seeded", "source": "manual-curation-pending-regeneration" }, - "registryDigest": "c86166ed37f558c3981a97c5f73bb08635b868e2e44e24dae8725f89ed9c3ff3", + "registryDigest": "74cd0bee853e82e6d13c8e997a729a7d530860aed8cb03187b324de2651eed55", "schemaVersion": 1, "triggers": { "skill:agent-runtime-gateway-smoke-test": [ diff --git a/skills/skill-comply/scripts/classifier.py b/skills/skill-comply/scripts/classifier.py index fdec3b7dc..3c3883e9d 100644 --- a/skills/skill-comply/scripts/classifier.py +++ b/skills/skill-comply/scripts/classifier.py @@ -80,11 +80,12 @@ def _parse_classification(text: str) -> dict[str, list[int]]: raise ClassifierParseError( f"classifier returned non-dict JSON: {type(parsed).__name__}" ) - return { - k: [int(i) for i in v] - for k, v in parsed.items() - if isinstance(v, list) - } + for value in parsed.values(): + if not isinstance(value, list): + raise ClassifierParseError( + f"classifier returned non-list JSON value: {type(value).__name__}" + ) + return {k: [int(i) for i in v] for k, v in parsed.items()} except (json.JSONDecodeError, ValueError, TypeError) as e: raise ClassifierParseError( f"failed to parse classification output: {e}" diff --git a/skills/skill-comply/tests/test_classifier.py b/skills/skill-comply/tests/test_classifier.py index 3144da903..c0ba32f4c 100644 --- a/skills/skill-comply/tests/test_classifier.py +++ b/skills/skill-comply/tests/test_classifier.py @@ -1,5 +1,6 @@ """Tests for scripts.classifier._parse_classification.""" +import json import subprocess from unittest.mock import patch @@ -17,8 +18,17 @@ class TestParseClassification: def test_fenced_json(self) -> None: assert _parse_classification('```json\n{"a": [1]}\n```') == {"a": [1]} - def test_dict_without_list_values_is_empty(self) -> None: - assert _parse_classification('{"a": "x"}') == {} + @pytest.mark.parametrize("value", ["x", None, True, 1, 1.5, {}]) + def test_non_list_values_raise(self, value: object) -> None: + with pytest.raises(classifier.ClassifierParseError, match="non-list"): + _parse_classification(json.dumps({"a": value})) + + def test_mixed_valid_and_non_list_values_raise(self) -> None: + with pytest.raises(classifier.ClassifierParseError, match="non-list"): + _parse_classification('{"a": [0], "other": "x"}') + + def test_existing_index_coercion_is_preserved(self) -> None: + assert _parse_classification('{"a": [true, 2.9, "3"]}') == {"a": [1, 2, 3]} def test_empty_object_is_valid(self) -> None: assert _parse_classification("{}") == {} @@ -39,7 +49,7 @@ class TestParseClassification: assert issubclass(classifier.ClassifierParseError, RuntimeError) -@pytest.mark.parametrize("response", ["not JSON", "[]", '{"a": ["x"]}']) +@pytest.mark.parametrize("response", ["not JSON", "[]", '{"a": ["x"]}', '{"a": "x"}', '{"a": [0], "other": null}']) def test_grader_propagates_classifier_parse_failure(response: str) -> None: """An invalid successful subprocess response cannot become a zero score.""" spec = ComplianceSpec(