diff --git a/features/invariant_cli_new_coverage.feature b/features/invariant_cli_new_coverage.feature index fa753f642..3e4728257 100644 --- a/features/invariant_cli_new_coverage.feature +++ b/features/invariant_cli_new_coverage.feature @@ -146,6 +146,17 @@ Feature: Invariant CLI commands coverage Then the invariant CLI exit code should be 0 And the invariant CLI output should contain "Never delete prod" + Scenario: List invariants with no flags passes no scope filter + Given a mocked InvariantService that returns two invariants for list + When I invoke invariant list with no filters + Then the invariant CLI exit code should be 0 + And the invariant service list was called with no scope filter + + Scenario: List invariants with conflicting scope flags rejected + Given a mocked InvariantService that returns two invariants for list + When I invoke invariant list with flags "--global --project myapp" + Then the invariant CLI exit code should be non-zero + # === invariant remove command === Scenario: Remove invariant with --yes flag diff --git a/features/steps/invariant_cli_new_coverage_steps.py b/features/steps/invariant_cli_new_coverage_steps.py index a5eed430a..1fe6d06b1 100644 --- a/features/steps/invariant_cli_new_coverage_steps.py +++ b/features/steps/invariant_cli_new_coverage_steps.py @@ -332,6 +332,19 @@ def step_check_list_effective(context): assert kwargs.get("effective") is True +@then("the invariant service list was called with no scope filter") +def step_check_list_no_scope(context): + call_kwargs = context.inv_mock_svc.list_invariants.call_args + assert call_kwargs is not None, "list_invariants was never called" + kwargs = call_kwargs[1] if call_kwargs[1] else {} + assert kwargs.get("scope") is None, ( + f"Expected scope=None (all scopes), got {kwargs.get('scope')!r}" + ) + assert kwargs.get("source_name") is None, ( + f"Expected source_name=None, got {kwargs.get('source_name')!r}" + ) + + # ================================================================ # invariant remove command steps # ================================================================ diff --git a/src/cleveragents/cli/commands/invariant.py b/src/cleveragents/cli/commands/invariant.py index b80233905..39203ced2 100644 --- a/src/cleveragents/cli/commands/invariant.py +++ b/src/cleveragents/cli/commands/invariant.py @@ -185,8 +185,31 @@ def list_invariants( try: service = _get_service() - # Use shared scope resolver for consistent mutual-exclusion validation - scope, source_name = _resolve_scope(is_global, project, plan, action) + # Mutual-exclusion validation (shared semantics with _resolve_scope). + # Unlike `add`, omitting all flags means "list ALL invariants" + # (scope=None, source_name=None) rather than defaulting to GLOBAL. + flags_set = sum( + [is_global, project is not None, plan is not None, action is not None] + ) + if flags_set > 1: + raise typer.BadParameter( + "Specify at most one scope flag: " + "--global, --project, --plan, or --action" + ) + + scope: InvariantScope | None = None + source_name: str | None = None + if is_global: + scope = InvariantScope.GLOBAL + elif project is not None: + scope = InvariantScope.PROJECT + source_name = project + elif plan is not None: + scope = InvariantScope.PLAN + source_name = plan + elif action is not None: + scope = InvariantScope.ACTION + source_name = action invariants = service.list_invariants( scope=scope,