"""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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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), "--prompt", 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