diff --git a/skills/skill-comply/scripts/classifier.py b/skills/skill-comply/scripts/classifier.py index 76d7c87aa..74361c8bf 100644 --- a/skills/skill-comply/scripts/classifier.py +++ b/skills/skill-comply/scripts/classifier.py @@ -85,6 +85,8 @@ def _parse_classification(text: str) -> dict[str, list[int]]: raise ClassifierParseError( f"classifier returned non-list JSON value: {type(value).__name__}" ) + if any(isinstance(i, bool) or (isinstance(i, float) and not i.is_integer()) for i in value): + raise ValueError("tool call indices must be whole numbers, not booleans") return {k: [int(i) for i in v] for k, v in parsed.items()} except (json.JSONDecodeError, ValueError, TypeError, OverflowError) as e: raise ClassifierParseError( diff --git a/skills/skill-comply/tests/test_classifier.py b/skills/skill-comply/tests/test_classifier.py index a94f44fe5..a9e80a599 100644 --- a/skills/skill-comply/tests/test_classifier.py +++ b/skills/skill-comply/tests/test_classifier.py @@ -28,7 +28,12 @@ class TestParseClassification: _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]} + assert _parse_classification('{"a": [0, 2.0, "3"]}') == {"a": [0, 2, 3]} + + @pytest.mark.parametrize("index", [True, False, 2.9, -0.1]) + def test_boolean_and_fractional_indices_raise(self, index: object) -> None: + with pytest.raises(classifier.ClassifierParseError, match="failed to parse"): + _parse_classification(json.dumps({"a": [index]})) def test_empty_object_is_valid(self) -> None: assert _parse_classification("{}") == {} @@ -54,7 +59,7 @@ class TestParseClassification: assert issubclass(classifier.ClassifierParseError, RuntimeError) -@pytest.mark.parametrize("response", ["not JSON", "[]", '{"a": ["x"]}', '{"a": "x"}', '{"a": [0], "other": null}', '{"a": [1e309]}', '{"a": [-1e309]}']) +@pytest.mark.parametrize("response", ["not JSON", "[]", '{"a": ["x"]}', '{"a": "x"}', '{"a": [0], "other": null}', '{"a": [1e309]}', '{"a": [-1e309]}', '{"a": [false]}', '{"a": [0.5]}']) def test_grader_propagates_classifier_parse_failure(response: str) -> None: """An invalid successful subprocess response cannot become a zero score.""" spec = ComplianceSpec(