diff --git a/features/invariant_cli_new_coverage.feature b/features/invariant_cli_new_coverage.feature index 1b5191ab4..aa40f4d2b 100644 --- a/features/invariant_cli_new_coverage.feature +++ b/features/invariant_cli_new_coverage.feature @@ -25,10 +25,9 @@ Feature: Invariant CLI commands coverage Then the resolved invariant scope should be "action" And the resolved invariant source name should be "deploy-service" - Scenario: Resolve scope with no flags defaults to GLOBAL + Scenario: Resolve scope with no flags raises BadParameter When I resolve invariant scope with no flags - Then the resolved invariant scope should be "global" - And the resolved invariant source name should be "system" + Then a BadParameter error should be raised for invariant scope Scenario: Resolve scope with conflicting flags raises BadParameter When I resolve invariant scope with global and project flags @@ -71,6 +70,12 @@ Feature: Invariant CLI commands coverage Then the invariant CLI exit code should be 0 And the invariant CLI output should contain "JSON constraint" + Scenario: Add invariant without scope flag raises error + Given a mocked InvariantService for invariant CLI add + When I invoke invariant add with no scope flags and text "Missing scope" + Then the invariant CLI exit code should be non-zero + And the invariant CLI output should contain "At least one scope flag is required" + Scenario: Add invariant handles CleverAgentsError Given a mocked InvariantService that raises CleverAgentsError on add When I invoke invariant add with "--global" and text "will fail" diff --git a/features/steps/invariant_cli_new_coverage_steps.py b/features/steps/invariant_cli_new_coverage_steps.py index 3a9074fb5..249b3fdbb 100644 --- a/features/steps/invariant_cli_new_coverage_steps.py +++ b/features/steps/invariant_cli_new_coverage_steps.py @@ -91,9 +91,13 @@ def step_resolve_scope_action(context, action): @when("I resolve invariant scope with no flags") def step_resolve_scope_default(context): - context.resolved_scope, context.resolved_source = _resolve_scope( - is_global=False, project=None, plan=None, action=None - ) + context.inv_bad_parameter_raised = False + try: + context.resolved_scope, context.resolved_source = _resolve_scope( + is_global=False, project=None, plan=None, action=None + ) + except typer.BadParameter: + context.inv_bad_parameter_raised = True @when("I resolve invariant scope with global and project flags") @@ -214,6 +218,12 @@ def step_run_add(context, flags, text): context.inv_result = _runner.invoke(app, args) +@when('I invoke invariant add with no scope flags and text "{text}"') +def step_run_add_no_scope(context, text): + args = ["add", text] + context.inv_result = _runner.invoke(app, args) + + @then("the invariant CLI exit code should be 0") def step_check_exit_zero(context): assert context.inv_result.exit_code == 0, ( diff --git a/src/cleveragents/cli/commands/invariant.py b/src/cleveragents/cli/commands/invariant.py index 2376cb5bd..64bb61bfc 100644 --- a/src/cleveragents/cli/commands/invariant.py +++ b/src/cleveragents/cli/commands/invariant.py @@ -20,7 +20,7 @@ Each invariant belongs to exactly one scope. Pass the matching flag: - ``--action ACTION``: Action-template invariant - ``--plan PLAN_ID``: Plan-specific invariant -If no scope flag is given, ``--global`` is assumed. +At least one scope flag must be provided. ## Examples @@ -86,6 +86,12 @@ def _resolve_scope( "Specify at most one scope flag: --global, --project, --plan, or --action" ) + if flags_set == 0: + raise typer.BadParameter( + "At least one scope flag is required: --global, --project, --plan, " + "or --action" + ) + if project is not None: return InvariantScope.PROJECT, project if plan is not None: @@ -93,7 +99,6 @@ def _resolve_scope( if action is not None: return InvariantScope.ACTION, action - # Default to global return InvariantScope.GLOBAL, "system"