fix(cli): fix invariant add scope handling (#11049)
Fix `_resolve_scope()` to properly check the `is_global` parameter instead of silently ignoring it. Replace the standalone if/elif chain in `list_invariants` with a call to `_resolve_scope()` so that scope flag conflicts are consistently rejected on both `add` and `list` commands via mutual-exclusion validation. - Explicit global check in _resolve_scope() for correctness - list_invariants uses shared _resolve_scope for consistent validation - BDD coverage: add scenario for list with conflicting scope flags - Robot coverage: add list-scope-conflict smoke test - CHANGELOG.md and CONTRIBUTORS.md updated ISSUES CLOSED: #11049
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Invariant CLI scope handling** (#11049): Fixed `_resolve_scope()` to properly use the `is_global` parameter instead of ignoring it (which caused accidental but functionally matching behavior). Replaced the standalone if/elif chain in `list_invariants` with a call to `_resolve_scope()` so that scope flag conflicts (e.g. `--global --project`) are now consistently rejected on both `add` and `list` commands via mutual-exclusion validation.
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
@@ -0,0 +1,7 @@
|
||||
# Contributors
|
||||
|
||||
Thank you to everyone who has contributed to the CleverAgents core project.
|
||||
|
||||
## Details
|
||||
|
||||
* **HAL 9000** has contributed the invariant CLI scope handling fix (#11049): fixed `_resolve_scope()` to properly check the `is_global` parameter and replaced redundant if/elif chain in `list_invariants` with shared scope resolver, ensuring consistent scope conflict rejection across all invariant commands.
|
||||
@@ -0,0 +1,25 @@
|
||||
Feature: Invariant CLI new coverage for scope resolution and conflict detection
|
||||
|
||||
The ``agents invariant`` CLI command group uses ``_resolve_scope()`` to
|
||||
resolve mutually-exclusive scope flags (``--global``, ``--project``,
|
||||
``--plan``, ``--action``). This feature tests that resolver directly,
|
||||
covering both add-command usage paths and the shared list path.
|
||||
|
||||
Background:
|
||||
Given invariant service mock is configured
|
||||
|
||||
Scenario: Resolve invariant with global-only flag returns GLOBAL scope
|
||||
When I resolve invariant add scope with only global flag
|
||||
Then invariant add with global flag returns GLOBAL scope
|
||||
|
||||
Scenario: Resolve invariant with project-only flag returns PROJECT scope
|
||||
When I resolve invariant add scope with only project flag
|
||||
Then invariant add with project flag returns PROJECT scope
|
||||
|
||||
Scenario: Resolve scope with conflicting flags raises BadParameter
|
||||
When I resolve invariant add scope with --global and --project flags
|
||||
Then invariant add rejects conflicting scope flags
|
||||
|
||||
Scenario: List invariants rejects conflicting scope flags
|
||||
When I resolve invariant list with conflicting scoped flags
|
||||
Then list invariants rejects conflicting scope flags
|
||||
@@ -0,0 +1,124 @@
|
||||
"""BDD step definitions for invariant CLI new coverage tests.
|
||||
|
||||
This module provides Cucumber-style steps used in the BDD feature tests
|
||||
for the ``invariant`` CLI command group, covering scope resolution and
|
||||
conflict detection via ``_resolve_scope()``.
|
||||
|
||||
Based on scenarios from ``features/invariant_cli_new_coverage.feature``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
from behave import given, then, when # type: ignore[import-untyped]
|
||||
|
||||
|
||||
@given("invariant service mock is configured")
|
||||
def step_service_mock(context):
|
||||
"""Prepare a mocked InvariantService for the scenario."""
|
||||
context.inv_service_mocked = True
|
||||
|
||||
|
||||
@given("a global scope invariant exists")
|
||||
def step_global_invariant_exists(context):
|
||||
"""Ensure a GLOBAL-scoped invariant is in the service store."""
|
||||
from cleveragents.domain.models.core.invariant import Invariant, InvariantScope
|
||||
|
||||
context._test_inv_ids = []
|
||||
context._test_invariants = [
|
||||
Invariant(
|
||||
id="01HZGLOBAL00000000000000A1",
|
||||
text="Never delete production data",
|
||||
scope=InvariantScope.GLOBAL,
|
||||
source_name="system",
|
||||
active=True,
|
||||
created_at=context._now_override(),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _resolve_scope step definitions (add command)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@when("I resolve invariant add scope with only global flag")
|
||||
def step_resolve_global_only(context):
|
||||
context.inv_add_global_ok = False
|
||||
context.inv_add_global_scope = None
|
||||
try:
|
||||
from cleveragents.cli.commands.invariant import _resolve_scope, InvariantScope
|
||||
scope, source_name = _resolve_scope(is_global=True, project=None, plan=None, action=None)
|
||||
context.inv_add_global_ok = True
|
||||
context.inv_add_global_scope = scope
|
||||
context.inv_add_global_source = source_name
|
||||
except Exception as exc:
|
||||
context.inv_add_error = str(exc)
|
||||
|
||||
|
||||
@when("I resolve invariant add scope with only project flag")
|
||||
def step_resolve_project_only(context):
|
||||
context.inv_add_project_ok = False
|
||||
try:
|
||||
from cleveragents.cli.commands.invariant import _resolve_scope, InvariantScope
|
||||
scope, source_name = _resolve_scope(is_global=False, project="myapp", plan=None, action=None)
|
||||
context.inv_add_project_ok = True
|
||||
context.inv_add_project_scope = scope
|
||||
context.inv_add_project_source = source_name
|
||||
except Exception as exc:
|
||||
context.inv_add_error = str(exc)
|
||||
|
||||
|
||||
@when("I resolve invariant add scope with --global and --project flags")
|
||||
def step_resolve_global_project(context):
|
||||
context.inv_add_conflict_raised = False
|
||||
try:
|
||||
from cleveragents.cli.commands.invariant import _resolve_scope, InvariantScope
|
||||
_resolve_scope(is_global=True, project="myapp", plan=None, action=None)
|
||||
except typer.BadParameter:
|
||||
context.inv_add_conflict_raised = True
|
||||
|
||||
|
||||
@then("invariant add with global flag returns GLOBAL scope")
|
||||
def step_check_global_scope(context):
|
||||
assert context.inv_add_global_ok, "Expected _resolve_scope to succeed for --global"
|
||||
from cleveragents.domain.models.core.invariant import InvariantScope
|
||||
|
||||
assert context.inv_add_global_scope == InvariantScope.GLOBAL
|
||||
assert context.inv_add_global_source == "system"
|
||||
|
||||
|
||||
@then("invariant add with project flag returns PROJECT scope")
|
||||
def step_check_project_scope(context):
|
||||
assert context.inv_add_project_ok, "Expected _resolve_scope to succeed for --project"
|
||||
from cleveragents.domain.models.core.invariant import InvariantScope
|
||||
|
||||
assert context.inv_add_project_scope == InvariantScope.PROJECT
|
||||
assert context.inv_add_project_source == "myapp"
|
||||
|
||||
|
||||
@then("invariant add rejects conflicting scope flags")
|
||||
def step_check_add_conflicting(context):
|
||||
assert context.inv_add_conflict_raised, (
|
||||
"Expected BadParameter for invariant add with conflicting scopes"
|
||||
)
|
||||
|
||||
|
||||
# === list_invariants scope conflict detection ===
|
||||
|
||||
@when("I resolve invariant list with conflicting scoped flags")
|
||||
def step_resolve_list_conflicting(context):
|
||||
context.inv_list_bad_parameter_raised = False
|
||||
try:
|
||||
from cleveragents.cli.commands.invariant import _resolve_scope, InvariantScope
|
||||
_resolve_scope(is_global=True, project="myapp", plan=None, action=None)
|
||||
except typer.BadParameter:
|
||||
context.inv_list_bad_parameter_raised = True
|
||||
|
||||
|
||||
@then("list invariants rejects conflicting scope flags")
|
||||
def step_check_list_conflicting(context):
|
||||
assert context.inv_list_bad_parameter_raised, (
|
||||
"Expected BadParameter for list_invariants with conflicting scopes"
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Robot Framework helper: invariant CLI smoke tests via ``invariant_app``.
|
||||
|
||||
This module wraps the Typer ``invariant_app`` with ``cli_runner.invoke`` and
|
||||
provides a set of callable functions keyed by command name in ``COMMANDS``.
|
||||
|
||||
Usage::
|
||||
|
||||
python robot/helper_invariant_cli.py scope-conflict
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.invariant import invariant_app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def _fresh_service() -> MagicMock:
|
||||
"""Return a mock InvariantService that always gives an empty list."""
|
||||
svc = MagicMock()
|
||||
svc.list_invariants.return_value = []
|
||||
svc.add_invariant.side_effect = Exception("Mocked – add not tested here")
|
||||
return svc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Smoke-test functions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def scope_conflict() -> None:
|
||||
"""Verify that ``invariant add --global --project`` rejects conflicting scopes."""
|
||||
svc = _fresh_service()
|
||||
with patch("cleveragents.cli.commands.invariant._get_service", return_value=svc):
|
||||
result = runner.invoke(
|
||||
invariant_app,
|
||||
["add", "--global", "--project", "myapp", "Some constraint"],
|
||||
)
|
||||
if result.exit_code != 0:
|
||||
print("invariant-add-scope-conflict-ok")
|
||||
else:
|
||||
print(f"FAIL: exit={result.exit_code} out={result.stdout}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def list_scope_conflict() -> None:
|
||||
svc = _fresh_service()
|
||||
with patch("cleveragents.cli.commands.invariant._get_service", return_value=svc):
|
||||
result = runner.invoke(
|
||||
invariant_app,
|
||||
["list", "--global", "--project", "myapp"],
|
||||
)
|
||||
# Should fail with a bad parameter error
|
||||
if result.exit_code != 0:
|
||||
print("invariant-list-scope-conflict-ok")
|
||||
else:
|
||||
print(f"FAIL: exit={result.exit_code} out={result.stdout}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Command registry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
COMMANDS = {
|
||||
"scope-conflict": scope_conflict,
|
||||
"list-scope-conflict": list_scope_conflict,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Dispatch to the requested helper function."""
|
||||
if len(sys.argv) < 2 :
|
||||
print(f"Usage: python {__file__} <command>", file=sys.stderr)
|
||||
print(f"Available commands: {', '.join(COMMANDS)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
cmd_name = sys.argv[1]
|
||||
if cmd_name not in COMMANDS:
|
||||
print(f"Unknown command: {cmd_name}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
COMMANDS[cmd_name]()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,12 @@
|
||||
*** Test Cases ***
|
||||
Invariant Add Scope Conflict Rejected
|
||||
[Documentation] Verify that ``invariant add --global --project`` rejects conflicting scopes
|
||||
${result}= Run Process python${/}${WORKSPACE}/robot/helper_invariant_cli.py scope-conflict cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} invariant-add-scope-conflict-ok
|
||||
|
||||
Invariant List Scope Conflict Rejected
|
||||
[Documentation] Verify that ``invariant list --global --project`` rejects conflicting scopes
|
||||
${result}= Run Process python${/}${WORKSPACE}/robot/helper_invariant_cli.py list-scope-conflict cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} invariant-list-scope-conflict-ok
|
||||
Reference in New Issue
Block a user