Files
temp/features/steps/actor_add_update_enforcement_steps.py
freemo 491781714f fix(actors): enforce --update flag in agents actor add - reject re-adding existing actor without --update
Before calling upsert_actor(), check if the actor already exists using
registry.get_actor() or service.get_actor(). If the actor exists and
update_existing is False, print the spec-required error panel and exit
with error code 1.

Error panel includes:
- Actor name
- Registration timestamp (formatted as YYYY-MM-DD HH:MM)
- Hint to use --update flag

Adds Behave tests for:
- Duplicate-without-update failure case (exit code 1, error panel shown)
- Duplicate-with-update success case (exit code 0, actor updated)
- New actor without --update success case (exit code 0, actor added)

ISSUES CLOSED: #2609
2026-04-05 07:49:49 +00:00

157 lines
5.6 KiB
Python

"""Step definitions for actor add --update flag enforcement (issue #2609).
Tests that `agents actor add` rejects re-adding an existing actor without
the --update flag, and succeeds when --update is provided.
"""
from __future__ import annotations
import json
import tempfile
from datetime import datetime
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.core.exceptions import NotFoundError
from cleveragents.domain.models.core.actor import Actor
def _make_actor(
*,
name: str = "local/existing-actor",
provider: str = "openai",
model: str = "gpt-4",
config: dict[str, Any] | None = None,
updated_at: datetime | None = None,
) -> Actor:
blob = config or {}
return Actor(
id=1,
name=name,
provider=provider,
model=model,
config_blob=blob,
config_hash=Actor.compute_hash(blob),
unsafe=False,
is_built_in=False,
is_default=False,
updated_at=updated_at or datetime(2026, 2, 7, 14, 22, 0),
)
def _register_cleanup(context: Any, path: Path) -> None:
context._cleanup_handlers.append(lambda: path.unlink(missing_ok=True))
def _write_actor_config(name: str = "local/existing-actor") -> Path:
config_data = {
"name": name,
"provider": "openai",
"model": "gpt-4",
}
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w", encoding="utf-8"
) as handle:
json.dump(config_data, handle)
handle.flush()
return Path(handle.name)
@given("an actor add CLI runner where the actor already exists")
def step_given_actor_exists(context: Any) -> None:
context.runner = CliRunner()
context.actor_name = "local/existing-actor"
context.actor_config_path = _write_actor_config(context.actor_name)
_register_cleanup(context, context.actor_config_path)
# The existing actor that will be returned by get_actor
context.existing_actor = _make_actor(
name=context.actor_name,
updated_at=datetime(2026, 2, 7, 14, 22, 0),
)
# The actor returned after a successful upsert (for --update case)
context.updated_actor = _make_actor(
name=context.actor_name,
updated_at=datetime(2026, 2, 7, 14, 22, 0),
)
@given("an actor add CLI runner where the actor does not exist")
def step_given_actor_not_exists(context: Any) -> None:
context.runner = CliRunner()
context.actor_name = "local/new-actor"
context.actor_config_path = _write_actor_config(context.actor_name)
_register_cleanup(context, context.actor_config_path)
context.new_actor = _make_actor(name=context.actor_name)
@when("I run actor add without the --update flag")
def step_when_add_without_update(context: Any) -> None:
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
if hasattr(context, "existing_actor"):
# Actor exists — get_actor returns it, triggering the error path
registry.get_actor.return_value = context.existing_actor
else:
# Actor does not exist — get_actor raises NotFoundError
registry.get_actor.side_effect = NotFoundError(
resource_type="actor", resource_id=context.actor_name
)
registry.upsert_actor.return_value = context.new_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 --update flag")
def step_when_add_with_update(context: Any) -> None:
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
# get_actor returns the existing actor (simulating actor already exists)
registry.get_actor.return_value = context.existing_actor
# upsert_actor returns the updated actor
registry.upsert_actor.return_value = context.updated_actor
mock_svc.return_value = (MagicMock(), registry)
context.result = context.runner.invoke(
actor_app,
["add", "--config", str(context.actor_config_path), "--update"],
)
@then("the actor-add-enforcement exit code should be {code:d}")
def step_then_exit_code(context: Any, code: int) -> None:
assert context.result.exit_code == code, (
f"Expected exit code {code}, got {context.result.exit_code}.\n"
f"Output:\n{context.result.output}"
)
@then('the actor-add-enforcement output should contain an error panel with "{text}"')
def step_then_output_contains_error_panel(context: Any, text: str) -> None:
output = context.result.output
assert text in output, f"Expected '{text}' in output:\n{output}"
assert "Error" in output, f"Expected 'Error' panel title in output:\n{output}"
@then('the actor-add-enforcement output should contain "{text}"')
def step_then_output_contains(context: Any, text: str) -> None:
output = context.result.output
assert text in output, f"Expected '{text}' in output:\n{output}"
@then("the actor-add-enforcement output should contain the registration timestamp")
def step_then_output_contains_timestamp(context: Any) -> None:
output = context.result.output
# The timestamp should be formatted as "YYYY-MM-DD HH:MM"
assert "2026-02-07 14:22" in output, (
f"Expected registration timestamp '2026-02-07 14:22' in output:\n{output}"
)