Merge branch 'master' into feature/m5-estimation-actor
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 26s
CI / lint (pull_request) Successful in 3m21s
CI / quality (pull_request) Successful in 3m44s
CI / typecheck (pull_request) Successful in 3m56s
CI / security (pull_request) Successful in 4m4s
CI / e2e_tests (pull_request) Successful in 4m51s
CI / unit_tests (pull_request) Successful in 6m5s
CI / integration_tests (pull_request) Successful in 6m44s
CI / docker (pull_request) Successful in 57s
CI / coverage (pull_request) Successful in 11m3s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Successful in 36m36s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 26s
CI / lint (pull_request) Successful in 3m21s
CI / quality (pull_request) Successful in 3m44s
CI / typecheck (pull_request) Successful in 3m56s
CI / security (pull_request) Successful in 4m4s
CI / e2e_tests (pull_request) Successful in 4m51s
CI / unit_tests (pull_request) Successful in 6m5s
CI / integration_tests (pull_request) Successful in 6m44s
CI / docker (pull_request) Successful in 57s
CI / coverage (pull_request) Successful in 11m3s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Successful in 36m36s
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Added CLI polish infrastructure: shared constants.py (exit codes, format
|
||||
constants), centralized errors.py (cli_error, cli_not_found, cli_warning),
|
||||
and completion command for shell tab-completion generation. (#861)
|
||||
- Added tool-level execution environment preferences with NONE, REQUIRED,
|
||||
PREFERRED, and SPECIFIC modes. ToolRunner routes tool execution based on
|
||||
preference mode with caller-override precedence. (#879)
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
Feature: CLI consistency and UX polish
|
||||
As a developer
|
||||
I want all CLI commands to follow consistent UX patterns
|
||||
So that the tool is predictable and easy to use
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Exit code constants
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
Scenario: Exit code constants are defined
|
||||
Given the CLI constants module is imported
|
||||
Then EXIT_SUCCESS should equal 0
|
||||
And EXIT_ERROR should equal 1
|
||||
And EXIT_USAGE should equal 2
|
||||
And EXIT_NOT_FOUND should equal 3
|
||||
And EXIT_CONFLICT should equal 4
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Error formatting
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
Scenario: cli_error displays standardized error and exits
|
||||
Given the CLI errors module is imported
|
||||
When I call cli_error with only message "something broke"
|
||||
Then the process should exit with code 1
|
||||
And the stderr output should contain "Error:"
|
||||
And the stderr output should contain "something broke"
|
||||
|
||||
Scenario: cli_error displays hint when provided
|
||||
Given the CLI errors module is imported
|
||||
When I call cli_error with message "not found" and hint "check the name"
|
||||
Then the process should exit with code 1
|
||||
And the stderr output should contain "Hint: check the name"
|
||||
|
||||
Scenario: cli_error uses custom exit code
|
||||
Given the CLI errors module is imported
|
||||
When I call cli_error with message "bad usage" and exit code 2
|
||||
Then the process should exit with code 2
|
||||
|
||||
Scenario: cli_warning displays warning without exiting
|
||||
Given the CLI errors module is imported
|
||||
When I call cli_warning with only message "heads up"
|
||||
Then the stderr output should contain "Warning:"
|
||||
And the stderr output should contain "heads up"
|
||||
|
||||
Scenario: cli_warning displays hint when provided
|
||||
Given the CLI errors module is imported
|
||||
When I call cli_warning with message "slow" and hint "use cache"
|
||||
Then the stderr output should contain "Hint: use cache"
|
||||
|
||||
Scenario: cli_not_found formats correctly and exits with code 3
|
||||
Given the CLI errors module is imported
|
||||
When I call cli_not_found with type "Project" and name "missing-proj"
|
||||
Then the process should exit with code 3
|
||||
And the stderr output should contain "Project 'missing-proj' not found"
|
||||
And the stderr output should contain "Hint:"
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Format constants
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
Scenario: Format constants are consistent
|
||||
Given the CLI constants module is imported
|
||||
Then FORMAT_HELP should mention "json"
|
||||
And FORMAT_HELP should mention "yaml"
|
||||
And FORMAT_HELP should mention "plain"
|
||||
And DEFAULT_FORMAT should equal "rich"
|
||||
And VALID_FORMATS should contain "json"
|
||||
And VALID_FORMATS should contain "yaml"
|
||||
And VALID_FORMATS should contain "plain"
|
||||
And VALID_FORMATS should contain "table"
|
||||
And VALID_FORMATS should contain "rich"
|
||||
|
||||
Scenario: Named format shortcut constants are defined
|
||||
Given the CLI constants module is imported
|
||||
Then FORMAT_TEXT should equal "plain"
|
||||
And FORMAT_JSON should equal "json"
|
||||
And FORMAT_TABLE should equal "table"
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Help text consistency
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
Scenario: Main CLI provides help output
|
||||
Given a CLI consistency test runner
|
||||
When I invoke the CLI with "--help"
|
||||
Then the invoked CLI exit code should be 0
|
||||
And the invoked CLI output should contain "AI-powered development assistant"
|
||||
|
||||
Scenario: Version command provides output
|
||||
Given a CLI consistency test runner
|
||||
When I invoke the CLI with "--version"
|
||||
Then the invoked CLI exit code should be 0
|
||||
And the invoked CLI output should contain "CleverAgents"
|
||||
|
||||
Scenario: Invalid command returns usage exit code
|
||||
Given a CLI consistency test runner
|
||||
When I invoke the CLI with "nonexistent-command-xyz"
|
||||
Then the invoked CLI exit code should be 2
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Shell completion command
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
Scenario: Completion command exists
|
||||
Given a CLI consistency test runner
|
||||
When I invoke the CLI with "completion --help"
|
||||
Then the invoked CLI exit code should be 0
|
||||
And the invoked CLI output should contain "completion"
|
||||
|
||||
Scenario: Completion rejects unsupported shell
|
||||
Given a CLI consistency test runner
|
||||
When I invoke the CLI with "completion ksh"
|
||||
Then the invoked CLI exit code should be 2
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# JSON/text format switching
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
Scenario: Version command supports JSON format
|
||||
Given a CLI consistency test runner
|
||||
When I invoke the CLI with "version --format json"
|
||||
Then the invoked CLI exit code should be 0
|
||||
And the invoked CLI output should be valid JSON
|
||||
|
||||
Scenario: Version command supports YAML format
|
||||
Given a CLI consistency test runner
|
||||
When I invoke the CLI with "version --format yaml"
|
||||
Then the invoked CLI exit code should be 0
|
||||
And the invoked CLI output should contain "version:"
|
||||
|
||||
Scenario: Version command supports plain format
|
||||
Given a CLI consistency test runner
|
||||
When I invoke the CLI with "version --format plain"
|
||||
Then the invoked CLI exit code should be 0
|
||||
And the invoked CLI output should contain "version:"
|
||||
@@ -0,0 +1,291 @@
|
||||
"""Step definitions for CLI consistency and UX polish feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _call_cli_error(
|
||||
context: Context,
|
||||
message: str,
|
||||
hint: str | None = None,
|
||||
exit_code: int | None = None,
|
||||
) -> None:
|
||||
"""Invoke cli_error with a captured stderr console."""
|
||||
from rich.console import Console
|
||||
|
||||
from cleveragents.cli.errors import cli_error
|
||||
|
||||
buf = StringIO()
|
||||
mock_console = Console(file=buf, stderr=True, no_color=True)
|
||||
kwargs: dict[str, object] = {}
|
||||
if hint is not None:
|
||||
kwargs["hint"] = hint
|
||||
if exit_code is not None:
|
||||
kwargs["exit_code"] = exit_code
|
||||
with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console):
|
||||
try:
|
||||
cli_error(message, **kwargs) # type: ignore[arg-type]
|
||||
except SystemExit as exc:
|
||||
context.exit_code = exc.code
|
||||
context.stderr_capture = buf
|
||||
|
||||
|
||||
def _call_cli_warning(
|
||||
context: Context,
|
||||
message: str,
|
||||
hint: str | None = None,
|
||||
) -> None:
|
||||
"""Invoke cli_warning with a captured stderr console."""
|
||||
from rich.console import Console
|
||||
|
||||
from cleveragents.cli.errors import cli_warning
|
||||
|
||||
buf = StringIO()
|
||||
mock_console = Console(file=buf, stderr=True, no_color=True)
|
||||
with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console):
|
||||
if hint is not None:
|
||||
cli_warning(message, hint=hint)
|
||||
else:
|
||||
cli_warning(message)
|
||||
context.stderr_capture = buf
|
||||
|
||||
|
||||
def _call_cli_not_found(
|
||||
context: Context,
|
||||
resource_type: str,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Invoke cli_not_found with a captured stderr console."""
|
||||
from rich.console import Console
|
||||
|
||||
from cleveragents.cli.errors import cli_not_found
|
||||
|
||||
buf = StringIO()
|
||||
mock_console = Console(file=buf, stderr=True, no_color=True)
|
||||
with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console):
|
||||
try:
|
||||
cli_not_found(resource_type, name)
|
||||
except SystemExit as exc:
|
||||
context.exit_code = exc.code
|
||||
context.stderr_capture = buf
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Constants module
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("the CLI constants module is imported")
|
||||
def step_import_constants(context: Context) -> None:
|
||||
from cleveragents.cli.constants import (
|
||||
DEFAULT_FORMAT,
|
||||
EXIT_CONFLICT,
|
||||
EXIT_ERROR,
|
||||
EXIT_NOT_FOUND,
|
||||
EXIT_SUCCESS,
|
||||
EXIT_USAGE,
|
||||
FORMAT_HELP,
|
||||
FORMAT_JSON,
|
||||
FORMAT_TABLE,
|
||||
FORMAT_TEXT,
|
||||
VALID_FORMATS,
|
||||
)
|
||||
|
||||
context.const = {
|
||||
"EXIT_SUCCESS": EXIT_SUCCESS,
|
||||
"EXIT_ERROR": EXIT_ERROR,
|
||||
"EXIT_USAGE": EXIT_USAGE,
|
||||
"EXIT_NOT_FOUND": EXIT_NOT_FOUND,
|
||||
"EXIT_CONFLICT": EXIT_CONFLICT,
|
||||
"FORMAT_HELP": FORMAT_HELP,
|
||||
"DEFAULT_FORMAT": DEFAULT_FORMAT,
|
||||
"VALID_FORMATS": VALID_FORMATS,
|
||||
"FORMAT_TEXT": FORMAT_TEXT,
|
||||
"FORMAT_JSON": FORMAT_JSON,
|
||||
"FORMAT_TABLE": FORMAT_TABLE,
|
||||
}
|
||||
|
||||
|
||||
@then("EXIT_SUCCESS should equal {value:d}")
|
||||
def step_exit_success_equals(context: Context, value: int) -> None:
|
||||
assert context.const["EXIT_SUCCESS"] == value
|
||||
|
||||
|
||||
@then("EXIT_ERROR should equal {value:d}")
|
||||
def step_exit_error_equals(context: Context, value: int) -> None:
|
||||
assert context.const["EXIT_ERROR"] == value
|
||||
|
||||
|
||||
@then("EXIT_USAGE should equal {value:d}")
|
||||
def step_exit_usage_equals(context: Context, value: int) -> None:
|
||||
assert context.const["EXIT_USAGE"] == value
|
||||
|
||||
|
||||
@then("EXIT_NOT_FOUND should equal {value:d}")
|
||||
def step_exit_not_found_equals(context: Context, value: int) -> None:
|
||||
assert context.const["EXIT_NOT_FOUND"] == value
|
||||
|
||||
|
||||
@then("EXIT_CONFLICT should equal {value:d}")
|
||||
def step_exit_conflict_equals(context: Context, value: int) -> None:
|
||||
assert context.const["EXIT_CONFLICT"] == value
|
||||
|
||||
|
||||
@then('FORMAT_HELP should mention "{text}"')
|
||||
def step_format_help_mentions(context: Context, text: str) -> None:
|
||||
assert text in context.const["FORMAT_HELP"], (
|
||||
f"Expected '{text}' in FORMAT_HELP: {context.const['FORMAT_HELP']}"
|
||||
)
|
||||
|
||||
|
||||
@then('DEFAULT_FORMAT should equal "{value}"')
|
||||
def step_default_format_equals(context: Context, value: str) -> None:
|
||||
assert context.const["DEFAULT_FORMAT"] == value
|
||||
|
||||
|
||||
@then('VALID_FORMATS should contain "{value}"')
|
||||
def step_valid_formats_contains(context: Context, value: str) -> None:
|
||||
assert value in context.const["VALID_FORMATS"]
|
||||
|
||||
|
||||
@then('FORMAT_TEXT should equal "{value}"')
|
||||
def step_format_text_equals(context: Context, value: str) -> None:
|
||||
assert context.const["FORMAT_TEXT"] == value
|
||||
|
||||
|
||||
@then('FORMAT_JSON should equal "{value}"')
|
||||
def step_format_json_equals(context: Context, value: str) -> None:
|
||||
assert context.const["FORMAT_JSON"] == value
|
||||
|
||||
|
||||
@then('FORMAT_TABLE should equal "{value}"')
|
||||
def step_format_table_equals(context: Context, value: str) -> None:
|
||||
assert context.const["FORMAT_TABLE"] == value
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Error formatting (parameterised steps matching feature scenarios)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("the CLI errors module is imported")
|
||||
def step_import_errors(context: Context) -> None:
|
||||
context.stderr_capture = StringIO()
|
||||
context.exit_code = None
|
||||
|
||||
|
||||
@when('I call cli_error with only message "{message}"')
|
||||
def step_call_cli_error_message(context: Context, message: str) -> None:
|
||||
_call_cli_error(context, message)
|
||||
|
||||
|
||||
@when('I call cli_error with message "{message}" and hint "{hint}"')
|
||||
def step_call_cli_error_message_hint(context: Context, message: str, hint: str) -> None:
|
||||
_call_cli_error(context, message, hint=hint)
|
||||
|
||||
|
||||
@when('I call cli_error with message "{message}" and exit code {code:d}')
|
||||
def step_call_cli_error_custom_code(context: Context, message: str, code: int) -> None:
|
||||
_call_cli_error(context, message, exit_code=code)
|
||||
|
||||
|
||||
@when('I call cli_warning with only message "{message}"')
|
||||
def step_call_cli_warning_message(context: Context, message: str) -> None:
|
||||
_call_cli_warning(context, message)
|
||||
|
||||
|
||||
@when('I call cli_warning with message "{message}" and hint "{hint}"')
|
||||
def step_call_cli_warning_with_hint(context: Context, message: str, hint: str) -> None:
|
||||
_call_cli_warning(context, message, hint=hint)
|
||||
|
||||
|
||||
@when('I call cli_not_found with type "{resource_type}" and name "{name}"')
|
||||
def step_call_cli_not_found(context: Context, resource_type: str, name: str) -> None:
|
||||
_call_cli_not_found(context, resource_type, name)
|
||||
|
||||
|
||||
@then("the process should exit with code {code:d}")
|
||||
def step_exit_code_check(context: Context, code: int) -> None:
|
||||
assert context.exit_code == code, (
|
||||
f"Expected exit code {code}, got {context.exit_code}"
|
||||
)
|
||||
|
||||
|
||||
@then('the stderr output should contain "{text}"')
|
||||
def step_stderr_contains(context: Context, text: str) -> None:
|
||||
output = context.stderr_capture.getvalue()
|
||||
assert text in output, f"Expected '{text}' in stderr: {output!r}"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# CLI runner tests
|
||||
# ------------------------------------------------------------------
|
||||
# NOTE: The following steps are defined in cli_steps.py and
|
||||
# garbage_collection_cli_steps.py respectively:
|
||||
# - @given("a CLI test runner")
|
||||
# - @when('I run the CLI with "{args}"')
|
||||
# - @then("the CLI exit code should be {code:d}")
|
||||
# - @then('the CLI output should contain "{text}"')
|
||||
# - @then("the CLI output should be valid JSON")
|
||||
#
|
||||
# Behave loads all step files, so those shared definitions are
|
||||
# available here without re-declaration. We only need to register
|
||||
# the ``a CLI test runner`` and ``I run the CLI with`` steps because
|
||||
# cli_steps.py uses a different phrasing ("I run the *CleverAgents*
|
||||
# CLI with").
|
||||
|
||||
|
||||
@given("a CLI consistency test runner")
|
||||
def step_cli_consistency_runner(context: Context) -> None:
|
||||
from typer.testing import CliRunner
|
||||
|
||||
context.cli_runner = CliRunner()
|
||||
|
||||
|
||||
@when('I invoke the CLI with "{args}"')
|
||||
def step_invoke_cli(context: Context, args: str) -> None:
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.main import app, ensure_cli_commands_registered
|
||||
|
||||
ensure_cli_commands_registered()
|
||||
if not hasattr(context, "cli_runner"):
|
||||
context.cli_runner = CliRunner()
|
||||
runner = context.cli_runner
|
||||
arg_list = args.split()
|
||||
context.cli_result = runner.invoke(app, arg_list)
|
||||
|
||||
|
||||
@then("the invoked CLI exit code should be {code:d}")
|
||||
def step_invoked_cli_exit_code(context: Context, code: int) -> None:
|
||||
actual = context.cli_result.exit_code
|
||||
assert actual == code, (
|
||||
f"Expected exit code {code}, got {actual}.\nstdout: {context.cli_result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then('the invoked CLI output should contain "{text}"')
|
||||
def step_invoked_cli_output_contains(context: Context, text: str) -> None:
|
||||
output = context.cli_result.output
|
||||
assert text in output, f"Expected '{text}' in output: {output!r}"
|
||||
|
||||
|
||||
@then("the invoked CLI output should be valid JSON")
|
||||
def step_invoked_cli_output_valid_json(context: Context) -> None:
|
||||
output = context.cli_result.output.strip()
|
||||
try:
|
||||
json.loads(output)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise AssertionError(
|
||||
f"Output is not valid JSON: {exc}\nOutput: {output!r}"
|
||||
) from exc
|
||||
@@ -0,0 +1,135 @@
|
||||
*** Settings ***
|
||||
Documentation CLI Consistency and UX Polish Integration Tests
|
||||
...
|
||||
... Verifies that shared exit-code constants, centralized error
|
||||
... formatting, shell completion generation, and uniform
|
||||
... --format json|text output switching work end-to-end.
|
||||
Resource ${CURDIR}/common.resource
|
||||
Library Process
|
||||
Library OperatingSystem
|
||||
Library String
|
||||
Library Collections
|
||||
Library ${CURDIR}/helper_cli_consistency.py
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${PYTHON} python
|
||||
|
||||
*** Test Cases ***
|
||||
Exit Code Constants Are Defined Correctly
|
||||
[Documentation] Verify all standardized exit codes exist with correct values
|
||||
${constants}= Get Exit Code Constants
|
||||
Should Be Equal As Integers ${constants}[EXIT_SUCCESS] 0
|
||||
Should Be Equal As Integers ${constants}[EXIT_ERROR] 1
|
||||
Should Be Equal As Integers ${constants}[EXIT_USAGE] 2
|
||||
Should Be Equal As Integers ${constants}[EXIT_NOT_FOUND] 3
|
||||
Should Be Equal As Integers ${constants}[EXIT_CONFLICT] 4
|
||||
|
||||
Format Constants Are Defined Correctly
|
||||
[Documentation] Verify format-related constants
|
||||
${constants}= Get Format Constants
|
||||
Should Contain ${constants}[FORMAT_HELP] json
|
||||
Should Contain ${constants}[FORMAT_HELP] yaml
|
||||
Should Contain ${constants}[FORMAT_HELP] plain
|
||||
Should Be Equal As Strings ${constants}[DEFAULT_FORMAT] rich
|
||||
${formats}= Set Variable ${constants}[VALID_FORMATS]
|
||||
Should Contain ${formats} json
|
||||
Should Contain ${formats} yaml
|
||||
Should Contain ${formats} plain
|
||||
Should Contain ${formats} table
|
||||
Should Contain ${formats} rich
|
||||
Should Be Equal As Strings ${constants}[FORMAT_TEXT] plain
|
||||
Should Be Equal As Strings ${constants}[FORMAT_JSON] json
|
||||
Should Be Equal As Strings ${constants}[FORMAT_TABLE] table
|
||||
|
||||
CLI Help Output Is Consistent
|
||||
[Documentation] Verify --help output mentions key elements
|
||||
${result}= Run Process ${PYTHON} -m cleveragents --help
|
||||
... timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} AI-powered development assistant
|
||||
|
||||
CLI Version Output Works
|
||||
[Documentation] Verify --version flag produces version string
|
||||
${result}= Run Process ${PYTHON} -m cleveragents --version
|
||||
... timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} CleverAgents
|
||||
|
||||
Invalid Command Returns Usage Exit Code
|
||||
[Documentation] Invalid command should return exit code 2
|
||||
${result}= Run Process ${PYTHON} -m cleveragents nonexistent-xyz
|
||||
... timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 2
|
||||
Should Contain ${result.stderr} Error: Invalid command
|
||||
|
||||
Completion Command Help Available
|
||||
[Documentation] Completion subcommand should have help text
|
||||
${result}= Run Process ${PYTHON} -m cleveragents completion --help
|
||||
... timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} completion
|
||||
|
||||
Completion Rejects Unsupported Shell
|
||||
[Documentation] Completion with unknown shell should fail with usage exit code
|
||||
${result}= Run Process ${PYTHON} -m cleveragents completion ksh
|
||||
... timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 2
|
||||
|
||||
Version JSON Format Output
|
||||
[Documentation] version --format json should produce valid JSON
|
||||
${result}= Run Process ${PYTHON} -m cleveragents version --format json
|
||||
... timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
${valid}= Is Valid Json ${result.stdout}
|
||||
Should Be True ${valid} Output is not valid JSON: ${result.stdout}
|
||||
|
||||
Version YAML Format Output
|
||||
[Documentation] version --format yaml should produce YAML with version key
|
||||
${result}= Run Process ${PYTHON} -m cleveragents version --format yaml
|
||||
... timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} version:
|
||||
|
||||
Version Plain Format Output
|
||||
[Documentation] version --format plain should produce plain key-value output
|
||||
${result}= Run Process ${PYTHON} -m cleveragents version --format plain
|
||||
... timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} version:
|
||||
|
||||
CLI Error Format Is Standardized
|
||||
[Documentation] cli_error should output Error: prefix and Hint: line
|
||||
${result}= Verify Cli Error Format ${PYTHON}
|
||||
Should Be Equal As Integers ${result}[rc] 1
|
||||
Should Contain ${result}[stdout] Error:
|
||||
Should Contain ${result}[stdout] Hint:
|
||||
|
||||
CLI Not Found Format Is Standardized
|
||||
[Documentation] cli_not_found should output not found message and exit 3
|
||||
${result}= Verify Cli Not Found Format ${PYTHON}
|
||||
Should Be Equal As Integers ${result}[rc] 3
|
||||
Should Contain ${result}[stdout] not found
|
||||
Should Contain ${result}[stdout] Hint:
|
||||
|
||||
Info Command JSON Format
|
||||
[Documentation] info --format json should produce valid JSON
|
||||
${result}= Run Process ${PYTHON} -m cleveragents info --format json
|
||||
... timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
${valid}= Is Valid Json ${result.stdout}
|
||||
Should Be True ${valid} Output is not valid JSON: ${result.stdout}
|
||||
|
||||
Diagnostics Command Works
|
||||
[Documentation] diagnostics command should return exit code 0
|
||||
${result}= Run Process ${PYTHON} -m cleveragents diagnostics
|
||||
... timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
Help Output Lists Completion Command
|
||||
[Documentation] Help output should mention the completion command
|
||||
${result}= Run Process ${PYTHON} -m cleveragents --help
|
||||
... timeout=120s on_timeout=kill
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} completion
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Helper library for CLI consistency Robot Framework tests.
|
||||
|
||||
Provides keyword implementations that verify shared exit-code constants,
|
||||
error formatting utilities, and format-switching behaviour.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def get_exit_code_constants() -> dict[str, int]:
|
||||
"""Return the CLI exit code constants as a dict."""
|
||||
from cleveragents.cli.constants import (
|
||||
EXIT_CONFLICT,
|
||||
EXIT_ERROR,
|
||||
EXIT_NOT_FOUND,
|
||||
EXIT_SUCCESS,
|
||||
EXIT_USAGE,
|
||||
)
|
||||
|
||||
return {
|
||||
"EXIT_SUCCESS": EXIT_SUCCESS,
|
||||
"EXIT_ERROR": EXIT_ERROR,
|
||||
"EXIT_USAGE": EXIT_USAGE,
|
||||
"EXIT_NOT_FOUND": EXIT_NOT_FOUND,
|
||||
"EXIT_CONFLICT": EXIT_CONFLICT,
|
||||
}
|
||||
|
||||
|
||||
def get_format_constants() -> dict[str, object]:
|
||||
"""Return the CLI format constants as a dict."""
|
||||
from cleveragents.cli.constants import (
|
||||
DEFAULT_FORMAT,
|
||||
FORMAT_HELP,
|
||||
FORMAT_JSON,
|
||||
FORMAT_TABLE,
|
||||
FORMAT_TEXT,
|
||||
VALID_FORMATS,
|
||||
)
|
||||
|
||||
return {
|
||||
"FORMAT_HELP": FORMAT_HELP,
|
||||
"DEFAULT_FORMAT": DEFAULT_FORMAT,
|
||||
"VALID_FORMATS": list(VALID_FORMATS),
|
||||
"FORMAT_TEXT": FORMAT_TEXT,
|
||||
"FORMAT_JSON": FORMAT_JSON,
|
||||
"FORMAT_TABLE": FORMAT_TABLE,
|
||||
}
|
||||
|
||||
|
||||
def run_cli_command(python_path: str, *args: str) -> dict[str, object]:
|
||||
"""Run a cleveragents CLI command and return rc, stdout, stderr."""
|
||||
cmd = [python_path, "-m", "cleveragents", *args]
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
return {
|
||||
"rc": result.returncode,
|
||||
"stdout": result.stdout,
|
||||
"stderr": result.stderr,
|
||||
}
|
||||
|
||||
|
||||
def is_valid_json(text: str) -> bool:
|
||||
"""Return True if *text* is valid JSON."""
|
||||
try:
|
||||
json.loads(text)
|
||||
except (json.JSONDecodeError, TypeError, ValueError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _run_error_script(python_path: str, script: str) -> dict[str, object]:
|
||||
"""Execute a multi-line Python script in a subprocess."""
|
||||
result = subprocess.run(
|
||||
[python_path, "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
return {
|
||||
"rc": result.returncode,
|
||||
"stdout": result.stdout,
|
||||
"stderr": result.stderr,
|
||||
}
|
||||
|
||||
|
||||
def verify_cli_error_format(python_path: str) -> dict[str, object]:
|
||||
"""Run a helper script that exercises cli_error and capture output."""
|
||||
target = "cleveragents.cli.errors._get_err_console"
|
||||
script = "\n".join(
|
||||
[
|
||||
"import sys",
|
||||
"from io import StringIO",
|
||||
"from unittest.mock import patch",
|
||||
"from rich.console import Console",
|
||||
"from cleveragents.cli.errors import cli_error",
|
||||
"buf = StringIO()",
|
||||
"c = Console(file=buf, no_color=True)",
|
||||
"try:",
|
||||
f" with patch('{target}', return_value=c):",
|
||||
" cli_error('test error', hint='try again')",
|
||||
"except SystemExit as e:",
|
||||
" print(buf.getvalue(), end='')",
|
||||
" sys.exit(e.code)",
|
||||
]
|
||||
)
|
||||
return _run_error_script(python_path, script)
|
||||
|
||||
|
||||
def verify_cli_not_found_format(python_path: str) -> dict[str, object]:
|
||||
"""Run a helper script that exercises cli_not_found and capture output."""
|
||||
target = "cleveragents.cli.errors._get_err_console"
|
||||
script = "\n".join(
|
||||
[
|
||||
"import sys",
|
||||
"from io import StringIO",
|
||||
"from unittest.mock import patch",
|
||||
"from rich.console import Console",
|
||||
"from cleveragents.cli.errors import cli_not_found",
|
||||
"buf = StringIO()",
|
||||
"c = Console(file=buf, no_color=True)",
|
||||
"try:",
|
||||
f" with patch('{target}', return_value=c):",
|
||||
" cli_not_found('Project', 'ghost')",
|
||||
"except SystemExit as e:",
|
||||
" print(buf.getvalue(), end='')",
|
||||
" sys.exit(e.code)",
|
||||
]
|
||||
)
|
||||
return _run_error_script(python_path, script)
|
||||
|
||||
|
||||
def get_python_path() -> str:
|
||||
"""Return the current Python interpreter path."""
|
||||
return sys.executable
|
||||
@@ -13,6 +13,20 @@ Responsibilities:
|
||||
- Convert between CLI and domain types
|
||||
"""
|
||||
|
||||
from cleveragents.cli.constants import (
|
||||
DEFAULT_FORMAT,
|
||||
EXIT_CONFLICT,
|
||||
EXIT_ERROR,
|
||||
EXIT_NOT_FOUND,
|
||||
EXIT_SUCCESS,
|
||||
EXIT_USAGE,
|
||||
FORMAT_HELP,
|
||||
FORMAT_JSON,
|
||||
FORMAT_TABLE,
|
||||
FORMAT_TEXT,
|
||||
VALID_FORMATS,
|
||||
)
|
||||
from cleveragents.cli.errors import cli_error, cli_not_found, cli_warning
|
||||
from cleveragents.cli.main import (
|
||||
app,
|
||||
cli,
|
||||
@@ -27,4 +41,24 @@ from cleveragents.cli.main import (
|
||||
# Export main as main_cli for backward compatibility
|
||||
main_cli = main
|
||||
|
||||
__all__ = ["app", "cli", "convert_exit_code", "main", "main_cli"]
|
||||
__all__ = [
|
||||
"DEFAULT_FORMAT",
|
||||
"EXIT_CONFLICT",
|
||||
"EXIT_ERROR",
|
||||
"EXIT_NOT_FOUND",
|
||||
"EXIT_SUCCESS",
|
||||
"EXIT_USAGE",
|
||||
"FORMAT_HELP",
|
||||
"FORMAT_JSON",
|
||||
"FORMAT_TABLE",
|
||||
"FORMAT_TEXT",
|
||||
"VALID_FORMATS",
|
||||
"app",
|
||||
"cli",
|
||||
"cli_error",
|
||||
"cli_not_found",
|
||||
"cli_warning",
|
||||
"convert_exit_code",
|
||||
"main",
|
||||
"main_cli",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Shared CLI constants for CleverAgents.
|
||||
|
||||
Defines standardized exit codes and format options used across all CLI
|
||||
command modules to ensure consistent UX behaviour.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exit codes
|
||||
# ---------------------------------------------------------------------------
|
||||
# These mirror common UNIX conventions and are used by ``cli_error()`` in
|
||||
# ``cleveragents.cli.errors`` as well as individual command handlers.
|
||||
|
||||
EXIT_SUCCESS: int = 0
|
||||
"""Command completed successfully."""
|
||||
|
||||
EXIT_ERROR: int = 1
|
||||
"""General runtime error."""
|
||||
|
||||
EXIT_USAGE: int = 2
|
||||
"""Invalid command-line usage (bad arguments, unknown command)."""
|
||||
|
||||
EXIT_NOT_FOUND: int = 3
|
||||
"""Requested entity was not found."""
|
||||
|
||||
EXIT_CONFLICT: int = 4
|
||||
"""Operation conflicts with existing state (e.g. duplicate name)."""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Format option defaults
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FORMAT_HELP: str = (
|
||||
"Output format: json, yaml, plain, table, rich, or color (default: rich)"
|
||||
)
|
||||
"""Reusable help text for the ``--format`` / ``-f`` option."""
|
||||
|
||||
DEFAULT_FORMAT: str = "rich"
|
||||
"""Default output format when ``--format`` is not specified."""
|
||||
|
||||
VALID_FORMATS: tuple[str, ...] = ("json", "yaml", "plain", "table", "rich", "color")
|
||||
"""Accepted values for the ``--format`` option."""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Named format shortcuts
|
||||
# ---------------------------------------------------------------------------
|
||||
# Convenience aliases so callers can write ``FORMAT_JSON`` instead of a
|
||||
# string literal, reducing typo risk and enabling IDE autocompletion.
|
||||
|
||||
FORMAT_TEXT: str = "plain"
|
||||
"""Plain-text output (alias for ``plain``)."""
|
||||
|
||||
FORMAT_JSON: str = "json"
|
||||
"""JSON output."""
|
||||
|
||||
FORMAT_TABLE: str = "table"
|
||||
"""ASCII table output."""
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_FORMAT",
|
||||
"EXIT_CONFLICT",
|
||||
"EXIT_ERROR",
|
||||
"EXIT_NOT_FOUND",
|
||||
"EXIT_SUCCESS",
|
||||
"EXIT_USAGE",
|
||||
"FORMAT_HELP",
|
||||
"FORMAT_JSON",
|
||||
"FORMAT_TABLE",
|
||||
"FORMAT_TEXT",
|
||||
"VALID_FORMATS",
|
||||
]
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Centralized CLI error formatting utilities.
|
||||
|
||||
Provides ``cli_error()`` for displaying Rich-formatted error messages
|
||||
with optional recovery hints, and ``cli_warning()`` for non-fatal
|
||||
diagnostic messages. All output is written to *stderr* so that
|
||||
machine-readable stdout (JSON/YAML) is never polluted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, NoReturn
|
||||
|
||||
from cleveragents.cli.constants import EXIT_ERROR
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.console import Console
|
||||
|
||||
|
||||
def _get_err_console() -> Console:
|
||||
"""Return the stderr Rich console (lazy import to reduce startup time)."""
|
||||
from cleveragents.cli.main import get_err_console
|
||||
|
||||
return get_err_console()
|
||||
|
||||
|
||||
def cli_error(
|
||||
message: str,
|
||||
*,
|
||||
hint: str | None = None,
|
||||
exit_code: int = EXIT_ERROR,
|
||||
) -> NoReturn:
|
||||
"""Display a standardized error message and exit.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message:
|
||||
A human-readable description of what went wrong.
|
||||
hint:
|
||||
An optional suggestion for how the user can fix the problem.
|
||||
exit_code:
|
||||
The process exit code (defaults to ``EXIT_ERROR``).
|
||||
|
||||
Raises
|
||||
------
|
||||
SystemExit
|
||||
Always raised so that callers can treat this as ``NoReturn``.
|
||||
"""
|
||||
console = _get_err_console()
|
||||
console.print(f"[red bold]Error:[/] {message}")
|
||||
if hint:
|
||||
console.print(f"[dim]Hint: {hint}[/]")
|
||||
raise SystemExit(exit_code)
|
||||
|
||||
|
||||
def cli_warning(message: str, *, hint: str | None = None) -> None:
|
||||
"""Display a non-fatal warning message on stderr.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message:
|
||||
A human-readable description of the warning.
|
||||
hint:
|
||||
An optional suggestion for the user.
|
||||
"""
|
||||
console = _get_err_console()
|
||||
console.print(f"[yellow bold]Warning:[/] {message}")
|
||||
if hint:
|
||||
console.print(f"[dim]Hint: {hint}[/]")
|
||||
|
||||
|
||||
def cli_not_found(
|
||||
resource_type: str,
|
||||
name: str,
|
||||
*,
|
||||
cli_command: str | None = None,
|
||||
) -> NoReturn:
|
||||
"""Report a missing resource and exit.
|
||||
|
||||
Prints a standardized "not found" error with a hint suggesting
|
||||
the ``list`` subcommand for the given resource type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
resource_type:
|
||||
Human-readable type label (e.g. ``"Project"``, ``"Automation Profile"``).
|
||||
name:
|
||||
The identifier the user supplied.
|
||||
cli_command:
|
||||
Optional CLI command name override. When *None* (default), the
|
||||
command is derived from *resource_type* by lower-casing and
|
||||
replacing spaces/underscores with hyphens (e.g.
|
||||
``"Automation Profile"`` -> ``"automation-profile"``).
|
||||
|
||||
Raises
|
||||
------
|
||||
SystemExit
|
||||
Always raised with exit code ``EXIT_NOT_FOUND``.
|
||||
"""
|
||||
import re as _re
|
||||
|
||||
from cleveragents.cli.constants import EXIT_NOT_FOUND
|
||||
|
||||
if cli_command is None:
|
||||
cli_command = _re.sub(r"[\s_]+", "-", resource_type.lower())
|
||||
|
||||
resource_plural = f"{resource_type.lower()}s"
|
||||
cli_error(
|
||||
f"{resource_type} '{name}' not found",
|
||||
hint=f"Run 'agents {cli_command} list' to see available {resource_plural}",
|
||||
exit_code=EXIT_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"cli_error",
|
||||
"cli_not_found",
|
||||
"cli_warning",
|
||||
]
|
||||
@@ -50,9 +50,10 @@ def get_err_console() -> "Console":
|
||||
return _err_console
|
||||
|
||||
|
||||
# Expose stderr console for patching/testing convenience
|
||||
# Expose stderr console for patching/testing convenience.
|
||||
err_console = get_err_console()
|
||||
|
||||
|
||||
# Create the main Typer app
|
||||
app: Any = typer.Typer(
|
||||
name="cleveragents",
|
||||
@@ -246,6 +247,7 @@ def _print_basic_help() -> None:
|
||||
typer.echo(" auto-debug Auto-debug operations")
|
||||
typer.echo(" repl Interactive REPL session")
|
||||
typer.echo(" tui Textual terminal UI")
|
||||
typer.echo(" completion Generate shell completion script")
|
||||
typer.echo(" version Show version")
|
||||
typer.echo("")
|
||||
typer.echo("Actors: set a default with 'agents actor set-default <name>'.")
|
||||
@@ -535,6 +537,59 @@ def context_add_shortcut(
|
||||
context_add(paths=paths, recursive=recursive)
|
||||
|
||||
|
||||
@app.command()
|
||||
def completion(
|
||||
shell: Annotated[
|
||||
str,
|
||||
typer.Argument(help="Target shell: bash, zsh, fish, or powershell"),
|
||||
] = "bash",
|
||||
) -> None:
|
||||
"""Generate a shell completion script.
|
||||
|
||||
Print a completion script for the given shell to stdout.
|
||||
Source or eval the output in your shell profile to enable
|
||||
tab-completion for all CleverAgents commands.
|
||||
|
||||
Examples::
|
||||
|
||||
# Bash
|
||||
agents completion bash >> ~/.bashrc
|
||||
|
||||
# Zsh
|
||||
agents completion zsh >> ~/.zshrc
|
||||
|
||||
# Fish
|
||||
agents completion fish > ~/.config/fish/completions/agents.fish
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
from cleveragents.cli.constants import EXIT_USAGE
|
||||
from cleveragents.cli.errors import cli_error
|
||||
|
||||
shell_lower = shell.lower()
|
||||
supported = ("bash", "zsh", "fish", "powershell")
|
||||
if shell_lower not in supported:
|
||||
cli_error(
|
||||
f"Unsupported shell: {shell}",
|
||||
hint=f"Supported shells: {', '.join(supported)}",
|
||||
exit_code=EXIT_USAGE,
|
||||
)
|
||||
|
||||
env_var = f"_CLEVERAGENTS_COMPLETE={shell_lower}_source"
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "cleveragents"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**__import__("os").environ, env_var: "1"},
|
||||
)
|
||||
if result.stdout:
|
||||
typer.echo(result.stdout, nl=False)
|
||||
else:
|
||||
typer.echo(f"# Completion script for {shell_lower}")
|
||||
typer.echo(f"# Shell: {shell_lower}")
|
||||
typer.echo("# Source this file or add it to your shell profile.")
|
||||
|
||||
|
||||
def convert_exit_code(code: Any) -> int:
|
||||
"""Convert exit code to valid range.
|
||||
|
||||
@@ -639,6 +694,7 @@ def main(args: list[str] | None = None) -> int:
|
||||
"apply", # Shortcut for plan apply
|
||||
"context-load", # Shortcut for context add
|
||||
"context-add", # Shortcut
|
||||
"completion", # Shell completion generation
|
||||
"--help",
|
||||
"-h",
|
||||
"--version",
|
||||
|
||||
@@ -13,6 +13,28 @@ build_data # noqa: B018, F821
|
||||
# SubplanFailureHandler.should_stop_others parameter required by public API
|
||||
failed_status # noqa: B018, F821
|
||||
|
||||
# CLI constants module — standardized exit codes and format options
|
||||
EXIT_SUCCESS # noqa: B018, F821
|
||||
EXIT_ERROR # noqa: B018, F821
|
||||
EXIT_USAGE # noqa: B018, F821
|
||||
EXIT_NOT_FOUND # noqa: B018, F821
|
||||
EXIT_CONFLICT # noqa: B018, F821
|
||||
FORMAT_HELP # noqa: B018, F821
|
||||
DEFAULT_FORMAT # noqa: B018, F821
|
||||
|
||||
# CLI constants module — named format shortcuts
|
||||
FORMAT_TEXT # noqa: B018, F821
|
||||
FORMAT_JSON # noqa: B018, F821
|
||||
FORMAT_TABLE # noqa: B018, F821
|
||||
|
||||
# CLI error utilities — centralized error formatting
|
||||
cli_error # noqa: B018, F821
|
||||
cli_warning # noqa: B018, F821
|
||||
cli_not_found # noqa: B018, F821
|
||||
|
||||
# CLI completion command — shell completion script generation
|
||||
completion # noqa: B018, F821
|
||||
|
||||
# CLI formatting module - public API called dynamically by CLI commands
|
||||
format_output # noqa: B018, F821
|
||||
OutputFormat # noqa: B018, F821
|
||||
|
||||
Reference in New Issue
Block a user