From d547029200b502a7af2a45f9b9bb6229a7e84693 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Thu, 16 Apr 2026 20:01:31 +0000 Subject: [PATCH 1/5] fix(cli): fix invariant add scope handling (#6331) Ensure invariant add enforces explicit scope selection and covers missing flag error in CLI tests. ISSUES CLOSED: #6331 --- CHANGELOG.md | 5 ++++- features/invariant_cli_new_coverage.feature | 11 ++++++++--- .../steps/invariant_cli_new_coverage_steps.py | 13 ++++++++++--- robot/helper_invariant_cli.py | 15 +++++++++++++++ robot/invariant_cli.robot | 6 ++++++ src/cleveragents/cli/commands/invariant.py | 17 +++++++++++++---- 6 files changed, 56 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4b9977f..3392dd157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -622,7 +622,6 @@ _ALL_DATA_COLUMNS + ") " "SELECT " + _ALL_DATA_COLUMNS + " FROM v3_plans"`. specification to `docs/specification.md`. Defines `cleveragents.subplans` module boundaries, public interfaces, and forbidden dependencies. Specifies `Subplan`, `SubplanResult`, and `SubplanTree` data models with full field definitions. Documents - PostgreSQL schema with indexes for parent/root plan lookups and status filtering. Describes the 8-step spawning algorithm during the Execute phase (LLM decomposition → parallel dispatch → hierarchical lifecycle → parent wait). Specifies concurrency control via per-plan semaphores (`max_parallel` default 4, max 16, `fail_fast` support). @@ -1358,6 +1357,10 @@ iteration` and data corruption under concurrent plan execution. All public The `export` command gains `--output-format` and the `import` command gains `--format` to select the output envelope format independently of the export/import file format. +- **Invariant add scope enforcement** (#6331): `agents invariant add` now fails when no + scope flag is provided, and Robot coverage ensures the CLI surfaces the explicit + error message instead of silently defaulting to global scope. + - **Robot Framework TDD Listener Guards** (#5436): Added three guard conditions to the `tdd_expected_fail_listener` `end_test()` function to prevent blindly inverting ALL test failures to passes, which was masking infrastructure errors and causing flaky CI behavior. diff --git a/features/invariant_cli_new_coverage.feature b/features/invariant_cli_new_coverage.feature index 1b5191ab4..991f48519 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 @@ -65,6 +64,12 @@ Feature: Invariant CLI commands coverage Then the invariant CLI exit code should be 0 And the invariant CLI output should contain "Invariant added" + Scenario: Add invariant without scope flag via CLI + Given a mocked InvariantService for invariant CLI add + When I invoke invariant add without scope flags and text "Missing scope flag" + Then the invariant CLI exit code should be non-zero + And the invariant CLI output should contain "Exactly one scope flag is required" + Scenario: Add invariant with --format json via CLI Given a mocked InvariantService for invariant CLI add When I invoke invariant add with "--global --format json" and text "JSON constraint" diff --git a/features/steps/invariant_cli_new_coverage_steps.py b/features/steps/invariant_cli_new_coverage_steps.py index 3a9074fb5..9bb3afa18 100644 --- a/features/steps/invariant_cli_new_coverage_steps.py +++ b/features/steps/invariant_cli_new_coverage_steps.py @@ -91,9 +91,11 @@ 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: + _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 +216,11 @@ def step_run_add(context, flags, text): context.inv_result = _runner.invoke(app, args) +@when('I invoke invariant add without scope flags and text "{text}"') +def step_run_add_no_flags(context, text): + context.inv_result = _runner.invoke(app, ["add", text]) + + @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/robot/helper_invariant_cli.py b/robot/helper_invariant_cli.py index 453e5a136..e564b06de 100644 --- a/robot/helper_invariant_cli.py +++ b/robot/helper_invariant_cli.py @@ -140,6 +140,20 @@ def scope_conflict() -> None: sys.exit(1) +def add_no_scope() -> None: + svc = _fresh_service() + with patch("cleveragents.cli.commands.invariant._get_service", return_value=svc): + result = runner.invoke(invariant_app, ["add", "Missing scope invariant"]) + output = result.stdout or "" + if result.stderr: + output += result.stderr + if result.exit_code != 0 and "Exactly one scope flag is required" in output: + print("invariant-add-no-scope-ok") + else: + print(f"FAIL: exit={result.exit_code} out={result.stdout}") + sys.exit(1) + + def list_json() -> None: svc = _fresh_service() svc.add_invariant("JSON test rule", scope=_scope("global"), source_name="system") @@ -174,6 +188,7 @@ COMMANDS = { "list-filter": list_filter, "remove": remove, "scope-conflict": scope_conflict, + "add-no-scope": add_no_scope, "list-json": list_json, } diff --git a/robot/invariant_cli.robot b/robot/invariant_cli.robot index 9a489c98a..2ef61fc10 100644 --- a/robot/invariant_cli.robot +++ b/robot/invariant_cli.robot @@ -58,6 +58,12 @@ Invariant Scope Conflict Rejected Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} invariant-scope-conflict-ok +Invariant Add Missing Scope Rejected + [Documentation] Verify that ``invariant add`` fails when no scope flag is provided + ${result}= Run Process ${PYTHON} ${HELPER} add-no-scope cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} invariant-add-no-scope-ok + Invariant List JSON Format [Documentation] Verify that ``invariant list --format json`` outputs JSON ${result}= Run Process ${PYTHON} ${HELPER} list-json cwd=${WORKSPACE} diff --git a/src/cleveragents/cli/commands/invariant.py b/src/cleveragents/cli/commands/invariant.py index 2376cb5bd..f0c2541be 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. +Exactly one scope flag must be provided; commands error when omitted. ## Examples @@ -81,11 +81,18 @@ def _resolve_scope( flags_set = sum( [is_global, project is not None, plan is not None, action is not None] ) + if flags_set == 0: + raise typer.BadParameter( + "Exactly one scope flag is required: " + "--global, --project, --plan, or --action" + ) if flags_set > 1: raise typer.BadParameter( - "Specify at most one scope flag: --global, --project, --plan, or --action" + "Specify only one scope flag: --global, --project, --plan, or --action" ) + if is_global: + return InvariantScope.GLOBAL, "system" if project is not None: return InvariantScope.PROJECT, project if plan is not None: @@ -93,8 +100,10 @@ def _resolve_scope( if action is not None: return InvariantScope.ACTION, action - # Default to global - return InvariantScope.GLOBAL, "system" + # This line is unreachable because flags_set == 0 already raises BadParameter. + raise typer.BadParameter( + "Exactly one scope flag is required: --global, --project, --plan, or --action" + ) def _invariant_dict(inv: Invariant) -> dict[str, object]: -- 2.52.0 From 1f1120a9e5b9ff1fe1f4ea95eb583511601d87b6 Mon Sep 17 00:00:00 2001 From: controller-ci-rerun Date: Thu, 11 Jun 2026 02:15:04 -0400 Subject: [PATCH 2/5] chore: re-trigger CI [controller] -- 2.52.0 From d4df1c84c1dc19dcbe9bb23c3cec1c4a53e18a34 Mon Sep 17 00:00:00 2001 From: controller-ci-rerun Date: Fri, 12 Jun 2026 14:14:54 -0400 Subject: [PATCH 3/5] chore: re-trigger CI [controller] -- 2.52.0 From 98a139771d876faa4fdf6dd4f8e147e367995d24 Mon Sep 17 00:00:00 2001 From: controller-ci-rerun Date: Fri, 12 Jun 2026 15:03:54 -0400 Subject: [PATCH 4/5] chore: re-trigger CI [controller] -- 2.52.0 From 7543fc2bfc3d53ca3cfc669c55e7922b82244874 Mon Sep 17 00:00:00 2001 From: controller-ci-rerun Date: Sun, 14 Jun 2026 14:24:17 -0400 Subject: [PATCH 5/5] chore: re-trigger CI [controller] -- 2.52.0