Files
temp/features/steps/actor_add_name_positional_steps.py
freemo 2d07cd5ef8 fix(cli): add NAME positional argument to agents actor add command per spec
The spec (docs/reference/actor_cli.md) defines the synopsis for
`agents actor add` as:

  agents actor add <NAME> --config <FILE> [--update] [--unsafe]
  [--set-default] [--option key=value] [--format FORMAT]

The implementation was missing the required <NAME> positional argument,
silently reading the actor name from the config file's `name` field
instead. This deviates from the spec and breaks the expected CLI UX.

Changes:
- Add `name` as a required positional Argument to the `add` command
- Update docstring to match spec synopsis exactly
- The positional NAME takes precedence over any `name` field in config
- Remove the now-redundant config-file name validation (name comes from CLI)
- Add Behave BDD feature + steps for the NAME positional argument (TDD)
- Update all existing Behave step invocations to pass NAME positional arg
- Update Robot Framework helpers to pass NAME positional arg

ISSUES CLOSED: #2905
2026-04-05 08:12:32 +00:00

178 lines
5.9 KiB
Python

"""Step definitions for actor add NAME positional argument 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 cleveragents.cli.commands.actor import app as actor_app
from cleveragents.domain.models.core.actor import Actor
def _make_actor(
*,
name: str = "local/test-actor",
provider: str = "openai",
model: str = "gpt-4o-mini",
config: 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=None,
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("I have an actor JSON config file without a name field")
def step_impl(context: Any) -> None:
context.actor_config_data = {
"provider": "openai",
"model": "gpt-4o-mini",
"temperature": 0.5,
}
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)
@given("I have an actor JSON config file with a different name")
def step_impl(context: Any) -> None:
context.actor_config_data = {
"name": "local/config-name-actor",
"provider": "openai",
"model": "gpt-4o-mini",
}
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)
@when("I run actor add with NAME positional argument and config")
def step_impl(context: Any) -> None:
context.positional_name = "local/my-actor"
mock_actor = _make_actor(name=context.positional_name)
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
registry.upsert_actor.return_value = mock_actor
mock_svc.return_value = (MagicMock(), registry)
context.result = context.runner.invoke(
actor_app,
[
"add",
context.positional_name,
"--config",
str(context.actor_config_path),
],
)
context.mock_registry = registry
@when("I run actor add with NAME positional argument overriding config name")
def step_impl(context: Any) -> None:
context.positional_name = "local/positional-name-actor"
mock_actor = _make_actor(name=context.positional_name)
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
registry.upsert_actor.return_value = mock_actor
mock_svc.return_value = (MagicMock(), registry)
context.result = context.runner.invoke(
actor_app,
[
"add",
context.positional_name,
"--config",
str(context.actor_config_path),
],
)
context.mock_registry = registry
@when("I run actor add with config but no NAME positional argument")
def step_impl(context: Any) -> None:
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
mock_svc.return_value = (MagicMock(), registry)
context.result = context.runner.invoke(
actor_app,
[
"add",
"--config",
str(context.actor_config_path),
],
)
@then("the actor add should succeed with the positional name")
def step_impl(context: Any) -> None:
assert context.result.exit_code == 0, (
f"Expected exit_code=0, got {context.result.exit_code}.\n"
f"Output:\n{context.result.output}"
)
# Verify the registry was called with the positional name
assert context.mock_registry.upsert_actor.called, (
"Expected upsert_actor to be called on the registry"
)
call_kwargs = context.mock_registry.upsert_actor.call_args
actual_name = call_kwargs.kwargs.get("name") or (
call_kwargs.args[0] if call_kwargs.args else None
)
assert actual_name == context.positional_name, (
f"Expected upsert_actor called with name={context.positional_name!r}, "
f"got name={actual_name!r}"
)
@then("the actor add should use the positional NAME not the config name")
def step_impl(context: Any) -> None:
assert context.result.exit_code == 0, (
f"Expected exit_code=0, got {context.result.exit_code}.\n"
f"Output:\n{context.result.output}"
)
assert context.mock_registry.upsert_actor.called, (
"Expected upsert_actor to be called on the registry"
)
call_kwargs = context.mock_registry.upsert_actor.call_args
actual_name = call_kwargs.kwargs.get("name") or (
call_kwargs.args[0] if call_kwargs.args else None
)
assert actual_name == context.positional_name, (
f"Expected upsert_actor called with positional name={context.positional_name!r}, "
f"but got name={actual_name!r} (config name was 'local/config-name-actor')"
)
@then("the actor command should fail with missing argument error")
def step_impl(context: Any) -> None:
assert context.result.exit_code != 0, (
f"Expected non-zero exit_code, got {context.result.exit_code}.\n"
f"Output:\n{context.result.output}"
)