From c0c197851d9692e5446fe6d81db364865bb47283 Mon Sep 17 00:00:00 2001 From: Samuel Hassine Date: Sat, 18 Jul 2026 23:23:07 +0200 Subject: [PATCH 1/2] fix(contracts): emit predefinedExpectations from availableExpectations (#309) --- pyoaev/contracts/contract_config.py | 18 ++++ test/contracts/__init__.py | 0 test/contracts/test_contract_expectations.py | 90 ++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 test/contracts/__init__.py create mode 100644 test/contracts/test_contract_expectations.py diff --git a/pyoaev/contracts/contract_config.py b/pyoaev/contracts/contract_config.py index d22a6d7..a0a2e98 100644 --- a/pyoaev/contracts/contract_config.py +++ b/pyoaev/contracts/contract_config.py @@ -285,7 +285,25 @@ def get_type(self) -> str: @dataclass class ContractExpectations(ContractCardinalityElement): cardinality: str = ContractCardinality.Multiple + # Full set the user may choose from in the "add expectation" picker. availableExpectations: List[Expectation] = field(default_factory=list) + # Subset pre-filled by default when the inject / atomic testing is created. + # The OpenAEV platform reads this field-level array (not the per-expectation + # flag) to pre-populate expectations, so it must be emitted in the contract. + predefinedExpectations: List[Expectation] = field(default_factory=list) + + def __post_init__(self): + super().__post_init__() + # When the caller did not pass an explicit predefined list, derive it from + # the expectations flagged predefined in the available set. This keeps the + # ergonomic single-list API (flag each Expectation with + # expectation_is_predefined=True) working end-to-end on the platform. + if not self.predefinedExpectations and self.availableExpectations: + self.predefinedExpectations = [ + expectation + for expectation in self.availableExpectations + if getattr(expectation, "expectation_is_predefined", False) + ] @property def get_type(self) -> str: diff --git a/test/contracts/__init__.py b/test/contracts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/contracts/test_contract_expectations.py b/test/contracts/test_contract_expectations.py new file mode 100644 index 0000000..3fc772a --- /dev/null +++ b/test/contracts/test_contract_expectations.py @@ -0,0 +1,90 @@ +import json +import unittest + +from pyoaev import utils +from pyoaev.contracts.contract_config import ( + ContractCardinality, + ContractExpectations, + Expectation, + ExpectationType, +) + + +def _expectation(expectation_type, name, is_predefined): + return Expectation( + expectation_type=expectation_type, + expectation_name=name, + expectation_description="", + expectation_score=100, + expectation_expectation_group=False, + expectation_is_predefined=is_predefined, + ) + + +def _serialize(field): + return json.loads(json.dumps(field, cls=utils.EnhancedJSONEncoder)) + + +class TestContractExpectations(unittest.TestCase): + def test_predefined_derived_from_flag(self): + """ + The platform pre-fills expectations from the field-level + ``predefinedExpectations`` array, so it must be populated from the + expectations flagged ``expectation_is_predefined=True``. + """ + field = ContractExpectations( + key="expectations", + label="Expectations", + cardinality=ContractCardinality.Multiple, + availableExpectations=[ + _expectation(ExpectationType.detection, "Detection", True), + _expectation(ExpectationType.prevention, "Prevention", True), + _expectation(ExpectationType.vulnerability, "Not vulnerable", False), + ], + ) + + data = _serialize(field) + + self.assertEqual("expectation", data["type"]) + self.assertEqual( + ["DETECTION", "PREVENTION", "VULNERABILITY"], + [e["expectation_type"] for e in data["availableExpectations"]], + ) + # Only the flagged ones are pre-filled. + self.assertEqual( + ["DETECTION", "PREVENTION"], + [e["expectation_type"] for e in data["predefinedExpectations"]], + ) + + def test_explicit_predefined_is_kept(self): + detection = _expectation(ExpectationType.detection, "Detection", False) + field = ContractExpectations( + key="expectations", + label="Expectations", + availableExpectations=[detection], + predefinedExpectations=[detection], + ) + + data = _serialize(field) + + self.assertEqual( + ["DETECTION"], + [e["expectation_type"] for e in data["predefinedExpectations"]], + ) + + def test_no_flag_yields_no_predefined(self): + field = ContractExpectations( + key="expectations", + label="Expectations", + availableExpectations=[ + _expectation(ExpectationType.vulnerability, "Not vulnerable", False), + ], + ) + + data = _serialize(field) + + self.assertEqual([], data["predefinedExpectations"]) + + +if __name__ == "__main__": + unittest.main() From 98a72220ca955f1033f81b9582faa012145702bf Mon Sep 17 00:00:00 2001 From: Samuel Hassine Date: Sun, 19 Jul 2026 05:46:18 +0200 Subject: [PATCH 2/2] fix(contracts): use None sentinel for predefinedExpectations override (#309) Derive predefinedExpectations from the expectation_is_predefined flags only when the caller did not provide the list, so an explicit empty list can suppress pre-fill. Also drop a needless getattr fallback, add a test for the explicit-empty override and normalize the test file to LF. --- pyoaev/contracts/contract_config.py | 11 +- test/contracts/test_contract_expectations.py | 199 ++++++++++--------- 2 files changed, 116 insertions(+), 94 deletions(-) diff --git a/pyoaev/contracts/contract_config.py b/pyoaev/contracts/contract_config.py index a0a2e98..b2ea50f 100644 --- a/pyoaev/contracts/contract_config.py +++ b/pyoaev/contracts/contract_config.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field from enum import Enum -from typing import Dict, List +from typing import Dict, List, Optional from pyoaev import utils from pyoaev.contracts.contract_utils import ContractCardinality, ContractVariable @@ -290,7 +290,10 @@ class ContractExpectations(ContractCardinalityElement): # Subset pre-filled by default when the inject / atomic testing is created. # The OpenAEV platform reads this field-level array (not the per-expectation # flag) to pre-populate expectations, so it must be emitted in the contract. - predefinedExpectations: List[Expectation] = field(default_factory=list) + # None means "not provided": the list is then derived from the expectations + # flagged expectation_is_predefined=True. Pass an explicit list (possibly + # empty) to override the derivation. + predefinedExpectations: Optional[List[Expectation]] = None def __post_init__(self): super().__post_init__() @@ -298,11 +301,11 @@ def __post_init__(self): # the expectations flagged predefined in the available set. This keeps the # ergonomic single-list API (flag each Expectation with # expectation_is_predefined=True) working end-to-end on the platform. - if not self.predefinedExpectations and self.availableExpectations: + if self.predefinedExpectations is None: self.predefinedExpectations = [ expectation for expectation in self.availableExpectations - if getattr(expectation, "expectation_is_predefined", False) + if expectation.expectation_is_predefined ] @property diff --git a/test/contracts/test_contract_expectations.py b/test/contracts/test_contract_expectations.py index 3fc772a..f054517 100644 --- a/test/contracts/test_contract_expectations.py +++ b/test/contracts/test_contract_expectations.py @@ -1,90 +1,109 @@ -import json -import unittest - -from pyoaev import utils -from pyoaev.contracts.contract_config import ( - ContractCardinality, - ContractExpectations, - Expectation, - ExpectationType, -) - - -def _expectation(expectation_type, name, is_predefined): - return Expectation( - expectation_type=expectation_type, - expectation_name=name, - expectation_description="", - expectation_score=100, - expectation_expectation_group=False, - expectation_is_predefined=is_predefined, - ) - - -def _serialize(field): - return json.loads(json.dumps(field, cls=utils.EnhancedJSONEncoder)) - - -class TestContractExpectations(unittest.TestCase): - def test_predefined_derived_from_flag(self): - """ - The platform pre-fills expectations from the field-level - ``predefinedExpectations`` array, so it must be populated from the - expectations flagged ``expectation_is_predefined=True``. - """ - field = ContractExpectations( - key="expectations", - label="Expectations", - cardinality=ContractCardinality.Multiple, - availableExpectations=[ - _expectation(ExpectationType.detection, "Detection", True), - _expectation(ExpectationType.prevention, "Prevention", True), - _expectation(ExpectationType.vulnerability, "Not vulnerable", False), - ], - ) - - data = _serialize(field) - - self.assertEqual("expectation", data["type"]) - self.assertEqual( - ["DETECTION", "PREVENTION", "VULNERABILITY"], - [e["expectation_type"] for e in data["availableExpectations"]], - ) - # Only the flagged ones are pre-filled. - self.assertEqual( - ["DETECTION", "PREVENTION"], - [e["expectation_type"] for e in data["predefinedExpectations"]], - ) - - def test_explicit_predefined_is_kept(self): - detection = _expectation(ExpectationType.detection, "Detection", False) - field = ContractExpectations( - key="expectations", - label="Expectations", - availableExpectations=[detection], - predefinedExpectations=[detection], - ) - - data = _serialize(field) - - self.assertEqual( - ["DETECTION"], - [e["expectation_type"] for e in data["predefinedExpectations"]], - ) - - def test_no_flag_yields_no_predefined(self): - field = ContractExpectations( - key="expectations", - label="Expectations", - availableExpectations=[ - _expectation(ExpectationType.vulnerability, "Not vulnerable", False), - ], - ) - - data = _serialize(field) - - self.assertEqual([], data["predefinedExpectations"]) - - -if __name__ == "__main__": - unittest.main() +import json +import unittest + +from pyoaev import utils +from pyoaev.contracts.contract_config import ( + ContractCardinality, + ContractExpectations, + Expectation, + ExpectationType, +) + + +def _expectation(expectation_type, name, is_predefined): + return Expectation( + expectation_type=expectation_type, + expectation_name=name, + expectation_description="", + expectation_score=100, + expectation_expectation_group=False, + expectation_is_predefined=is_predefined, + ) + + +def _serialize(field): + return json.loads(json.dumps(field, cls=utils.EnhancedJSONEncoder)) + + +class TestContractExpectations(unittest.TestCase): + def test_predefined_derived_from_flag(self): + """ + The platform pre-fills expectations from the field-level + ``predefinedExpectations`` array, so it must be populated from the + expectations flagged ``expectation_is_predefined=True``. + """ + field = ContractExpectations( + key="expectations", + label="Expectations", + cardinality=ContractCardinality.Multiple, + availableExpectations=[ + _expectation(ExpectationType.detection, "Detection", True), + _expectation(ExpectationType.prevention, "Prevention", True), + _expectation(ExpectationType.vulnerability, "Not vulnerable", False), + ], + ) + + data = _serialize(field) + + self.assertEqual("expectation", data["type"]) + self.assertEqual( + ["DETECTION", "PREVENTION", "VULNERABILITY"], + [e["expectation_type"] for e in data["availableExpectations"]], + ) + # Only the flagged ones are pre-filled. + self.assertEqual( + ["DETECTION", "PREVENTION"], + [e["expectation_type"] for e in data["predefinedExpectations"]], + ) + + def test_explicit_predefined_is_kept(self): + detection = _expectation(ExpectationType.detection, "Detection", False) + field = ContractExpectations( + key="expectations", + label="Expectations", + availableExpectations=[detection], + predefinedExpectations=[detection], + ) + + data = _serialize(field) + + self.assertEqual( + ["DETECTION"], + [e["expectation_type"] for e in data["predefinedExpectations"]], + ) + + def test_explicit_empty_list_overrides_flags(self): + """ + An explicit empty ``predefinedExpectations`` list must suppress the + derivation even when some available expectations carry the predefined + flag, while still serializing as an (empty) array. + """ + field = ContractExpectations( + key="expectations", + label="Expectations", + availableExpectations=[ + _expectation(ExpectationType.detection, "Detection", True), + ], + predefinedExpectations=[], + ) + + data = _serialize(field) + + self.assertEqual([], data["predefinedExpectations"]) + + def test_no_flag_yields_no_predefined(self): + field = ContractExpectations( + key="expectations", + label="Expectations", + availableExpectations=[ + _expectation(ExpectationType.vulnerability, "Not vulnerable", False), + ], + ) + + data = _serialize(field) + + self.assertEqual([], data["predefinedExpectations"]) + + +if __name__ == "__main__": + unittest.main()