Files
cleveragents-core/robot/helper_plan_cli_spec.py
freemo 655947c8ba
CI / push-validation (pull_request) Successful in 21s
CI / helm (pull_request) Successful in 29s
CI / lint (pull_request) Successful in 3m45s
CI / build (pull_request) Successful in 3m42s
CI / typecheck (pull_request) Successful in 4m24s
CI / quality (pull_request) Successful in 4m26s
CI / security (pull_request) Successful in 4m46s
CI / integration_tests (pull_request) Successful in 7m31s
CI / unit_tests (pull_request) Successful in 7m47s
CI / e2e_tests (pull_request) Successful in 7m39s
CI / docker (pull_request) Successful in 1m35s
CI / coverage (pull_request) Successful in 14m19s
CI / benchmark-regression (push) Waiting to run
CI / benchmark-publish (push) Waiting to run
CI / status-check (pull_request) Successful in 3s
CI / push-validation (push) Successful in 23s
CI / helm (push) Successful in 31s
CI / build (push) Successful in 3m48s
CI / lint (push) Successful in 3m54s
CI / quality (push) Successful in 4m21s
CI / typecheck (push) Successful in 4m29s
CI / security (push) Successful in 4m46s
CI / e2e_tests (push) Successful in 6m55s
CI / integration_tests (push) Successful in 7m18s
CI / unit_tests (push) Successful in 8m45s
CI / docker (push) Successful in 1m33s
CI / coverage (push) Successful in 15m12s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 24m35s
fix(cli): add <REGEX> positional argument to agents plan list command
The <REGEX> positional argument was already implemented in the CLI code
(src/cleveragents/cli/commands/plan.py) in a prior commit. This commit
adds the missing Robot Framework integration test that verifies the
argument is accepted and correctly filters plan names by regex pattern.

Changes:
- robot/helper_plan_cli_spec.py: Add list_regex() helper function that
  creates two mock plans (local/smoke-plan and local/other-plan), invokes
  'agents plan list --format json smoke.*', and verifies that only the
  matching plan appears in the output.
- robot/plan_cli_spec.robot: Add 'Plan Lifecycle List Accepts Regex
  Positional Argument' test case that calls the list-regex helper and
  asserts the plan-cli-list-regex-ok sentinel is printed.

The Behave unit test scenario ('Plan list with regex filter') already
existed in features/plan_cli_spec_alignment.feature and the step
definition in features/steps/plan_cli_spec_alignment_steps.py.

Closes #3436
2026-04-22 03:26:49 +00:00

398 lines
14 KiB
Python

"""Helper script for plan_cli_spec.robot smoke tests.
Each subcommand is a self-contained check that prints a sentinel on success.
"""
from __future__ import annotations
import sys
from datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock, patch
# Ensure the local source tree takes priority over any installed copy.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
# Remove any *other* src entries (e.g. /app/src from the parent clone)
sys.path = [_SRC] + [
p
for p in sys.path
if p != _SRC and not (p.endswith("/src") and "cleveragents" not in p.split("/")[-1])
]
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
# Flush any already-loaded cleveragents modules so they reimport from _SRC
for _mod_name in sorted(sys.modules):
if _mod_name.startswith("cleveragents"):
del sys.modules[_mod_name]
from typer.testing import CliRunner # noqa: E402
from cleveragents.cli.commands.plan import app as plan_app # noqa: E402
from cleveragents.domain.models.core.action import Action, ActionState # noqa: E402
from cleveragents.domain.models.core.plan import ( # noqa: E402
AutomationProfileProvenance,
AutomationProfileRef,
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
PlanTimestamps,
ProcessingState,
ProjectLink,
)
runner = CliRunner()
_PLAN_ULID = "01KHDE6WWS2171PWW3GJEBXZ8S"
def _mock_action(name: str = "local/smoke-action") -> Action:
return Action(
namespaced_name=NamespacedName.parse(name),
description="Smoke test action",
long_description=None,
definition_of_done="All smoke tests pass",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
reusable=True,
read_only=False,
state=ActionState.AVAILABLE,
created_by=None,
created_at=datetime.now(),
updated_at=datetime.now(),
)
def _mock_plan(
project_links: list[ProjectLink] | None = None,
arguments: dict[str, object] | None = None,
) -> Plan:
now = datetime.now()
return Plan(
identity=PlanIdentity(plan_id=_PLAN_ULID),
namespaced_name=NamespacedName.parse("local/smoke-plan"),
description="Smoke test plan",
definition_of_done="All smoke tests pass",
action_name="local/smoke-action",
phase=PlanPhase.STRATEGIZE,
processing_state=ProcessingState.QUEUED,
project_links=project_links or [],
arguments=dict(arguments) if arguments else {},
arguments_order=list((arguments or {}).keys()),
automation_profile=AutomationProfileRef(
profile_name="trusted",
provenance=AutomationProfileProvenance.PLAN,
),
estimation_actor="openai/gpt-4",
invariant_actor="openai/gpt-4",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
reusable=True,
read_only=False,
created_by=None,
timestamps=PlanTimestamps(created_at=now, updated_at=now),
)
# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------
def use_positional_projects() -> None:
"""Verify plan use accepts multiple positional projects."""
mock_service = MagicMock()
mock_service.get_action_by_name.return_value = _mock_action()
mock_service.use_action.return_value = _mock_plan(
project_links=[
ProjectLink(project_name="proj-a"),
ProjectLink(project_name="proj-b"),
],
)
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=mock_service,
):
result = runner.invoke(
plan_app, ["use", "local/smoke-action", "proj-a", "proj-b"]
)
if result.exit_code == 0:
print("plan-cli-use-positional-ok")
else:
print(f"FAIL: use returned {result.exit_code}", file=sys.stderr)
print(result.output, file=sys.stderr)
sys.exit(1)
def use_automation_profile() -> None:
"""Verify plan use accepts --automation-profile."""
mock_service = MagicMock()
mock_service.get_action_by_name.return_value = _mock_action()
mock_service.use_action.return_value = _mock_plan()
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=mock_service,
):
result = runner.invoke(
plan_app,
[
"use",
"local/smoke-action",
"proj-a",
"--automation-profile",
"trusted",
],
)
if result.exit_code == 0:
print("plan-cli-use-profile-ok")
else:
print(f"FAIL: use returned {result.exit_code}", file=sys.stderr)
print(result.output, file=sys.stderr)
sys.exit(1)
def use_invariant() -> None:
"""Verify plan use accepts repeatable --invariant."""
mock_service = MagicMock()
mock_service.get_action_by_name.return_value = _mock_action()
mock_service.use_action.return_value = _mock_plan()
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=mock_service,
):
result = runner.invoke(
plan_app,
[
"use",
"local/smoke-action",
"proj-a",
"--invariant",
"No warnings",
"--invariant",
"Keep compat",
],
)
if result.exit_code == 0:
print("plan-cli-use-invariant-ok")
else:
print(f"FAIL: use returned {result.exit_code}", file=sys.stderr)
print(result.output, file=sys.stderr)
sys.exit(1)
def use_actor_overrides() -> None:
"""Verify plan use accepts actor override flags."""
mock_service = MagicMock()
mock_service.get_action_by_name.return_value = _mock_action()
mock_service.use_action.return_value = _mock_plan()
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=mock_service,
):
result = runner.invoke(
plan_app,
[
"use",
"local/smoke-action",
"proj-a",
"--strategy-actor",
"openai/gpt-4",
"--execution-actor",
"anthropic/claude-3",
"--estimation-actor",
"openai/gpt-4",
"--invariant-actor",
"openai/gpt-4",
],
)
if result.exit_code == 0:
print("plan-cli-use-actors-ok")
else:
print(f"FAIL: use returned {result.exit_code}", file=sys.stderr)
print(result.output, file=sys.stderr)
sys.exit(1)
def list_filters() -> None:
"""Verify list accepts filter flags."""
mock_service = MagicMock()
mock_service.list_plans.return_value = [
_mock_plan(project_links=[ProjectLink(project_name="proj-a")]),
]
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=mock_service,
):
r1 = runner.invoke(plan_app, ["list", "--phase", "strategize"])
r2 = runner.invoke(plan_app, ["list", "--state", "queued"])
r3 = runner.invoke(plan_app, ["list", "--project", "proj-a"])
r4 = runner.invoke(plan_app, ["list", "--action", "local/smoke-action"])
if all(r.exit_code == 0 for r in [r1, r2, r3, r4]):
print("plan-cli-list-filters-ok")
else:
codes = [r1.exit_code, r2.exit_code, r3.exit_code, r4.exit_code]
print(f"FAIL: list filters exit codes: {codes}", file=sys.stderr)
sys.exit(1)
def list_columns() -> None:
"""Verify plan list rich output includes Name, Updated, and Invariants columns."""
mock_service = MagicMock()
mock_service.list_plans.return_value = [
_mock_plan(project_links=[ProjectLink(project_name="proj-a")]),
]
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=mock_service,
):
# Use a wide terminal so column headers are not truncated
wide_runner = CliRunner(mix_stderr=False)
result = wide_runner.invoke(plan_app, ["list"], env={"COLUMNS": "200"})
if result.exit_code != 0:
print(f"FAIL: list returned {result.exit_code}", file=sys.stderr)
print(result.output, file=sys.stderr)
sys.exit(1)
output = result.output
# Check for column headers (rich may truncate in narrow terminals,
# so we check for the first 5 chars which are always rendered)
required_columns = ["Name", "Updat", "Invar"]
missing = [col for col in required_columns if col not in output]
if missing:
print(f"FAIL: missing columns: {missing}", file=sys.stderr)
print(output, file=sys.stderr)
sys.exit(1)
# Verify the plan name appears in the output
if "local/smoke-plan" not in output and "local/smok" not in output:
print("FAIL: plan name not in output", file=sys.stderr)
print(output, file=sys.stderr)
sys.exit(1)
print("plan-cli-list-columns-ok")
def list_regex() -> None:
"""Verify list accepts a positional <REGEX> argument and filters by name."""
mock_service = MagicMock()
matching_plan = _mock_plan() # name: local/smoke-plan, action: local/smoke-action
now = matching_plan.timestamps.created_at
non_matching_plan = Plan(
identity=PlanIdentity(plan_id="01KHDE6WWS2171PWW3GJEBXZ8S"),
namespaced_name=NamespacedName.parse("local/other-plan"),
description="Other plan",
definition_of_done="Done",
action_name="local/other-action",
phase=PlanPhase.STRATEGIZE,
processing_state=ProcessingState.QUEUED,
project_links=[],
arguments={},
arguments_order=[],
automation_profile=AutomationProfileRef(
profile_name="trusted",
provenance=AutomationProfileProvenance.PLAN,
),
estimation_actor="openai/gpt-4",
invariant_actor="openai/gpt-4",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
reusable=True,
read_only=False,
created_by=None,
timestamps=PlanTimestamps(created_at=now, updated_at=now),
)
mock_service.list_plans.return_value = [matching_plan, non_matching_plan]
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=mock_service,
):
# Use JSON format so full names are visible (rich table truncates them)
result = runner.invoke(plan_app, ["list", "--format", "json", "smoke.*"])
if result.exit_code != 0:
print(f"FAIL: list regex returned {result.exit_code}", file=sys.stderr)
print(result.output, file=sys.stderr)
sys.exit(1)
if "smoke-plan" not in result.output:
print("FAIL: matching plan not in output", file=sys.stderr)
print(result.output, file=sys.stderr)
sys.exit(1)
if "other-plan" in result.output:
print("FAIL: non-matching plan appeared in output", file=sys.stderr)
print(result.output, file=sys.stderr)
sys.exit(1)
print("plan-cli-list-regex-ok")
def status_fields() -> None:
"""Verify plan status renders required fields."""
mock_service = MagicMock()
plan = _mock_plan(
project_links=[ProjectLink(project_name="proj-a", alias="api")],
arguments={"coverage": 80},
)
mock_service.get_plan.return_value = plan
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=mock_service,
):
result = runner.invoke(plan_app, ["status", _PLAN_ULID])
if result.exit_code == 0:
output = result.output.lower()
required = ["action", "phase", "processing state", "projects", "created"]
missing = [f for f in required if f not in output]
if missing:
print(f"FAIL: missing fields: {missing}", file=sys.stderr)
print(result.output, file=sys.stderr)
sys.exit(1)
print("plan-cli-status-fields-ok")
else:
print(f"FAIL: status returned {result.exit_code}", file=sys.stderr)
print(result.output, file=sys.stderr)
sys.exit(1)
def cancel_reason() -> None:
"""Verify plan cancel accepts --reason."""
mock_service = MagicMock()
plan = _mock_plan()
plan.processing_state = ProcessingState.CANCELLED
mock_service.cancel_plan.return_value = plan
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=mock_service,
):
result = runner.invoke(
plan_app, ["cancel", _PLAN_ULID, "--reason", "Requirements changed"]
)
if result.exit_code == 0 and "Requirements changed" in result.output:
print("plan-cli-cancel-reason-ok")
else:
print(f"FAIL: cancel returned {result.exit_code}", file=sys.stderr)
print(result.output, file=sys.stderr)
sys.exit(1)
# ---------------------------------------------------------------------------
# Main dispatcher
# ---------------------------------------------------------------------------
_COMMANDS = {
"use-positional-projects": use_positional_projects,
"use-automation-profile": use_automation_profile,
"use-invariant": use_invariant,
"use-actor-overrides": use_actor_overrides,
"list-filters": list_filters,
"list-columns": list_columns,
"list-regex": list_regex,
"status-fields": status_fields,
"cancel-reason": cancel_reason,
}
def main() -> None:
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr)
sys.exit(2)
_COMMANDS[sys.argv[1]]()
if __name__ == "__main__":
main()