From e687f779d910e67e4e686ee4579a828ba28db79d Mon Sep 17 00:00:00 2001 From: CleverThis Date: Tue, 14 Apr 2026 12:28:26 +0000 Subject: [PATCH] fix(cli): require at least one scope flag for invariant add per spec The invariant add command was silently defaulting to global scope when no scope flag was provided, violating the specification which requires at least one scope flag (--global, --project, --plan, or --action). This fix: - Updates _resolve_scope() to raise typer.BadParameter when no flags are set - Updates the docstring to reflect the requirement - Adds a new BDD scenario to test the missing scope flag error - Updates the existing scenario that expected default to global behavior --- features/invariant_cli_new_coverage.feature | 11 ++++++++--- .../steps/invariant_cli_new_coverage_steps.py | 16 +++++++++++++--- src/cleveragents/cli/commands/invariant.py | 9 +++++++-- 3 files changed, 28 insertions(+), 8 deletions(-) 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"