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
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
398 lines
14 KiB
Python
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()
|