fix(cli): add --namespace/-n option to agents plan list command #2616
@@ -61,6 +61,31 @@ Feature: Plan CLI spec alignment
|
||||
And the plan spec use should pass argument "target_coverage" with value 80
|
||||
|
||||
# ---- plan list: filter combinations ----
|
||||
Scenario: Plan list with --namespace filter (long form)
|
||||
Given plan spec alignment plans exist
|
||||
When I run plan list with namespace "myteam"
|
||||
Then the plan spec list should succeed
|
||||
And the plan spec list should pass namespace "myteam" to service
|
||||
|
||||
Scenario: Plan list with -n namespace filter (short form)
|
||||
Given plan spec alignment plans exist
|
||||
When I run plan list with short namespace "local"
|
||||
Then the plan spec list should succeed
|
||||
And the plan spec list should pass namespace "local" to service
|
||||
|
||||
Scenario: Plan list with --namespace shows Namespace in Filters panel
|
||||
Given plan spec alignment plans exist
|
||||
When I run plan list with namespace "myteam"
|
||||
Then the plan spec list should succeed
|
||||
And the plan spec list output should contain "Namespace"
|
||||
And the plan spec list output should contain "myteam"
|
||||
|
||||
Scenario: Plan list with --namespace combined with --state
|
||||
Given plan spec alignment plans exist
|
||||
When I run plan list filtering by namespace "myteam" and processing state "queued"
|
||||
Then the plan spec list should succeed
|
||||
And the plan spec list should pass namespace "myteam" to service
|
||||
|
||||
Scenario: Plan list with --phase filter
|
||||
Given plan spec alignment plans exist
|
||||
When I run plan list with phase "strategize"
|
||||
|
||||
@@ -280,6 +280,30 @@ def step_plan_use_arg(context: Context, arg_str: str) -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I run plan list with namespace "{namespace}"')
|
||||
def step_plan_list_namespace(context: Context, namespace: str) -> None:
|
||||
"""Run list with --namespace filter (long form)."""
|
||||
context.result = context.runner.invoke(plan_app, ["list", "--namespace", namespace])
|
||||
|
||||
|
||||
@when('I run plan list with short namespace "{namespace}"')
|
||||
def step_plan_list_short_namespace(context: Context, namespace: str) -> None:
|
||||
"""Run list with -n namespace filter (short form)."""
|
||||
context.result = context.runner.invoke(plan_app, ["list", "-n", namespace])
|
||||
|
||||
|
||||
@when(
|
||||
'I run plan list filtering by namespace "{namespace}" and processing state "{state}"'
|
||||
)
|
||||
def step_plan_list_namespace_and_state(
|
||||
context: Context, namespace: str, state: str
|
||||
) -> None:
|
||||
"""Run list with --namespace and --state combined."""
|
||||
context.result = context.runner.invoke(
|
||||
plan_app, ["list", "--namespace", namespace, "--state", state]
|
||||
)
|
||||
|
||||
|
||||
@when('I run plan list with phase "{phase}"')
|
||||
def step_plan_list_phase(context: Context, phase: str) -> None:
|
||||
"""Run list with --phase filter."""
|
||||
@@ -416,6 +440,30 @@ def step_plan_list_ok(context: Context) -> None:
|
||||
)
|
||||
|
||||
|
||||
@then('the plan spec list should pass namespace "{namespace}" to service')
|
||||
def step_plan_list_namespace_passed(context: Context, namespace: str) -> None:
|
||||
"""Verify the namespace was passed to list_plans service call."""
|
||||
call_kwargs = context.mock_service.list_plans.call_args
|
||||
assert call_kwargs is not None, "list_plans was not called"
|
||||
# Support both positional and keyword argument styles
|
||||
kwargs = call_kwargs[1] if call_kwargs[1] else {}
|
||||
args = call_kwargs[0] if call_kwargs[0] else ()
|
||||
passed_namespace = kwargs.get("namespace", args[0] if args else None)
|
||||
assert passed_namespace == namespace, (
|
||||
f"Expected namespace='{namespace}', got namespace='{passed_namespace}'"
|
||||
)
|
||||
|
||||
|
||||
@then('the plan spec list output should contain "{text}"')
|
||||
def step_plan_list_output_contains(context: Context, text: str) -> None:
|
||||
"""Verify the list output contains expected text (case-insensitive)."""
|
||||
output_lower = context.result.output.lower()
|
||||
text_lower = text.lower()
|
||||
assert text_lower in output_lower, (
|
||||
f"Expected '{text}' in list output but got:\n{context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the plan spec status should succeed")
|
||||
def step_plan_status_ok(context: Context) -> None:
|
||||
"""Verify plan status succeeded."""
|
||||
|
||||
@@ -8,7 +8,9 @@ from cleveragents.domain.models.core.plan import NamespacedName
|
||||
|
||||
|
||||
@when('I parse the namespaced name "{full_name}" expecting an error')
|
||||
def step_when_parse_namespaced_name_expecting_error(context: Context, full_name: str) -> None:
|
||||
def step_when_parse_namespaced_name_expecting_error(
|
||||
context: Context, full_name: str
|
||||
) -> None:
|
||||
"""Parse a namespaced name string and capture any error."""
|
||||
try:
|
||||
context.namespaced_name = NamespacedName.parse(full_name)
|
||||
@@ -18,8 +20,12 @@ def step_when_parse_namespaced_name_expecting_error(context: Context, full_name:
|
||||
context.namespaced_name = None
|
||||
|
||||
|
||||
@when('I construct a NamespacedName with namespace "{namespace}" and name "{name}" expecting an error')
|
||||
def step_when_construct_namespaced_name_expecting_error(context: Context, namespace: str, name: str) -> None:
|
||||
@when(
|
||||
'I construct a NamespacedName with namespace "{namespace}" and name "{name}" expecting an error'
|
||||
)
|
||||
def step_when_construct_namespaced_name_expecting_error(
|
||||
context: Context, namespace: str, name: str
|
||||
) -> None:
|
||||
"""Create a NamespacedName and capture any error."""
|
||||
try:
|
||||
context.namespaced_name = NamespacedName(namespace=namespace, name=name)
|
||||
|
||||
@@ -71,3 +71,19 @@ Plan Lifecycle List Shows Plans
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} cli-lifecycle-plan-list-ok
|
||||
|
||||
Plan List Namespace Option Filters By Namespace
|
||||
[Documentation] Verify agents plan list --namespace myteam returns only plans in that namespace
|
||||
${result}= Run Process ${PYTHON} ${HELPER} plan-list-namespace cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} cli-lifecycle-plan-list-namespace-ok
|
||||
|
||||
Plan List Short Namespace Option Works
|
||||
[Documentation] Verify agents plan list -n myteam short-form alias works correctly
|
||||
${result}= Run Process ${PYTHON} ${HELPER} plan-list-namespace-short cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} cli-lifecycle-plan-list-namespace-short-ok
|
||||
|
||||
@@ -322,6 +322,84 @@ def full_lifecycle() -> None:
|
||||
os.unlink(yaml_path)
|
||||
|
||||
|
||||
def plan_list_namespace() -> None:
|
||||
"""Verify plan list --namespace myteam filters plans by namespace."""
|
||||
mock_service = MagicMock()
|
||||
# Only plans in "myteam" namespace should be returned by the service
|
||||
mock_service.list_plans.return_value = [
|
||||
_mock_plan(name="myteam/plan-a", plan_id="01KHDE6WWS2171PWW3GJEBXZ8C"),
|
||||
]
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
):
|
||||
result = runner.invoke(plan_app, ["list", "--namespace", "myteam"])
|
||||
if result.exit_code != 0:
|
||||
print(
|
||||
f"FAIL: plan list --namespace returned {result.exit_code}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(result.output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Verify the namespace was passed to the service
|
||||
call_kwargs = mock_service.list_plans.call_args
|
||||
passed_namespace = call_kwargs[1].get("namespace") if call_kwargs[1] else None
|
||||
if passed_namespace != "myteam":
|
||||
print(
|
||||
"FAIL: expected namespace='myteam', "
|
||||
f"got namespace='{passed_namespace}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Verify Filters panel shows Namespace
|
||||
if "Namespace" not in result.output or "myteam" not in result.output:
|
||||
print(
|
||||
"FAIL: Filters panel missing 'Namespace: myteam'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(result.output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("cli-lifecycle-plan-list-namespace-ok")
|
||||
|
||||
|
||||
def plan_list_namespace_short() -> None:
|
||||
"""Verify plan list -n myteam short-form alias works correctly."""
|
||||
mock_service = MagicMock()
|
||||
mock_service.list_plans.return_value = [
|
||||
_mock_plan(name="myteam/plan-b", plan_id="01KHDE6WWS2171PWW3GJEBXZ8D"),
|
||||
]
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
):
|
||||
result = runner.invoke(plan_app, ["list", "-n", "myteam"])
|
||||
if result.exit_code != 0:
|
||||
print(
|
||||
f"FAIL: plan list -n returned {result.exit_code}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(result.output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Verify the namespace was passed to the service via short form
|
||||
call_kwargs = mock_service.list_plans.call_args
|
||||
passed_namespace = call_kwargs[1].get("namespace") if call_kwargs[1] else None
|
||||
if passed_namespace != "myteam":
|
||||
print(
|
||||
"FAIL: expected namespace='myteam', "
|
||||
f"got namespace='{passed_namespace}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
print("cli-lifecycle-plan-list-namespace-short-ok")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main dispatcher
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -334,6 +412,8 @@ _COMMANDS = {
|
||||
"plan-status": plan_status,
|
||||
"plan-cancel": plan_cancel,
|
||||
"plan-list": plan_list,
|
||||
"plan-list-namespace": plan_list_namespace,
|
||||
"plan-list-namespace-short": plan_list_namespace_short,
|
||||
"full-lifecycle": full_lifecycle,
|
||||
}
|
||||
|
||||
|
||||
@@ -2311,6 +2311,14 @@ def lifecycle_list_plans(
|
||||
help="Optional regex pattern to filter plan names",
|
||||
),
|
||||
] = None,
|
||||
namespace: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--namespace",
|
||||
"-n",
|
||||
help="Filter plans by namespace",
|
||||
),
|
||||
] = None,
|
||||
phase: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
@@ -2363,6 +2371,12 @@ def lifecycle_list_plans(
|
||||
|
||||
This command lists plans created through the new plan lifecycle
|
||||
(Strategize -> Execute -> Apply -> Applied).
|
||||
|
||||
Examples:
|
||||
agents plan list
|
||||
agents plan list --namespace myteam
|
||||
agents plan list -n myteam --state processing
|
||||
agents plan list --phase strategize
|
||||
"""
|
||||
try:
|
||||
import re
|
||||
@@ -2399,7 +2413,9 @@ def lifecycle_list_plans(
|
||||
)
|
||||
raise typer.Abort() from exc
|
||||
|
||||
plans = service.list_plans(phase=phase_filter, project_name=project_id)
|
||||
plans = service.list_plans(
|
||||
namespace=namespace, phase=phase_filter, project_name=project_id
|
||||
)
|
||||
|
||||
# Apply processing state filter
|
||||
if state_filter:
|
||||
@@ -2472,6 +2488,11 @@ def lifecycle_list_plans(
|
||||
|
||||
# Display Filters panel (only if any filter is active)
|
||||
active_filters = []
|
||||
if namespace:
|
||||
active_filters.append(f"[yellow]Namespace:[/yellow] {namespace}")
|
||||
else:
|
||||
active_filters.append("[yellow]Namespace:[/yellow] (any)")
|
||||
|
||||
if phase_filter:
|
||||
active_filters.append(f"[yellow]Phase:[/yellow] {phase_filter}")
|
||||
else:
|
||||
@@ -2493,7 +2514,7 @@ def lifecycle_list_plans(
|
||||
active_filters.append("[yellow]Action:[/yellow] (any)")
|
||||
|
||||
# Only show Filters panel if at least one filter is active
|
||||
if phase_filter or state_filter or project_id or action_filter:
|
||||
if namespace or phase_filter or state_filter or project_id or action_filter:
|
||||
filters_text = "\n".join(active_filters)
|
||||
filters_panel = Panel(filters_text, title="Filters", border_style="dim")
|
||||
console.print(filters_panel)
|
||||
|
||||
Reference in New Issue
Block a user