diff --git a/CHANGELOG.md b/CHANGELOG.md index c21db01c0..29ea7842c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- **Action Argument Default Type Validation** (#9105): `ActionArgumentSchema` now + validates that default values match their declared types at parse time via a + `@model_validator`. Supports all argument types (string, integer, float, boolean, + list) with correct `bool`/`int` disambiguation and `float` widening from `int`. + Invalid configurations that previously passed silently now raise clear, actionable + `ValueError` messages indicating the argument name, declared type, and actual type. + - **UKO Runtime Layer 2 (Paradigm) Indexing** (#9351): Added missing `rdf:type uko-oo:Class` triple emission in `PythonAnalyzer._extract_class()` so that Python class definitions are now correctly classified at layer 2 (paradigm/OO) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 637dd5b31..dc99edcff 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -20,3 +20,4 @@ Below are some of the specific details of various contributions. * This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc. * HAL 9000 has contributed automated bug fixes, CLI output formatting improvements, and ongoing maintenance as part of the CleverAgents automation system. * HAL 9000 has contributed the file edit encoding parameter fix (PR #8258 / issue #7559). +* HAL 9000 has contributed the action argument default value type validation fix (PR #9178 / issue #9105): added `@model_validator` to `ActionArgumentSchema` that validates default values match their declared types, with correct `bool`/`int` disambiguation and `float` widening from `int`. diff --git a/features/action_schema_default_type_validation.feature b/features/action_schema_default_type_validation.feature new file mode 100644 index 000000000..9c71945a4 --- /dev/null +++ b/features/action_schema_default_type_validation.feature @@ -0,0 +1,163 @@ +@a2a @domain @action +Feature: Action Schema Default Value Type Validation + Validates that ActionArgumentSchema enforces type matching between + the declared 'type' field and the 'default' value. + + Background: + Given an action YAML string with only required fields + + # ============================================================ + # String Type Tests + # ============================================================ + + Scenario: String argument with string default passes validation + Given an action YAML string with an argument of type "string" and default "hello" + When I validate the action schema + Then the action schema validation should succeed + + Scenario: String argument with integer default fails validation + Given an action YAML string with an argument of type "string" and default 42 + When I validate the action schema expecting failure + Then the action schema validation should fail + And the action schema error should mention "has type 'string' but default value" + + Scenario: String argument with boolean default fails validation + Given an action YAML string with an argument of type "string" and default true + When I validate the action schema expecting failure + Then the action schema validation should fail + And the action schema error should mention "has type 'string' but default value" + + # ============================================================ + # Integer Type Tests + # ============================================================ + + Scenario: Integer argument with integer default passes validation + Given an action YAML string with an argument of type "integer" and default 42 + When I validate the action schema + Then the action schema validation should succeed + + Scenario: Integer argument with string default fails validation + Given an action YAML string with an argument of type "integer" and default "42" + When I validate the action schema expecting failure + Then the action schema validation should fail + And the action schema error should mention "has type 'integer' but default value" + + Scenario: Integer argument with boolean default fails validation + Given an action YAML string with an argument of type "integer" and default true + When I validate the action schema expecting failure + Then the action schema validation should fail + And the action schema error should mention "has type 'integer' but default value" + And the action schema error should mention "is bool, not int" + + Scenario: Integer argument with float default fails validation + Given an action YAML string with an argument of type "integer" and default 3.14 + When I validate the action schema expecting failure + Then the action schema validation should fail + And the action schema error should mention "has type 'integer' but default value" + + # ============================================================ + # Float Type Tests + # ============================================================ + + Scenario: Float argument with float default passes validation + Given an action YAML string with an argument of type "float" and default 3.14 + When I validate the action schema + Then the action schema validation should succeed + + Scenario: Float argument with integer default passes validation + Given an action YAML string with an argument of type "float" and default 42 + When I validate the action schema + Then the action schema validation should succeed + + Scenario: Float argument with string default fails validation + Given an action YAML string with an argument of type "float" and default "3.14" + When I validate the action schema expecting failure + Then the action schema validation should fail + And the action schema error should mention "has type 'float' but default value" + + Scenario: Float argument with boolean default fails validation + Given an action YAML string with an argument of type "float" and default true + When I validate the action schema expecting failure + Then the action schema validation should fail + And the action schema error should mention "has type 'float' but default value" + + # ============================================================ + # Boolean Type Tests + # ============================================================ + + Scenario: Boolean argument with boolean true default passes validation + Given an action YAML string with an argument of type "boolean" and default true + When I validate the action schema + Then the action schema validation should succeed + + Scenario: Boolean argument with boolean false default passes validation + Given an action YAML string with an argument of type "boolean" and default false + When I validate the action schema + Then the action schema validation should succeed + + Scenario: Boolean argument with integer default fails validation + Given an action YAML string with an argument of type "boolean" and default 1 + When I validate the action schema expecting failure + Then the action schema validation should fail + And the action schema error should mention "has type 'boolean' but default value" + + Scenario: Boolean argument with string default fails validation + Given an action YAML string with an argument of type "boolean" and default "true" + When I validate the action schema expecting failure + Then the action schema validation should fail + And the action schema error should mention "has type 'boolean' but default value" + + # ============================================================ + # List Type Tests + # ============================================================ + + Scenario: List argument with list default passes validation + Given an action YAML string with an argument of type "list" and default ["item1", "item2"] + When I validate the action schema + Then the action schema validation should succeed + + Scenario: List argument with empty list default passes validation + Given an action YAML string with an argument of type "list" and default [] + When I validate the action schema + Then the action schema validation should succeed + + Scenario: List argument with string default fails validation + Given an action YAML string with an argument of type "list" and default "item1" + When I validate the action schema expecting failure + Then the action schema validation should fail + And the action schema error should mention "has type 'list' but default value" + + Scenario: List argument with integer default fails validation + Given an action YAML string with an argument of type "list" and default 42 + When I validate the action schema expecting failure + Then the action schema validation should fail + And the action schema error should mention "has type 'list' but default value" + + # ============================================================ + # None Default Tests (Always Valid) + # ============================================================ + + Scenario: String argument with None default passes validation + Given an action YAML string with an argument of type "string" and no default + When I validate the action schema + Then the action schema validation should succeed + + Scenario: Integer argument with None default passes validation + Given an action YAML string with an argument of type "integer" and no default + When I validate the action schema + Then the action schema validation should succeed + + Scenario: Float argument with None default passes validation + Given an action YAML string with an argument of type "float" and no default + When I validate the action schema + Then the action schema validation should succeed + + Scenario: Boolean argument with None default passes validation + Given an action YAML string with an argument of type "boolean" and no default + When I validate the action schema + Then the action schema validation should succeed + + Scenario: List argument with None default passes validation + Given an action YAML string with an argument of type "list" and no default + When I validate the action schema + Then the action schema validation should succeed diff --git a/features/steps/action_schema_steps.py b/features/steps/action_schema_steps.py index 7d567e76b..335b2fc82 100644 --- a/features/steps/action_schema_steps.py +++ b/features/steps/action_schema_steps.py @@ -7,6 +7,7 @@ environment variable interpolation, and error messages. from __future__ import annotations +import json from pathlib import Path from typing import Any @@ -131,6 +132,82 @@ def step_given_yaml_with_name(context: Context, name: str) -> None: context.action_yaml_string = _MINIMAL_YAML.replace("local/simple-action", name) +# NOTE: More specific steps with "and default" / "and no default" MUST be +# defined BEFORE the shorter pattern 'with an argument of type "{arg_type}"' +# so that Behave's parse matcher selects the longer match first. + + +@given( + 'an action YAML string with an argument of type "{arg_type}" and default {default_value}' +) +def step_given_yaml_with_arg_and_default( + context: Context, arg_type: str, default_value: str +) -> None: + """Provide YAML with an argument that has a specific type and default value.""" + # Parse the default value from the string representation + # Handle special cases: true/false for booleans, numbers, strings, lists + if default_value.lower() == "true": + parsed_default: str | int | float | bool | list[str] = True + elif default_value.lower() == "false": + parsed_default = False + elif default_value.startswith("[") and default_value.endswith("]"): + # Parse list + parsed_default = json.loads(default_value) + elif default_value.startswith('"') and default_value.endswith('"'): + # String value + parsed_default = default_value[1:-1] + else: + # Try to parse as number + try: + if "." in default_value: + parsed_default = float(default_value) + else: + parsed_default = int(default_value) + except ValueError: + # Treat as string if not a number + parsed_default = default_value + + # Format the default value for YAML + if isinstance(parsed_default, bool): + yaml_default = "true" if parsed_default else "false" + elif isinstance(parsed_default, list): + yaml_default = json.dumps(parsed_default) + elif isinstance(parsed_default, str): + yaml_default = f'"{parsed_default}"' + else: + yaml_default = str(parsed_default) + + # Build YAML with the argument + context.action_yaml_string = f"""\ +name: local/test-default-type +description: Test argument with default value +strategy_actor: openai/gpt-4 +execution_actor: openai/gpt-4 +definition_of_done: Done +arguments: + - name: test_arg + type: {arg_type} + required: false + default: {yaml_default} +""" + + +@given('an action YAML string with an argument of type "{arg_type}" and no default') +def step_given_yaml_with_arg_no_default(context: Context, arg_type: str) -> None: + """Provide YAML with an argument that has no default value.""" + context.action_yaml_string = f"""\ +name: local/test-no-default +description: Test argument without default value +strategy_actor: openai/gpt-4 +execution_actor: openai/gpt-4 +definition_of_done: Done +arguments: + - name: test_arg + type: {arg_type} + required: false +""" + + @given('an action YAML string with an argument of type "{arg_type}"') def step_given_yaml_with_bad_arg_type(context: Context, arg_type: str) -> None: """Provide YAML with an invalid argument type.""" diff --git a/src/cleveragents/action/schema.py b/src/cleveragents/action/schema.py index 8ce054207..548687956 100644 --- a/src/cleveragents/action/schema.py +++ b/src/cleveragents/action/schema.py @@ -119,6 +119,11 @@ class ActionArgumentSchema(BaseModel): description="Maximum value for numeric arguments.", ) + model_config = ConfigDict( + str_strip_whitespace=True, + extra="forbid", + ) + @field_validator("name") @classmethod def validate_name(cls, v: str) -> str: @@ -140,10 +145,62 @@ class ActionArgumentSchema(BaseModel): raise ValueError(f"Invalid argument type '{v}'. Allowed types: {valid}.") return v_lower - model_config = ConfigDict( - str_strip_whitespace=True, - extra="forbid", - ) + @model_validator(mode="after") + def validate_default_type(self) -> ActionArgumentSchema: + """Validate that the default value's type matches the declared type field. + + Raises: + ValueError: If the default value's type does not match the declared type. + """ + # None default is always valid (means no default provided) + if self.default is None: + return self + + arg_type = self.type.lower() + default_value = self.default + + # Check type compatibility + if arg_type == "string": + if not isinstance(default_value, str): + raise ValueError( + f"Argument '{self.name}' has type 'string' but default value " + f"'{default_value}' is {type(default_value).__name__}, not str." + ) + elif arg_type == "integer": + # Explicitly check for bool first, since bool is a subclass of int + if isinstance(default_value, bool): + raise ValueError( + f"Argument '{self.name}' has type 'integer' but default value " + f"'{default_value}' is bool, not int." + ) + if not isinstance(default_value, int): + raise ValueError( + f"Argument '{self.name}' has type 'integer' but default value " + f"'{default_value}' is {type(default_value).__name__}, not int." + ) + elif arg_type == "float": + # float or int are acceptable for float type + is_bool = isinstance(default_value, bool) + is_numeric = isinstance(default_value, (float, int)) + if not is_numeric or is_bool: + raise ValueError( + f"Argument '{self.name}' has type 'float' but default value " + f"'{default_value}' is {type(default_value).__name__}, " + "not float or int." + ) + elif arg_type == "boolean": + if not isinstance(default_value, bool): + raise ValueError( + f"Argument '{self.name}' has type 'boolean' but default value " + f"'{default_value}' is {type(default_value).__name__}, not bool." + ) + elif arg_type == "list" and not isinstance(default_value, list): + raise ValueError( + f"Argument '{self.name}' has type 'list' but default value " + f"'{default_value}' is {type(default_value).__name__}, not list." + ) + + return self # ────────────────────────────────────────────────────────────