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
This commit is contained in:
+4
-1
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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, (
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user