fix(cli): raise on subcommand registration failure instead of partial silent return #3264

Merged
freemo merged 1 commits from fix/cli-incomplete-subcommand-registration-on-error into master 2026-04-05 21:13:59 +00:00
3 changed files with 153 additions and 2 deletions
@@ -0,0 +1,119 @@
"""Step definitions for tdd_cli_incomplete_subcommand_registration.feature.
This test captures bug #2604: _register_subcommands() in
src/cleveragents/cli/main.py catches all exceptions during subcommand
import and registration, then silently returns instead of raising.
This violates CONTRIBUTING.md error-handling standards.
The fix replaces the bare ``return`` with ``raise SystemExit(1)`` so the
CLI exits with a non-zero exit code instead of entering a partially
initialized state.
Once the fix for #2604 is merged, the ``@tdd_expected_fail`` scenario
(which asserts the old silent-return behaviour) must be removed so the
remaining scenarios run as regression guards.
"""
from __future__ import annotations
from unittest.mock import patch
from behave import given, then, when # type: ignore[import-untyped]
from behave.runner import Context # type: ignore[import-untyped]
from typer.testing import CliRunner
runner = CliRunner(mix_stderr=False)
@given("the CLI subcommand import raises an ImportError during registration")
def step_given_import_error(context: Context) -> None:
"""Arrange: patch the subcommand import to raise ImportError."""
context.import_error = ImportError("simulated subcommand import failure #2604")
@when("the CLI is invoked with any command")
def step_when_cli_invoked(context: Context) -> None:
"""Act: invoke the CLI while the subcommand import is broken."""
import cleveragents.cli.main as cli_main
# Reset the registration flag so _register_subcommands() runs again.
original_flag = cli_main._subcommands_registered
cli_main._subcommands_registered = False
try:
with patch(
"cleveragents.cli.main._register_subcommands",
side_effect=context.import_error,
):
# We patch _register_subcommands itself to raise so we can test
# the caller's behaviour without needing to break real imports.
# The real fix is inside _register_subcommands, but the caller
# (ensure_cli_commands_registered / the app callback) must also
# propagate the SystemExit.
from cleveragents.cli.main import app
result = runner.invoke(app, ["--help"], catch_exceptions=True)
context.cli_result = result
finally:
cli_main._subcommands_registered = original_flag
@then("the CLI exits with a non-zero exit code")
def step_then_non_zero_exit(context: Context) -> None:
"""Assert: the CLI must not exit with 0 when registration fails."""
assert context.cli_result.exit_code != 0, (
f"Bug #2604: CLI exited with code 0 after subcommand registration "
f"failure. The CLI must exit with a non-zero code to signal the "
f"error. Got exit_code={context.cli_result.exit_code}. "
f"Output: {context.cli_result.output!r}"
)
@then('the error output contains "Failed to register subcommands"')
def step_then_error_message(context: Context) -> None:
"""Assert: the error message must be present in the output."""
# CliRunner with mix_stderr=False separates stdout and stderr.
# The error is printed to stderr via get_err_console().
combined = (context.cli_result.output or "") + (
getattr(context.cli_result, "stderr", "") or ""
)
# When _register_subcommands itself is patched to raise, the message
# won't appear (it's inside the function). We test the exit code path
# separately. For the message test, we need to invoke the real function
# with a broken import.
import cleveragents.cli.main as cli_main
original_flag = cli_main._subcommands_registered
cli_main._subcommands_registered = False
try:
# Patch the internal import inside _register_subcommands by making
# the first import inside the try block raise.
with patch(
"cleveragents.cli.commands.project",
side_effect=ImportError("simulated import failure #2604"),
):
from cleveragents.cli.main import app
result = runner.invoke(app, ["--help"], catch_exceptions=True)
combined = (result.output or "") + (getattr(result, "stderr", "") or "")
finally:
cli_main._subcommands_registered = original_flag
assert "Failed to register subcommands" in combined, (
f"Bug #2604: Expected 'Failed to register subcommands' in CLI output "
f"when subcommand registration fails, but got: {combined!r}"
)
@then("the CLI exits with exit code 0")
def step_then_zero_exit(context: Context) -> None:
"""Assert the OLD (buggy) behaviour: CLI exits with 0 on registration error.
This scenario is tagged @tdd_expected_fail because it asserts the bug.
Once the fix is merged, this scenario must be removed.
"""
assert context.cli_result.exit_code == 0, (
f"Expected exit code 0 (old buggy behaviour), "
f"got {context.cli_result.exit_code}"
)
@@ -0,0 +1,32 @@
@tdd_issue @tdd_issue_2604
Feature: TDD Issue #2604 — Incomplete subcommand registration on error
As a CLI user
I want the CLI to fail loudly when subcommand registration encounters an error
So that the CLI is never left in a partially initialized state
This test captures bug #2604. The _register_subcommands() function in
src/cleveragents/cli/main.py catches all exceptions during subcommand
import and registration, prints a traceback, then silently returns.
This violates CONTRIBUTING.md error-handling standards: "Errors must
never be suppressed. Exceptions should propagate to the top-level
execution handler."
The fix replaces the bare `return` with `raise SystemExit(1)` so the
CLI exits with a non-zero exit code instead of entering a partially
initialized state.
Scenario: Bug #2604 — subcommand registration failure exits with non-zero code
Given the CLI subcommand import raises an ImportError during registration
When the CLI is invoked with any command
Then the CLI exits with a non-zero exit code
Scenario: Bug #2604 — subcommand registration failure prints error message
Given the CLI subcommand import raises an ImportError during registration
When the CLI is invoked with any command
Then the error output contains "Failed to register subcommands"
@tdd_expected_fail
Scenario: Bug #2604 — old behaviour silently returns on registration error
Given the CLI subcommand import raises an ImportError during registration
When the CLI is invoked with any command
Then the CLI exits with exit code 0
+2 -2
View File
@@ -101,13 +101,13 @@ def _register_subcommands() -> None:
from cleveragents.cli.commands.db import app as db_app
from cleveragents.cli.commands.repl import _repl_app
from cleveragents.cli.commands.server import app as server_app
except Exception as exc: # pragma: no cover
except Exception as exc:
import traceback
err = get_err_console()
err.print(f"[red]Failed to register subcommands:[/red] {exc}")
err.print(traceback.format_exc())
return
raise SystemExit(1) from exc
app.add_typer(project.app, name="project", help="Project management")