forked from cleveragents/cleveragents-core
fix(cli): add missing Type field, Config, Capabilities, and Tools panels to actor add rich output
Extended `_print_actor()` to render the full spec-required output for `agents actor add`: the Actor Added panel now includes a Type field, and three additional panels (Config, Capabilities, Tools) plus a success status line are rendered when `show_add_panels=True`. - Add `Type:` field to the Actor Added panel (from `config_blob["type"]`) - Implement Config panel: Path, Hash, Options count, Nodes count, Edges count - Implement Capabilities panel: bulleted list (rendered only when non-empty) - Implement Tools panel: Rich table with Tool, Read-Only, Safe columns (rendered only when non-empty; string tool entries default to yes/yes) - Add `✓ OK Actor added` success status line after all panels - Pass `config_path` and `show_add_panels=True` from `add()` command - Add Behave BDD scenarios covering each new panel and the success line - Add Robot Framework integration test verifying the full rich output ISSUES CLOSED: #1499
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
Feature: actor add rich output panels
|
||||
As a user of the CleverAgents CLI
|
||||
I want `agents actor add` to display all spec-required panels
|
||||
So that I can see the full details of a newly added actor
|
||||
|
||||
Scenario: Actor Added panel includes Type field
|
||||
Given an actor add CLI runner with a typed config
|
||||
When I run actor add with the typed config
|
||||
Then the actor added panel should contain the Type field
|
||||
|
||||
Scenario: Config panel is rendered after actor add
|
||||
Given an actor add CLI runner with a typed config
|
||||
When I run actor add with the typed config
|
||||
Then the output should contain a Config panel
|
||||
|
||||
Scenario: Config panel shows Path, Hash, Options, Nodes, Edges
|
||||
Given an actor add CLI runner with a typed config
|
||||
When I run actor add with the typed config
|
||||
Then the Config panel should show Path Hash Options Nodes and Edges
|
||||
|
||||
Scenario: Capabilities panel is rendered when capabilities are present
|
||||
Given an actor add CLI runner with capabilities in config
|
||||
When I run actor add with the capabilities config
|
||||
Then the output should contain a Capabilities panel with the capability list
|
||||
|
||||
Scenario: Tools panel is rendered when tools are present
|
||||
Given an actor add CLI runner with tools in config
|
||||
When I run actor add with the tools config
|
||||
Then the output should contain a Tools panel with tool rows
|
||||
|
||||
Scenario: Success status line is printed after all panels
|
||||
Given an actor add CLI runner with a typed config
|
||||
When I run actor add with the typed config
|
||||
Then the output should end with the actor added success line
|
||||
|
||||
Scenario: No Capabilities panel when capabilities list is empty
|
||||
Given an actor add CLI runner with a typed config
|
||||
When I run actor add with the typed config
|
||||
Then the output should not contain a Capabilities panel
|
||||
|
||||
Scenario: No Tools panel when tools list is empty
|
||||
Given an actor add CLI runner with a typed config
|
||||
When I run actor add with the typed config
|
||||
Then the output should not contain a Tools panel
|
||||
|
||||
Scenario: Non-rich format skips all extra panels
|
||||
Given an actor add CLI runner with a typed config
|
||||
When I run actor add with the typed config in json format
|
||||
Then the output should be valid JSON without panels
|
||||
@@ -0,0 +1,274 @@
|
||||
"""Step definitions for actor add rich output panels feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.actor import app as actor_app
|
||||
from cleveragents.domain.models.core.actor import Actor
|
||||
|
||||
|
||||
def _make_actor(
|
||||
*,
|
||||
name: str = "local/rich-actor",
|
||||
provider: str = "openai",
|
||||
model: str = "gpt-4",
|
||||
config: dict[str, Any] | None = None,
|
||||
graph_descriptor: dict[str, Any] | None = None,
|
||||
unsafe: bool = False,
|
||||
is_default: bool = False,
|
||||
is_built_in: bool = False,
|
||||
) -> Actor:
|
||||
blob = config or {}
|
||||
return Actor(
|
||||
id=1,
|
||||
name=name,
|
||||
provider=provider,
|
||||
model=model,
|
||||
config_blob=blob,
|
||||
config_hash=Actor.compute_hash(blob),
|
||||
graph_descriptor=graph_descriptor,
|
||||
unsafe=unsafe,
|
||||
is_built_in=is_built_in,
|
||||
is_default=is_default,
|
||||
)
|
||||
|
||||
|
||||
def _register_cleanup(context: Any, path: Path) -> None:
|
||||
context._cleanup_handlers.append(lambda: path.unlink(missing_ok=True))
|
||||
|
||||
|
||||
@given("an actor add CLI runner with a typed config")
|
||||
def step_impl(context: Any) -> None:
|
||||
context.runner = CliRunner()
|
||||
context.actor_config_data = {
|
||||
"name": "local/rich-actor",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"type": "graph",
|
||||
}
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".json", mode="w", encoding="utf-8"
|
||||
) as handle:
|
||||
json.dump(context.actor_config_data, handle)
|
||||
handle.flush()
|
||||
context.actor_config_path = Path(handle.name)
|
||||
_register_cleanup(context, context.actor_config_path)
|
||||
|
||||
graph_descriptor = {
|
||||
"nodes": [{"id": "n1"}, {"id": "n2"}, {"id": "n3"}],
|
||||
"edges": [{"from": "n1", "to": "n2"}, {"from": "n2", "to": "n3"}],
|
||||
}
|
||||
context.mock_actor = _make_actor(
|
||||
config=context.actor_config_data,
|
||||
graph_descriptor=graph_descriptor,
|
||||
)
|
||||
|
||||
|
||||
@given("an actor add CLI runner with capabilities in config")
|
||||
def step_impl(context: Any) -> None:
|
||||
context.runner = CliRunner()
|
||||
context.actor_config_data = {
|
||||
"name": "local/cap-actor",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"type": "llm",
|
||||
"capabilities": ["code review", "diff summarization", "lint guidance"],
|
||||
}
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".json", mode="w", encoding="utf-8"
|
||||
) as handle:
|
||||
json.dump(context.actor_config_data, handle)
|
||||
handle.flush()
|
||||
context.actor_config_path = Path(handle.name)
|
||||
_register_cleanup(context, context.actor_config_path)
|
||||
context.mock_actor = _make_actor(config=context.actor_config_data)
|
||||
|
||||
|
||||
@given("an actor add CLI runner with tools in config")
|
||||
def step_impl(context: Any) -> None:
|
||||
context.runner = CliRunner()
|
||||
context.actor_config_data = {
|
||||
"name": "local/tool-actor",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"type": "llm",
|
||||
"tools": [
|
||||
{"name": "read_file", "read_only": True, "safe": True},
|
||||
{"name": "write_file", "read_only": False, "safe": False},
|
||||
],
|
||||
}
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".json", mode="w", encoding="utf-8"
|
||||
) as handle:
|
||||
json.dump(context.actor_config_data, handle)
|
||||
handle.flush()
|
||||
context.actor_config_path = Path(handle.name)
|
||||
_register_cleanup(context, context.actor_config_path)
|
||||
context.mock_actor = _make_actor(config=context.actor_config_data)
|
||||
|
||||
|
||||
@when("I run actor add with the typed config")
|
||||
def step_impl(context: Any) -> None:
|
||||
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
|
||||
registry = MagicMock()
|
||||
registry.upsert_actor.return_value = context.mock_actor
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
context.result = context.runner.invoke(
|
||||
actor_app,
|
||||
["add", "--config", str(context.actor_config_path)],
|
||||
)
|
||||
|
||||
|
||||
@when("I run actor add with the capabilities config")
|
||||
def step_impl(context: Any) -> None:
|
||||
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
|
||||
registry = MagicMock()
|
||||
registry.upsert_actor.return_value = context.mock_actor
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
context.result = context.runner.invoke(
|
||||
actor_app,
|
||||
["add", "--config", str(context.actor_config_path)],
|
||||
)
|
||||
|
||||
|
||||
@when("I run actor add with the tools config")
|
||||
def step_impl(context: Any) -> None:
|
||||
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
|
||||
registry = MagicMock()
|
||||
registry.upsert_actor.return_value = context.mock_actor
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
context.result = context.runner.invoke(
|
||||
actor_app,
|
||||
["add", "--config", str(context.actor_config_path)],
|
||||
)
|
||||
|
||||
|
||||
@when("I run actor add with the typed config in json format")
|
||||
def step_impl(context: Any) -> None:
|
||||
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
|
||||
registry = MagicMock()
|
||||
registry.upsert_actor.return_value = context.mock_actor
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
context.result = context.runner.invoke(
|
||||
actor_app,
|
||||
["add", "--config", str(context.actor_config_path), "--format", "json"],
|
||||
)
|
||||
|
||||
|
||||
@then("the actor added panel should contain the Type field")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code == 0, (
|
||||
f"exit_code={context.result.exit_code}, output={context.result.output}"
|
||||
)
|
||||
assert "Type:" in context.result.output, (
|
||||
f"Expected 'Type:' in output:\n{context.result.output}"
|
||||
)
|
||||
assert "graph" in context.result.output, (
|
||||
f"Expected actor type value 'graph' in output:\n{context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the output should contain a Config panel")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code == 0, (
|
||||
f"exit_code={context.result.exit_code}, output={context.result.output}"
|
||||
)
|
||||
assert "Config" in context.result.output, (
|
||||
f"Expected 'Config' panel in output:\n{context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the Config panel should show Path Hash Options Nodes and Edges")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code == 0, (
|
||||
f"exit_code={context.result.exit_code}, output={context.result.output}"
|
||||
)
|
||||
output = context.result.output
|
||||
for field in ("Path:", "Hash:", "Options:", "Nodes:", "Edges:"):
|
||||
assert field in output, f"Expected '{field}' in Config panel output:\n{output}"
|
||||
|
||||
|
||||
@then("the output should contain a Capabilities panel with the capability list")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code == 0, (
|
||||
f"exit_code={context.result.exit_code}, output={context.result.output}"
|
||||
)
|
||||
output = context.result.output
|
||||
assert "Capabilities" in output, (
|
||||
f"Expected 'Capabilities' panel in output:\n{output}"
|
||||
)
|
||||
for cap in context.actor_config_data.get("capabilities", []):
|
||||
assert cap in output, f"Expected capability '{cap}' in output:\n{output}"
|
||||
|
||||
|
||||
@then("the output should contain a Tools panel with tool rows")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code == 0, (
|
||||
f"exit_code={context.result.exit_code}, output={context.result.output}"
|
||||
)
|
||||
output = context.result.output
|
||||
assert "Tools" in output, f"Expected 'Tools' panel in output:\n{output}"
|
||||
for tool_entry in context.actor_config_data.get("tools", []):
|
||||
if isinstance(tool_entry, dict):
|
||||
tool_name = tool_entry.get("name", "")
|
||||
else:
|
||||
tool_name = str(tool_entry)
|
||||
assert tool_name in output, f"Expected tool '{tool_name}' in output:\n{output}"
|
||||
|
||||
|
||||
@then("the output should end with the actor added success line")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code == 0, (
|
||||
f"exit_code={context.result.exit_code}, output={context.result.output}"
|
||||
)
|
||||
assert "✓ OK" in context.result.output, (
|
||||
f"Expected '✓ OK' in output:\n{context.result.output}"
|
||||
)
|
||||
assert "Actor added" in context.result.output, (
|
||||
f"Expected 'Actor added' in output:\n{context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the output should not contain a Capabilities panel")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code == 0, (
|
||||
f"exit_code={context.result.exit_code}, output={context.result.output}"
|
||||
)
|
||||
assert "Capabilities" not in context.result.output, (
|
||||
f"Did not expect 'Capabilities' panel in output:\n{context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the output should not contain a Tools panel")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code == 0, (
|
||||
f"exit_code={context.result.exit_code}, output={context.result.output}"
|
||||
)
|
||||
assert "Tools" not in context.result.output, (
|
||||
f"Did not expect 'Tools' panel in output:\n{context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the output should be valid JSON without panels")
|
||||
def step_impl(context: Any) -> None:
|
||||
assert context.result.exit_code == 0, (
|
||||
f"exit_code={context.result.exit_code}, output={context.result.output}"
|
||||
)
|
||||
output = context.result.output.strip()
|
||||
data = json.loads(output)
|
||||
assert isinstance(data, dict), f"Expected JSON object, got: {type(data)}"
|
||||
assert "name" in data, f"Expected 'name' field in JSON output: {data}"
|
||||
assert "Config" not in output, (
|
||||
f"Did not expect 'Config' panel in JSON output:\n{output}"
|
||||
)
|
||||
assert "✓ OK" not in output, (
|
||||
f"Did not expect success line in JSON output:\n{output}"
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
*** Settings ***
|
||||
Documentation Integration tests for actor add rich output panels
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} ${CURDIR}/helper_actor_add_rich_output.py
|
||||
|
||||
*** Test Cases ***
|
||||
Actor Add Rich Output Contains Type Field
|
||||
[Documentation] Verify that ``actor add`` rich output includes the Type field
|
||||
${result}= Run Process ${PYTHON} ${HELPER} add-type-field cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} actor-add-type-field-ok
|
||||
|
||||
Actor Add Rich Output Contains Config Panel
|
||||
[Documentation] Verify that ``actor add`` rich output includes the Config panel
|
||||
${result}= Run Process ${PYTHON} ${HELPER} add-config-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} actor-add-config-panel-ok
|
||||
|
||||
Actor Add Rich Output Contains Capabilities Panel
|
||||
[Documentation] Verify that ``actor add`` rich output includes the Capabilities panel
|
||||
${result}= Run Process ${PYTHON} ${HELPER} add-capabilities-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} actor-add-capabilities-panel-ok
|
||||
|
||||
Actor Add Rich Output Contains Tools Panel
|
||||
[Documentation] Verify that ``actor add`` rich output includes the Tools panel
|
||||
${result}= Run Process ${PYTHON} ${HELPER} add-tools-panel cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} actor-add-tools-panel-ok
|
||||
|
||||
Actor Add Rich Output Contains Success Line
|
||||
[Documentation] Verify that ``actor add`` rich output ends with the success status line
|
||||
${result}= Run Process ${PYTHON} ${HELPER} add-success-line cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} actor-add-success-line-ok
|
||||
@@ -0,0 +1,196 @@
|
||||
"""Helper script for actor add rich output panels Robot test."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.actor import app as actor_app
|
||||
from cleveragents.domain.models.core.actor import Actor
|
||||
|
||||
|
||||
def _make_actor(
|
||||
*,
|
||||
name: str = "local/robot-add-actor",
|
||||
provider: str = "openai",
|
||||
model: str = "gpt-4",
|
||||
config: dict[str, Any] | None = None,
|
||||
graph_descriptor: dict[str, Any] | None = None,
|
||||
unsafe: bool = False,
|
||||
is_default: bool = False,
|
||||
is_built_in: bool = False,
|
||||
) -> Actor:
|
||||
blob = config or {}
|
||||
return Actor(
|
||||
id=1,
|
||||
name=name,
|
||||
provider=provider,
|
||||
model=model,
|
||||
config_blob=blob,
|
||||
config_hash=Actor.compute_hash(blob),
|
||||
graph_descriptor=graph_descriptor,
|
||||
unsafe=unsafe,
|
||||
is_built_in=is_built_in,
|
||||
is_default=is_default,
|
||||
)
|
||||
|
||||
|
||||
def _run_actor_add(
|
||||
config_data: dict[str, Any],
|
||||
actor: Actor,
|
||||
) -> str:
|
||||
"""Invoke actor add with a temp config file and return the output."""
|
||||
runner = CliRunner()
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".json", mode="w", encoding="utf-8"
|
||||
) as handle:
|
||||
json.dump(config_data, handle)
|
||||
handle.flush()
|
||||
config_path = Path(handle.name)
|
||||
|
||||
try:
|
||||
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
|
||||
registry = MagicMock()
|
||||
registry.upsert_actor.return_value = actor
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
result = runner.invoke(
|
||||
actor_app,
|
||||
["add", "--config", str(config_path)],
|
||||
)
|
||||
finally:
|
||||
config_path.unlink(missing_ok=True)
|
||||
|
||||
assert result.exit_code == 0, (
|
||||
f"actor add failed (exit_code={result.exit_code}):\n{result.output}"
|
||||
)
|
||||
return result.output
|
||||
|
||||
|
||||
def test_add_type_field() -> None:
|
||||
"""Verify actor add rich output includes the Type field."""
|
||||
config_data: dict[str, Any] = {
|
||||
"name": "local/robot-add-actor",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"type": "graph",
|
||||
}
|
||||
actor = _make_actor(config=config_data)
|
||||
output = _run_actor_add(config_data, actor)
|
||||
|
||||
assert "Type:" in output, f"Expected 'Type:' in output:\n{output}"
|
||||
assert "graph" in output, f"Expected type value 'graph' in output:\n{output}"
|
||||
print("actor-add-type-field-ok")
|
||||
|
||||
|
||||
def test_add_config_panel() -> None:
|
||||
"""Verify actor add rich output includes the Config panel with all fields."""
|
||||
config_data: dict[str, Any] = {
|
||||
"name": "local/robot-add-actor",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"type": "graph",
|
||||
"options": {"temperature": 0.5, "max_tokens": 256},
|
||||
}
|
||||
graph_descriptor = {
|
||||
"nodes": [{"id": "n1"}, {"id": "n2"}],
|
||||
"edges": [{"from": "n1", "to": "n2"}],
|
||||
}
|
||||
actor = _make_actor(config=config_data, graph_descriptor=graph_descriptor)
|
||||
output = _run_actor_add(config_data, actor)
|
||||
|
||||
for field in ("Config", "Path:", "Hash:", "Options:", "Nodes:", "Edges:"):
|
||||
assert field in output, f"Expected '{field}' in output:\n{output}"
|
||||
|
||||
# Verify counts
|
||||
assert "2" in output, f"Expected node count '2' in output:\n{output}"
|
||||
assert "1" in output, f"Expected edge count '1' in output:\n{output}"
|
||||
print("actor-add-config-panel-ok")
|
||||
|
||||
|
||||
def test_add_capabilities_panel() -> None:
|
||||
"""Verify actor add rich output includes the Capabilities panel."""
|
||||
config_data: dict[str, Any] = {
|
||||
"name": "local/robot-add-actor",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"type": "llm",
|
||||
"capabilities": ["code review", "diff summarization", "lint guidance"],
|
||||
}
|
||||
actor = _make_actor(config=config_data)
|
||||
output = _run_actor_add(config_data, actor)
|
||||
|
||||
assert "Capabilities" in output, (
|
||||
f"Expected 'Capabilities' panel in output:\n{output}"
|
||||
)
|
||||
for cap in config_data["capabilities"]:
|
||||
assert cap in output, f"Expected capability '{cap}' in output:\n{output}"
|
||||
print("actor-add-capabilities-panel-ok")
|
||||
|
||||
|
||||
def test_add_tools_panel() -> None:
|
||||
"""Verify actor add rich output includes the Tools panel."""
|
||||
config_data: dict[str, Any] = {
|
||||
"name": "local/robot-add-actor",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"type": "llm",
|
||||
"tools": [
|
||||
{"name": "read_file", "read_only": True, "safe": True},
|
||||
{"name": "search_files", "read_only": True, "safe": True},
|
||||
{"name": "git_diff", "read_only": True, "safe": True},
|
||||
],
|
||||
}
|
||||
actor = _make_actor(config=config_data)
|
||||
output = _run_actor_add(config_data, actor)
|
||||
|
||||
assert "Tools" in output, f"Expected 'Tools' panel in output:\n{output}"
|
||||
assert "Tool" in output, f"Expected 'Tool' column header in output:\n{output}"
|
||||
assert "Read-Only" in output, f"Expected 'Read-Only' column in output:\n{output}"
|
||||
assert "Safe" in output, f"Expected 'Safe' column in output:\n{output}"
|
||||
for tool in config_data["tools"]:
|
||||
assert tool["name"] in output, (
|
||||
f"Expected tool '{tool['name']}' in output:\n{output}"
|
||||
)
|
||||
print("actor-add-tools-panel-ok")
|
||||
|
||||
|
||||
def test_add_success_line() -> None:
|
||||
"""Verify actor add rich output ends with the success status line."""
|
||||
config_data: dict[str, Any] = {
|
||||
"name": "local/robot-add-actor",
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"type": "graph",
|
||||
}
|
||||
actor = _make_actor(config=config_data)
|
||||
output = _run_actor_add(config_data, actor)
|
||||
|
||||
assert "✓ OK" in output, f"Expected '✓ OK' in output:\n{output}"
|
||||
assert "Actor added" in output, f"Expected 'Actor added' in output:\n{output}"
|
||||
print("actor-add-success-line-ok")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
command = sys.argv[1] if len(sys.argv) > 1 else "add-type-field"
|
||||
dispatch = {
|
||||
"add-type-field": test_add_type_field,
|
||||
"add-config-panel": test_add_config_panel,
|
||||
"add-capabilities-panel": test_add_capabilities_panel,
|
||||
"add-tools-panel": test_add_tools_panel,
|
||||
"add-success-line": test_add_success_line,
|
||||
}
|
||||
fn = dispatch.get(command)
|
||||
if fn is None:
|
||||
print(f"Unknown command: {command}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
fn()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -340,25 +340,81 @@ def _print_actor(
|
||||
actor: Actor,
|
||||
title: str = "Actor",
|
||||
fmt: str = OutputFormat.RICH.value,
|
||||
config_path: Path | None = None,
|
||||
show_add_panels: bool = False,
|
||||
) -> None:
|
||||
"""Print actor details in the requested format."""
|
||||
"""Print actor details in the requested format.
|
||||
|
||||
When ``show_add_panels`` is True (used by the ``add`` command), renders
|
||||
the full spec-required output: Actor Added panel (with Type field), Config
|
||||
panel, Capabilities panel, Tools panel, and a success status line.
|
||||
"""
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data = _actor_spec_dict(actor)
|
||||
console.print(format_output(data, fmt))
|
||||
return
|
||||
|
||||
blob = actor.config_blob or {}
|
||||
actor_type = str(blob.get("type", ""))
|
||||
|
||||
details = (
|
||||
f"[bold]Name:[/bold] {actor.name}\n"
|
||||
f"[bold]Provider:[/bold] {actor.provider}\n"
|
||||
f"[bold]Model:[/bold] {actor.model}\n"
|
||||
f"[bold]Unsafe:[/bold] {'yes' if actor.unsafe else 'no'}\n"
|
||||
f"[bold]Default:[/bold] {'yes' if actor.is_default else 'no'}\n"
|
||||
f"[bold]Built-in:[/bold] {'yes' if actor.is_built_in else 'no'}\n"
|
||||
f"[bold]Config Hash:[/bold] {actor.config_hash}\n"
|
||||
f"[bold]Updated:[/bold] {actor.updated_at}"
|
||||
f"[bold]Unsafe:[/bold] {'yes' if actor.unsafe else 'no'}\n"
|
||||
f"[bold]Type:[/bold] {actor_type}"
|
||||
)
|
||||
console.print(Panel(details, title=title, expand=False))
|
||||
|
||||
if not show_add_panels:
|
||||
return
|
||||
|
||||
# ── Config panel ──────────────────────────────────────────────────────────
|
||||
path_str = str(config_path) if config_path is not None else ""
|
||||
options_count = len(blob.get("options", {}) or {})
|
||||
|
||||
graph_descriptor = actor.graph_descriptor or {}
|
||||
nodes_count = len(graph_descriptor.get("nodes", []) or [])
|
||||
edges_count = len(graph_descriptor.get("edges", []) or [])
|
||||
|
||||
config_details = (
|
||||
f"[bold]Path:[/bold] {path_str}\n"
|
||||
f"[bold]Hash:[/bold] {actor.config_hash}\n"
|
||||
f"[bold]Options:[/bold] {options_count}\n"
|
||||
f"[bold]Nodes:[/bold] {nodes_count}\n"
|
||||
f"[bold]Edges:[/bold] {edges_count}"
|
||||
)
|
||||
console.print(Panel(config_details, title="Config", expand=False))
|
||||
|
||||
# ── Capabilities panel ────────────────────────────────────────────────────
|
||||
capabilities: list[str] = list(blob.get("capabilities", []) or [])
|
||||
if capabilities:
|
||||
cap_lines = "\n".join(f"- {cap}" for cap in capabilities)
|
||||
console.print(Panel(cap_lines, title="Capabilities", expand=False))
|
||||
|
||||
# ── Tools panel ───────────────────────────────────────────────────────────
|
||||
raw_tools: list[Any] = list(blob.get("tools", []) or [])
|
||||
if raw_tools:
|
||||
tools_table = Table(show_header=True, header_style="bold", box=None)
|
||||
tools_table.add_column("Tool")
|
||||
tools_table.add_column("Read-Only", justify="center")
|
||||
tools_table.add_column("Safe", justify="center")
|
||||
for tool_entry in raw_tools:
|
||||
if isinstance(tool_entry, dict):
|
||||
tool_name = str(tool_entry.get("name", ""))
|
||||
read_only = "yes" if tool_entry.get("read_only", False) else "no"
|
||||
safe = "yes" if tool_entry.get("safe", False) else "no"
|
||||
else:
|
||||
tool_name = str(tool_entry)
|
||||
read_only = "yes"
|
||||
safe = "yes"
|
||||
tools_table.add_row(tool_name, read_only, safe)
|
||||
console.print(Panel(tools_table, title="Tools", expand=False))
|
||||
|
||||
# ── Success status line ───────────────────────────────────────────────────
|
||||
console.print("[green bold]✓ OK[/green bold] Actor added")
|
||||
|
||||
|
||||
def _print_role_warnings(config_blob: dict[str, Any]) -> None:
|
||||
"""Print non-fatal role compatibility warnings for actor configs."""
|
||||
@@ -462,7 +518,13 @@ def add(
|
||||
set_default=set_default,
|
||||
)
|
||||
title = "Actor updated" if update_existing else "Actor added"
|
||||
_print_actor(actor, title=title, fmt=fmt)
|
||||
_print_actor(
|
||||
actor,
|
||||
title=title,
|
||||
fmt=fmt,
|
||||
config_path=config,
|
||||
show_add_panels=(not update_existing),
|
||||
)
|
||||
except (ValidationError, BusinessRuleViolation) as exc:
|
||||
console.print(f"[red]Error:[/red] {exc}")
|
||||
raise typer.Abort() from exc
|
||||
@@ -639,8 +701,7 @@ def remove(name: Annotated[str, typer.Argument(help="Actor name to remove")]) ->
|
||||
|
||||
# Display Cleanup panel
|
||||
cleanup_info = (
|
||||
"[blue]Config:[/blue] kept on disk\n"
|
||||
"[blue]Contexts:[/blue] 0 orphaned"
|
||||
"[blue]Config:[/blue] kept on disk\n[blue]Contexts:[/blue] 0 orphaned"
|
||||
)
|
||||
cleanup_panel = Panel(cleanup_info, title="Cleanup", border_style="dim")
|
||||
console.print(cleanup_panel)
|
||||
|
||||
Reference in New Issue
Block a user