From 4d31f0ed02b1d21e8c813812a01d8432b8e890a3 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Sun, 5 Apr 2026 08:39:14 +0000 Subject: [PATCH] fix(cli): promote --format to global CLI callback option per spec Add `fmt: OutputFormat` parameter to `main_callback()` in `src/cleveragents/cli/main.py` and store the selected format in `ctx.obj["format"]` so all subcommands can read it without needing their own per-command `--format` flag. Remove per-command `--format` / `fmt` parameters from `version()`, `info()`, and `diagnostics()` commands. These commands now read the format from `ctx.obj.get("format", OutputFormat.RICH.value)`. The specification states: "The framework supports six distinct output formats, selectable via the global `--format` flag." This change aligns the implementation with the spec by making `--format` a global option on the root `agents` command (via the Typer callback). All six formats (json, yaml, plain, rich, table, color) are supported via the global flag and the `-f` shorthand. Add Behave BDD scenarios covering global `--format` flag propagation to subcommands for all six formats. Update Robot Framework integration tests to exercise the global `--format` flag. Update existing tests that used per-command `--format` for version/info/diagnostics to use the global flag instead. ISSUES CLOSED: #2908 --- features/cli_consistency.feature | 12 +- features/cli_global_format_flag.feature | 156 +++++++++++++++++ .../steps/cli_global_format_flag_steps.py | 158 ++++++++++++++++++ .../cli_main_uncovered_branches_steps.py | 9 +- robot/cli_consistency.robot | 16 +- robot/cli_core.robot | 32 ++-- robot/cli_formats.robot | 49 ++++++ robot/e2e/wf07_cicd.robot | 2 +- robot/e2e/wf14_server_mode.robot | 2 +- robot/helper_cli_formats.py | 88 ++++++++++ src/cleveragents/cli/main.py | 58 ++++--- 11 files changed, 524 insertions(+), 58 deletions(-) create mode 100644 features/cli_global_format_flag.feature create mode 100644 features/steps/cli_global_format_flag_steps.py diff --git a/features/cli_consistency.feature b/features/cli_consistency.feature index 61f3f4c2b..386adde38 100644 --- a/features/cli_consistency.feature +++ b/features/cli_consistency.feature @@ -117,20 +117,20 @@ Feature: CLI consistency and UX polish # JSON/text format switching # ----------------------------------------------------------------- - Scenario: Version command supports JSON format + Scenario: Version command supports JSON format via global --format flag Given a CLI consistency test runner - When I invoke the CLI with "version --format json" + When I invoke the CLI with "--format json version" Then the invoked CLI exit code should be 0 And the invoked CLI output should be valid JSON - Scenario: Version command supports YAML format + Scenario: Version command supports YAML format via global --format flag Given a CLI consistency test runner - When I invoke the CLI with "version --format yaml" + When I invoke the CLI with "--format yaml version" Then the invoked CLI exit code should be 0 And the invoked CLI output should contain "version:" - Scenario: Version command supports plain format + Scenario: Version command supports plain format via global --format flag Given a CLI consistency test runner - When I invoke the CLI with "version --format plain" + When I invoke the CLI with "--format plain version" Then the invoked CLI exit code should be 0 And the invoked CLI output should contain "version:" diff --git a/features/cli_global_format_flag.feature b/features/cli_global_format_flag.feature new file mode 100644 index 000000000..f23a4b353 --- /dev/null +++ b/features/cli_global_format_flag.feature @@ -0,0 +1,156 @@ +Feature: Global --format flag propagation to subcommands + As a developer using the CleverAgents CLI + I want the --format flag to be a global option on the root `agents` command + So that I can set the output format once and have it apply to all subcommands + Per the specification: "The framework supports six distinct output formats, + selectable via the global --format flag." + + Background: + Given the global format flag test runner is set up + + # ----------------------------------------------------------------------- + # Subtask 1 & 2: main_callback accepts --format and stores in ctx.obj + # ----------------------------------------------------------------------- + + Scenario: Global --format json flag is accepted by the root command + When I invoke the CLI with global args "--format json version" + Then the global format command should succeed + And the global format output should be valid JSON + + Scenario: Global --format yaml flag is accepted by the root command + When I invoke the CLI with global args "--format yaml version" + Then the global format command should succeed + And the global format output should be valid YAML + + Scenario: Global --format plain flag is accepted by the root command + When I invoke the CLI with global args "--format plain version" + Then the global format command should succeed + And the global format output should contain plain text + + Scenario: Global --format rich flag is accepted by the root command + When I invoke the CLI with global args "--format rich version" + Then the global format command should succeed + + Scenario: Global --format table flag is accepted by the root command + When I invoke the CLI with global args "--format table version" + Then the global format command should succeed + + Scenario: Global --format color flag is accepted by the root command + When I invoke the CLI with global args "--format color version" + Then the global format command should succeed + + Scenario: Global -f shorthand flag is accepted by the root command + When I invoke the CLI with global args "-f json version" + Then the global format command should succeed + And the global format output should be valid JSON + + # ----------------------------------------------------------------------- + # Subtask 3: version, info, diagnostics read format from global ctx.obj + # ----------------------------------------------------------------------- + + Scenario: version command reads format from global ctx.obj + When I invoke the CLI with global args "--format json version" + Then the global format command should succeed + And the global format output should be valid JSON + And the global format JSON output should contain key "version" + + Scenario: info command reads format from global ctx.obj + When I invoke the CLI with global args "--format json info" + Then the global format command should succeed + And the global format output should be valid JSON + + Scenario: diagnostics command reads format from global ctx.obj + When I invoke the CLI with global args "--format json diagnostics" + Then the global format command should succeed + And the global format output should be valid JSON + + Scenario: version command defaults to rich format when no --format given + When I invoke the CLI with global args "version" + Then the global format command should succeed + + Scenario: info command defaults to rich format when no --format given + When I invoke the CLI with global args "info" + Then the global format command should succeed + + Scenario: diagnostics command defaults to rich format when no --format given + When I invoke the CLI with global args "diagnostics" + Then the global format command should succeed + + # ----------------------------------------------------------------------- + # Subtask 4: ctx.obj["format"] is stored and accessible + # ----------------------------------------------------------------------- + + Scenario: ctx.obj stores the selected format value json + When I call main_callback directly with format "json" + Then the context object should have format "json" + + Scenario: ctx.obj stores yaml format value + When I call main_callback directly with format "yaml" + Then the context object should have format "yaml" + + Scenario: ctx.obj stores plain format value + When I call main_callback directly with format "plain" + Then the context object should have format "plain" + + Scenario: ctx.obj stores rich format value (default) + When I call main_callback directly with format "rich" + Then the context object should have format "rich" + + Scenario: ctx.obj stores table format value + When I call main_callback directly with format "table" + Then the context object should have format "table" + + Scenario: ctx.obj stores color format value + When I call main_callback directly with format "color" + Then the context object should have format "color" + + # ----------------------------------------------------------------------- + # All six formats work end-to-end with version command + # ----------------------------------------------------------------------- + + Scenario Outline: All six formats work with the version command via global flag + When I invoke the CLI with global args "--format version" + Then the global format command should succeed + + Examples: + | format | + | json | + | yaml | + | plain | + | rich | + | table | + | color | + + # ----------------------------------------------------------------------- + # All six formats work end-to-end with info command + # ----------------------------------------------------------------------- + + Scenario Outline: All six formats work with the info command via global flag + When I invoke the CLI with global args "--format info" + Then the global format command should succeed + + Examples: + | format | + | json | + | yaml | + | plain | + | rich | + | table | + | color | + + # ----------------------------------------------------------------------- + # All six formats work end-to-end with diagnostics command + # ----------------------------------------------------------------------- + + Scenario Outline: All six formats work with the diagnostics command via global flag + When I invoke the CLI with global args "--format diagnostics" + Then the global format command should succeed + + Examples: + | format | + | json | + | yaml | + | plain | + | rich | + | table | + | color | diff --git a/features/steps/cli_global_format_flag_steps.py b/features/steps/cli_global_format_flag_steps.py new file mode 100644 index 000000000..8d1ab51d4 --- /dev/null +++ b/features/steps/cli_global_format_flag_steps.py @@ -0,0 +1,158 @@ +"""Step definitions for global --format flag propagation tests. + +Tests that the --format flag is a global option on the root `agents` command +(via the Typer callback), not a per-command option, per the specification. +""" + +from __future__ import annotations + +import json +from typing import Any +from unittest.mock import patch + +import yaml +from behave import given, then, when +from typer.testing import CliRunner + +from cleveragents.cli.main import app + + +@given("the global format flag test runner is set up") +def step_setup_runner(context: Any) -> None: + """Set up the test runner for global format flag tests.""" + context.runner = CliRunner() + context.cli_result = None + context.ctx_obj = {} + + +def _invoke_with_mocked_system(runner: CliRunner, args: list[str]) -> Any: + """Invoke the CLI app with mocked system commands.""" + with ( + patch( + "cleveragents.cli.commands.system.build_version_data", + return_value={ + "version": "3.7.0", + "python": "3.12.0", + "platform": "linux", + }, + ), + patch( + "cleveragents.cli.commands.system.render_version_rich", + ), + patch( + "cleveragents.cli.commands.system.build_info_data", + return_value={ + "version": "3.7.0", + "config_path": "/tmp/config.toml", + "db_url": "sqlite:///test.db", + }, + ), + patch( + "cleveragents.cli.commands.system.render_info_rich", + ), + patch( + "cleveragents.cli.commands.system.build_diagnostics_data", + return_value={ + "checks": [], + "has_errors": False, + }, + ), + patch( + "cleveragents.cli.commands.system.render_diagnostics_rich", + ), + ): + return runner.invoke(app, args) + + +@when('I invoke the CLI with global args "{args_str}"') +def step_invoke_cli_global_args(context: Any, args_str: str) -> None: + """Invoke the CLI with the given argument string.""" + args = args_str.split() + result = _invoke_with_mocked_system(context.runner, args) + context.cli_result = result + context.cli_output = result.output + + +@then("the global format command should succeed") +def step_global_format_command_succeeds(context: Any) -> None: + """Verify the command exited with code 0.""" + result = context.cli_result + assert result.exit_code == 0, ( + f"Expected exit code 0, got {result.exit_code}.\n" + f"Output: {result.output}\n" + f"Exception: {result.exception}" + ) + + +@then("the global format output should be valid JSON") +def step_global_format_output_is_valid_json(context: Any) -> None: + """Verify the output is parseable JSON.""" + output = context.cli_output.strip() + try: + parsed = json.loads(output) + context.parsed_json = parsed + except json.JSONDecodeError as exc: + raise AssertionError( + f"Output is not valid JSON: {exc}\nOutput was: {output!r}" + ) from exc + + +@then("the global format output should be valid YAML") +def step_global_format_output_is_valid_yaml(context: Any) -> None: + """Verify the output is parseable YAML.""" + output = context.cli_output.strip() + try: + parsed = yaml.safe_load(output) + context.parsed_yaml = parsed + except yaml.YAMLError as exc: + raise AssertionError( + f"Output is not valid YAML: {exc}\nOutput was: {output!r}" + ) from exc + + +@then("the global format output should contain plain text") +def step_global_format_output_contains_plain_text(context: Any) -> None: + """Verify the output contains plain key: value text.""" + output = context.cli_output.strip() + assert output, f"Expected non-empty plain text output, got: {output!r}" + + +@then('the global format JSON output should contain key "{key}"') +def step_global_format_json_output_contains_key(context: Any, key: str) -> None: + """Verify the parsed JSON output contains the given key.""" + parsed = getattr(context, "parsed_json", None) + if parsed is None: + output = context.cli_output.strip() + parsed = json.loads(output) + assert key in parsed, ( + f"Expected key '{key}' in JSON output. Keys found: {list(parsed.keys())}" + ) + + +@when('I call main_callback directly with format "{fmt}"') +def step_call_main_callback_directly(context: Any, fmt: str) -> None: + """Call main_callback via the CLI runner to test ctx.obj storage.""" + result = _invoke_with_mocked_system(context.runner, ["--format", fmt, "version"]) + context.cli_result = result + context.captured_fmt = fmt + + +@then('the context object should have format "{expected_fmt}"') +def step_ctx_obj_has_format(context: Any, expected_fmt: str) -> None: + """Verify the format was stored correctly. + + Since we can't easily inspect ctx.obj after the fact without modifying + the command, we verify indirectly: the command succeeded and the format + was accepted (no 'Invalid value' error for the format option). + """ + result = context.cli_result + # The command should succeed (format was accepted) + assert result.exit_code == 0, ( + f"Expected exit code 0 for format '{expected_fmt}', " + f"got {result.exit_code}.\nOutput: {result.output}" + ) + # Verify no "Invalid value" error for the format option + assert "Invalid value" not in result.output, ( + f"Got 'Invalid value' error for format '{expected_fmt}': {result.output}" + ) + context.verified_fmt = expected_fmt diff --git a/features/steps/cli_main_uncovered_branches_steps.py b/features/steps/cli_main_uncovered_branches_steps.py index 7f7d6b65a..8bff6c4d5 100644 --- a/features/steps/cli_main_uncovered_branches_steps.py +++ b/features/steps/cli_main_uncovered_branches_steps.py @@ -61,7 +61,8 @@ def step_version_non_rich(context: Any, fmt: str) -> None: return_value="formatted-version", ) as mock_fmt, ): - result = runner.invoke(app, ["version", "--format", fmt]) + # --format is now a global flag on the root command, not per-command + result = runner.invoke(app, ["--format", fmt, "version"]) context.cli_result = result context.mock_format_output = mock_fmt context.mock_render_rich = mock_render @@ -99,7 +100,8 @@ def step_info_non_rich(context: Any, fmt: str) -> None: return_value="formatted-info", ) as mock_fmt, ): - result = runner.invoke(app, ["info", "--format", fmt]) + # --format is now a global flag on the root command, not per-command + result = runner.invoke(app, ["--format", fmt, "info"]) context.cli_result = result context.mock_format_output = mock_fmt context.mock_render_rich = mock_render @@ -136,7 +138,8 @@ def step_diagnostics_non_rich(context: Any, fmt: str) -> None: return_value="formatted-diag", ) as mock_fmt, ): - result = runner.invoke(app, ["diagnostics", "--format", fmt]) + # --format is now a global flag on the root command, not per-command + result = runner.invoke(app, ["--format", fmt, "diagnostics"]) context.cli_result = result context.mock_format_output = mock_fmt context.mock_render_rich = mock_render diff --git a/robot/cli_consistency.robot b/robot/cli_consistency.robot index f08517e1f..de3ab6615 100644 --- a/robot/cli_consistency.robot +++ b/robot/cli_consistency.robot @@ -78,23 +78,23 @@ Completion Rejects Unsupported Shell 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 + [Documentation] --format json version should produce valid JSON (global flag) + ${result}= Run Process ${PYTHON} -m cleveragents --format json version ... 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 + [Documentation] --format yaml version should produce YAML with version key (global flag) + ${result}= Run Process ${PYTHON} -m cleveragents --format yaml version ... 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 + [Documentation] --format plain version should produce plain key-value output (global flag) + ${result}= Run Process ${PYTHON} -m cleveragents --format plain version ... timeout=120s on_timeout=kill Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} version: @@ -114,8 +114,8 @@ CLI Not Found Format Is Standardized 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 + [Documentation] --format json info should produce valid JSON (global flag) + ${result}= Run Process ${PYTHON} -m cleveragents --format json info ... timeout=120s on_timeout=kill Should Be Equal As Integers ${result.rc} 0 ${valid}= Is Valid Json ${result.stdout} diff --git a/robot/cli_core.robot b/robot/cli_core.robot index 88314f534..a2321dabf 100644 --- a/robot/cli_core.robot +++ b/robot/cli_core.robot @@ -28,8 +28,8 @@ Version Command Default Rich Format Should Contain ${result.stdout} CleverAgents Version Command JSON Format - [Documentation] Version command with --format json returns valid JSON - ${result}= Run Process ${PYTHON} -m cleveragents version --format json timeout=120s on_timeout=kill + [Documentation] Version command with global --format json returns valid JSON + ${result}= Run Process ${PYTHON} -m cleveragents --format json version timeout=120s on_timeout=kill Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} "version": "1.0.0" Should Contain ${result.stdout} "schema": "v3" @@ -40,15 +40,15 @@ Version Command JSON Format Should Contain ${result.stdout} "commit" Version Command Plain Format - [Documentation] Version command with --format plain returns key-value pairs - ${result}= Run Process ${PYTHON} -m cleveragents version --format plain timeout=120s on_timeout=kill + [Documentation] Version command with global --format plain returns key-value pairs + ${result}= Run Process ${PYTHON} -m cleveragents --format plain version timeout=120s on_timeout=kill Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} version: 1.0.0 Should Contain ${result.stdout} schema: v3 Version Command YAML Format - [Documentation] Version command with --format yaml returns YAML - ${result}= Run Process ${PYTHON} -m cleveragents version --format yaml timeout=120s on_timeout=kill + [Documentation] Version command with global --format yaml returns YAML + ${result}= Run Process ${PYTHON} -m cleveragents --format yaml version timeout=120s on_timeout=kill Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} version: 1.0.0 Should Contain ${result.stdout} schema: v3 @@ -61,8 +61,8 @@ Info Command Default Rich Format Should Contain ${result.stdout} Runtime Info Command JSON Format - [Documentation] Info command with --format json returns structured data - ${result}= Run CLI With Clean Home ${PYTHON} -m cleveragents info --format json + [Documentation] Info command with global --format json returns structured data + ${result}= Run CLI With Clean Home ${PYTHON} -m cleveragents --format json info Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} "version": "1.0.0" Should Contain ${result.stdout} "data_dir" @@ -70,8 +70,8 @@ Info Command JSON Format Should Contain ${result.stdout} "server_mode" Info Command Plain Format - [Documentation] Info command with --format plain returns key-value pairs - ${result}= Run CLI With Clean Home ${PYTHON} -m cleveragents info --format plain + [Documentation] Info command with global --format plain returns key-value pairs + ${result}= Run CLI With Clean Home ${PYTHON} -m cleveragents --format plain info Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} version: 1.0.0 Should Contain ${result.stdout} server_mode: @@ -84,8 +84,8 @@ Diagnostics Command Default Rich Format Should Contain ${result.stdout} Summary Diagnostics Command JSON Format - [Documentation] Diagnostics command with --format json returns structured data - ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=120s on_timeout=kill + [Documentation] Diagnostics command with global --format json returns structured data + ${result}= Run Process ${PYTHON} -m cleveragents --format json diagnostics timeout=120s on_timeout=kill Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} "checks" Should Contain ${result.stdout} "summary" @@ -95,8 +95,8 @@ Diagnostics Command JSON Format Should Contain ${result.stdout} "errors" Diagnostics Command Plain Format - [Documentation] Diagnostics command with --format plain returns key-value pairs - ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format plain timeout=120s on_timeout=kill + [Documentation] Diagnostics command with global --format plain returns key-value pairs + ${result}= Run Process ${PYTHON} -m cleveragents --format plain diagnostics timeout=120s on_timeout=kill Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} checks: Should Contain ${result.stdout} summary: @@ -104,7 +104,7 @@ Diagnostics Command Plain Format Diagnostics Command Check Flag Returns Valid Exit Code [Documentation] Diagnostics --check exits 0 when no critical errors are found. ... Disk-space errors are tolerated since CI runners may have low disk. - ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --check --format json timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents --format json diagnostics --check timeout=120s on_timeout=kill Should Contain ${result.stdout} "checks" Should Contain ${result.stdout} "has_errors" # Accept exit code 0 (clean) or 1 if the only error is disk space (CI environment) @@ -115,7 +115,7 @@ Diagnostics Command Check Flag Returns Valid Exit Code Diagnostics Command Performance [Documentation] Diagnostics command completes within acceptable time ${start}= Get Time epoch - ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents --format json diagnostics timeout=120s on_timeout=kill ${end}= Get Time epoch ${duration}= Evaluate ${end} - ${start} Should Be True ${duration} < 90 Diagnostics took too long: ${duration}s diff --git a/robot/cli_formats.robot b/robot/cli_formats.robot index 7716fc28e..1e562d139 100644 --- a/robot/cli_formats.robot +++ b/robot/cli_formats.robot @@ -1,5 +1,6 @@ *** Settings *** Documentation Smoke tests for CLI --format flag parity (json/yaml/plain/table/rich) +... and global --format flag propagation to subcommands. Resource ${CURDIR}/common.resource Suite Setup Setup Test Environment Suite Teardown Cleanup Test Environment @@ -33,3 +34,51 @@ Plan Status Format Plain Outputs Key-Value Pairs ${result}= Run Process ${PYTHON} ${HELPER} plan-status-plain cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} cli-formats-plan-status-plain-ok + +# --------------------------------------------------------------------------- +# Global --format flag propagation tests (Issue #2908) +# --------------------------------------------------------------------------- + +Global Format Flag JSON Propagates To Version Command + [Documentation] Verify that global ``--format json`` propagates to the version command + ${result}= Run Process ${PYTHON} ${HELPER} global-format-json-version cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} cli-global-format-json-version-ok + +Global Format Flag YAML Propagates To Version Command + [Documentation] Verify that global ``--format yaml`` propagates to the version command + ${result}= Run Process ${PYTHON} ${HELPER} global-format-yaml-version cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} cli-global-format-yaml-version-ok + +Global Format Flag Plain Propagates To Version Command + [Documentation] Verify that global ``--format plain`` propagates to the version command + ${result}= Run Process ${PYTHON} ${HELPER} global-format-plain-version cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} cli-global-format-plain-version-ok + +Global Format Flag JSON Propagates To Info Command + [Documentation] Verify that global ``--format json`` propagates to the info command + ${result}= Run Process ${PYTHON} ${HELPER} global-format-json-info cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} cli-global-format-json-info-ok + +Global Format Flag JSON Propagates To Diagnostics Command + [Documentation] Verify that global ``--format json`` propagates to the diagnostics command + ${result}= Run Process ${PYTHON} ${HELPER} global-format-json-diagnostics cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} cli-global-format-json-diagnostics-ok + +Global Format Shorthand Flag Works + [Documentation] Verify that global ``-f json`` shorthand propagates to the version command + ${result}= Run Process ${PYTHON} ${HELPER} global-format-shorthand cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} cli-global-format-shorthand-ok + +All Six Formats Work With Version Command + [Documentation] Verify all six output formats work via the global --format flag + ${result}= Run Process ${PYTHON} ${HELPER} global-format-all-six cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} cli-global-format-all-six-ok diff --git a/robot/e2e/wf07_cicd.robot b/robot/e2e/wf07_cicd.robot index 4bd92e2bb..cd640de02 100644 --- a/robot/e2e/wf07_cicd.robot +++ b/robot/e2e/wf07_cicd.robot @@ -264,7 +264,7 @@ WF07 E2E JSON Output Verification ... output from the version command. [Tags] E2E [Teardown] Log JSON Output Verification teardown complete - ${result}= Run CleverAgents Command version --format json + ${result}= Run CleverAgents Command --format json version Should Not Be Empty ${result.stdout} # Parse as JSON and verify structure ${stdout}= Set Variable ${result.stdout} diff --git a/robot/e2e/wf14_server_mode.robot b/robot/e2e/wf14_server_mode.robot index 30ec3b209..e8a63de01 100644 --- a/robot/e2e/wf14_server_mode.robot +++ b/robot/e2e/wf14_server_mode.robot @@ -59,7 +59,7 @@ WF14 E2E Diagnostics ... without a live server. Server-mode diagnostics (Server ... connectivity, Namespace membership) shown in the spec are ... not yet implemented in the diagnostics command (spec gap). - ${result}= Run CleverAgents Command diagnostics --format plain expected_rc=0 + ${result}= Run CleverAgents Command --format plain diagnostics expected_rc=0 # Basic diagnostic categories should appear in the output ${combined}= Set Variable ${result.stdout}\n${result.stderr} Should Contain Any ${combined} config Config configuration diff --git a/robot/helper_cli_formats.py b/robot/helper_cli_formats.py index 060f4968e..04859bcfb 100644 --- a/robot/helper_cli_formats.py +++ b/robot/helper_cli_formats.py @@ -133,11 +133,99 @@ def plan_status_plain() -> None: print("cli-formats-plan-status-plain-ok") +def global_format_json_version() -> None: + """Test global --format json propagates to version command.""" + from cleveragents.cli.main import app as main_app + + result = runner.invoke(main_app, ["--format", "json", "version"]) + assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}" + parsed = json.loads(result.output.strip()) + assert isinstance(parsed, dict) + assert "version" in parsed + print("cli-global-format-json-version-ok") + + +def global_format_yaml_version() -> None: + """Test global --format yaml propagates to version command.""" + from cleveragents.cli.main import app as main_app + + result = runner.invoke(main_app, ["--format", "yaml", "version"]) + assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}" + parsed = yaml.safe_load(result.output.strip()) + assert isinstance(parsed, dict) + assert "version" in parsed + print("cli-global-format-yaml-version-ok") + + +def global_format_plain_version() -> None: + """Test global --format plain propagates to version command.""" + from cleveragents.cli.main import app as main_app + + result = runner.invoke(main_app, ["--format", "plain", "version"]) + assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}" + assert "version:" in result.output + print("cli-global-format-plain-version-ok") + + +def global_format_json_info() -> None: + """Test global --format json propagates to info command.""" + from cleveragents.cli.main import app as main_app + + result = runner.invoke(main_app, ["--format", "json", "info"]) + assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}" + parsed = json.loads(result.output.strip()) + assert isinstance(parsed, dict) + print("cli-global-format-json-info-ok") + + +def global_format_json_diagnostics() -> None: + """Test global --format json propagates to diagnostics command.""" + from cleveragents.cli.main import app as main_app + + result = runner.invoke(main_app, ["--format", "json", "diagnostics"]) + assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}" + parsed = json.loads(result.output.strip()) + assert isinstance(parsed, dict) + print("cli-global-format-json-diagnostics-ok") + + +def global_format_shorthand() -> None: + """Test global -f json shorthand propagates to version command.""" + from cleveragents.cli.main import app as main_app + + result = runner.invoke(main_app, ["-f", "json", "version"]) + assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}" + parsed = json.loads(result.output.strip()) + assert isinstance(parsed, dict) + assert "version" in parsed + print("cli-global-format-shorthand-ok") + + +def global_format_all_six() -> None: + """Test all six output formats work via the global --format flag.""" + from cleveragents.cli.main import app as main_app + + formats = ["json", "yaml", "plain", "rich", "table", "color"] + for fmt in formats: + result = runner.invoke(main_app, ["--format", fmt, "version"]) + assert result.exit_code == 0, ( + f"Format '{fmt}' failed: exit={result.exit_code}: {result.output}" + ) + print("cli-global-format-all-six-ok") + + _COMMANDS = { "action-list-json": action_list_json, "action-show-yaml": action_show_yaml, "plan-list-json": plan_list_json, "plan-status-plain": plan_status_plain, + "global-format-json-version": global_format_json_version, + "global-format-yaml-version": global_format_yaml_version, + "global-format-plain-version": global_format_plain_version, + "global-format-json-info": global_format_json_info, + "global-format-json-diagnostics": global_format_json_diagnostics, + "global-format-shorthand": global_format_shorthand, + "global-format-all-six": global_format_all_six, } if __name__ == "__main__": diff --git a/src/cleveragents/cli/main.py b/src/cleveragents/cli/main.py index 58a3639bb..f1969a7e7 100644 --- a/src/cleveragents/cli/main.py +++ b/src/cleveragents/cli/main.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Annotated, Any import typer from cleveragents import __version__ +from cleveragents.cli.formatting import OutputFormat from cleveragents.core.exceptions import CleverAgentsError if TYPE_CHECKING: @@ -291,6 +292,7 @@ def show_secrets_callback(value: bool) -> None: @app.callback() def main_callback( + ctx: typer.Context, version: bool = typer.Option( None, "--version", @@ -305,6 +307,17 @@ def main_callback( is_eager=True, help="Reveal secrets in CLI output (default: masked)", ), + fmt: Annotated[ + OutputFormat, + typer.Option( + "--format", + "-f", + help=( + "Output format for all subcommands: " + "rich, color, table, plain, json, or yaml (default: rich)" + ), + ), + ] = OutputFormat.RICH, ) -> None: """CleverAgents - AI-powered development assistant.""" # Suppress debug-level logs on stdout for ALL commands so machine-readable @@ -315,15 +328,15 @@ def main_callback( configure_structlog(log_level="WARNING") _register_subcommands() + # Store the selected output format in the Typer context so all subcommands + # can read it via ctx.obj["format"] without needing their own --format flag. + ctx.ensure_object(dict) + ctx.obj["format"] = fmt.value + @app.command() def version( - fmt: str = typer.Option( - "rich", - "--format", - "-f", - help="Output format: rich, plain, json, yaml", - ), + ctx: typer.Context, ) -> None: """Display version information.""" from cleveragents.cli.commands.system import ( @@ -332,8 +345,11 @@ def version( ) from cleveragents.cli.formatting import format_output + ctx.ensure_object(dict) + fmt: str = ctx.obj.get("format", OutputFormat.RICH.value) + data = build_version_data() - if fmt.lower() == "rich": + if fmt.lower() == OutputFormat.RICH.value: render_version_rich(data) else: typer.echo(format_output(data, fmt)) @@ -341,18 +357,16 @@ def version( @app.command() def info( - fmt: str = typer.Option( - "rich", - "--format", - "-f", - help="Output format: rich, plain, json, yaml", - ), + ctx: typer.Context, ) -> None: """Display system information and configuration.""" from cleveragents.cli.commands.system import build_info_data, render_info_rich from cleveragents.cli.formatting import format_output - if fmt.lower() != "rich": + ctx.ensure_object(dict) + fmt: str = ctx.obj.get("format", OutputFormat.RICH.value) + + if fmt.lower() != OutputFormat.RICH.value: # Suppress debug-level log output so machine-readable formats # (json, yaml, plain) receive clean stdout without log lines mixed in. from cleveragents.config.logging import configure_structlog @@ -360,7 +374,7 @@ def info( configure_structlog(log_level="WARNING") data = build_info_data() - if fmt.lower() == "rich": + if fmt.lower() == OutputFormat.RICH.value: render_info_rich(data) else: typer.echo(format_output(data, fmt)) @@ -368,17 +382,12 @@ def info( @app.command() def diagnostics( + ctx: typer.Context, check: bool = typer.Option( False, "--check", help="Exit non-zero when any check fails", ), - fmt: str = typer.Option( - "rich", - "--format", - "-f", - help="Output format: rich, plain, json, yaml", - ), ) -> None: """Run system diagnostics and health checks.""" from cleveragents.cli.commands.system import ( @@ -387,13 +396,16 @@ def diagnostics( ) from cleveragents.cli.formatting import format_output - if fmt.lower() != "rich": + ctx.ensure_object(dict) + fmt: str = ctx.obj.get("format", OutputFormat.RICH.value) + + if fmt.lower() != OutputFormat.RICH.value: from cleveragents.config.logging import configure_structlog configure_structlog(log_level="WARNING") data = build_diagnostics_data() - if fmt.lower() == "rich": + if fmt.lower() == OutputFormat.RICH.value: render_diagnostics_rich(data) else: typer.echo(format_output(data, fmt)) -- 2.52.0