diff --git a/src/openjd/model/v2023_09/_model.py b/src/openjd/model/v2023_09/_model.py index 53fe0f12..1255017c 100644 --- a/src/openjd/model/v2023_09/_model.py +++ b/src/openjd/model/v2023_09/_model.py @@ -2893,7 +2893,7 @@ class JobTemplate(OpenJDModel_v2023_09): """ specificationVersion: Literal[TemplateSpecificationVersion.JOBTEMPLATE_v2023_09] # noqa: N815 - extensions: Optional[ExtensionNameList] = None + extensions: Optional[ExtensionNameList] = Field(default=None, validate_default=True) name: JobTemplateName steps: StepTemplateList description: Optional[Description] = None @@ -3116,7 +3116,7 @@ class EnvironmentTemplate(OpenJDModel_v2023_09): """ specificationVersion: Literal[TemplateSpecificationVersion.ENVIRONMENT_v2023_09] - extensions: Optional[ExtensionNameList] = None + extensions: Optional[ExtensionNameList] = Field(default=None, validate_default=True) parameterDefinitions: Optional[JobParameterDefinitionList] = None environment: Environment diff --git a/test/openjd/model/v2023_09/test_feature_bundle_1.py b/test/openjd/model/v2023_09/test_feature_bundle_1.py index c165bbe7..8e07a918 100644 --- a/test/openjd/model/v2023_09/test_feature_bundle_1.py +++ b/test/openjd/model/v2023_09/test_feature_bundle_1.py @@ -4,7 +4,7 @@ import pytest from pydantic import ValidationError -from openjd.model import create_job +from openjd.model import create_job, decode_job_template from openjd.model._parse import _parse_model from openjd.model.v2023_09 import ( Action, @@ -57,6 +57,59 @@ def test_extension_not_supported(self) -> None: assert "FEATURE_BUNDLE_1" in str(excinfo.value) +class TestExtensionFieldEnablement: + """Tests for extension enablement based on template's extensions field and supported_extensions.""" + + @pytest.mark.parametrize( + "template_declares_ext,supported_extensions,param_count,should_pass", + [ + # Template declares extension, supported includes it - passes with 51 params + (True, ["FEATURE_BUNDLE_1"], 51, True), + # Template declares extension, supported excludes it - fails (unsupported ext) + (True, ["TASK_CHUNKING"], 51, False), + # Template declares extension, supported is None - fails (unsupported ext) + (True, None, 51, False), + # Template omits extension, supported includes it - fails (50 param limit) + (False, ["FEATURE_BUNDLE_1"], 51, False), + # Template omits extension, all supported - fails (50 param limit) + (False, ["TASK_CHUNKING", "REDACTED_ENV_VARS", "FEATURE_BUNDLE_1"], 51, False), + # Template omits extension, none supported - fails (50 param limit) + (False, [], 51, False), + # Template omits extension, supported is None - fails (50 param limit) + (False, None, 51, False), + # Template within default limit - passes regardless + (False, ["FEATURE_BUNDLE_1"], 50, True), + # Template omits extension, supported is None, within limit - passes + (False, None, 50, True), + ], + ) + def test_extension_enablement( + self, + template_declares_ext: bool, + supported_extensions: list[str], + param_count: int, + should_pass: bool, + ) -> None: + """Test that extension features require explicit declaration in template.""" + params = [{"name": f"P{i}", "type": "INT", "default": i} for i in range(param_count)] + data: dict = { + "specificationVersion": "jobtemplate-2023-09", + "name": "Test", + "parameterDefinitions": params, + "steps": [{"name": "s", "script": STEP_SCRIPT}], + } + if template_declares_ext: + data["extensions"] = ["FEATURE_BUNDLE_1"] + + if should_pass: + result = decode_job_template(template=data, supported_extensions=supported_extensions) + assert result.parameterDefinitions is not None + assert len(result.parameterDefinitions) == param_count + else: + with pytest.raises(Exception): + decode_job_template(template=data, supported_extensions=supported_extensions) + + class TestActionTimeoutFormatString: """Tests for timeout format string support in Action."""