Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 904d16bef9 | |||
| 299db8e7b6 | |||
| 6fddc9f943 | |||
| 9cb75ab5e8 |
@@ -321,6 +321,25 @@ ensuring data is stored with proper parameter values.
|
||||
|
||||
- **BDD Feature File Tag Coverage** (#9124): Added required `@a2a`, `@session`, and `@cli` Gherkin tags to all A2A, session, and CLI feature files (30 files) to enable tag-based test filtering via `behave --tags=a2a,session,cli`. This restores the ability to selectively run test categories and enables CI to execute targeted test suites without running the full suite.
|
||||
|
||||
- **ActionArgumentSchema default value type validation** (#9178, #9105): Added
|
||||
a `@model_validator` to `ActionArgumentSchema` that validates the default value
|
||||
matches its declared argument type. Type mappings enforced:
|
||||
* "string" → str
|
||||
* "integer" → int (not bool — explicit rejection since Python bool is
|
||||
subclass of int)
|
||||
* "float" → float or int (int coerces to float)
|
||||
* "boolean" → bool
|
||||
* "list" → list. None defaults are always valid. Comprehensive BDD scenarios
|
||||
cover all type combinations and mismatch cases with clear, actionable error
|
||||
messages. This fixes issue #9105 where invalid configurations could pass
|
||||
silently. Also validates min_value/max_value bounds against the default when present.
|
||||
|
||||
- **ActionArgument domain model default value type validation** (#9178, #9105): Added
|
||||
a matching `@model_validator` to `ActionArgument` in the domain model so that
|
||||
CLI-parsed and YAML-mapped arguments are also validated at instance construction
|
||||
time, not only during schema loading. This prevents inconsistent argument definitions
|
||||
from ever reaching the TUI component renderer or plan use path.
|
||||
|
||||
- **Cross-actor subgraph cycle detection reads actor_ref field** (#1431): Fixed
|
||||
`_detect_subgraph_cycles()`, `_map_node()`, and the `compile_actor()` main loop
|
||||
in `src/cleveragents/actor/compiler.py` to read `actor_ref` from the top-level
|
||||
|
||||
@@ -68,3 +68,4 @@ Below are some of the specific details of various contributions.
|
||||
* HAL 9000 has contributed the plan correct JSON output envelope fix (PR #8662 / issue #8584): restructured `agents plan correct --format json` output to nest correction fields under `data.correction` and pass `command="plan correct"` to `format_output`, producing the spec-required CLI envelope. Added three BDD scenarios validating `data.correction.mode` (revert and append modes) and the `command` field.
|
||||
* HAL 9000 has contributed BDD feature file tag coverage improvements (#9124 / pr #9183): added required `@a2a`, `@session`, and `@cli` Gherkin tags to 30 feature files (8 A2A, 7 session, 15 CLI) to enable selective tag-based test filtering via `behave --tags=a2a,session,cli`.
|
||||
* HAL 9000 has contributed the plan tree JSON `decision_id` fix (#9096): updated `step_tree_json_valid` in features/steps/plan_explain_steps.py to correctly handle the {"data": [...]} envelope structure produced by format_output, and removed @tdd_expected_fail from the @tdd_issue_4254 scenario so it runs as a permanent regression guard.
|
||||
* HAL 9000 has contributed the ActionArgumentSchema default value type validation (PR #9178 / issue #9105): added a @model_validator to ensure default values match their declared argument type — enforcing string→str, integer→int (not bool), float→float/int, boolean→bool, list→list. None defaults are always valid. Comprehensive BDD scenarios cover all type combinations and mismatch cases with clear, actionable error messages.
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
@domain @schema
|
||||
Feature: Action Schema Validation
|
||||
|
||||
As a developer defining action configurations in YAML
|
||||
I want the schema to validate argument defaults match declared types
|
||||
So that invalid default values are caught at config load time rather than at runtime
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Basic valid cases — all supported types with matching defaults
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@happy_path
|
||||
Scenario: Valid action YAML with typed arguments and matching defaults loads successfully
|
||||
Given an action YAML with typed arguments and defaults
|
||||
When I validate the action schema
|
||||
Then the action schema validation should succeed
|
||||
And the action config should have 5 arguments with defaults
|
||||
And action argument 0 name should be "str_arg" with default "hello"
|
||||
And action argument 0 type should be "string"
|
||||
And action argument 1 name should be "int_arg" with default "42"
|
||||
And action argument 1 type should be "integer"
|
||||
And action argument 3 name should be "bool_arg" with default "true"
|
||||
And action argument 3 type should be "boolean"
|
||||
And None argument does not trigger type validation error
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Invalid default: string where integer expected
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@tdd_issue @tdd_issue_9105
|
||||
Scenario: String default for integer argument fails validation
|
||||
Given an action YAML with integer arg as string "forty-two"
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "count"
|
||||
And the actual type should be mentioned
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Invalid default: integer where string expected
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@tdd_issue @tdd_issue_9105
|
||||
Scenario: Integer default for string argument fails validation
|
||||
Given an action YAML string with argument named "my_arg" as string type and integer default 123
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "my_arg"
|
||||
And the expected type should be mentioned
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Invalid default: string where boolean expected
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@tdd_issue @tdd_issue_9105
|
||||
Scenario: String "true" default for boolean argument fails validation
|
||||
Given an action YAML string with argument named "verbose" as boolean type and string default "true"
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "Default value type mismatch"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Invalid default: string where float expected
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@tdd_issue @tdd_issue_9105
|
||||
Scenario: String default for float argument fails validation
|
||||
Given an action YAML string with argument named "rate" as float type and string default "high"
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "Default value type mismatch"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Invalid default: string where list expected
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@tdd_issue @tdd_issue_9105
|
||||
Scenario: String default for list argument fails validation
|
||||
Given an action YAML string with argument named "targets" as list type and string default "single-value"
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "Default value type mismatch"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Min/max bounds validation against defaults
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@tdd_issue @tdd_issue_9105
|
||||
Scenario: Default below min_value is rejected
|
||||
Given an action YAML string:
|
||||
"""
|
||||
name: local/below-min-default
|
||||
description: Action with default below min_value
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: Done
|
||||
arguments:
|
||||
- name: priority
|
||||
type: integer
|
||||
default: -10
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
"""
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "min_value"
|
||||
|
||||
@tdd_issue @tdd_issue_9105
|
||||
Scenario: Default above max_value is rejected
|
||||
Given an action YAML string:
|
||||
"""
|
||||
name: local/above-max-default
|
||||
description: Action with default above max_value
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: Done
|
||||
arguments:
|
||||
- name: priority
|
||||
type: integer
|
||||
default: 200
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
"""
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "max_value"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Valid min/max bounds — defaults within ranges pass
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@happy_path
|
||||
Scenario: Default within min/max range passes validation
|
||||
Given an action YAML with typed arguments and defaults
|
||||
When I validate the action schema
|
||||
Then the action schema validation should succeed
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bool vs int edge case — Python bool is subclass of int
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@tdd_issue @tdd_issue_9105
|
||||
Scenario: Boolean default for integer argument is rejected (bool is subclass of int)
|
||||
Given an action YAML string with argument named "count" as integer type and boolean default true
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "not a boolean"
|
||||
And the expected type should be mentioned
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Min/Max with numeric args — no default present (default is None)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@happy_path
|
||||
Scenario: Arguments with min/max but no default still validate normally
|
||||
Given an action YAML string:
|
||||
"""
|
||||
name: local/min-max-no-default
|
||||
description: Action with min/max but no default
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: Done
|
||||
arguments:
|
||||
- name: count
|
||||
type: integer
|
||||
required: true
|
||||
description: Count must be between 1 and 100
|
||||
min_value: 1
|
||||
max_value: 100
|
||||
"""
|
||||
When I validate the action schema
|
||||
Then the action schema validation should succeed
|
||||
@@ -848,3 +848,111 @@ Feature: Consolidated Action
|
||||
And the action config model_dump should contain key "strategy_actor"
|
||||
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Default value type validation (PR #9178, issue #9105)
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Scenario: Valid defaults pass type validation for string type
|
||||
Given an action YAML with typed arguments and defaults
|
||||
When I validate the action schema
|
||||
Then the action schema validation should succeed
|
||||
And the action config should have 5 arguments with defaults
|
||||
|
||||
|
||||
Scenario: Valid defaults pass type validation for integer type
|
||||
Given an action YAML with typed arguments and defaults
|
||||
When I validate the action schema
|
||||
Then the action schema validation should succeed
|
||||
And action argument 1 type should be "integer"
|
||||
|
||||
|
||||
Scenario: Valid defaults pass type validation for float type
|
||||
Given an action YAML with typed arguments and defaults
|
||||
When I validate the action schema
|
||||
Then the action schema validation should succeed
|
||||
And action argument 2 type should be "float"
|
||||
|
||||
|
||||
Scenario: Valid defaults pass type validation for boolean type
|
||||
Given an action YAML with typed arguments and defaults
|
||||
When I validate the action schema
|
||||
Then the action schema validation should succeed
|
||||
|
||||
|
||||
Scenario: Valid defaults pass type validation for list type
|
||||
Given an action YAML with typed arguments and defaults
|
||||
When I validate the action schema
|
||||
Then the action schema validation should succeed
|
||||
|
||||
|
||||
Scenario: None default does not trigger type validation error
|
||||
Given an action YAML with typed arguments and defaults
|
||||
When I validate the action schema
|
||||
Then the action schema validation should succeed
|
||||
And None argument does not trigger type validation error
|
||||
|
||||
|
||||
Scenario: Integer default that is a boolean fails type validation
|
||||
Given an action YAML string:
|
||||
"""\
|
||||
name: local/bad-default
|
||||
description: Action with bad default type
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: Done
|
||||
arguments:
|
||||
- name: count
|
||||
type: integer
|
||||
default: true
|
||||
"""
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "not a boolean"
|
||||
|
||||
|
||||
Scenario: Integer default that is a string fails type validation
|
||||
Given an action YAML with integer arg as string "forty-two"
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "Default value type mismatch"
|
||||
And the action schema error should mention "'integer'"
|
||||
|
||||
|
||||
Scenario: String default that is an integer fails type validation
|
||||
Given an action YAML string with argument named "my_arg" as string type and integer default 123
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "my_arg"
|
||||
And the action schema error should mention "'string'"
|
||||
|
||||
|
||||
Scenario: Boolean default that is a string fails type validation
|
||||
Given an action YAML string with argument named "verbose" as boolean type and string default "true"
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "Default value type mismatch"
|
||||
|
||||
|
||||
Scenario: Float default that is a string fails type validation
|
||||
Given an action YAML string with argument named "rate" as float type and string default "high"
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "Default value type mismatch"
|
||||
|
||||
|
||||
Scenario: List default that is a string fails type validation
|
||||
Given an action YAML string with argument named "targets" as list type and string default "single-value"
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "Default value type mismatch"
|
||||
|
||||
|
||||
Scenario: String in default for integer type mentions not a boolean when int-like
|
||||
Given an action YAML string with argument named "my_arg" as integer type and string default "nope"
|
||||
When I validate the action schema expecting failure
|
||||
Then the action schema validation should fail
|
||||
And the action schema error should mention "my_arg"
|
||||
And the action schema error should mention "'integer'"
|
||||
|
||||
|
||||
@@ -456,3 +456,272 @@ def step_then_model_dump_contains_key(context: Context, key: str) -> None:
|
||||
assert context.action_config is not None
|
||||
data: dict[str, Any] = context.action_config.model_dump()
|
||||
assert key in data, f"Key '{key}' not found in model_dump: {list(data.keys())}"
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Default value type validation step definitions (PR #9178)
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
_VALID_YAML_WITH_DEFAULTS = """\
|
||||
name: local/default-args-action
|
||||
description: Action with typed default values
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: All defaults match types
|
||||
arguments:
|
||||
- name: str_arg
|
||||
type: string
|
||||
default: "hello"
|
||||
- name: int_arg
|
||||
type: integer
|
||||
default: 42
|
||||
- name: float_arg
|
||||
type: float
|
||||
default: 3.14
|
||||
- name: bool_arg
|
||||
type: boolean
|
||||
default: true
|
||||
- name: list_arg
|
||||
type: list
|
||||
default: ["a", "b"]
|
||||
- name: none_arg
|
||||
type: string
|
||||
"""
|
||||
|
||||
|
||||
@then("the action config should have {count:d} arguments with defaults")
|
||||
def step_then_argument_defaults(context: Context, count: int) -> None:
|
||||
"""Assert the number of parsed arguments that have non-None defaults."""
|
||||
assert context.action_config is not None
|
||||
actual = sum(
|
||||
1 for arg in context.action_config.arguments if arg.default is not None
|
||||
)
|
||||
assert actual == count, (
|
||||
f"Expected {count} args with non-None defaults, got {actual}"
|
||||
)
|
||||
|
||||
|
||||
@then(
|
||||
'action argument {idx:d} name should be "{expected}" with default "{default_value}"'
|
||||
)
|
||||
def step_then_argument_with_default(
|
||||
context: Context, idx: int, expected: str, default_value: str
|
||||
) -> None:
|
||||
"""Assert an argument has a specific default value."""
|
||||
assert context.action_config is not None
|
||||
arg = context.action_config.arguments[idx]
|
||||
assert arg.name == expected
|
||||
if default_value.lower() == "true":
|
||||
assert arg.default is True
|
||||
elif default_value.lower() == "false":
|
||||
assert arg.default is False
|
||||
else:
|
||||
# Attempt numeric coercion so integer/float defaults compare correctly
|
||||
try:
|
||||
coerced: int | float = int(default_value)
|
||||
assert arg.default == coerced
|
||||
except ValueError:
|
||||
try:
|
||||
coerced_f: float = float(default_value)
|
||||
assert arg.default == coerced_f
|
||||
except ValueError:
|
||||
assert arg.default == default_value
|
||||
|
||||
|
||||
@then("the actual type should be mentioned")
|
||||
def step_then_actual_type_mentioned(context: Context) -> None:
|
||||
"""Assert the error message includes a Python type name (str, int, float, bool, list)."""
|
||||
assert context.action_schema_error is not None, "No error was captured"
|
||||
assert any(
|
||||
t in context.action_schema_error
|
||||
for t in ["str", "int", "float", "bool", "list"]
|
||||
), (
|
||||
f"Expected an actual Python type name in error, got: {context.action_schema_error}"
|
||||
)
|
||||
|
||||
|
||||
@then("the expected type should be mentioned")
|
||||
def step_then_expected_type_mentioned(context: Context) -> None:
|
||||
"""Assert the error message includes a declared type name (string, integer, float, boolean, list)."""
|
||||
assert context.action_schema_error is not None, "No error was captured"
|
||||
assert any(
|
||||
t in context.action_schema_error
|
||||
for t in ["string", "integer", "float", "boolean", "list"]
|
||||
), f"Expected a declared type name in error, got: {context.action_schema_error}"
|
||||
|
||||
|
||||
@then("the action schema error should mention {match_text}")
|
||||
def step_then_error_mentions_match(context: Context, match_text: str) -> None:
|
||||
"""Assert the error message contains a specific pattern (supports regex-like matching)."""
|
||||
assert context.action_schema_error is not None, "No error was captured"
|
||||
|
||||
# Handle quoted strings vs plain identifiers
|
||||
if match_text.startswith('"') and match_text.endswith('"'):
|
||||
match_str = match_text.strip('"')
|
||||
assert match_str.lower() in context.action_schema_error.lower(), (
|
||||
f"Expected error to mention {match_text!r}, got: {context.action_schema_error}"
|
||||
)
|
||||
elif "not a boolean" in match_text:
|
||||
assert "not a boolean" in context.action_schema_error, (
|
||||
f"Expected error to say 'not a boolean', got: {context.action_schema_error}"
|
||||
)
|
||||
else:
|
||||
assert match_text.lower() in context.action_schema_error.lower(), (
|
||||
f"Expected error to mention '{match_text}', got: {context.action_schema_error}"
|
||||
)
|
||||
|
||||
|
||||
@then("None argument does not trigger type validation error")
|
||||
def step_then_none_arg_passthrough(context: Context) -> None:
|
||||
"""Assert that an argument with no default value passes validation."""
|
||||
assert context.action_config is not None
|
||||
# Find the none_arg in arguments and confirm it has no default
|
||||
for arg in context.action_config.arguments:
|
||||
if arg.name == "none_arg":
|
||||
assert arg.default is None
|
||||
|
||||
|
||||
# ── Additional given steps for default value type tests ────────────────
|
||||
|
||||
|
||||
@given("an action YAML with typed arguments and defaults")
|
||||
def step_given_yaml_with_typed_defaults(context: Context) -> None:
|
||||
"""Provide YAML with all valid default types."""
|
||||
context.action_yaml_string = _VALID_YAML_WITH_DEFAULTS
|
||||
|
||||
|
||||
@given("an action YAML string:")
|
||||
def step_given_yaml_multiline(context: Context) -> None:
|
||||
"""Provide a multi-line YAML string from the scenario docstring."""
|
||||
assert context.text is not None, "Expected a docstring in the scenario"
|
||||
context.action_yaml_string = context.text.strip()
|
||||
|
||||
|
||||
# ── Steps for specific bad-default scenarios ───────────────────────────
|
||||
|
||||
_BAD_INT_STRING_YAML = """\
|
||||
name: local/bad-int-str
|
||||
description: Action with string default for integer arg
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: Done
|
||||
arguments:
|
||||
- name: count
|
||||
type: integer
|
||||
default: "forty-two"
|
||||
"""
|
||||
|
||||
_BAD_STR_INT_YAML = """\
|
||||
name: local/bad-str-int
|
||||
description: Action with integer default for string arg
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: Done
|
||||
arguments:
|
||||
- name: "my_arg"
|
||||
type: string
|
||||
default: 123
|
||||
"""
|
||||
|
||||
_BAD_BOOL_STR_YAML = """\
|
||||
name: local/bad-bool-str
|
||||
description: Action with string default for boolean arg
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: Done
|
||||
arguments:
|
||||
- name: verbose
|
||||
type: boolean
|
||||
default: "true"
|
||||
"""
|
||||
|
||||
_BAD_FLOAT_STR_YAML = """\
|
||||
name: local/bad-float-str
|
||||
description: Action with string default for float arg
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: Done
|
||||
arguments:
|
||||
- name: rate
|
||||
type: float
|
||||
default: "high"
|
||||
"""
|
||||
|
||||
_BAD_LIST_STR_YAML = """\
|
||||
name: local/bad-list-str
|
||||
description: Action with string default for list arg
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: Done
|
||||
arguments:
|
||||
- name: targets
|
||||
type: list
|
||||
default: "single-value"
|
||||
"""
|
||||
|
||||
|
||||
@given('an action YAML with integer arg as string "forty-two"')
|
||||
def step_given_bad_int_for_string(context: Context) -> None:
|
||||
"""Provide YAML with a string default where int is expected."""
|
||||
context.action_yaml_string = _BAD_INT_STRING_YAML
|
||||
|
||||
|
||||
@given(
|
||||
'an action YAML string with argument named "{arg_name}" as {type_} type and integer default 123'
|
||||
)
|
||||
def step_given_bad_str_for_int(context: Context, arg_name: str, type_: str) -> None:
|
||||
"""Provide YAML with an int default where a string is expected."""
|
||||
context.action_yaml_string = _BAD_STR_INT_YAML
|
||||
|
||||
|
||||
@given(
|
||||
'an action YAML string with argument named "{arg_name}" as {type_} type and string default "true"'
|
||||
)
|
||||
def step_given_bad_str_for_bool(context: Context, arg_name: str, type_: str) -> None:
|
||||
"""Provide YAML with a string default where bool is expected."""
|
||||
context.action_yaml_string = _BAD_BOOL_STR_YAML
|
||||
|
||||
|
||||
@given(
|
||||
'an action YAML string with argument named "{arg_name}" as {type_} type and string default "high"'
|
||||
)
|
||||
def step_given_bad_str_for_float(context: Context, arg_name: str, type_: str) -> None:
|
||||
"""Provide YAML with a string default where float is expected."""
|
||||
context.action_yaml_string = _BAD_FLOAT_STR_YAML
|
||||
|
||||
|
||||
@given(
|
||||
'an action YAML string with argument named "{arg_name}" as {type_} type and string default "single-value"'
|
||||
)
|
||||
def step_given_bad_str_for_list(context: Context, arg_name: str, type_: str) -> None:
|
||||
"""Provide YAML with a string default where list is expected."""
|
||||
context.action_yaml_string = _BAD_LIST_STR_YAML
|
||||
|
||||
|
||||
@given(
|
||||
'an action YAML string with argument named "{arg_name}" as integer type and string default "nope"'
|
||||
)
|
||||
def step_given_bad_int_for_str_pattern(context: Context, arg_name: str) -> None:
|
||||
"""Provide YAML for error pattern testing."""
|
||||
context.action_yaml_string = _BAD_INT_STRING_YAML
|
||||
|
||||
|
||||
_BAD_BOOL_INT_YAML = """\
|
||||
name: local/bad-bool-int
|
||||
description: Action with boolean default for integer arg
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: Done
|
||||
arguments:
|
||||
- name: count
|
||||
type: integer
|
||||
default: true
|
||||
"""
|
||||
|
||||
|
||||
@given(
|
||||
'an action YAML string with argument named "{arg_name}" as integer type and boolean default true'
|
||||
)
|
||||
def step_given_bad_bool_for_int(context: Context, arg_name: str) -> None:
|
||||
"""Provide YAML with a boolean default where integer is expected."""
|
||||
context.action_yaml_string = _BAD_BOOL_INT_YAML
|
||||
|
||||
@@ -6,6 +6,7 @@ Provides :class:`ActionConfigSchema`, a Pydantic model that:
|
||||
* Normalizes camelCase keys to snake_case before validation.
|
||||
* Interpolates ``${ENV_VAR}`` placeholders from environment variables.
|
||||
* Trims, de-duplicates, and drops blank invariant strings.
|
||||
* Validates default value types against declared argument types.
|
||||
* Produces clear, actionable error messages for every validation failure.
|
||||
|
||||
Schema definition lives in ``docs/schema/action.schema.yaml``.
|
||||
@@ -141,6 +142,102 @@ class ActionArgumentSchema(BaseModel):
|
||||
raise ValueError(f"Invalid argument type '{v}'. Allowed types: {valid}.")
|
||||
return v_lower
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_default_value_type(self) -> ActionArgumentSchema:
|
||||
"""Validate that the default value matches the declared type.
|
||||
|
||||
Type mappings enforced:
|
||||
- "string" → str
|
||||
- "integer" → int (not bool, since Python bool is subclass of int)
|
||||
- "float" → float or int
|
||||
- "boolean" → bool
|
||||
- "list" → list
|
||||
None defaults are always valid.
|
||||
|
||||
Also validates min_value/max_value bounds against the default when present.
|
||||
"""
|
||||
default_value = self.default
|
||||
declared_type = self.type
|
||||
|
||||
# None defaults are always valid (argument is optional)
|
||||
if default_value is None:
|
||||
return self
|
||||
|
||||
match declared_type:
|
||||
case "string":
|
||||
if not isinstance(default_value, str):
|
||||
actual = type(default_value).__name__
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type!r} but got {actual!r}. "
|
||||
"The default value must be a string."
|
||||
)
|
||||
|
||||
case "integer":
|
||||
# Python bool is a subclass of int — reject booleans explicitly
|
||||
is_bool = isinstance(default_value, bool)
|
||||
is_not_int = is_bool or not isinstance(default_value, int)
|
||||
if is_not_int:
|
||||
actual = type(default_value).__name__
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type!r} but got {actual!r}. "
|
||||
"The default value must be an integer (not a boolean)."
|
||||
)
|
||||
|
||||
case "float":
|
||||
# Float or int are both valid (int coerces to float)
|
||||
if isinstance(default_value, bool):
|
||||
actual = "bool"
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type!r} but got {actual!r}. "
|
||||
"The default value must be a float or integer."
|
||||
)
|
||||
if not isinstance(default_value, (float, int)):
|
||||
actual = type(default_value).__name__
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type!r} but got {actual!r}. "
|
||||
"The default value must be a float or integer."
|
||||
)
|
||||
|
||||
case "boolean":
|
||||
if not isinstance(default_value, bool):
|
||||
actual = type(default_value).__name__
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type!r} but got {actual!r}. "
|
||||
"The default value must be a boolean."
|
||||
)
|
||||
|
||||
case "list":
|
||||
if not isinstance(default_value, list):
|
||||
actual = type(default_value).__name__
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type!r} but got {actual!r}. "
|
||||
"The default value must be a list."
|
||||
)
|
||||
|
||||
# Validate min/max bounds when default is present and numeric.
|
||||
# Use isinstance directly so Pyright can narrow the type for comparisons.
|
||||
if isinstance(default_value, (int, float)) and not isinstance(
|
||||
default_value, bool
|
||||
):
|
||||
if self.min_value is not None and default_value < self.min_value:
|
||||
raise ValueError(
|
||||
f"Argument '{self.name}': default {default_value} "
|
||||
f"is less than min_value {self.min_value}."
|
||||
)
|
||||
if self.max_value is not None and default_value > self.max_value:
|
||||
raise ValueError(
|
||||
f"Argument '{self.name}': default {default_value} "
|
||||
f"is greater than max_value {self.max_value}."
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
model_config = ConfigDict(
|
||||
str_strip_whitespace=True,
|
||||
extra="forbid",
|
||||
|
||||
@@ -188,6 +188,100 @@ class ActionArgument(BaseModel):
|
||||
)
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_default_value_type(self) -> ActionArgument:
|
||||
"""Validate that the default value matches the declared argument type.
|
||||
|
||||
Type mappings enforced:
|
||||
- "string" → str
|
||||
- "integer" → int (not bool — Python bool is subclass of int)
|
||||
- "float" → float or int (int coerces to float)
|
||||
- "boolean" → bool
|
||||
- "list" → list
|
||||
None defaults are always valid.
|
||||
"""
|
||||
default_value = self.default_value
|
||||
declared_type = self.arg_type
|
||||
|
||||
# None defaults are always valid (argument is optional)
|
||||
if default_value is None:
|
||||
return self
|
||||
|
||||
match declared_type:
|
||||
case ArgumentType.STRING:
|
||||
if not isinstance(default_value, str):
|
||||
actual = type(default_value).__name__
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type.value!r} but got {actual!r}. "
|
||||
"The default value must be a string."
|
||||
)
|
||||
|
||||
case ArgumentType.INTEGER:
|
||||
# Python bool is a subclass of int — reject booleans explicitly
|
||||
is_bool = isinstance(default_value, bool)
|
||||
is_not_int = is_bool or not isinstance(default_value, int)
|
||||
if is_not_int:
|
||||
actual = type(default_value).__name__
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type.value!r} but got {actual!r}. "
|
||||
"The default value must be an integer (not a boolean)."
|
||||
)
|
||||
|
||||
case ArgumentType.FLOAT:
|
||||
# Float or int are both valid (int coerces to float)
|
||||
if isinstance(default_value, bool):
|
||||
actual = "bool"
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type.value!r} but got {actual!r}. "
|
||||
"The default value must be a float or integer."
|
||||
)
|
||||
if not isinstance(default_value, (float, int)):
|
||||
actual = type(default_value).__name__
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type.value!r} but got {actual!r}. "
|
||||
"The default value must be a float or integer."
|
||||
)
|
||||
|
||||
case ArgumentType.BOOLEAN:
|
||||
if not isinstance(default_value, bool):
|
||||
actual = type(default_value).__name__
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type.value!r} but got {actual!r}. "
|
||||
"The default value must be a boolean."
|
||||
)
|
||||
|
||||
case ArgumentType.LIST:
|
||||
if not isinstance(default_value, list):
|
||||
actual = type(default_value).__name__
|
||||
raise ValueError(
|
||||
f"Default value type mismatch for argument '{self.name}': "
|
||||
f"expected {declared_type.value!r} but got {actual!r}. "
|
||||
"The default value must be a list."
|
||||
)
|
||||
|
||||
# Validate min/max bounds when default is present and numeric.
|
||||
# Use isinstance directly so Pyright can narrow the type for comparisons.
|
||||
if isinstance(default_value, (int, float)) and not isinstance(
|
||||
default_value, bool
|
||||
):
|
||||
if self.min_value is not None and default_value < self.min_value:
|
||||
raise ValueError(
|
||||
f"Argument '{self.name}': default {default_value} "
|
||||
f"is less than min_value {self.min_value}."
|
||||
)
|
||||
if self.max_value is not None and default_value > self.max_value:
|
||||
raise ValueError(
|
||||
f"Argument '{self.name}': default {default_value} "
|
||||
f"is greater than max_value {self.max_value}."
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
# -- Factory: parse from CLI colon-delimited string --------------------
|
||||
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user