From 7f3eae4e3ad790ae862b9a3894d6907d24fcf5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ktu=C4=9F=20=C3=96zkan=2C=20MD?= Date: Sun, 26 Jul 2026 14:57:57 +0300 Subject: [PATCH 1/2] Reject scalar acceptance input rows --- src/evalopt_graph/kernel.py | 17 +++++++++++++---- tests/test_kernel.py | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/evalopt_graph/kernel.py b/src/evalopt_graph/kernel.py index cc18763..3e8ba7e 100644 --- a/src/evalopt_graph/kernel.py +++ b/src/evalopt_graph/kernel.py @@ -157,10 +157,19 @@ def __post_init__(self) -> None: raise ValueError("observed_at must be a string") def rows(name: str, values: Iterable[Iterable[str]], width: int) -> tuple[tuple[str, ...], ...]: - normalized = tuple(tuple(row) for row in values) + if isinstance(values, str | bytes): + raise ValueError(f"{name} entries require {width} string fields") + normalized: list[tuple[str, ...]] = [] + for row in values: + if isinstance(row, str | bytes): + raise ValueError(f"{name} entries require {width} string fields") + try: + normalized.append(tuple(row)) + except TypeError as exc: + raise ValueError(f"{name} entries require {width} string fields") from exc if any(len(row) != width or not all(isinstance(item, str) for item in row) for row in normalized): raise ValueError(f"{name} entries require {width} string fields") - return normalized + return tuple(normalized) object.__setattr__(self, "gate_results", rows("gate_results", self.gate_results, 2)) object.__setattr__(self, "contradictions", rows("contradictions", self.contradictions, 3)) @@ -217,10 +226,10 @@ def to_dict(self) -> dict[str, Any]: def from_dict(cls, value: Mapping[str, Any]) -> AcceptanceInput: return cls( observed_at=str(value.get("observed_at", "")), - gate_results=tuple(tuple(item) for item in value.get("gate_results", ())), + gate_results=value.get("gate_results", ()), criteria=tuple(value.get("criteria", ())), claims=tuple(ClaimRecord(**item) for item in value.get("claims", ())), - contradictions=tuple(tuple(item) for item in value.get("contradictions", ())), + contradictions=value.get("contradictions", ()), attestations=tuple(EvidenceAttestation.from_dict(item) for item in value.get("attestations", ())), assessments=tuple(SupportAssessment.from_dict(item) for item in value.get("assessments", ())), tests_weakened=value.get("tests_weakened", False), diff --git a/tests/test_kernel.py b/tests/test_kernel.py index 524ec1d..45f50af 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -245,6 +245,27 @@ def test_nested_input_rows_are_copied_before_evaluation(): assert before.status == "ACCEPTED" +def test_gate_result_rows_reject_bare_strings_instead_of_splitting_characters(): + with pytest.raises(ValueError, match="gate_results entries"): + kernel.AcceptanceInput(observed_at=NOW, gate_results=("ok",)) + + with pytest.raises(ValueError, match="gate_results entries"): + kernel.AcceptanceInput.from_dict( + { + "schema_version": "evalopt.acceptance-input.v1", + "observed_at": NOW, + "gate_results": ["ok"], + "criteria": [], + "claims": [], + "contradictions": [], + "attestations": [], + "assessments": [], + "tests_weakened": False, + "evaluator_score": None, + } + ) + + def test_contradiction_enums_are_normalized_and_unknown_values_rejected(): policy, input_ = _input(contradictions=(("x1", "HIGH", "RESOLVED"),)) From e5757016ebf18423451fb12ac7868a91842fa66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ktu=C4=9F=20=C3=96zkan=2C=20MD?= Date: Sun, 26 Jul 2026 18:02:27 +0300 Subject: [PATCH 2/2] Cover scalar row validation edge cases --- src/evalopt_graph/kernel.py | 6 +++++- tests/test_kernel.py | 34 +++++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/evalopt_graph/kernel.py b/src/evalopt_graph/kernel.py index 3e8ba7e..e4f1dd8 100644 --- a/src/evalopt_graph/kernel.py +++ b/src/evalopt_graph/kernel.py @@ -160,7 +160,11 @@ def rows(name: str, values: Iterable[Iterable[str]], width: int) -> tuple[tuple[ if isinstance(values, str | bytes): raise ValueError(f"{name} entries require {width} string fields") normalized: list[tuple[str, ...]] = [] - for row in values: + try: + iterator = iter(values) + except TypeError as exc: + raise ValueError(f"{name} entries require {width} string fields") from exc + for row in iterator: if isinstance(row, str | bytes): raise ValueError(f"{name} entries require {width} string fields") try: diff --git a/tests/test_kernel.py b/tests/test_kernel.py index 45f50af..7b91617 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -245,16 +245,25 @@ def test_nested_input_rows_are_copied_before_evaluation(): assert before.status == "ACCEPTED" -def test_gate_result_rows_reject_bare_strings_instead_of_splitting_characters(): - with pytest.raises(ValueError, match="gate_results entries"): - kernel.AcceptanceInput(observed_at=NOW, gate_results=("ok",)) +@pytest.mark.parametrize( + ("field", "bad_row"), + [ + ("gate_results", "ok"), + ("gate_results", b"ok"), + ("contradictions", "abc"), + ("contradictions", b"abc"), + ], +) +def test_acceptance_input_rows_reject_scalar_rows_instead_of_splitting_characters(field, bad_row): + with pytest.raises(ValueError, match=f"{field} entries"): + kernel.AcceptanceInput(observed_at=NOW, **{field: (bad_row,)}) - with pytest.raises(ValueError, match="gate_results entries"): + with pytest.raises(ValueError, match=f"{field} entries"): kernel.AcceptanceInput.from_dict( { "schema_version": "evalopt.acceptance-input.v1", "observed_at": NOW, - "gate_results": ["ok"], + "gate_results": [], "criteria": [], "claims": [], "contradictions": [], @@ -262,10 +271,25 @@ def test_gate_result_rows_reject_bare_strings_instead_of_splitting_characters(): "assessments": [], "tests_weakened": False, "evaluator_score": None, + field: [bad_row], } ) +@pytest.mark.parametrize( + ("field", "bad_value"), + [ + ("gate_results", None), + ("gate_results", 1), + ("contradictions", None), + ("contradictions", 1), + ], +) +def test_acceptance_input_rows_reject_non_iterable_row_containers(field, bad_value): + with pytest.raises(ValueError, match=f"{field} entries"): + kernel.AcceptanceInput(observed_at=NOW, **{field: bad_value}) + + def test_contradiction_enums_are_normalized_and_unknown_values_rejected(): policy, input_ = _input(contradictions=(("x1", "HIGH", "RESOLVED"),))