fix(cli): add project switch command
CI / load-versions (pull_request) Successful in 17s
CI / push-validation (pull_request) Successful in 25s
CI / quality (pull_request) Successful in 54s
CI / lint (pull_request) Successful in 1m1s
CI / security (pull_request) Successful in 1m9s
CI / typecheck (pull_request) Successful in 1m16s
CI / build (pull_request) Successful in 30s
CI / helm (pull_request) Successful in 36s
CI / unit_tests (pull_request) Successful in 6m3s
CI / docker (pull_request) Successful in 2m16s
CI / integration_tests (pull_request) Successful in 8m28s
CI / coverage (pull_request) Successful in 12m21s
CI / status-check (pull_request) Successful in 3s

ISSUES CLOSED: #8675
This commit is contained in:
CleverAgents Bot
2026-06-17 23:31:23 -04:00
committed by Forgejo
parent fe3b9b22ca
commit d0e685aed4
5 changed files with 250 additions and 8 deletions
+42
View File
@@ -0,0 +1,42 @@
Feature: Project CLI switch command
As a developer
I want to switch between registered projects
So that I can manage my active project without changing directories
Background:
Given a project CLI commands test database is initialized
# switch command
Scenario: switch command with rich output sets active project
Given a project "local/switch-proj" is created in the commands DB
When I invoke project-switch for "local/switch-proj" with yes default format
Then the project cmd output should contain "Active Project Switched"
And the project cmd output should contain "switch-proj"
And the project cmd should succeed
Scenario: switch command with json format
Given a project "local/switch-json" is created in the commands DB
When I invoke project-switch for "local/switch-json" with yes format "json"
Then the project cmd output should contain "active"
And the project cmd JSON data should include "previous" key
Scenario: switch to nonexistent project fails
When I invoke project-switch for "local/ghost-proj" with yes default format
Then the project cmd should fail
Scenario: switch command with yaml format
Given a project "local/switch-yaml" is created in the commands DB
When I invoke project-switch for "local/switch-yaml" with yes format "yaml"
Then the project cmd should succeed
Scenario: switch accepts --yes to skip confirmation
Given a project "local/switch-yes" is created in the commands DB
When I invoke project-switch for "local/switch-yes" with yes and short flag
Then the project cmd output should contain "Active Project Switched"
And the project cmd should succeed
Scenario: switch command can be interrupted by user confirmation denial
Given a project "local/switch-cancel" is created in the commands DB
When I invoke project-switch for "local/switch-cancel" without yes
Then the project cmd should fail
+85 -8
View File
@@ -162,6 +162,25 @@ def _capture(context: Any, func: Any, *args: Any, **kwargs: Any) -> None:
context._cmd_failed = failed
def _load_cmd_json_output(context: Any) -> dict[str, Any]:
"""Parse a JSON command envelope from captured CLI output."""
output = context._cmd_output.strip()
json_start = output.find("{")
if json_start > 0:
output = output[json_start:]
try:
envelope = json.loads(output)
except json.JSONDecodeError as exc:
raise AssertionError(
f"Command output is not valid JSON:\n{context._cmd_output}"
) from exc
assert isinstance(envelope, dict), (
f"Expected command JSON envelope to be a dict, got "
f"{type(envelope).__name__}: {envelope}"
)
return envelope
# ---------------------------------------------------------------------------
# Reusable helpers
# ---------------------------------------------------------------------------
@@ -652,11 +671,76 @@ def step_invoke_delete_short_force(context: Any, name: str) -> None:
_unpatch_project_mod()
# ---------------------------------------------------------------------------
# Switch command
# ---------------------------------------------------------------------------
@when('I invoke project-switch for "{name}" with yes default format')
def step_invoke_switch(context: Any, name: str) -> None:
from cleveragents.cli.commands.project import switch
_capture(context, switch, project=name, yes=True)
@when('I invoke project-switch for "{name}" with yes format "{fmt}"')
def step_invoke_switch_fmt(context: Any, name: str, fmt: str) -> None:
from cleveragents.cli.commands.project import switch
_capture(context, switch, project=name, yes=True, output_format=fmt)
@when('I invoke project-switch for "{name}" without yes')
def step_invoke_switch_no_confirm(context: Any, name: str) -> None:
"""Invoke switch without --yes, denying the confirmation prompt."""
from typer.testing import CliRunner
from cleveragents.cli.commands.project import app as project_app
_patch_project_mod(context)
try:
cli_runner = CliRunner()
result = cli_runner.invoke(project_app, ["switch", name], input="n\n")
context._cmd_output = result.output
context._cmd_failed = result.exit_code != 0
finally:
_unpatch_project_mod()
@when('I invoke project-switch for "{name}" with yes and short flag')
def step_invoke_switch_short_flag(context: Any, name: str) -> None:
"""Invoke 'project switch' via CliRunner using the ``-y`` short flag."""
from typer.testing import CliRunner
from cleveragents.cli.commands.project import app as project_app
_patch_project_mod(context)
try:
cli_runner = CliRunner()
result = cli_runner.invoke(project_app, ["switch", name, "-y"])
context._cmd_output = result.output
context._cmd_failed = result.exit_code != 0
finally:
_unpatch_project_mod()
# ---------------------------------------------------------------------------
# Then assertions
# ---------------------------------------------------------------------------
@then('the project cmd JSON data should include "previous" key')
def step_cmd_json_has_previous(context: Any) -> None:
envelope = _load_cmd_json_output(context)
data = envelope.get("data")
assert isinstance(data, dict), (
f"Expected envelope data to be a dict, got {type(data).__name__}: {data}"
)
assert "previous" in data, (
f"Expected 'previous' in command JSON data, got keys {list(data.keys())}"
)
@then('the project cmd output should contain "{text}"')
def step_cmd_output_contains(context: Any, text: str) -> None:
assert text.lower() in context._cmd_output.lower(), (
@@ -680,14 +764,7 @@ def step_cmd_fail(context: Any) -> None:
@then("the project cmd JSON data should include deleted_at")
def step_cmd_json_has_deleted_at(context: Any) -> None:
output = context._cmd_output.strip()
try:
envelope = json.loads(output)
except json.JSONDecodeError as exc:
raise AssertionError(
f"Command output is not valid JSON:\n{context._cmd_output}"
) from exc
envelope = _load_cmd_json_output(context)
data = envelope.get("data")
assert isinstance(data, dict), (
f"Expected envelope data to be a dict, got {type(data).__name__}: {data}"