Files
temp/robot/helper_actor_add_yaml_first_path.py
freemo 62ded31c24 fix(cli): route 'agents actor add' through ActorRegistry.add() YAML-first path
Route the 'agents actor add' CLI command through ActorRegistry.add() instead
of the legacy registry.upsert_actor() path. This ensures the original YAML
text, schema_version, and compiled_metadata are preserved in the database.

Changes:
- src/cleveragents/cli/commands/actor.py: Add _load_config_text() helper that
  returns both raw text and parsed dict. Refactor add() to call registry.add()
  with the raw yaml_text and update=update_existing flag when a registry is
  available. The service fallback path (no registry) is unchanged.
- features/steps/actor_cli_steps.py: Update add command step definitions to
  mock registry.add() instead of registry.upsert_actor(). Update 'the actor
  add should pass the loaded config' assertion to verify registry.add() is
  called with a non-empty yaml_text string.
- features/steps/actor_cli_yaml_steps.py: Update add command steps to mock
  registry.add() instead of registry.upsert_actor().
- features/steps/actor_add_rich_output_steps.py: Update add command steps to
  mock registry.add() instead of registry.upsert_actor().
- robot/helper_actor_add_rich_output.py: Update helper to mock registry.add()
  instead of registry.upsert_actor().
- features/actor_add_yaml_first_path.feature: New Behave feature verifying
  the YAML-first persistence path is used by actor add.
- features/steps/actor_add_yaml_first_path_steps.py: Step definitions for
  the new YAML-first path feature.
- robot/actor_add_yaml_first_path.robot: New Robot integration tests verifying
  yaml_text is preserved and upsert_actor is not called.
- robot/helper_actor_add_yaml_first_path.py: Helper script for Robot tests.

Fixes #3426

ISSUES CLOSED: #3426
2026-04-05 21:19:40 +00:00

241 lines
8.1 KiB
Python

"""Helper script for actor add YAML-first path Robot integration tests.
Verifies that ``agents actor add`` routes through ActorRegistry.upsert_actor()
with ``yaml_text`` threaded through so that the original YAML text is preserved
in the database while all CLI flags (--set-default, --option) are honoured.
"""
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.core.exceptions import NotFoundError
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,
yaml_text: str | None = None,
schema_version: str = "1.0",
) -> 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=False,
is_built_in=False,
is_default=False,
yaml_text=yaml_text,
schema_version=schema_version,
)
def test_yaml_text_preserved() -> None:
"""Verify registry.upsert_actor() is called with raw yaml_text."""
runner = CliRunner()
yaml_content = "name: local/test-actor\nprovider: openai\nmodel: gpt-4o-mini\n"
with tempfile.NamedTemporaryFile(
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
) as handle:
handle.write(yaml_content)
config_path = Path(handle.name)
try:
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
mock_actor = _make_actor(yaml_text=yaml_content)
registry.upsert_actor.return_value = mock_actor
registry.get_actor.side_effect = NotFoundError("not found")
mock_svc.return_value = (MagicMock(), registry)
result = runner.invoke(
actor_app,
["add", "local/test-actor", "--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}"
)
assert registry.upsert_actor.called, "Expected registry.upsert_actor() to be called"
call_kwargs = registry.upsert_actor.call_args.kwargs
yaml_text_arg = call_kwargs.get("yaml_text", "")
assert isinstance(yaml_text_arg, str) and yaml_text_arg.strip(), (
f"Expected yaml_text to be a non-empty string, got: {yaml_text_arg!r}"
)
assert yaml_text_arg == yaml_content, (
f"yaml_text mismatch.\nExpected: {yaml_content!r}\nGot: {yaml_text_arg!r}"
)
print("yaml-text-preserved-ok")
def test_set_default_flag() -> None:
"""Verify --set-default flag passes set_default=True to registry.upsert_actor()."""
runner = CliRunner()
yaml_content = "name: local/test-actor\nprovider: openai\nmodel: gpt-4o-mini\n"
with tempfile.NamedTemporaryFile(
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
) as handle:
handle.write(yaml_content)
config_path = Path(handle.name)
try:
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
registry.upsert_actor.return_value = _make_actor(yaml_text=yaml_content)
registry.get_actor.side_effect = NotFoundError("not found")
mock_svc.return_value = (MagicMock(), registry)
result = runner.invoke(
actor_app,
[
"add",
"local/test-actor",
"--config",
str(config_path),
"--set-default",
],
)
finally:
config_path.unlink(missing_ok=True)
assert result.exit_code == 0, (
f"actor add --set-default failed "
f"(exit_code={result.exit_code}):\n{result.output}"
)
assert registry.upsert_actor.called, "Expected registry.upsert_actor() to be called"
call_kwargs = registry.upsert_actor.call_args.kwargs
assert call_kwargs.get("set_default") is True, (
f"Expected set_default=True in registry.upsert_actor() call, got: {call_kwargs}"
)
print("set-default-flag-ok")
def test_option_override_flag() -> None:
"""Verify --option flag passes option_overrides to registry.upsert_actor()."""
runner = CliRunner()
yaml_content = "name: local/test-actor\nprovider: openai\nmodel: gpt-4o-mini\n"
with tempfile.NamedTemporaryFile(
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
) as handle:
handle.write(yaml_content)
config_path = Path(handle.name)
try:
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
registry.upsert_actor.return_value = _make_actor(yaml_text=yaml_content)
registry.get_actor.side_effect = NotFoundError("not found")
mock_svc.return_value = (MagicMock(), registry)
result = runner.invoke(
actor_app,
[
"add",
"local/test-actor",
"--config",
str(config_path),
"--option",
"temperature=0.5",
],
)
finally:
config_path.unlink(missing_ok=True)
assert result.exit_code == 0, (
f"actor add --option failed (exit_code={result.exit_code}):\n{result.output}"
)
assert registry.upsert_actor.called, "Expected registry.upsert_actor() to be called"
call_kwargs = registry.upsert_actor.call_args.kwargs
option_overrides = call_kwargs.get("option_overrides")
assert option_overrides is not None, (
f"Expected option_overrides to be set, got: {call_kwargs}"
)
assert option_overrides.get("temperature") == 0.5, (
f"Expected option_overrides['temperature'] == 0.5, got: {option_overrides}"
)
print("option-override-flag-ok")
def test_update_flag() -> None:
"""Verify --update flag results in upsert semantics (no duplicate error)."""
runner = CliRunner()
config_data = {
"name": "local/test-actor",
"provider": "openai",
"model": "gpt-4o-mini",
}
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w", encoding="utf-8"
) as handle:
json.dump(config_data, handle)
config_path = Path(handle.name)
try:
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
registry.upsert_actor.return_value = _make_actor()
mock_svc.return_value = (MagicMock(), registry)
result = runner.invoke(
actor_app,
["add", "local/test-actor", "--config", str(config_path), "--update"],
)
finally:
config_path.unlink(missing_ok=True)
assert result.exit_code == 0, (
f"actor add --update failed (exit_code={result.exit_code}):\n{result.output}"
)
assert registry.upsert_actor.called, "Expected registry.upsert_actor() to be called"
print("update-flag-ok")
TESTS = {
"yaml_text_preserved": test_yaml_text_preserved,
"set_default_flag": test_set_default_flag,
"option_override_flag": test_option_override_flag,
"update_flag": test_update_flag,
}
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: helper_actor_add_yaml_first_path.py <test_name>", file=sys.stderr)
sys.exit(1)
test_name = sys.argv[1]
if test_name not in TESTS:
print(f"Unknown test: {test_name}", file=sys.stderr)
sys.exit(1)
try:
TESTS[test_name]()
except AssertionError as exc:
print(f"ASSERTION FAILED: {exc}", file=sys.stderr)
sys.exit(1)
except Exception as exc:
print(f"ERROR: {exc}", file=sys.stderr)
sys.exit(1)