fix(action): add default value type validation to ActionArgumentSchema
CI / quality (pull_request) Failing after 1s
CI / security (pull_request) Failing after 1s
CI / lint (pull_request) Failing after 1s
CI / typecheck (pull_request) Failing after 1s
CI / coverage (pull_request) Has been skipped
CI / integration_tests (pull_request) Failing after 1s
CI / e2e_tests (pull_request) Failing after 1s
CI / unit_tests (pull_request) Failing after 1s
CI / docker (pull_request) Has been skipped
CI / build (pull_request) Failing after 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 0s
CI / helm (pull_request) Failing after 1s
CI / push-validation (pull_request) Failing after 0s
CI / status-check (pull_request) Failing after 0s
CI / quality (pull_request) Failing after 1s
CI / security (pull_request) Failing after 1s
CI / lint (pull_request) Failing after 1s
CI / typecheck (pull_request) Failing after 1s
CI / coverage (pull_request) Has been skipped
CI / integration_tests (pull_request) Failing after 1s
CI / e2e_tests (pull_request) Failing after 1s
CI / unit_tests (pull_request) Failing after 1s
CI / docker (pull_request) Has been skipped
CI / build (pull_request) Failing after 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 0s
CI / helm (pull_request) Failing after 1s
CI / push-validation (pull_request) Failing after 0s
CI / status-check (pull_request) Failing after 0s
Added @model_validator(mode="after") to ActionArgumentSchema that validates default value type against declared type field. Supports all argument types: string, integer, float, boolean, list. Handles edge cases: None defaults (always valid), bool vs int disambiguation (bool checked before int since bool is a subclass of int), float widening from int. Added @a2a @domain @action tags to the BDD feature file. Reordered step definitions so specific patterns match before generic ones. Added CHANGELOG and CONTRIBUTORS entries. Moved model_config before validators per Pydantic convention. ISSUES CLOSED: #9105
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user