fix: reject lossy classifier tool-call indices

RED: 6 failures proved booleans and fractional indices could invent evidence. GREEN: 49 targeted tests; full skill-comply suites pass on Windows (88 passed, 5 platform skips) and Linux (93 passed). Parser coverage 100%; exact integer floats and numeric strings remain supported. Pi/catalog checks pass.
(cherry picked from commit 6dde588860)
This commit is contained in:
Eneko Ruiz authored and haelyra committed 2026-10-20 17:40:59 -04:00
1 parent 7b7ec11b2a
commit 99b6a9bb8f
2 files changed
+9 -2

No files matched your search

@@ -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(
+7 -2
View File
@@ -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(