diff --git a/features/config_cli_safety_net_coverage.feature b/features/config_cli_safety_net_coverage.feature index 848422d10..ab1d69240 100644 --- a/features/config_cli_safety_net_coverage.feature +++ b/features/config_cli_safety_net_coverage.feature @@ -3,6 +3,15 @@ Feature: Config CLI safety-net coverage I want thorough safety-net tests for every function in config.py So that 100% line and branch coverage is maintained as the code evolves + # ===================================================================== + # _get_config_dir (L53-58) - CLEVERAGENTS_HOME env var path + # ===================================================================== + + Scenario: safety-net _get_config_dir returns CLEVERAGENTS_HOME path when set + Given the safety-net env var "CLEVERAGENTS_HOME" is set to "/tmp/sn-cleveragents-home-xyz" + When the safety-net config dir resolver runs + Then the safety-net config dir result should be "/tmp/sn-cleveragents-home-xyz" + # ===================================================================== # _normalize_key (L91-96) # ===================================================================== diff --git a/features/plan_cli_spec_alignment.feature b/features/plan_cli_spec_alignment.feature index a6d38419e..3c375ccc8 100644 --- a/features/plan_cli_spec_alignment.feature +++ b/features/plan_cli_spec_alignment.feature @@ -86,6 +86,11 @@ Feature: Plan CLI spec alignment When I run plan list with action "local/test-action" Then the plan spec list should succeed + Scenario: Plan list with --namespace filter + Given plan spec alignment plans exist + When I run plan list with namespace "myteam" + Then the plan spec list should succeed + Scenario: Plan list with regex filter Given plan spec alignment plans exist When I run plan list with regex "test-action" diff --git a/features/steps/config_cli_safety_net_coverage_steps.py b/features/steps/config_cli_safety_net_coverage_steps.py index 3698ed558..88c936c37 100644 --- a/features/steps/config_cli_safety_net_coverage_steps.py +++ b/features/steps/config_cli_safety_net_coverage_steps.py @@ -34,6 +34,7 @@ from cleveragents.cli.commands._config_helpers import ( _validate_key, ) from cleveragents.cli.commands.config import ( + _get_config_dir, _read_config_file, _resolution_chain, _resolve_source, @@ -201,6 +202,23 @@ def step_sn_mock_modified_field(context: Context) -> None: context._cleanup_handlers.append(context._sn_resolve_patch3.stop) +# =================================================================== +# _get_config_dir (L53-58) - CLEVERAGENTS_HOME env var path +# =================================================================== + + +@when("the safety-net config dir resolver runs") +def step_sn_get_config_dir(context: Context) -> None: + context._sn_config_dir = _get_config_dir() + + +@then('the safety-net config dir result should be "{expected}"') +def step_sn_config_dir_equals(context: Context, expected: str) -> None: + assert str(context._sn_config_dir) == expected, ( + f"Expected '{expected}', got '{context._sn_config_dir}'" + ) + + # =================================================================== # _normalize_key (L91-96) # =================================================================== diff --git a/features/steps/plan_cli_spec_alignment_steps.py b/features/steps/plan_cli_spec_alignment_steps.py index 0c495b911..05180db3f 100644 --- a/features/steps/plan_cli_spec_alignment_steps.py +++ b/features/steps/plan_cli_spec_alignment_steps.py @@ -312,6 +312,12 @@ def step_plan_list_action(context: Context, action: str) -> None: context.result = context.runner.invoke(plan_app, ["list", "--action", action]) +@when('I run plan list with namespace "{namespace}"') +def step_plan_list_namespace(context: Context, namespace: str) -> None: + """Run list with --namespace filter.""" + context.result = context.runner.invoke(plan_app, ["list", "--namespace", namespace]) + + @when('I run plan list with regex "{regex}"') def step_plan_list_regex(context: Context, regex: str) -> None: """Run list with a regex positional argument.""" diff --git a/robot/automation_profile_cli.robot b/robot/automation_profile_cli.robot index 726847613..2feb4d659 100644 --- a/robot/automation_profile_cli.robot +++ b/robot/automation_profile_cli.robot @@ -22,7 +22,7 @@ Show Automation Profile Show Automation Profile JSON [Documentation] Show a profile in JSON format - [Tags] tdd_issue tdd_issue_4204 tdd_expected_fail + [Tags] tdd_issue tdd_issue_4204 ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} show-json cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} show-json-ok @@ -41,7 +41,7 @@ List Automation Profiles List Automation Profiles JSON [Documentation] List profiles in JSON format - [Tags] tdd_issue tdd_issue_4204 tdd_expected_fail + [Tags] tdd_issue tdd_issue_4204 ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} list-json cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} list-json-ok @@ -60,7 +60,7 @@ Remove Built-in Profile Fails All Automation Profile CLI Tests [Documentation] Run all automation-profile CLI tests - [Tags] tdd_issue tdd_issue_4204 tdd_expected_fail + [Tags] tdd_issue tdd_issue_4204 ${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} all cwd=${WORKSPACE} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} all-tests-ok diff --git a/robot/config_cli.robot b/robot/config_cli.robot index 81de6d087..71073e604 100644 --- a/robot/config_cli.robot +++ b/robot/config_cli.robot @@ -26,7 +26,7 @@ Config List JSON Format Config Set Get Roundtrip [Documentation] Verify that ``config set`` followed by ``config get`` returns the value - [Tags] tdd_issue tdd_issue_4302 tdd_expected_fail + [Tags] tdd_issue tdd_issue_4302 ${result}= Run Process ${PYTHON} ${HELPER} set-get-roundtrip cwd=${WORKSPACE} Log ${result.stdout} diff --git a/robot/helper_automation_profile_cli.py b/robot/helper_automation_profile_cli.py index d1a71ebca..05a26a1c6 100644 --- a/robot/helper_automation_profile_cli.py +++ b/robot/helper_automation_profile_cli.py @@ -149,7 +149,9 @@ def test_show_json() -> None: assert result.exit_code == 0, f"show json failed: {result.output}" parsed = _extract_json(result.output) assert isinstance(parsed, dict) - assert parsed["name"] == "manual" + # JSON output is wrapped in a CLI response envelope: {"data": {...}, ...} + data = parsed.get("data", parsed) + assert data["name"] == "manual" print("show-json-ok") @@ -186,9 +188,11 @@ def test_list_json() -> None: f"Expected dict at top level, got {type(parsed).__name__}. " f"Output: {result.output[:300]}" ) + # JSON output is wrapped in a CLI response envelope: {"data": {...}, ...} + inner = parsed.get("data", parsed) # Validate profiles wrapper - assert "profiles" in parsed, f"Missing 'profiles' key. Keys: {list(parsed.keys())}" - profiles = parsed["profiles"] + assert "profiles" in inner, f"Missing 'profiles' key. Keys: {list(inner.keys())}" + profiles = inner["profiles"] assert isinstance(profiles, list), ( f"'profiles' must be a list, got {type(profiles)}" ) @@ -202,8 +206,8 @@ def test_list_json() -> None: "Full profile dict leaked into list output (found 'phase_transitions')" ) # Validate summary - assert "summary" in parsed, f"Missing 'summary' key. Keys: {list(parsed.keys())}" - summary = parsed["summary"] + assert "summary" in inner, f"Missing 'summary' key. Keys: {list(inner.keys())}" + summary = inner["summary"] assert "built_in" in summary, f"Missing 'built_in' in summary: {summary}" assert "total" in summary, f"Missing 'total' in summary: {summary}" assert summary["built_in"] >= 8, ( diff --git a/robot/helper_config_cli.py b/robot/helper_config_cli.py index a207f274c..db341a3b2 100644 --- a/robot/helper_config_cli.py +++ b/robot/helper_config_cli.py @@ -109,7 +109,7 @@ def config_set_get_roundtrip() -> None: data = ( envelope.get("data", envelope) if isinstance(envelope, dict) else envelope ) - if data.get("source") in ("config_file", "global"): + if data.get("source") in ("config", "config_file", "global"): print("config-cli-set-get-roundtrip-ok") else: print(f"FAIL: unexpected source {data.get('source')}", file=sys.stderr) diff --git a/src/cleveragents/cli/commands/config.py b/src/cleveragents/cli/commands/config.py index 56d5f18ae..942f9e387 100644 --- a/src/cleveragents/cli/commands/config.py +++ b/src/cleveragents/cli/commands/config.py @@ -12,6 +12,7 @@ from __future__ import annotations import fnmatch import json +import os import re import time from datetime import UTC, datetime @@ -48,7 +49,16 @@ from cleveragents.cli.renderers import _get_console app = typer.Typer(help="Manage configuration settings for CleverAgents.") console = _get_console() -_CONFIG_DIR = Path.home() / ".cleveragents" + +def _get_config_dir() -> Path: + """Return the config directory, respecting CLEVERAGENTS_HOME if set.""" + home_env = os.environ.get("CLEVERAGENTS_HOME", "").strip() + if home_env: + return Path(home_env) + return Path.home() / ".cleveragents" + + +_CONFIG_DIR = _get_config_dir() _CONFIG_PATH = _CONFIG_DIR / "config.toml" _FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)" diff --git a/src/cleveragents/cli/commands/plan.py b/src/cleveragents/cli/commands/plan.py index e025f1c14..2936d15a2 100644 --- a/src/cleveragents/cli/commands/plan.py +++ b/src/cleveragents/cli/commands/plan.py @@ -2945,6 +2945,14 @@ def lifecycle_list_plans( help="Filter by action name", ), ] = None, + namespace: Annotated[ + str | None, + typer.Option( + "--namespace", + "-n", + help="Filter by namespace (e.g. 'local', 'myteam')", + ), + ] = None, fmt: Annotated[ str, typer.Option( @@ -2964,6 +2972,7 @@ def lifecycle_list_plans( agents plan list --phase strategize agents plan list --state processing agents plan list "^myteam/" + agents plan list --namespace myteam """ try: import re @@ -3000,7 +3009,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: @@ -3116,8 +3127,13 @@ def lifecycle_list_plans( else: active_filters.append("[yellow]Action:[/yellow] (any)") + if namespace: + active_filters.append(f"[yellow]Namespace:[/yellow] {namespace}") + else: + active_filters.append("[yellow]Namespace:[/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 phase_filter or state_filter or project_id or action_filter or namespace: filters_text = "\n".join(active_filters) filters_panel = Panel(filters_text, title="Filters", border_style="dim") console.print(filters_panel)