Files
cleveragents-core/robot/helper_validation_attach_named_options.py
HAL9000 06d6acab94 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.
2026-06-14 17:33:45 -04:00

225 lines
7.1 KiB
Python

"""Helper script for validation_attach_named_options.robot E2E tests.
Each subcommand is a self-contained check that prints a sentinel on success.
Tests that ``agents validation attach`` accepts ``--key value`` named option
format for extra validation arguments, as required by the spec.
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from pathlib import Path
from unittest.mock import MagicMock, patch
# Ensure local source tree is importable
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from helpers_common import reset_global_state # noqa: E402
from typer.testing import CliRunner # noqa: E402
from cleveragents.cli.commands.validation import app as validation_app # noqa: E402
runner = CliRunner()
_ATTACHMENT_ULID = "01NAMEDOPT000000000000001"
def _make_mock_attachment(validation_name: str, resource_id: str) -> MagicMock:
"""Create a mock attachment object."""
mock_attachment = MagicMock()
mock_attachment.attachment_id = _ATTACHMENT_ULID
mock_attachment.validation_name = validation_name
mock_attachment.resource_id = resource_id
mock_attachment.mode = "required"
mock_attachment.project_name = None
mock_attachment.plan_id = None
mock_attachment.created_at = "2026-01-01T00:00:00"
return mock_attachment
# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------
def attach_with_coverage_threshold() -> None:
"""Verify that --coverage-threshold 90 named option is accepted and forwarded."""
mock_svc = MagicMock()
mock_svc.attach_validation.return_value = _make_mock_attachment(
"local/coverage-check", "git-checkout/my-repo"
)
with patch(
"cleveragents.cli.commands.validation._get_tool_registry_service",
return_value=mock_svc,
):
result = runner.invoke(
validation_app,
[
"attach",
"git-checkout/my-repo",
"local/coverage-check",
"--coverage-threshold",
"90",
"--format",
"plain",
],
)
if result.exit_code != 0:
print(
f"FAIL: exit={result.exit_code} output={result.output!r} "
f"exception={result.exception!r}"
)
sys.exit(1)
# Verify the service received the correct args dict
call_kwargs = mock_svc.attach_validation.call_args
if call_kwargs is None:
print("FAIL: attach_validation was not called")
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}")
sys.exit(1)
print("validation-attach-named-option-coverage-threshold-ok")
def attach_with_multiple_named_options() -> None:
"""Verify that multiple --key value named options are all forwarded."""
mock_svc = MagicMock()
mock_svc.attach_validation.return_value = _make_mock_attachment(
"local/lint-check", "git-checkout/my-repo"
)
with patch(
"cleveragents.cli.commands.validation._get_tool_registry_service",
return_value=mock_svc,
):
result = runner.invoke(
validation_app,
[
"attach",
"git-checkout/my-repo",
"local/lint-check",
"--threshold",
"70",
"--strict",
"true",
"--format",
"plain",
],
)
if result.exit_code != 0:
print(
f"FAIL: exit={result.exit_code} output={result.output!r} "
f"exception={result.exception!r}"
)
sys.exit(1)
call_kwargs = mock_svc.attach_validation.call_args
if call_kwargs is None:
print("FAIL: attach_validation was not called")
sys.exit(1)
args_dict = call_kwargs.kwargs.get("args") or {}
if args_dict.get("threshold") != "70":
print(f"FAIL: expected threshold='70', got {args_dict!r}")
sys.exit(1)
if args_dict.get("strict") != "true":
print(f"FAIL: expected strict='true', got {args_dict!r}")
sys.exit(1)
print("validation-attach-named-option-multiple-ok")
def attach_without_extra_args() -> None:
"""Verify that attach without extra named options still works."""
mock_svc = MagicMock()
mock_svc.attach_validation.return_value = _make_mock_attachment(
"local/basic-check", "git-checkout/my-repo"
)
with patch(
"cleveragents.cli.commands.validation._get_tool_registry_service",
return_value=mock_svc,
):
result = runner.invoke(
validation_app,
[
"attach",
"git-checkout/my-repo",
"local/basic-check",
"--format",
"plain",
],
)
if result.exit_code != 0:
print(
f"FAIL: exit={result.exit_code} output={result.output!r} "
f"exception={result.exception!r}"
)
sys.exit(1)
print("validation-attach-named-option-no-extra-args-ok")
def attach_with_positional_key_value_rejected() -> None:
"""Verify that old-style positional key=value format is rejected."""
mock_svc = MagicMock()
mock_svc.attach_validation.return_value = _make_mock_attachment(
"local/coverage-check", "git-checkout/my-repo"
)
with patch(
"cleveragents.cli.commands.validation._get_tool_registry_service",
return_value=mock_svc,
):
result = runner.invoke(
validation_app,
[
"attach",
"git-checkout/my-repo",
"local/coverage-check",
"coverage-threshold=90",
"--format",
"plain",
],
)
# Must be rejected (non-zero exit) and output must mention "Invalid argument format"
if result.exit_code != 0 and "Invalid argument format" in result.output:
print("validation-attach-positional-key-value-rejected-ok")
else:
print(
f"FAIL: exit={result.exit_code} "
f"output={result.output!r} "
f"(expected non-zero exit and 'Invalid argument format' in output)"
)
sys.exit(1)
# ---------------------------------------------------------------------------
# Dispatcher
# ---------------------------------------------------------------------------
_COMMANDS: dict[str, Callable[[], None]] = {
"attach-with-coverage-threshold": attach_with_coverage_threshold,
"attach-with-multiple-named-options": attach_with_multiple_named_options,
"attach-without-extra-args": attach_without_extra_args,
"attach-with-positional-key-value-rejected": (
attach_with_positional_key_value_rejected
),
}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>")
sys.exit(1)
reset_global_state()
fn = _COMMANDS[sys.argv[1]]
fn()