fix(validation): normalise --key value option keys and reject consecutive flags

Restore the two behaviours that master had but were lost in this branch:

1. Convert hyphens to underscores in named-option keys before forwarding
   to the service layer (--coverage-threshold → coverage_threshold). This
   matches Typer/Click's universal convention and the documented
   attach_validation(args=...) contract, where keys must be Python-
   identifier-style strings.

2. Detect a missing value when the next token starts with -- (e.g.
   "--threshold --strict true" no longer silently sets threshold to the
   literal string "--strict"; it errors with a clear "Missing value for
   option" message).

Update the Behave step definitions and the Robot helper to assert the
underscore-normalised key (coverage_threshold), matching the restored
behaviour. The CLI-level option name (--coverage-threshold) is unchanged
in the feature file and Robot test — only the dict key the service
receives is normalised.
This commit is contained in:
2026-05-30 18:17:50 -04:00
committed by Forgejo
parent 66b0f362d3
commit 06d6acab94
3 changed files with 27 additions and 17 deletions
@@ -115,15 +115,19 @@ def step_named_opts_attach_succeeds(context: Context) -> None:
def step_named_opts_received_coverage_threshold( def step_named_opts_received_coverage_threshold(
context: Context, expected_val: str context: Context, expected_val: str
) -> None: ) -> None:
"""Verify the service received coverage-threshold in the args dict.""" """Verify the service received coverage_threshold in the args dict.
The CLI converts hyphens in option names to underscores when forwarding
to the service layer (``--coverage-threshold`` → ``coverage_threshold``).
"""
call_kwargs = context.named_opts_mock_service.attach_validation.call_args call_kwargs = context.named_opts_mock_service.attach_validation.call_args
assert call_kwargs is not None, "attach_validation was not called" assert call_kwargs is not None, "attach_validation was not called"
args_dict = call_kwargs.kwargs.get("args") or {} args_dict = call_kwargs.kwargs.get("args") or {}
assert "coverage-threshold" in args_dict, ( assert "coverage_threshold" in args_dict, (
f"Expected 'coverage-threshold' in args, got: {args_dict}" f"Expected 'coverage_threshold' in args, got: {args_dict}"
) )
assert args_dict["coverage-threshold"] == expected_val, ( assert args_dict["coverage_threshold"] == expected_val, (
f"Expected coverage-threshold={expected_val!r}, got {args_dict['coverage-threshold']!r}" f"Expected coverage_threshold={expected_val!r}, got {args_dict['coverage_threshold']!r}"
) )
@@ -82,8 +82,8 @@ def attach_with_coverage_threshold() -> None:
sys.exit(1) sys.exit(1)
args_dict = call_kwargs.kwargs.get("args") or {} args_dict = call_kwargs.kwargs.get("args") or {}
if args_dict.get("coverage-threshold") != "90": if args_dict.get("coverage_threshold") != "90":
print(f"FAIL: expected args={{'coverage-threshold': '90'}}, got {args_dict!r}") print(f"FAIL: expected args={{'coverage_threshold': '90'}}, got {args_dict!r}")
sys.exit(1) sys.exit(1)
print("validation-attach-named-option-coverage-threshold-ok") print("validation-attach-named-option-coverage-threshold-ok")
+16 -10
View File
@@ -291,18 +291,19 @@ def attach(
) -> None: ) -> None:
"""Attach a validation to a resource. """Attach a validation to a resource.
Extra validation arguments are passed as named options using ``--key value``
format (e.g. ``--coverage-threshold 90``). The ``--`` prefix is stripped
and the remaining key/value pairs are forwarded to the validation service.
The validation mode (required or informational) is determined by the The validation mode (required or informational) is determined by the
validation's registered definition, not overridden at attach time. validation's registered definition, not overridden at attach time.
Validation-specific arguments are passed as named options using the
``--key value`` format (e.g. ``--coverage-threshold 90``). Hyphens in
option names are converted to underscores in the stored argument dict
(e.g. ``--coverage-threshold`` becomes ``coverage_threshold``).
Examples: Examples:
agents validation attach git-checkout/my-repo local/coverage-check agents validation attach git-checkout/my-repo local/coverage-check
agents validation attach --project myproj git-checkout/my-repo local/lint agents validation attach --project myproj git-checkout/my-repo local/lint
agents validation attach git-checkout/my-repo local/coverage-check \ agents validation attach --project local/api-service \\
--coverage-threshold 90 local/api-repo local/run-tests --coverage-threshold 90
""" """
try: try:
# Parse extra named options from ctx.args (--key value pairs). # Parse extra named options from ctx.args (--key value pairs).
@@ -315,15 +316,20 @@ def attach(
while i < len(raw_extra): while i < len(raw_extra):
token = raw_extra[i] token = raw_extra[i]
if token.startswith("--"): if token.startswith("--"):
key = token[2:] # strip leading "--" key = token[2:].replace(
"-", "_"
) # --coverage-threshold → coverage_threshold
if not key: if not key:
console.print( console.print(
"[red]Invalid option:[/red] bare '--' is not allowed" "[red]Invalid option:[/red] bare '--' is not allowed"
) )
raise typer.Abort() raise typer.Abort()
# Next token is the value # Next token is the value (must not be another --flag)
if i + 1 >= len(raw_extra): if i + 1 >= len(raw_extra) or raw_extra[i + 1].startswith("--"):
console.print(f"[red]Missing value for option:[/red] {token}") console.print(
f"[red]Missing value for option:[/red] {token} "
"(expected --key value, e.g. --coverage-threshold 90)"
)
raise typer.Abort() raise typer.Abort()
val = raw_extra[i + 1] val = raw_extra[i + 1]
extra_args[key] = val extra_args[key] = val