Files
temp/features/steps/actor_cli_run_steps.py
T
CoreRasurae e4c01492d5 refactor(cli): align actor run signature with spec positional args
Aligned the `agents actor run` command signature with the
specification by introducing positional NAME and PROMPT arguments.
The --config/-c option is preserved as an optional fallback for
direct YAML invocation. When NAME is provided without --config,
the actor is resolved from the Actor Registry.

Updated both actor_run.py and actor.py run commands. Added backward
compatibility: if --config is provided, it takes precedence over
name-based resolution.

Review fixes applied (code review round 1):
- P1-1: Narrowed bare `except Exception` to `except NotFoundError`
  in _resolve_config_files to avoid masking infrastructure errors.
- P1-2: Moved _resolve_config_files call inside the try block in
  run() so container/registry init errors get user-friendly messages.
  Added `except click.exceptions.Exit: raise` to let typer.Exit
  propagate through the broadened try scope.
- P1-3: Added atexit.register cleanup for temp files created by
  _resolve_config_files (resource leak fix).
- P1-4: Added CHANGELOG.md entry for the breaking CLI change.
- P2-1: Extracted duplicated _resolve_config_files to shared module
  `_resolve_actor.py`; both actor.py and actor_run.py now import it.
- P2-2: Added guard for actors with no configuration data
  (config_blob=None) to produce a clear error instead of invalid YAML.
- P2-3/P2-4: Added 5 BDD scenarios exercising the real
  resolve_config_files function (registry path, yaml_text path,
  config_blob fallback, no-config-data error, not-found error).
- P2-5: Added @coverage tags to all new BDD scenarios.
- P3-1: Added timeout=120s and on_timeout=kill to Robot tests.
- Fixed rxpy_route_validation.robot tests that used the removed
  --prompt/-p option (replaced with positional NAME + PROMPT args).

Review fixes applied (code review round 2):
- P2-1: Aligned actor_run.py exception handler from
  `CleverAgentsException` to `CleverAgentsError`, matching actor.py
  so infrastructure errors from registry resolution get user-friendly
  messages instead of falling through to the generic handler.
- P2-2: Changed `yaml.dump` to `yaml.safe_dump` in _resolve_actor.py
  for fail-fast behavior on unexpected types, consistent with the
  codebase's dominant pattern.
- P3-6: Replaced defensive `getattr(actor, ...)` calls with direct
  Pydantic model attribute access (`actor.yaml_text`, `actor.config_blob`)
  for type-checker coverage.
- P3-1: Switched BDD temp file cleanup from post-assertion
  `unlink()` to `context.add_cleanup()` for leak-proof teardown.
- P2-3/P3-2/P3-3/P3-4: Added 3 BDD edge-case scenarios
  (empty config_blob dict, infrastructure error propagation, empty
  string name) and 1 Robot test case (actor_app registry resolution).

Review fixes applied (code review round 3):
- P1-1: Migrated 48 remaining `-p` invocations across 9 Robot test
  files to the new positional `NAME PROMPT` pattern (context_delete_all_yes,
  load_context_test, scientific_paper_e2e_test, routing_prefix_stripping,
  scientific_paper_basic, scientific_paper_writer_test,
  context_management_test, initial_next_command_test,
  system_prompt_template_rendering).
- P2-1: Documented `--config/-c` as a spec deviation in
  `_resolve_actor.py` module docstring (spec lines 4562-4566 define
  `actor run` with no --config option; issue #901 AC accepts keeping
  it as optional).
- P2-2: Corrected `--config` help text from "fallback" to "overrides
  registry-based name resolution" — the option takes precedence, not
  the other way around.
- P3-1: Added `.strip()` to `yaml_text` emptiness check in
  `_resolve_actor.py` to handle whitespace-only values that would
  otherwise bypass the `config_blob` fallback.
- P3-2: Added `from None` to the no-configuration-data
  `typer.Exit(code=2)` for consistency with the not-found path.
- P3-3: Added BDD scenario testing `--config` precedence for
  `actor_run_app` (was only tested for `actor_app`).
- P3-4: Strengthened config_blob BDD scenario to verify generated
  YAML is parseable via `yaml.safe_load` round-trip.
- P3-7: Replaced hardcoded `/tmp/dummy.yaml` with
  `tempfile.gettempdir()` for portability.
- P3-8: Moved 5 inline imports to module level per CONTRIBUTING.md
  §1292-1294 (3x `import click`, 1x InfrastructureError in steps;
  1x `import typer` in robot helper).
- P3-9: Added `encoding="utf-8"` to `_write_yaml` in Robot helper
  for consistency with production code.

Review fixes applied (code review round 4):
- P2-1: Wrapped `yaml.safe_dump` in `_resolve_actor.py` with
  `try/except yaml.YAMLError` so non-serialisable config_blob values
  produce a user-friendly error message instead of a raw traceback.
- P2-2: Moved remaining inline `import yaml` to module level in
  `actor_run_signature_steps.py` per CONTRIBUTING.md §1292-1294.
- P2-3: Replaced 3 bare `assert` statements in Robot helper
  `helper_actor_run_signature.py` with diagnostic `if/print/sys.exit`
  pattern matching the rest of the file, improving failure diagnostics.

Review fixes applied (code review round 5):
- P3-4: Replaced per-call `atexit.register(lambda)` in
  `_resolve_actor.py` with a module-level `_temp_files` set and a
  single `atexit` handler (`_cleanup_temp_files`) to prevent
  unbounded handler accumulation in same-process usage (e.g. test
  suites running multiple CliRunner invocations).
- P3-1/P3-2/P3-3: Added 4 BDD scenarios: whitespace-only
  `yaml_text` fallback to config_blob, config-precedence
  registry-not-consulted assertion for both `actor_app` and
  `actor_run_app`, multiple `--config` files with positional NAME.
- P4-1: Strengthened error-path BDD assertions to verify error
  message content (not-found, no-config-data, serialisation-error)
  alongside exit codes via captured stderr.
- P4-2: Added Robot test case for `actor_app` unknown-name error
  path and corresponding helper function.

ISSUES CLOSED: #901
2026-03-26 13:41:31 +00:00

1041 lines
31 KiB
Python

"""Step definitions for actor CLI run coverage."""
from __future__ import annotations
import json
import tempfile
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from behave import given, then, when
from cleveragents.cli.commands.actor import app as actor_app
from cleveragents.cli.commands.actor_run import app as actor_run_app
from cleveragents.core.exceptions import (
CleverAgentsError,
CleverAgentsException,
UnsafeConfigurationError,
)
def _register_cleanup(context, path: Path) -> None:
context._cleanup_handlers.append(lambda: path.unlink(missing_ok=True))
def _make_app(
*,
result: str,
config_global_context: dict[str, Any] | None = None,
run_side_effect: Exception | None = None,
) -> MagicMock:
app_exec = MagicMock()
app_exec.config = SimpleNamespace(global_context=dict(config_global_context or {}))
app_exec.run_single_shot = AsyncMock(return_value=result)
if run_side_effect is not None:
app_exec.run_single_shot.side_effect = run_side_effect
return app_exec
def _make_context_manager(
*,
global_context: dict[str, Any] | None = None,
exists: bool = False,
) -> MagicMock:
ctx_mgr = MagicMock()
ctx_mgr.global_context = dict(global_context or {})
ctx_mgr.exists.return_value = exists
return ctx_mgr
@given("I have a saved context JSON file")
def step_saved_context_json(context):
context.load_context_data = {"global_context": {"session": "stored"}}
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w", encoding="utf-8"
) as handle:
json.dump(context.load_context_data, handle)
handle.flush()
context.load_context_path = Path(handle.name)
_register_cleanup(context, context.load_context_path)
@given("I have an actor output file path")
def step_actor_output_path(context):
with tempfile.NamedTemporaryFile(
delete=False, suffix=".txt", mode="w", encoding="utf-8"
) as handle:
pass
context.output_path = Path(handle.name)
_register_cleanup(context, context.output_path)
@when("I run actor run with load context and context name")
def step_run_actor_with_load_and_context(context):
context.prompt = "hello from context"
context.run_result = "actor response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
ctx_mgr = _make_context_manager(global_context={"from": "context"})
with (
patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
),
patch(
"cleveragents.cli.commands.actor.ContextManager",
return_value=ctx_mgr,
),
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--context",
"session-1",
"--load-context",
str(context.load_context_path),
"--output",
str(context.output_path),
],
)
context.app_exec = app_exec
context.ctx_mgr = ctx_mgr
@when("I run actor run with load context only")
def step_run_actor_with_load_only(context):
context.prompt = "load-only"
context.run_result = "loaded context response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
with patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--load-context",
str(context.load_context_path),
],
)
context.app_exec = app_exec
@when("I run actor run with context only")
def step_run_actor_with_context_only(context):
context.prompt = "context-only"
context.run_result = "context response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
ctx_mgr = _make_context_manager(
global_context={"cached": "value"},
exists=True,
)
with (
patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
),
patch(
"cleveragents.cli.commands.actor.ContextManager",
return_value=ctx_mgr,
),
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--context",
"session-2",
],
)
context.app_exec = app_exec
context.ctx_mgr = ctx_mgr
@when("I run actor run without context")
def step_run_actor_without_context(context):
context.prompt = "no-context"
context.run_result = "direct response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
with patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
],
)
context.app_exec = app_exec
@when("I run actor run with load context and context name allowing rxpy")
def step_run_actor_with_load_context_and_rxpy(context):
context.prompt = "rxpy-context"
context.run_result = "rxpy response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
ctx_mgr = _make_context_manager(global_context={"from": "context"})
with (
patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
),
patch(
"cleveragents.cli.commands.actor.ContextManager",
return_value=ctx_mgr,
),
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--context",
"session-rxpy",
"--load-context",
str(context.load_context_path),
"--output",
str(context.output_path),
"--allow-rxpy-in-run-mode",
],
)
context.app_exec = app_exec
context.ctx_mgr = ctx_mgr
@when("I run actor run with load context only allowing rxpy")
def step_run_actor_with_load_only_rxpy(context):
context.prompt = "rxpy-load-only"
context.run_result = "rxpy load response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
with patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--load-context",
str(context.load_context_path),
"--allow-rxpy-in-run-mode",
],
)
context.app_exec = app_exec
@when("I run actor run with context only allowing rxpy")
def step_run_actor_with_context_only_rxpy(context):
context.prompt = "rxpy-context-only"
context.run_result = "rxpy context response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
ctx_mgr = _make_context_manager(
global_context={"cached": "value"},
exists=True,
)
with (
patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
),
patch(
"cleveragents.cli.commands.actor.ContextManager",
return_value=ctx_mgr,
),
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--context",
"session-rxpy-only",
"--allow-rxpy-in-run-mode",
],
)
context.app_exec = app_exec
context.ctx_mgr = ctx_mgr
@when("I run actor run without context allowing rxpy")
def step_run_actor_without_context_rxpy(context):
context.prompt = "rxpy-no-context"
context.run_result = "rxpy direct response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
with patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--allow-rxpy-in-run-mode",
],
)
context.app_exec = app_exec
@when("I run actor run with unsafe configuration error")
def step_run_actor_unsafe_error(context):
context.prompt = "unsafe"
error = UnsafeConfigurationError("unsafe config")
app_exec = _make_app(
result="unused",
config_global_context={"existing": "value"},
run_side_effect=error,
)
with patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
],
)
context.expected_error = "Error: unsafe config"
context.expected_exit_code = 1
@when("I run actor run with clever agents error")
def step_run_actor_clever_agents_error(context):
context.prompt = "failed"
error = CleverAgentsError("request failed")
app_exec = _make_app(
result="unused",
config_global_context={"existing": "value"},
run_side_effect=error,
)
with patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
],
)
context.expected_error = "Error: request failed"
context.expected_exit_code = 2
@when("I invoke the actor-run command with load context and a context name")
def step_invoke_actor_run_with_load_and_context(context):
context.prompt = "hello from actor-run"
context.run_result = "actor-run response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
ctx_mgr = _make_context_manager(global_context={"from": "context"})
with (
patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
),
patch(
"cleveragents.cli.commands.actor_run.ContextManager",
return_value=ctx_mgr,
),
):
context.result = context.runner.invoke(
actor_run_app,
[
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--context",
"actor-run-session",
"--load-context",
str(context.load_context_path),
"--output",
str(context.output_path),
],
)
context.app_exec = app_exec
context.ctx_mgr = ctx_mgr
@when("I invoke the actor-run command with load context only")
def step_invoke_actor_run_with_load_only(context):
context.prompt = "actor-run load-only"
context.run_result = "actor-run loaded response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
with patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
):
context.result = context.runner.invoke(
actor_run_app,
[
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--load-context",
str(context.load_context_path),
],
)
context.app_exec = app_exec
@when("I invoke the actor-run command with context only")
def step_invoke_actor_run_with_context_only(context):
context.prompt = "actor-run context-only"
context.run_result = "actor-run context response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
ctx_mgr = _make_context_manager(
global_context={"cached": "value"},
exists=True,
)
with (
patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
),
patch(
"cleveragents.cli.commands.actor_run.ContextManager",
return_value=ctx_mgr,
),
):
context.result = context.runner.invoke(
actor_run_app,
[
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--context",
"actor-run-session-2",
],
)
context.app_exec = app_exec
context.ctx_mgr = ctx_mgr
@when("I invoke the actor-run command without any context")
def step_invoke_actor_run_without_context(context):
context.prompt = "actor-run no context"
context.run_result = "actor-run direct response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
with patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
):
context.result = context.runner.invoke(
actor_run_app,
[
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
],
)
context.app_exec = app_exec
@when("I invoke the actor-run command with an unsafe configuration error")
def step_invoke_actor_run_unsafe_error(context):
context.prompt = "actor-run unsafe"
error = UnsafeConfigurationError("unsafe config")
app_exec = _make_app(
result="unused",
config_global_context={"existing": "value"},
run_side_effect=error,
)
with patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
):
context.result = context.runner.invoke(
actor_run_app,
[
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
],
)
context.expected_error = "Error: unsafe config"
context.expected_exit_code = 1
@when("I invoke the actor-run command with a clever agents exception")
def step_invoke_actor_run_clever_agents_exception(context):
context.prompt = "actor-run failed"
error = CleverAgentsException("request failed")
app_exec = _make_app(
result="unused",
config_global_context={"existing": "value"},
run_side_effect=error,
)
with patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
):
context.result = context.runner.invoke(
actor_run_app,
[
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
],
)
context.expected_error = "Error: request failed"
context.expected_exit_code = 2
@then("the actor run should write output and persist context")
def step_actor_run_writes_output(context):
assert context.result.exit_code == 0
assert context.output_path.read_text() == context.run_result
assert f"Output written to {context.output_path}" in context.result.output
context.ctx_mgr.import_context.assert_called_once_with(context.load_context_path)
context.ctx_mgr.add_message.assert_any_call("user", context.prompt)
context.ctx_mgr.add_message.assert_any_call("assistant", context.run_result)
context.ctx_mgr.save_global_context.assert_called_once_with(
context.app_exec.config.global_context
)
assert context.app_exec.config.global_context.get("from") == "context"
@then("the actor run should update global context and echo result")
def step_actor_run_updates_global_context(context):
assert context.result.exit_code == 0
assert context.run_result in context.result.output
for key, value in context.load_context_data["global_context"].items():
assert context.app_exec.config.global_context.get(key) == value
call_kwargs = context.app_exec.run_single_shot.call_args.kwargs
assert call_kwargs.get("context_manager") is None
@then("the actor run should reuse saved context")
def step_actor_run_reuses_context(context):
assert context.result.exit_code == 0
context.ctx_mgr.exists.assert_called_once()
context.ctx_mgr.add_message.assert_any_call("user", context.prompt)
context.ctx_mgr.add_message.assert_any_call("assistant", context.run_result)
context.ctx_mgr.save_global_context.assert_called_once_with(
context.app_exec.config.global_context
)
call_kwargs = context.app_exec.run_single_shot.call_args.kwargs
assert call_kwargs.get("context_manager") == context.ctx_mgr
assert context.app_exec.config.global_context.get("cached") == "value"
@then("the actor run should call single shot without context manager")
def step_actor_run_single_shot_no_context(context):
assert context.result.exit_code == 0
assert context.run_result in context.result.output
call_kwargs = context.app_exec.run_single_shot.call_args.kwargs
assert call_kwargs.get("context_manager") is None
@then("the actor run should exit with error code 1")
def step_actor_run_exit_code_one(context):
assert context.result.exit_code == 1
assert context.expected_error in context.result.output
@then("the actor run should exit with error code 2")
def step_actor_run_exit_code_two(context):
assert context.result.exit_code == 2
assert context.expected_error in context.result.output
@then("the actor run should pass allow rxpy flag")
def step_actor_run_allow_rxpy_flag(context):
call_kwargs = context.app_exec.run_single_shot.call_args.kwargs
assert call_kwargs.get("allow_rxpy_in_run_mode") is True
# ---------------------------------------------------------------------------
# --skill flag: actor.py run command
# ---------------------------------------------------------------------------
@when("I run actor run with a single skill flag")
def step_run_actor_with_single_skill(context):
context.prompt = "skill-single"
context.run_result = "skill response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
with patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
) as mock_cls:
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
],
)
context.app_exec = app_exec
context.mock_cls = mock_cls
context.expected_skill_names = ["local/web-tools"]
@when("I run actor run with multiple skill flags")
def step_run_actor_with_multiple_skills(context):
context.prompt = "skill-multi"
context.run_result = "multi skill response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
with patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
) as mock_cls:
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
"--skill",
"local/db-tools",
],
)
context.app_exec = app_exec
context.mock_cls = mock_cls
context.expected_skill_names = ["local/web-tools", "local/db-tools"]
@when("I run actor run with an unknown skill flag")
def step_run_actor_with_unknown_skill(context):
"""Exercise the real error chain: ReactiveCleverAgentsApp.__init__
calls _resolve_skills(), which obtains SkillService from the DI
container. We mock only the container so the real constructor and
_resolve_skills() execute end-to-end.
"""
context.prompt = "skill-unknown"
mock_service = MagicMock()
mock_service.resolve_tools.side_effect = KeyError(
"Skill 'local/nonexistent' is not registered"
)
mock_container = MagicMock()
mock_container.skill_service.return_value = mock_service
with patch(
"cleveragents.reactive.application.get_container",
return_value=mock_container,
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--skill",
"local/nonexistent",
],
)
# ---------------------------------------------------------------------------
# --skill flag: actor_run.py run command
# ---------------------------------------------------------------------------
@when("I invoke the actor-run command with a single skill flag")
def step_invoke_actor_run_with_single_skill(context):
context.prompt = "actor-run skill-single"
context.run_result = "actor-run skill response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
with patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
) as mock_cls:
context.result = context.runner.invoke(
actor_run_app,
[
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
],
)
context.app_exec = app_exec
context.mock_cls = mock_cls
context.expected_skill_names = ["local/web-tools"]
@when("I invoke the actor-run command with multiple skill flags")
def step_invoke_actor_run_with_multiple_skills(context):
context.prompt = "actor-run skill-multi"
context.run_result = "actor-run multi skill response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
with patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
) as mock_cls:
context.result = context.runner.invoke(
actor_run_app,
[
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
"--skill",
"local/db-tools",
],
)
context.app_exec = app_exec
context.mock_cls = mock_cls
context.expected_skill_names = ["local/web-tools", "local/db-tools"]
@when("I invoke the actor-run command with an unknown skill flag")
def step_invoke_actor_run_with_unknown_skill(context):
"""Exercise the real error chain: ReactiveCleverAgentsApp.__init__
calls _resolve_skills(), which obtains SkillService from the DI
container. We mock only the container so the real constructor and
_resolve_skills() execute end-to-end.
"""
context.prompt = "actor-run skill-unknown"
mock_service = MagicMock()
mock_service.resolve_tools.side_effect = KeyError(
"Skill 'local/nonexistent' is not registered"
)
mock_container = MagicMock()
mock_container.skill_service.return_value = mock_service
with patch(
"cleveragents.reactive.application.get_container",
return_value=mock_container,
):
context.result = context.runner.invoke(
actor_run_app,
[
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--skill",
"local/nonexistent",
],
)
# ---------------------------------------------------------------------------
# --skill flag: shared assertions
# ---------------------------------------------------------------------------
@then("the actor run should pass skill names to the runtime")
@then("the actor run should pass all skill names to the runtime")
def step_actor_run_pass_skill_names(context):
assert context.result.exit_code == 0
call_kwargs = context.mock_cls.call_args.kwargs
assert call_kwargs.get("skill_names") == context.expected_skill_names
@then("the actor run should exit with skill not found error")
def step_actor_run_skill_not_found(context):
assert context.result.exit_code == 2
assert "Error: Skill 'local/nonexistent' not found in registry" in (
context.result.output
)
@then("the context manager should have been instantiated")
def step_context_manager_instantiated(context):
context.mock_ctx_cls.assert_called_once()
@then("the context manager exists should have been called")
def step_context_manager_exists_called(context):
context.ctx_mgr.exists.assert_called_once()
# ---------------------------------------------------------------------------
# --skill + --context combined flag test
# ---------------------------------------------------------------------------
@when("I run actor run with skill and context flags")
def step_run_actor_with_skill_and_context(context):
context.prompt = "skill-context-combo"
context.run_result = "skill context response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
ctx_mgr = _make_context_manager(
global_context={"cached": "value"},
exists=True,
)
with (
patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
) as mock_cls,
patch(
"cleveragents.cli.commands.actor.ContextManager",
return_value=ctx_mgr,
) as mock_ctx_cls,
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
"--context",
"test-session",
],
)
context.app_exec = app_exec
context.mock_cls = mock_cls
context.mock_ctx_cls = mock_ctx_cls
context.ctx_mgr = ctx_mgr
context.expected_skill_names = ["local/web-tools"]
# ---------------------------------------------------------------------------
# --skill + --context combined flag test for actor_run.py (M6)
# ---------------------------------------------------------------------------
@when("I invoke the actor-run command with skill and context flags")
def step_invoke_actor_run_with_skill_and_context(context):
context.prompt = "actor-run skill-context-combo"
context.run_result = "actor-run skill context response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
ctx_mgr = _make_context_manager(
global_context={"cached": "value"},
exists=True,
)
with (
patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
) as mock_cls,
patch(
"cleveragents.cli.commands.actor_run.ContextManager",
return_value=ctx_mgr,
) as mock_ctx_cls,
):
context.result = context.runner.invoke(
actor_run_app,
[
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
"--context",
"test-session",
],
)
context.app_exec = app_exec
context.mock_cls = mock_cls
context.mock_ctx_cls = mock_ctx_cls
context.ctx_mgr = ctx_mgr
context.expected_skill_names = ["local/web-tools"]
# ---------------------------------------------------------------------------
# Duplicate skill deduplication test (M7)
# ---------------------------------------------------------------------------
@when("I invoke the actor-run command with duplicate skill flags")
def step_invoke_actor_run_with_duplicate_skills(context):
context.prompt = "actor-run skill-dedup"
context.run_result = "actor-run dedup response"
app_exec = _make_app(
result=context.run_result,
config_global_context={"existing": "value"},
)
with patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
) as mock_cls:
context.result = context.runner.invoke(
actor_run_app,
[
"--config",
str(context.actor_config_path),
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
"--skill",
"local/web-tools",
],
)
context.mock_cls = mock_cls
@then("the actor run should pass duplicate skill names to the runtime")
def step_actor_run_pass_duplicate_skill_names(context):
assert context.result.exit_code == 0
call_kwargs = context.mock_cls.call_args.kwargs
# CLI passes duplicate names through unchanged; deduplication
# happens inside ReactiveCleverAgentsApp.__init__ via dict.fromkeys.
assert call_kwargs.get("skill_names") == [
"local/web-tools",
"local/web-tools",
]