Compare commits
3 Commits
pr-9246
...
fix/1468-impl
| Author | SHA1 | Date | |
|---|---|---|---|
| 0446ad1495 | |||
| 951bf75bbe | |||
| ae6d032b22 |
@@ -0,0 +1,275 @@
|
||||
"""Step definitions for TDD — agents plan use structured panels (issue #1468)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.plan import app as plan_app
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
PlanTimestamps,
|
||||
ProcessingState,
|
||||
ProjectLink,
|
||||
)
|
||||
|
||||
_PLAN_ULID = "01JTEST1468PANEMS0000000001"
|
||||
|
||||
|
||||
def _make_action(name: str = "local/test-action") -> Action:
|
||||
"""Create a minimal Action for plan use tests."""
|
||||
return Action(
|
||||
namespaced_name=NamespacedName.parse(name),
|
||||
description="Test action for structured panels",
|
||||
long_description=None,
|
||||
definition_of_done="All panels rendered correctly",
|
||||
strategy_actor="local/strategist",
|
||||
execution_actor="local/executor",
|
||||
reusable=True,
|
||||
read_only=False,
|
||||
state=ActionState.AVAILABLE,
|
||||
created_by=None,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
|
||||
def _make_plan(
|
||||
*,
|
||||
project_links: list[ProjectLink] | None = None,
|
||||
arguments: dict[str, object] | None = None,
|
||||
arguments_order: list[str] | None = None,
|
||||
automation_profile: AutomationProfileRef | None = None,
|
||||
strategy_actor: str | None = "local/strategist",
|
||||
execution_actor: str | None = "local/executor",
|
||||
estimation_actor: str | None = None,
|
||||
) -> Plan:
|
||||
"""Create a Plan instance for structured panels tests."""
|
||||
now = datetime.now()
|
||||
args = dict(arguments) if arguments else {}
|
||||
return Plan(
|
||||
identity=PlanIdentity(plan_id=_PLAN_ULID),
|
||||
namespaced_name=NamespacedName.parse("local/test-plan"),
|
||||
description="Test plan for structured panels",
|
||||
definition_of_done="All panels rendered correctly",
|
||||
action_name="local/test-action",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
project_links=project_links or [ProjectLink(project_name="local/api-service")],
|
||||
arguments=args,
|
||||
arguments_order=arguments_order or list(args.keys()),
|
||||
automation_profile=automation_profile,
|
||||
strategy_actor=strategy_actor,
|
||||
execution_actor=execution_actor,
|
||||
estimation_actor=estimation_actor,
|
||||
reusable=True,
|
||||
read_only=False,
|
||||
created_by=None,
|
||||
timestamps=PlanTimestamps(created_at=now, updated_at=now),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Background
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("a plan use panels CLI runner")
|
||||
def step_plan_use_panels_runner(context: Context) -> None:
|
||||
"""Set up the CLI runner."""
|
||||
context.runner = CliRunner()
|
||||
|
||||
|
||||
@given("a plan use panels mocked lifecycle service")
|
||||
def step_plan_use_panels_mock_service(context: Context) -> None:
|
||||
"""Set up a mocked lifecycle service."""
|
||||
context.mock_service = MagicMock()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Given steps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@given("a plan use panels action exists")
|
||||
def step_plan_use_panels_action_exists(context: Context) -> None:
|
||||
"""Configure the mock service with a standard action and plan."""
|
||||
context.mock_service.get_action_by_name.return_value = _make_action()
|
||||
context.mock_plan = _make_plan(
|
||||
automation_profile=AutomationProfileRef(
|
||||
profile_name="trusted",
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
),
|
||||
strategy_actor="local/strategist",
|
||||
execution_actor="local/executor",
|
||||
)
|
||||
context.mock_service.use_action.return_value = context.mock_plan
|
||||
|
||||
|
||||
@given("a plan use panels action exists with no automation profile")
|
||||
def step_plan_use_panels_action_no_profile(context: Context) -> None:
|
||||
"""Configure the mock service with an action and plan without automation profile."""
|
||||
context.mock_service.get_action_by_name.return_value = _make_action()
|
||||
context.mock_plan = _make_plan(
|
||||
automation_profile=None,
|
||||
strategy_actor="local/strategist",
|
||||
execution_actor="local/executor",
|
||||
)
|
||||
context.mock_service.use_action.return_value = context.mock_plan
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# When steps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when("I run plan use with rich output")
|
||||
def step_run_plan_use_rich(context: Context) -> None:
|
||||
"""Run plan use with default rich format."""
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=context.mock_service,
|
||||
):
|
||||
context.result = context.runner.invoke(
|
||||
plan_app,
|
||||
["use", "local/test-action", "local/api-service"],
|
||||
)
|
||||
|
||||
|
||||
@when('I run plan use with arg "{arg_str}"')
|
||||
def step_run_plan_use_with_arg(context: Context, arg_str: str) -> None:
|
||||
"""Run plan use with a specific --arg value."""
|
||||
# Parse the arg to set up the mock plan with that argument
|
||||
if "=" in arg_str:
|
||||
key, val_str = arg_str.split("=", 1)
|
||||
try:
|
||||
val: object = int(val_str)
|
||||
except ValueError:
|
||||
try:
|
||||
val = float(val_str)
|
||||
except ValueError:
|
||||
val = val_str
|
||||
context.mock_plan = _make_plan(
|
||||
arguments={key: val},
|
||||
arguments_order=[key],
|
||||
automation_profile=AutomationProfileRef(
|
||||
profile_name="trusted",
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
),
|
||||
)
|
||||
context.mock_service.use_action.return_value = context.mock_plan
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=context.mock_service,
|
||||
):
|
||||
context.result = context.runner.invoke(
|
||||
plan_app,
|
||||
["use", "local/test-action", "local/api-service", "--arg", arg_str],
|
||||
)
|
||||
|
||||
|
||||
@when('I run plan use with automation profile "{profile}"')
|
||||
def step_run_plan_use_with_profile(context: Context, profile: str) -> None:
|
||||
"""Run plan use with --automation-profile."""
|
||||
context.mock_plan = _make_plan(
|
||||
automation_profile=AutomationProfileRef(
|
||||
profile_name=profile,
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
),
|
||||
)
|
||||
context.mock_service.use_action.return_value = context.mock_plan
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=context.mock_service,
|
||||
):
|
||||
context.result = context.runner.invoke(
|
||||
plan_app,
|
||||
[
|
||||
"use",
|
||||
"local/test-action",
|
||||
"local/api-service",
|
||||
"--automation-profile",
|
||||
profile,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@when("I run plan use with json format")
|
||||
def step_run_plan_use_json(context: Context) -> None:
|
||||
"""Run plan use with --format json."""
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=context.mock_service,
|
||||
):
|
||||
context.result = context.runner.invoke(
|
||||
plan_app,
|
||||
["use", "local/test-action", "local/api-service", "--format", "json"],
|
||||
)
|
||||
|
||||
|
||||
@when("I run plan use with rich output and no arguments")
|
||||
def step_run_plan_use_no_args(context: Context) -> None:
|
||||
"""Run plan use with no --arg flags."""
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=context.mock_service,
|
||||
):
|
||||
context.result = context.runner.invoke(
|
||||
plan_app,
|
||||
["use", "local/test-action", "local/api-service"],
|
||||
)
|
||||
|
||||
|
||||
@when("I run plan use with rich output and no automation profile")
|
||||
def step_run_plan_use_no_profile(context: Context) -> None:
|
||||
"""Run plan use with no --automation-profile flag."""
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=context.mock_service,
|
||||
):
|
||||
context.result = context.runner.invoke(
|
||||
plan_app,
|
||||
["use", "local/test-action", "local/api-service"],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Then steps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the plan use output should succeed")
|
||||
def step_plan_use_output_succeed(context: Context) -> None:
|
||||
"""Assert the CLI invocation exited with code 0."""
|
||||
assert context.result.exit_code == 0, (
|
||||
f"Expected exit code 0, got {context.result.exit_code}.\n"
|
||||
f"Output:\n{context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the plan use output should contain the plan ID")
|
||||
def step_plan_use_output_contains_plan_id(context: Context) -> None:
|
||||
"""Assert the output contains the plan ULID."""
|
||||
assert _PLAN_ULID in context.result.output, (
|
||||
f"Expected plan ID '{_PLAN_ULID}' in output.\nOutput:\n{context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then('the plan use output should contain "{text}"')
|
||||
def step_plan_use_output_contains(context: Context, text: str) -> None:
|
||||
"""Assert the output contains the given text."""
|
||||
assert text in context.result.output, (
|
||||
f"Expected '{text}' in output.\nOutput:\n{context.result.output}"
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
@tdd_issue @tdd_issue_1468 @cli @plan @use
|
||||
Feature: TDD — agents plan use renders spec-required structured panels
|
||||
As a developer
|
||||
I want the ``agents plan use`` rich output to render six structured panels
|
||||
So that users see Plan Created, Inputs, Actors, Automation, Context, and Next Steps
|
||||
|
||||
Background:
|
||||
Given a plan use panels CLI runner
|
||||
And a plan use panels mocked lifecycle service
|
||||
|
||||
@tdd_issue @tdd_issue_1468
|
||||
Scenario: Plan Created panel is rendered with required fields
|
||||
Given a plan use panels action exists
|
||||
When I run plan use with rich output
|
||||
Then the plan use output should succeed
|
||||
And the plan use output should contain "Plan Created"
|
||||
And the plan use output should contain the plan ID
|
||||
And the plan use output should contain "Phase"
|
||||
And the plan use output should contain "Action"
|
||||
And the plan use output should contain "Project"
|
||||
And the plan use output should contain "Automation"
|
||||
And the plan use output should contain "Attempt"
|
||||
|
||||
@tdd_issue @tdd_issue_1468
|
||||
Scenario: Inputs panel is rendered with argument key=value pairs
|
||||
Given a plan use panels action exists
|
||||
When I run plan use with arg "target_coverage_percent=85"
|
||||
Then the plan use output should succeed
|
||||
And the plan use output should contain "Inputs"
|
||||
And the plan use output should contain "target_coverage_percent=85"
|
||||
|
||||
@tdd_issue @tdd_issue_1468
|
||||
Scenario: Actors panel is rendered with Strategy, Execution, Estimation actors
|
||||
Given a plan use panels action exists
|
||||
When I run plan use with rich output
|
||||
Then the plan use output should succeed
|
||||
And the plan use output should contain "Actors"
|
||||
And the plan use output should contain "Strategy"
|
||||
And the plan use output should contain "Execution"
|
||||
And the plan use output should contain "Estimation"
|
||||
|
||||
@tdd_issue @tdd_issue_1468
|
||||
Scenario: Automation panel is rendered with Profile, Source, Read-Only fields
|
||||
Given a plan use panels action exists
|
||||
When I run plan use with automation profile "trusted"
|
||||
Then the plan use output should succeed
|
||||
And the plan use output should contain "Automation"
|
||||
And the plan use output should contain "Profile"
|
||||
And the plan use output should contain "Source"
|
||||
And the plan use output should contain "Read-Only"
|
||||
|
||||
@tdd_issue @tdd_issue_1468
|
||||
Scenario: Context panel is rendered with Resources, Indexed Files, View, Hot Token Budget
|
||||
Given a plan use panels action exists
|
||||
When I run plan use with rich output
|
||||
Then the plan use output should succeed
|
||||
And the plan use output should contain "Context"
|
||||
And the plan use output should contain "Resources"
|
||||
And the plan use output should contain "Indexed Files"
|
||||
And the plan use output should contain "View"
|
||||
And the plan use output should contain "Hot Token Budget"
|
||||
|
||||
@tdd_issue @tdd_issue_1468
|
||||
Scenario: Next Steps panel is rendered with follow-up commands
|
||||
Given a plan use panels action exists
|
||||
When I run plan use with rich output
|
||||
Then the plan use output should succeed
|
||||
And the plan use output should contain "Next Steps"
|
||||
And the plan use output should contain "agents plan execute"
|
||||
And the plan use output should contain "agents plan status"
|
||||
|
||||
@tdd_issue @tdd_issue_1468
|
||||
Scenario: All six panels are rendered in a single plan use invocation
|
||||
Given a plan use panels action exists
|
||||
When I run plan use with rich output
|
||||
Then the plan use output should succeed
|
||||
And the plan use output should contain "Plan Created"
|
||||
And the plan use output should contain "Inputs"
|
||||
And the plan use output should contain "Actors"
|
||||
And the plan use output should contain "Automation"
|
||||
And the plan use output should contain "Context"
|
||||
And the plan use output should contain "Next Steps"
|
||||
|
||||
@tdd_issue @tdd_issue_1468
|
||||
Scenario: Non-rich format output is not affected by panel changes
|
||||
Given a plan use panels action exists
|
||||
When I run plan use with json format
|
||||
Then the plan use output should succeed
|
||||
And the plan use output should contain "plan_id"
|
||||
|
||||
@tdd_issue @tdd_issue_1468
|
||||
Scenario: Plan use with no arguments still renders Inputs panel
|
||||
Given a plan use panels action exists
|
||||
When I run plan use with rich output and no arguments
|
||||
Then the plan use output should succeed
|
||||
And the plan use output should contain "Inputs"
|
||||
|
||||
@tdd_issue @tdd_issue_1468
|
||||
Scenario: Plan use with no automation profile renders Automation panel with none
|
||||
Given a plan use panels action exists with no automation profile
|
||||
When I run plan use with rich output and no automation profile
|
||||
Then the plan use output should succeed
|
||||
And the plan use output should contain "Automation"
|
||||
And the plan use output should contain "(none)"
|
||||
@@ -0,0 +1,347 @@
|
||||
"""Helper script for tdd_plan_use_structured_panels.robot integration tests.
|
||||
|
||||
Each subcommand is a self-contained check that prints a sentinel on success.
|
||||
Tests verify that ``agents plan use`` rich output renders all six spec-required
|
||||
structured panels as defined in docs/specification.md §agents plan use.
|
||||
|
||||
Issue: #1468 — UAT: agents plan use rich output missing spec-required structured panels
|
||||
"""
|
||||
|
||||
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")
|
||||
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 = "01JTEST1468PANEMS0000000001"
|
||||
|
||||
|
||||
def _mock_action(name: str = "local/test-action") -> Action:
|
||||
"""Create a minimal Action for plan use tests."""
|
||||
return Action(
|
||||
namespaced_name=NamespacedName.parse(name),
|
||||
description="Test action for structured panels",
|
||||
long_description=None,
|
||||
definition_of_done="All panels rendered correctly",
|
||||
strategy_actor="local/strategist",
|
||||
execution_actor="local/executor",
|
||||
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,
|
||||
arguments_order: list[str] | None = None,
|
||||
automation_profile: AutomationProfileRef | None = None,
|
||||
strategy_actor: str | None = "local/strategist",
|
||||
execution_actor: str | None = "local/executor",
|
||||
estimation_actor: str | None = None,
|
||||
) -> Plan:
|
||||
"""Create a Plan instance for structured panels tests."""
|
||||
now = datetime.now()
|
||||
args = dict(arguments) if arguments else {}
|
||||
return Plan(
|
||||
identity=PlanIdentity(plan_id=_PLAN_ULID),
|
||||
namespaced_name=NamespacedName.parse("local/test-plan"),
|
||||
description="Test plan for structured panels",
|
||||
definition_of_done="All panels rendered correctly",
|
||||
action_name="local/test-action",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
project_links=project_links or [ProjectLink(project_name="local/api-service")],
|
||||
arguments=args,
|
||||
arguments_order=arguments_order or list(args.keys()),
|
||||
automation_profile=automation_profile,
|
||||
strategy_actor=strategy_actor,
|
||||
execution_actor=execution_actor,
|
||||
estimation_actor=estimation_actor,
|
||||
reusable=True,
|
||||
read_only=False,
|
||||
created_by=None,
|
||||
timestamps=PlanTimestamps(created_at=now, updated_at=now),
|
||||
)
|
||||
|
||||
|
||||
def _run_plan_use(
|
||||
extra_args: list[str] | None = None,
|
||||
plan: Plan | None = None,
|
||||
) -> tuple[int, str]:
|
||||
"""Run plan use and return (exit_code, output)."""
|
||||
mock_service = MagicMock()
|
||||
mock_service.get_action_by_name.return_value = _mock_action()
|
||||
mock_service.use_action.return_value = plan or _mock_plan(
|
||||
automation_profile=AutomationProfileRef(
|
||||
profile_name="trusted",
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
),
|
||||
)
|
||||
args = ["use", "local/test-action", "local/api-service"] + (extra_args or [])
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
):
|
||||
result = runner.invoke(plan_app, args)
|
||||
return result.exit_code, result.output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Subcommands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def check_plan_created_panel() -> None:
|
||||
"""Verify Plan Created panel renders with required fields."""
|
||||
exit_code, output = _run_plan_use()
|
||||
if exit_code != 0:
|
||||
print(f"FAIL: exit code {exit_code}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
required = [
|
||||
"Plan Created",
|
||||
_PLAN_ULID,
|
||||
"Phase",
|
||||
"Action",
|
||||
"Project",
|
||||
"Automation",
|
||||
"Attempt",
|
||||
]
|
||||
missing = [f for f in required if f not in output]
|
||||
if missing:
|
||||
print(f"FAIL: missing fields in Plan Created panel: {missing}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("tdd-1468-plan-created-ok")
|
||||
|
||||
|
||||
def check_inputs_panel() -> None:
|
||||
"""Verify Inputs panel renders with argument key=value pairs."""
|
||||
plan = _mock_plan(
|
||||
arguments={"target_coverage_percent": 85},
|
||||
arguments_order=["target_coverage_percent"],
|
||||
automation_profile=AutomationProfileRef(
|
||||
profile_name="trusted",
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
),
|
||||
)
|
||||
exit_code, output = _run_plan_use(
|
||||
extra_args=["--arg", "target_coverage_percent=85"],
|
||||
plan=plan,
|
||||
)
|
||||
if exit_code != 0:
|
||||
print(f"FAIL: exit code {exit_code}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
required = ["Inputs", "target_coverage_percent=85"]
|
||||
missing = [f for f in required if f not in output]
|
||||
if missing:
|
||||
print(f"FAIL: missing fields in Inputs panel: {missing}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("tdd-1468-inputs-ok")
|
||||
|
||||
|
||||
def check_actors_panel() -> None:
|
||||
"""Verify Actors panel renders with Strategy, Execution, Estimation fields."""
|
||||
exit_code, output = _run_plan_use()
|
||||
if exit_code != 0:
|
||||
print(f"FAIL: exit code {exit_code}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
required = ["Actors", "Strategy", "Execution", "Estimation"]
|
||||
missing = [f for f in required if f not in output]
|
||||
if missing:
|
||||
print(f"FAIL: missing fields in Actors panel: {missing}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("tdd-1468-actors-ok")
|
||||
|
||||
|
||||
def check_automation_panel() -> None:
|
||||
"""Verify Automation panel renders with Profile, Source, Read-Only fields."""
|
||||
exit_code, output = _run_plan_use(extra_args=["--automation-profile", "trusted"])
|
||||
if exit_code != 0:
|
||||
print(f"FAIL: exit code {exit_code}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
required = ["Automation", "Profile", "Source", "Read-Only"]
|
||||
missing = [f for f in required if f not in output]
|
||||
if missing:
|
||||
print(f"FAIL: missing fields in Automation panel: {missing}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("tdd-1468-automation-ok")
|
||||
|
||||
|
||||
def check_context_panel() -> None:
|
||||
"""Verify Context panel renders with Resources, Indexed Files, View, Budget."""
|
||||
exit_code, output = _run_plan_use()
|
||||
if exit_code != 0:
|
||||
print(f"FAIL: exit code {exit_code}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
required = ["Context", "Resources", "Indexed Files", "View", "Hot Token Budget"]
|
||||
missing = [f for f in required if f not in output]
|
||||
if missing:
|
||||
print(f"FAIL: missing fields in Context panel: {missing}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("tdd-1468-context-ok")
|
||||
|
||||
|
||||
def check_next_steps_panel() -> None:
|
||||
"""Verify Next Steps panel renders with follow-up commands."""
|
||||
exit_code, output = _run_plan_use()
|
||||
if exit_code != 0:
|
||||
print(f"FAIL: exit code {exit_code}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
required = ["Next Steps", "agents plan execute", "agents plan status"]
|
||||
missing = [f for f in required if f not in output]
|
||||
if missing:
|
||||
print(f"FAIL: missing fields in Next Steps panel: {missing}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("tdd-1468-next-steps-ok")
|
||||
|
||||
|
||||
def check_all_six_panels() -> None:
|
||||
"""Verify all six spec-required panels are rendered in a single invocation."""
|
||||
exit_code, output = _run_plan_use()
|
||||
if exit_code != 0:
|
||||
print(f"FAIL: exit code {exit_code}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
required_panels = [
|
||||
"Plan Created",
|
||||
"Inputs",
|
||||
"Actors",
|
||||
"Automation",
|
||||
"Context",
|
||||
"Next Steps",
|
||||
]
|
||||
missing = [p for p in required_panels if p not in output]
|
||||
if missing:
|
||||
print(f"FAIL: missing panels: {missing}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("tdd-1468-all-panels-ok")
|
||||
|
||||
|
||||
def check_json_format() -> None:
|
||||
"""Verify --format json output is not affected by panel changes."""
|
||||
exit_code, output = _run_plan_use(extra_args=["--format", "json"])
|
||||
if exit_code != 0:
|
||||
print(f"FAIL: exit code {exit_code}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if "plan_id" not in output:
|
||||
print("FAIL: 'plan_id' not found in JSON output", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("tdd-1468-json-ok")
|
||||
|
||||
|
||||
def check_no_profile() -> None:
|
||||
"""Verify Automation panel renders with (none) when no profile is set."""
|
||||
plan = _mock_plan(automation_profile=None)
|
||||
exit_code, output = _run_plan_use(plan=plan)
|
||||
if exit_code != 0:
|
||||
print(f"FAIL: exit code {exit_code}", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if "Automation" not in output:
|
||||
print("FAIL: 'Automation' panel not found in output", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if "(none)" not in output:
|
||||
print("FAIL: '(none)' not found in Automation panel output", file=sys.stderr)
|
||||
print(output, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("tdd-1468-no-profile-ok")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main dispatcher
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_COMMANDS = {
|
||||
"check-plan-created-panel": check_plan_created_panel,
|
||||
"check-inputs-panel": check_inputs_panel,
|
||||
"check-actors-panel": check_actors_panel,
|
||||
"check-automation-panel": check_automation_panel,
|
||||
"check-context-panel": check_context_panel,
|
||||
"check-next-steps-panel": check_next_steps_panel,
|
||||
"check-all-six-panels": check_all_six_panels,
|
||||
"check-json-format": check_json_format,
|
||||
"check-no-profile": check_no_profile,
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,93 @@
|
||||
*** Settings ***
|
||||
Documentation TDD integration tests for agents plan use structured panels (issue #1468)
|
||||
... Verifies that the rich output renders all six spec-required panels:
|
||||
... Plan Created, Inputs, Actors, Automation, Context, Next Steps.
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} ${CURDIR}/helper_tdd_plan_use_structured_panels.py
|
||||
|
||||
*** Test Cases ***
|
||||
Plan Use Renders Plan Created Panel
|
||||
[Documentation] Verify that ``plan use`` rich output renders the Plan Created panel
|
||||
... with Plan ID, Phase, Action, Project, Automation, and Attempt fields.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-plan-created-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-1468-plan-created-ok
|
||||
|
||||
Plan Use Renders Inputs Panel
|
||||
[Documentation] Verify that ``plan use`` rich output renders the Inputs panel
|
||||
... with argument key=value pairs.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-inputs-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-1468-inputs-ok
|
||||
|
||||
Plan Use Renders Actors Panel
|
||||
[Documentation] Verify that ``plan use`` rich output renders the Actors panel
|
||||
... with Strategy, Execution, and Estimation actor fields.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-actors-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-1468-actors-ok
|
||||
|
||||
Plan Use Renders Automation Panel
|
||||
[Documentation] Verify that ``plan use`` rich output renders the Automation panel
|
||||
... with Profile, Source, and Read-Only fields.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-automation-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-1468-automation-ok
|
||||
|
||||
Plan Use Renders Context Panel
|
||||
[Documentation] Verify that ``plan use`` rich output renders the Context panel
|
||||
... with Resources, Indexed Files, View, and Hot Token Budget fields.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-context-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-1468-context-ok
|
||||
|
||||
Plan Use Renders Next Steps Panel
|
||||
[Documentation] Verify that ``plan use`` rich output renders the Next Steps panel
|
||||
... with suggested follow-up commands including agents plan execute.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-next-steps-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-1468-next-steps-ok
|
||||
|
||||
Plan Use Renders All Six Panels
|
||||
[Documentation] Verify that ``plan use`` rich output renders all six spec-required panels
|
||||
... in a single invocation: Plan Created, Inputs, Actors, Automation,
|
||||
... Context, and Next Steps.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-all-six-panels cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-1468-all-panels-ok
|
||||
|
||||
Plan Use JSON Format Not Affected By Panel Changes
|
||||
[Documentation] Verify that ``plan use --format json`` output is not affected
|
||||
... by the structured panel changes and still returns valid JSON.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-json-format cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-1468-json-ok
|
||||
|
||||
Plan Use No Automation Profile Renders Automation Panel With None
|
||||
[Documentation] Verify that when no automation profile is set, the Automation panel
|
||||
... still renders with "(none)" values.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-no-profile cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tdd-1468-no-profile-ok
|
||||
@@ -1531,6 +1531,163 @@ def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None:
|
||||
console.print(Panel(details, title=title, expand=False))
|
||||
|
||||
|
||||
def _print_use_action_panels(plan: Any) -> None:
|
||||
"""Render the six spec-required structured panels for ``agents plan use`` output.
|
||||
|
||||
Implements the output contract defined in docs/specification.md §agents plan use.
|
||||
|
||||
Panels rendered:
|
||||
1. Plan Created — Plan ID, Phase, Action, Project, Automation, Attempt
|
||||
2. Inputs — argument key=value pairs
|
||||
3. Actors — Strategy, Execution, Estimation actors
|
||||
4. Automation — Profile, Source, Read-Only flag
|
||||
5. Context — Resources, Indexed Files, View, Hot Token Budget
|
||||
6. Next Steps — suggested follow-up commands
|
||||
|
||||
Args:
|
||||
plan: A v3 Plan object from the domain model.
|
||||
"""
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationProfileProvenance,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
Plan as LifecyclePlan,
|
||||
)
|
||||
|
||||
if not isinstance(plan, LifecyclePlan):
|
||||
# Fallback for unexpected plan types — should not occur in normal flow
|
||||
console.print(Panel(f"Plan: {plan}", title="Plan Created", expand=False))
|
||||
return
|
||||
|
||||
plan_id = plan.identity.plan_id
|
||||
phase_val = plan.phase.value if plan.phase else "unknown"
|
||||
action_val = plan.action_name or "(unknown)"
|
||||
|
||||
# Project: join all project names
|
||||
project_names = [link.project_name for link in plan.project_links]
|
||||
project_val = ", ".join(project_names) if project_names else "(none)"
|
||||
|
||||
# Automation profile name
|
||||
automation_val = (
|
||||
plan.automation_profile.profile_name if plan.automation_profile else "(none)"
|
||||
)
|
||||
|
||||
# Attempt: use identity attempt counter if available, else 1
|
||||
attempt_val = getattr(plan.identity, "attempt", 1) or 1
|
||||
|
||||
# ── Panel 1: Plan Created ──────────────────────────────────────────────
|
||||
created_content = (
|
||||
f"[cyan bold]Plan ID:[/cyan bold] {plan_id}\n"
|
||||
f"[yellow bold]Phase:[/yellow bold] {phase_val}\n"
|
||||
f"[magenta bold]Action:[/magenta bold] {action_val}\n"
|
||||
f"[blue bold]Project:[/blue bold] {project_val}\n"
|
||||
f"[green bold]Automation:[/green bold] {automation_val}\n"
|
||||
f"[blue bold]Attempt:[/blue bold] {attempt_val}"
|
||||
)
|
||||
console.print(Panel(created_content, title="Plan Created", expand=False))
|
||||
|
||||
# ── Panel 2: Inputs ───────────────────────────────────────────────────
|
||||
input_lines: list[str] = []
|
||||
if plan.arguments:
|
||||
for key in plan.arguments_order:
|
||||
if key in plan.arguments:
|
||||
input_lines.append(f"- {key}={plan.arguments[key]}")
|
||||
# Include any keys not in arguments_order (defensive)
|
||||
for key, val in plan.arguments.items():
|
||||
if key not in plan.arguments_order:
|
||||
input_lines.append(f"- {key}={val}")
|
||||
if plan.automation_profile:
|
||||
prof_name = plan.automation_profile.profile_name
|
||||
input_lines.append(f"- automation_profile={prof_name}")
|
||||
inputs_content = "\n".join(input_lines) if input_lines else "(none)"
|
||||
console.print(Panel(inputs_content, title="Inputs", expand=False))
|
||||
|
||||
# ── Panel 3: Actors ───────────────────────────────────────────────────
|
||||
strategy_val = plan.strategy_actor or "(none)"
|
||||
execution_val = plan.execution_actor or "(none)"
|
||||
estimation_val = plan.estimation_actor or "(none)"
|
||||
actors_content = (
|
||||
f"[magenta bold]Strategy:[/magenta bold] {strategy_val}\n"
|
||||
f"[magenta bold]Execution:[/magenta bold] {execution_val}\n"
|
||||
f"[blue bold]Estimation:[/blue bold] {estimation_val}"
|
||||
)
|
||||
console.print(Panel(actors_content, title="Actors", expand=False))
|
||||
|
||||
# ── Panel 4: Automation ───────────────────────────────────────────────
|
||||
if plan.automation_profile:
|
||||
profile_name = plan.automation_profile.profile_name
|
||||
provenance = plan.automation_profile.provenance
|
||||
# Map provenance enum to human-readable source label
|
||||
_provenance_labels: dict[AutomationProfileProvenance, str] = {
|
||||
AutomationProfileProvenance.PLAN: "CLI flag",
|
||||
AutomationProfileProvenance.ACTION: "action default",
|
||||
AutomationProfileProvenance.PROJECT: "project default",
|
||||
AutomationProfileProvenance.GLOBAL: "global default",
|
||||
}
|
||||
source_label = _provenance_labels.get(provenance, provenance.value)
|
||||
else:
|
||||
profile_name = "(none)"
|
||||
source_label = "(none)"
|
||||
|
||||
# Read-only: check if any project link is read-only
|
||||
read_only_val = (
|
||||
"yes" if any(link.read_only for link in plan.project_links) else "no"
|
||||
)
|
||||
|
||||
automation_content = (
|
||||
f"[magenta bold]Profile:[/magenta bold] {profile_name}\n"
|
||||
f"[blue bold]Source:[/blue bold] {source_label}\n"
|
||||
f"[blue bold]Read-Only:[/blue bold] {read_only_val}"
|
||||
)
|
||||
console.print(Panel(automation_content, title="Automation", expand=False))
|
||||
|
||||
# ── Panel 5: Context ──────────────────────────────────────────────────
|
||||
# Resources: count of project links (each project is a resource scope)
|
||||
resource_count = len(plan.project_links)
|
||||
resource_names = ", ".join(project_names) if project_names else "(none)"
|
||||
resources_val = (
|
||||
f"{resource_count} ({resource_names})" if resource_count > 0 else "0"
|
||||
)
|
||||
|
||||
# Indexed files: use compressed_tokens from skeleton_metadata as a proxy
|
||||
# for the number of indexed context tokens (spec field; actual file count
|
||||
# is not tracked separately in SkeletonMetadata).
|
||||
indexed_files_val: str
|
||||
if plan.skeleton_metadata is not None:
|
||||
indexed_files_val = str(plan.skeleton_metadata.compressed_tokens)
|
||||
else:
|
||||
indexed_files_val = "(unknown)"
|
||||
|
||||
# View: the current phase value (strategize at plan creation)
|
||||
view_val = phase_val
|
||||
|
||||
# Hot token budget: use budget_remaining from cost_metadata if available
|
||||
hot_token_budget_val: str
|
||||
if plan.cost_metadata is not None:
|
||||
budget = plan.cost_metadata.budget_remaining
|
||||
hot_token_budget_val = f"{budget:,.2f}" if budget is not None else "(unlimited)"
|
||||
else:
|
||||
hot_token_budget_val = "(unknown)"
|
||||
|
||||
context_content = (
|
||||
f"[blue bold]Resources:[/blue bold] {resources_val}\n"
|
||||
f"[blue bold]Indexed Files:[/blue bold] {indexed_files_val}\n"
|
||||
f"[blue bold]View:[/blue bold] {view_val}\n"
|
||||
f"[blue bold]Hot Token Budget:[/blue bold] {hot_token_budget_val}"
|
||||
)
|
||||
console.print(Panel(context_content, title="Context", expand=False))
|
||||
|
||||
# ── Panel 6: Next Steps ───────────────────────────────────────────────
|
||||
next_steps_content = (
|
||||
f"- agents plan execute {plan_id}\n"
|
||||
f"- agents plan status {plan_id}\n"
|
||||
f"- agents plan tree {plan_id}"
|
||||
)
|
||||
console.print(Panel(next_steps_content, title="Next Steps", expand=False))
|
||||
|
||||
console.print("\n[green bold]✓ OK[/green bold] Plan created")
|
||||
|
||||
|
||||
@app.command("use")
|
||||
def use_action(
|
||||
action_name: Annotated[
|
||||
@@ -1848,17 +2005,12 @@ def use_action(
|
||||
data = _plan_spec_dict(plan)
|
||||
console.print(format_output(data, fmt))
|
||||
else:
|
||||
_print_lifecycle_plan(plan, title="Plan Created")
|
||||
_print_use_action_panels(plan)
|
||||
if plan.is_terminal:
|
||||
console.print(
|
||||
f"\n[dim]Plan completed with state: "
|
||||
f"{plan.state.value if plan.state else 'unknown'}.[/dim]"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
"\n[dim]Plan is now in Strategize phase (queued). "
|
||||
"Run 'agents plan execute <id>' when ready.[/dim]"
|
||||
)
|
||||
|
||||
except ActionNotAvailableError as e:
|
||||
console.print(f"[red]Action not available:[/red] {e}")
|
||||
|
||||
Reference in New Issue
Block a user