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(
context: Context, expected_val: str
) -> 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
assert call_kwargs is not None, "attach_validation was not called"
args_dict = call_kwargs.kwargs.get("args") or {}
assert "coverage-threshold" in args_dict, (
f"Expected 'coverage-threshold' in args, got: {args_dict}"
assert "coverage_threshold" in args_dict, (
f"Expected 'coverage_threshold' in args, got: {args_dict}"
)
assert args_dict["coverage-threshold"] == expected_val, (
f"Expected coverage-threshold={expected_val!r}, got {args_dict['coverage-threshold']!r}"
assert args_dict["coverage_threshold"] == expected_val, (
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)
args_dict = call_kwargs.kwargs.get("args") or {}
if args_dict.get("coverage-threshold") != "90":
print(f"FAIL: expected args={{'coverage-threshold': '90'}}, got {args_dict!r}")
if args_dict.get("coverage_threshold") != "90":
print(f"FAIL: expected args={{'coverage_threshold': '90'}}, got {args_dict!r}")
sys.exit(1)
print("validation-attach-named-option-coverage-threshold-ok")
+16 -10
View File
@@ -291,18 +291,19 @@ def attach(
) -> None:
"""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
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:
agents validation attach git-checkout/my-repo local/coverage-check
agents validation attach --project myproj git-checkout/my-repo local/lint
agents validation attach git-checkout/my-repo local/coverage-check \
--coverage-threshold 90
agents validation attach --project local/api-service \\
local/api-repo local/run-tests --coverage-threshold 90
"""
try:
# Parse extra named options from ctx.args (--key value pairs).
@@ -315,15 +316,20 @@ def attach(
while i < len(raw_extra):
token = raw_extra[i]
if token.startswith("--"):
key = token[2:] # strip leading "--"
key = token[2:].replace(
"-", "_"
) # --coverage-threshold → coverage_threshold
if not key:
console.print(
"[red]Invalid option:[/red] bare '--' is not allowed"
)
raise typer.Abort()
# Next token is the value
if i + 1 >= len(raw_extra):
console.print(f"[red]Missing value for option:[/red] {token}")
# Next token is the value (must not be another --flag)
if i + 1 >= len(raw_extra) or raw_extra[i + 1].startswith("--"):
console.print(
f"[red]Missing value for option:[/red] {token} "
"(expected --key value, e.g. --coverage-threshold 90)"
)
raise typer.Abort()
val = raw_extra[i + 1]
extra_args[key] = val