refactor(cli): align actor run signature with spec positional args
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 15s
CI / lint (pull_request) Successful in 3m18s
CI / quality (pull_request) Successful in 3m47s
CI / typecheck (pull_request) Successful in 3m52s
CI / security (pull_request) Successful in 4m1s
CI / integration_tests (pull_request) Successful in 6m53s
CI / unit_tests (pull_request) Successful in 7m9s
CI / docker (pull_request) Successful in 1m9s
CI / e2e_tests (pull_request) Successful in 11m5s
CI / coverage (pull_request) Successful in 11m15s
CI / status-check (pull_request) Successful in 1s
CI / lint (push) Successful in 3m16s
CI / build (push) Successful in 31s
CI / typecheck (push) Successful in 4m0s
CI / security (push) Successful in 4m1s
CI / benchmark-regression (push) Has been skipped
CI / quality (push) Successful in 4m8s
CI / integration_tests (push) Successful in 6m10s
CI / unit_tests (push) Successful in 7m35s
CI / docker (push) Successful in 1m9s
CI / e2e_tests (push) Successful in 9m58s
CI / coverage (push) Successful in 11m7s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Failing after 17m22s
CI / benchmark-regression (pull_request) Successful in 56m7s

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
This commit was merged in pull request #1072.
This commit is contained in:
CoreRasurae
2026-03-19 14:53:33 +00:00
committed by Luis Mendes
parent 00881a3e5f
commit e4c01492d5
22 changed files with 1697 additions and 103 deletions
+11
View File
@@ -125,6 +125,17 @@
retention controls via `max_audit_log_size` and `clear_audit_log()`. Includes
expanded Behave and Robot integration coverage, 5 ASV benchmark suites, and
`vulture_whitelist.py` entry. (#587)
- **Breaking (CLI):** `agents actor run` now takes positional `<NAME>` and
`<PROMPT>` arguments, aligning the command signature with the specification.
The previous `--prompt/-p` option is removed. `--config/-c` is preserved as
an optional fallback that overrides registry-based name resolution (spec
deviation documented in `_resolve_actor.py`). Shared resolution logic
extracted to `_resolve_actor.py` with `yaml.safe_dump`, early name
validation, input sanitisation, resilient `atexit`-based temp file cleanup,
and graceful error handling for missing actors, empty config data, and
non-serialisable config blobs (without exposing serializer internals in
user-facing errors). Comprehensive BDD and Robot Framework
tests cover all resolution paths and edge cases. (#901)
- Added BuiltinAdapter class and MCP automatic resource slot creation.
BuiltinAdapter wraps register_file_tools/register_git_tools/register_subplan_tool
into a unified adapter interface. McpAdapter.infer_resource_slots() analyzes
+130
View File
@@ -0,0 +1,130 @@
@coverage @coverage_actor_run_signature
Feature: Actor run command positional arguments
The `agents actor run` command accepts positional NAME and PROMPT arguments
per the specification. The --config/-c option is preserved as an optional
fallback for direct YAML invocation.
Background:
Given an actor CLI runner
And I have an actor JSON config file
# ---------- Positional NAME and PROMPT arguments ----------
Scenario: Run with positional NAME and PROMPT using --config fallback
When I run actor run with positional args and config fallback
Then the actor run should call single shot without context manager
Scenario: Run with positional NAME resolved from actor registry
When I run actor run with name resolved from the actor registry
Then the actor run should call single shot without context manager
Scenario: Run with positional PROMPT containing flag-like text
When I run actor run with a positional prompt containing flag-like text
Then the actor run should pass the exact positional prompt
Scenario: Run with positional NAME through registry integration path
When I run actor run through registry integration path
Then the actor run should call single shot without context manager
And the actor registry should have been consulted with requested actor name
# ---------- Backward compatibility with --config ----------
Scenario: Config flag takes precedence over name-based resolution
When I run actor run with config flag taking precedence
Then the actor run should call single shot without context manager
# ---------- Error: actor name not found in registry ----------
Scenario: Error when actor name not found in registry
When I run actor run with an unknown actor name
Then the actor run should exit with registry not found error
# ---------- actor_run.py positional args ----------
Scenario: Actor-run module config flag takes precedence over name
When I invoke the actor-run module with config flag taking precedence
Then the actor run should call single shot without context manager
Scenario: Actor-run module with positional NAME and PROMPT
When I invoke the actor-run module with positional args and config fallback
Then the actor run should call single shot without context manager
Scenario: Actor-run module with name from registry
When I invoke the actor-run module with name from the actor registry
Then the actor run should call single shot without context manager
Scenario: Actor-run module error when name not found
When I invoke the actor-run module with an unknown actor name
Then the actor run should exit with registry not found error
# ---------- resolve_config_files unit tests ----------
Scenario: resolve_config_files returns config when provided
When I call resolve_config_files with a config list
Then the original config list is returned unchanged
Scenario: resolve_config_files resolves actor from registry with yaml_text
When I call resolve_config_files with a name and no config
Then a temporary YAML file is created from the registry actor
Scenario: resolve_config_files falls back to config_blob when yaml_text is empty
When I call resolve_config_files with an actor that has no yaml_text
Then a temporary YAML file is created from config_blob
Scenario: resolve_config_files raises exit when actor has no config data
When I call resolve_config_files with an actor that has no config data
Then it should exit with a no-configuration-data error
Scenario: resolve_config_files raises exit for unknown actor
When I call resolve_config_files with an unknown actor name directly
Then it should exit with a not-found error
# ---------- Edge cases and error propagation ----------
Scenario: resolve_config_files raises exit when config_blob is empty dict
When I call resolve_config_files with an actor that has empty config_blob
Then it should exit with a no-configuration-data error
Scenario: Infrastructure errors propagate through resolve_config_files
When I call resolve_config_files and the container raises an infrastructure error
Then the infrastructure error should propagate uncaught
Scenario: resolve_config_files rejects empty string name with validation error
When I call resolve_config_files with an empty string name
Then the empty name should be rejected with a validation error
Scenario: resolve_config_files sanitizes control characters in actor name errors
When I call resolve_config_files with a control-character actor name
Then the not-found error should not include control characters
Scenario: resolve_config_files exits when config_blob cannot be serialised to YAML
When I call resolve_config_files with an actor whose config_blob is not YAML-serialisable
Then it should exit with a serialisation error
# ---------- Review round 5 fixes ----------
Scenario: resolve_config_files falls back to config_blob when yaml_text is whitespace-only
When I call resolve_config_files with an actor that has whitespace-only yaml_text
Then a temporary YAML file is created from the whitespace actor config_blob
Scenario: Config precedence does not consult the actor registry
When I run actor run with config flag and a registry spy
Then the actor registry should not have been consulted
Scenario: Actor-run config precedence does not consult the actor registry
When I invoke the actor-run module with config flag and a registry spy
Then the actor registry should not have been consulted
Scenario: Multiple config files are forwarded with positional NAME
When I run actor run with multiple config files and a positional name
Then all config files should be passed to the application
# ---------- Review round 6 fixes ----------
Scenario: _cleanup_temp_files removes tracked temporary files
When I call resolve_config_files and then cleanup_temp_files
Then the temporary file should be removed by cleanup
Scenario: actor_registry() infrastructure error propagates through resolve_config_files
When I call resolve_config_files and actor_registry raises an infrastructure error
Then the actor_registry infrastructure error should propagate uncaught
+25 -25
View File
@@ -97,7 +97,7 @@ def step_run_actor_with_load_and_context(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--context",
"session-1",
@@ -131,7 +131,7 @@ def step_run_actor_with_load_only(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--load-context",
str(context.load_context_path),
@@ -170,7 +170,7 @@ def step_run_actor_with_context_only(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--context",
"session-2",
@@ -200,7 +200,7 @@ def step_run_actor_without_context(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
],
)
@@ -234,7 +234,7 @@ def step_run_actor_with_load_context_and_rxpy(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--context",
"session-rxpy",
@@ -269,7 +269,7 @@ def step_run_actor_with_load_only_rxpy(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--load-context",
str(context.load_context_path),
@@ -309,7 +309,7 @@ def step_run_actor_with_context_only_rxpy(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--context",
"session-rxpy-only",
@@ -340,7 +340,7 @@ def step_run_actor_without_context_rxpy(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--allow-rxpy-in-run-mode",
],
@@ -369,7 +369,7 @@ def step_run_actor_unsafe_error(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
],
)
@@ -398,7 +398,7 @@ def step_run_actor_clever_agents_error(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
],
)
@@ -432,7 +432,7 @@ def step_invoke_actor_run_with_load_and_context(context):
[
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--context",
"actor-run-session",
@@ -465,7 +465,7 @@ def step_invoke_actor_run_with_load_only(context):
[
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--load-context",
str(context.load_context_path),
@@ -503,7 +503,7 @@ def step_invoke_actor_run_with_context_only(context):
[
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--context",
"actor-run-session-2",
@@ -532,7 +532,7 @@ def step_invoke_actor_run_without_context(context):
[
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
],
)
@@ -559,7 +559,7 @@ def step_invoke_actor_run_unsafe_error(context):
[
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
],
)
@@ -587,7 +587,7 @@ def step_invoke_actor_run_clever_agents_exception(context):
[
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
],
)
@@ -684,7 +684,7 @@ def step_run_actor_with_single_skill(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
@@ -715,7 +715,7 @@ def step_run_actor_with_multiple_skills(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
@@ -755,7 +755,7 @@ def step_run_actor_with_unknown_skill(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--skill",
"local/nonexistent",
@@ -786,7 +786,7 @@ def step_invoke_actor_run_with_single_skill(context):
[
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
@@ -816,7 +816,7 @@ def step_invoke_actor_run_with_multiple_skills(context):
[
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
@@ -855,7 +855,7 @@ def step_invoke_actor_run_with_unknown_skill(context):
[
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--skill",
"local/nonexistent",
@@ -928,7 +928,7 @@ def step_run_actor_with_skill_and_context(context):
"run",
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
@@ -977,7 +977,7 @@ def step_invoke_actor_run_with_skill_and_context(context):
[
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
@@ -1016,7 +1016,7 @@ def step_invoke_actor_run_with_duplicate_skills(context):
[
"--config",
str(context.actor_config_path),
"--prompt",
"test-actor",
context.prompt,
"--skill",
"local/web-tools",
@@ -0,0 +1,453 @@
"""CLI invocation step definitions for actor run positional-argument tests.
Covers the ``actor.py`` and ``actor_run.py`` CLI entry-points with
positional NAME and PROMPT arguments, config fallback, registry
resolution, error paths, config-precedence registry spy assertions,
and multiple config-file forwarding.
"""
from __future__ import annotations
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
import typer
from behave import 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 features.steps.actor_run_signature_resolve_steps import (
_get_combined_output,
_make_app,
)
def _not_found_resolve(name: str, config: list[Any]) -> list[Any]:
"""Side-effect for _resolve_config_files that simulates registry miss."""
typer.echo(
f"Error: Actor '{name}' not found in registry and no --config provided.",
err=True,
)
raise typer.Exit(code=2)
# ---------------------------------------------------------------------------
# actor.py run command — positional args with --config fallback
# ---------------------------------------------------------------------------
@when("I run actor run with positional args and config fallback")
def step_run_actor_positional_with_config(context: Any) -> None:
context.prompt = "hello positional"
context.run_result = "positional response"
app_exec = _make_app(result=context.run_result)
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),
"my-actor",
context.prompt,
],
)
context.app_exec = app_exec
@when("I run actor run with name resolved from the actor registry")
def step_run_actor_name_from_registry(context: Any) -> None:
context.prompt = "registry prompt"
context.run_result = "registry response"
app_exec = _make_app(result=context.run_result)
# Mock _resolve_config_files to return the existing config path
# (simulating successful registry resolution)
def _mock_resolve(name: str, config: list[Any]) -> list[Any]:
return [context.actor_config_path]
with (
patch(
"cleveragents.cli.commands.actor._resolve_config_files",
side_effect=_mock_resolve,
),
patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
),
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"local/my-actor",
context.prompt,
],
)
context.app_exec = app_exec
@when("I run actor run with a positional prompt containing flag-like text")
def step_run_actor_prompt_with_flag_like_text(context: Any) -> None:
context.prompt = "Fix the --output flag without changing --context"
context.run_result = "flag-like prompt response"
app_exec = _make_app(result=context.run_result)
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),
"my-actor",
context.prompt,
],
)
context.app_exec = app_exec
@when("I run actor run through registry integration path")
def step_run_actor_registry_integration_path(context: Any) -> None:
context.prompt = "registry integration prompt"
context.run_result = "registry integration response"
app_exec = _make_app(result=context.run_result)
mock_actor = MagicMock()
mock_actor.yaml_text = "name: integration-actor\ntype: custom\n"
mock_actor.config_blob = None
mock_registry = MagicMock()
mock_registry.get.return_value = mock_actor
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
with (
patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
),
patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
) as mock_app_cls,
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"local/integration-actor",
context.prompt,
],
)
call_kwargs = mock_app_cls.call_args.kwargs
config_files = call_kwargs.get("config_files", [])
if config_files:
temp_config = Path(config_files[0])
context.add_cleanup(lambda p=temp_config: p.unlink(missing_ok=True))
context.app_exec = app_exec
context.mock_registry = mock_registry
@when("I run actor run with config flag taking precedence")
def step_run_actor_config_takes_precedence(context: Any) -> None:
context.prompt = "precedence prompt"
context.run_result = "precedence response"
app_exec = _make_app(result=context.run_result)
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),
"ignored-name",
context.prompt,
],
)
context.app_exec = app_exec
@when("I run actor run with an unknown actor name")
def step_run_actor_unknown_name(context: Any) -> None:
with patch(
"cleveragents.cli.commands.actor._resolve_config_files",
side_effect=_not_found_resolve,
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"nonexistent/actor",
"test prompt",
],
)
# ---------------------------------------------------------------------------
# actor_run.py module — positional args
# ---------------------------------------------------------------------------
@when("I invoke the actor-run module with config flag taking precedence")
def step_invoke_actor_run_config_takes_precedence(context: Any) -> None:
context.prompt = "actor-run precedence prompt"
context.run_result = "actor-run precedence response"
app_exec = _make_app(result=context.run_result)
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),
"ignored-name",
context.prompt,
],
)
context.app_exec = app_exec
@when("I invoke the actor-run module with positional args and config fallback")
def step_invoke_actor_run_positional_with_config(context: Any) -> None:
context.prompt = "actor-run positional"
context.run_result = "actor-run positional response"
app_exec = _make_app(result=context.run_result)
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),
"my-actor",
context.prompt,
],
)
context.app_exec = app_exec
@when("I invoke the actor-run module with name from the actor registry")
def step_invoke_actor_run_name_from_registry(context: Any) -> None:
context.prompt = "actor-run registry prompt"
context.run_result = "actor-run registry response"
app_exec = _make_app(result=context.run_result)
# Mock _resolve_config_files to return the existing config path
# (simulating successful registry resolution)
def _mock_resolve(name: str, config: list[Any]) -> list[Any]:
return [context.actor_config_path]
with (
patch(
"cleveragents.cli.commands.actor_run._resolve_config_files",
side_effect=_mock_resolve,
),
patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
),
):
context.result = context.runner.invoke(
actor_run_app,
[
"local/my-actor",
context.prompt,
],
)
context.app_exec = app_exec
@when("I invoke the actor-run module with an unknown actor name")
def step_invoke_actor_run_unknown_name(context: Any) -> None:
with patch(
"cleveragents.cli.commands.actor_run._resolve_config_files",
side_effect=_not_found_resolve,
):
context.result = context.runner.invoke(
actor_run_app,
[
"nonexistent/actor",
"test prompt",
],
)
# ---------------------------------------------------------------------------
# Shared assertions
# ---------------------------------------------------------------------------
@then("the actor run should exit with registry not found error")
def step_actor_run_registry_not_found(context: Any) -> None:
assert context.result.exit_code == 2
combined = _get_combined_output(context.result)
assert "not found in registry" in combined
@then("the actor run should pass the exact positional prompt")
def step_actor_run_exact_prompt(context: Any) -> None:
assert context.result.exit_code == 0
run_args = context.app_exec.run_single_shot.call_args.args
assert run_args[0] == context.prompt
@then("the actor registry should have been consulted with requested actor name")
def step_registry_consulted_with_actor_name(context: Any) -> None:
context.mock_registry.get.assert_called_once_with("local/integration-actor")
# ---------------------------------------------------------------------------
# Review round 5 — P3-2: config precedence with registry spy
# ---------------------------------------------------------------------------
@when("I run actor run with config flag and a registry spy")
def step_run_actor_config_with_registry_spy(context: Any) -> None:
context.prompt = "spy prompt"
context.run_result = "spy response"
app_exec = _make_app(result=context.run_result)
mock_registry = MagicMock()
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
with (
patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
),
patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
),
):
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"ignored-name",
context.prompt,
],
)
context.mock_registry = mock_registry
@when("I invoke the actor-run module with config flag and a registry spy")
def step_invoke_actor_run_config_with_registry_spy(context: Any) -> None:
context.prompt = "actor-run spy prompt"
context.run_result = "actor-run spy response"
app_exec = _make_app(result=context.run_result)
mock_registry = MagicMock()
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
with (
patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
),
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),
"ignored-name",
context.prompt,
],
)
context.mock_registry = mock_registry
@then("the actor registry should not have been consulted")
def step_registry_not_consulted(context: Any) -> None:
assert context.result.exit_code == 0
context.mock_registry.get.assert_not_called()
# ---------------------------------------------------------------------------
# Review round 5 — P3-3: multiple config files with positional NAME
# ---------------------------------------------------------------------------
@when("I run actor run with multiple config files and a positional name")
def step_run_actor_multiple_configs(context: Any) -> None:
context.prompt = "multi-config prompt"
context.run_result = "multi-config response"
app_exec = _make_app(result=context.run_result)
second_config = Path(tempfile.gettempdir()) / "second_actor_config.yaml"
second_config.write_text(
"name: second\ntype: custom\nprovider: openai\nmodel: gpt-4\n",
encoding="utf-8",
)
context.add_cleanup(lambda: second_config.unlink(missing_ok=True))
context.second_actor_config_path = second_config
with patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
) as mock_app_cls:
context.result = context.runner.invoke(
actor_app,
[
"run",
"--config",
str(context.actor_config_path),
"--config",
str(second_config),
"ignored-name",
context.prompt,
],
)
context.mock_app_cls = mock_app_cls
@then("all config files should be passed to the application")
def step_all_configs_passed(context: Any) -> None:
assert context.result.exit_code == 0
call_kwargs = context.mock_app_cls.call_args
config_files = call_kwargs[1].get(
"config_files", call_kwargs[0][0] if call_kwargs[0] else None
)
assert config_files is not None
assert len(config_files) == 2
assert Path(config_files[0]) == context.actor_config_path
assert Path(config_files[1]) == context.second_actor_config_path
@@ -0,0 +1,472 @@
"""Step definitions for resolve_config_files unit tests, edge cases, and
error propagation in actor run positional-argument tests.
This module contains the ``resolve_config_files`` unit-level BDD
scenarios as well as shared helpers used by
``actor_run_signature_cli_steps.py``.
"""
from __future__ import annotations
import tempfile
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import click
import typer
import yaml
from behave import then, when
from cleveragents.cli.commands._resolve_actor import (
_cleanup_temp_files,
_temp_files,
resolve_config_files,
)
from cleveragents.core.exceptions import InfrastructureError, NotFoundError
# ---------------------------------------------------------------------------
# Shared helpers (also imported by actor_run_signature_cli_steps)
# ---------------------------------------------------------------------------
def _make_app(
*,
result: str,
config_global_context: dict[str, Any] | 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)
return app_exec
def _get_combined_output(result: Any) -> str:
"""Return combined stdout + stderr from a CliRunner result."""
output = result.output or ""
stderr = getattr(result, "stderr", None) or ""
return output + stderr
# ---------------------------------------------------------------------------
# resolve_config_files unit tests (P2-3 / P2-4)
# ---------------------------------------------------------------------------
@when("I call resolve_config_files with a config list")
def step_resolve_with_config_list(context: Any) -> None:
cfg = [Path(tempfile.gettempdir()) / "dummy.yaml"]
context.resolve_result = resolve_config_files("ignored", cfg)
context.expected_config = cfg
@then("the original config list is returned unchanged")
def step_config_returned_unchanged(context: Any) -> None:
assert context.resolve_result is context.expected_config
@when("I call resolve_config_files with a name and no config")
def step_resolve_with_name_no_config(context: Any) -> None:
mock_actor = MagicMock()
mock_actor.yaml_text = "name: test-actor\ntype: custom\n"
mock_actor.config_blob = None
mock_registry = MagicMock()
mock_registry.get.return_value = mock_actor
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
with patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
):
context.resolve_result = resolve_config_files("local/test-actor", [])
context.mock_registry = mock_registry
@then("a temporary YAML file is created from the registry actor")
def step_temp_file_from_registry(context: Any) -> None:
assert len(context.resolve_result) == 1
tmp_path = context.resolve_result[0]
context.add_cleanup(lambda p=tmp_path: p.unlink(missing_ok=True))
assert tmp_path.exists()
content = tmp_path.read_text(encoding="utf-8")
assert "test-actor" in content
context.mock_registry.get.assert_called_once_with("local/test-actor")
@when("I call resolve_config_files with an actor that has no yaml_text")
def step_resolve_with_no_yaml_text(context: Any) -> None:
mock_actor = MagicMock()
mock_actor.yaml_text = ""
mock_actor.config_blob = {"name": "blob-actor", "type": "custom"}
mock_registry = MagicMock()
mock_registry.get.return_value = mock_actor
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
with patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
):
context.resolve_result = resolve_config_files("local/blob-actor", [])
@then("a temporary YAML file is created from config_blob")
def step_temp_file_from_config_blob(context: Any) -> None:
assert len(context.resolve_result) == 1
tmp_path = context.resolve_result[0]
context.add_cleanup(lambda p=tmp_path: p.unlink(missing_ok=True))
assert tmp_path.exists()
content = tmp_path.read_text(encoding="utf-8")
assert "blob-actor" in content
parsed = yaml.safe_load(content)
assert isinstance(parsed, dict)
assert parsed["name"] == "blob-actor"
@when("I call resolve_config_files with an actor that has no config data")
def step_resolve_with_no_config_data(context: Any) -> None:
mock_actor = MagicMock()
mock_actor.yaml_text = ""
mock_actor.config_blob = None
mock_registry = MagicMock()
mock_registry.get.return_value = mock_actor
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
captured_stderr: list[str] = []
original_echo = typer.echo
def _capture_echo(message: str = "", err: bool = False, **kwargs: Any) -> None:
if err:
captured_stderr.append(message)
original_echo(message, err=err, **kwargs)
with (
patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
),
patch(
"cleveragents.cli.commands._resolve_actor.typer.echo",
side_effect=_capture_echo,
),
):
try:
resolve_config_files("local/empty-actor", [])
context.resolve_exit_code = 0
except (SystemExit, click.exceptions.Exit) as exc:
context.resolve_exit_code = getattr(
exc, "exit_code", getattr(exc, "code", 1)
)
context.resolve_stderr = " ".join(captured_stderr)
@then("it should exit with a no-configuration-data error")
def step_exit_no_config_data(context: Any) -> None:
assert context.resolve_exit_code == 2
assert "no configuration data" in context.resolve_stderr
@when("I call resolve_config_files with an unknown actor name directly")
def step_resolve_unknown_actor_directly(context: Any) -> None:
mock_registry = MagicMock()
mock_registry.get.side_effect = NotFoundError(
resource_type="actor", resource_id="nonexistent/actor"
)
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
captured_stderr: list[str] = []
original_echo = typer.echo
def _capture_echo(message: str = "", err: bool = False, **kwargs: Any) -> None:
if err:
captured_stderr.append(message)
original_echo(message, err=err, **kwargs)
with (
patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
),
patch(
"cleveragents.cli.commands._resolve_actor.typer.echo",
side_effect=_capture_echo,
),
):
try:
resolve_config_files("nonexistent/actor", [])
context.resolve_exit_code = 0
except (SystemExit, click.exceptions.Exit) as exc:
context.resolve_exit_code = getattr(
exc, "exit_code", getattr(exc, "code", 1)
)
context.resolve_stderr = " ".join(captured_stderr)
@then("it should exit with a not-found error")
def step_exit_not_found(context: Any) -> None:
assert context.resolve_exit_code == 2
assert "not found in registry" in context.resolve_stderr
# ---------------------------------------------------------------------------
# Edge cases and error propagation (P2-3, P3-2, P3-4)
# ---------------------------------------------------------------------------
@when("I call resolve_config_files with an actor that has empty config_blob")
def step_resolve_with_empty_config_blob(context: Any) -> None:
mock_actor = MagicMock()
mock_actor.yaml_text = ""
mock_actor.config_blob = {}
mock_registry = MagicMock()
mock_registry.get.return_value = mock_actor
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
captured_stderr: list[str] = []
original_echo = typer.echo
def _capture_echo(message: str = "", err: bool = False, **kwargs: Any) -> None:
if err:
captured_stderr.append(message)
original_echo(message, err=err, **kwargs)
with (
patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
),
patch(
"cleveragents.cli.commands._resolve_actor.typer.echo",
side_effect=_capture_echo,
),
):
try:
resolve_config_files("local/empty-blob-actor", [])
context.resolve_exit_code = 0
except (SystemExit, click.exceptions.Exit) as exc:
context.resolve_exit_code = getattr(
exc, "exit_code", getattr(exc, "code", 1)
)
context.resolve_stderr = " ".join(captured_stderr)
@when("I call resolve_config_files and the container raises an infrastructure error")
def step_resolve_with_infrastructure_error(context: Any) -> None:
with patch(
"cleveragents.cli.commands._resolve_actor.get_container",
side_effect=InfrastructureError("database unavailable"),
):
try:
resolve_config_files("local/some-actor", [])
context.infra_error_propagated = False
except InfrastructureError:
context.infra_error_propagated = True
@then("the infrastructure error should propagate uncaught")
def step_infra_error_propagated(context: Any) -> None:
assert context.infra_error_propagated is True
@when("I call resolve_config_files with an empty string name")
def step_resolve_with_empty_name(context: Any) -> None:
captured_stderr: list[str] = []
original_echo = typer.echo
def _capture_echo(message: str = "", err: bool = False, **kwargs: Any) -> None:
if err:
captured_stderr.append(message)
original_echo(message, err=err, **kwargs)
with patch(
"cleveragents.cli.commands._resolve_actor.typer.echo",
side_effect=_capture_echo,
):
try:
resolve_config_files("", [])
context.empty_name_exit_code = 0
except (SystemExit, typer.Exit) as exc:
context.empty_name_exit_code = getattr(
exc, "exit_code", getattr(exc, "code", 1)
)
context.resolve_stderr = " ".join(captured_stderr)
@then("the empty name should be rejected with a validation error")
def step_empty_name_rejected(context: Any) -> None:
assert context.empty_name_exit_code == 2
assert "must not be empty" in context.resolve_stderr
# ---------------------------------------------------------------------------
# YAML serialisation failure (P2-1)
# ---------------------------------------------------------------------------
@when(
"I call resolve_config_files with an actor whose config_blob is not YAML-serialisable"
)
def step_resolve_with_unserializable_config_blob(context: Any) -> None:
mock_actor = MagicMock()
mock_actor.yaml_text = ""
# yaml.safe_dump cannot serialise arbitrary objects
mock_actor.config_blob = {"bad_value": object()}
mock_registry = MagicMock()
mock_registry.get.return_value = mock_actor
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
captured_stderr: list[str] = []
original_echo = typer.echo
def _capture_echo(message: str = "", err: bool = False, **kwargs: Any) -> None:
if err:
captured_stderr.append(message)
original_echo(message, err=err, **kwargs)
with (
patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
),
patch(
"cleveragents.cli.commands._resolve_actor.typer.echo",
side_effect=_capture_echo,
),
):
try:
resolve_config_files("local/bad-blob-actor", [])
context.resolve_exit_code = 0
except (SystemExit, click.exceptions.Exit) as exc:
context.resolve_exit_code = getattr(
exc, "exit_code", getattr(exc, "code", 1)
)
context.resolve_stderr = " ".join(captured_stderr)
@then("it should exit with a serialisation error")
def step_exit_serialisation_error(context: Any) -> None:
assert context.resolve_exit_code == 2
assert "could not be serialised" in context.resolve_stderr
# ---------------------------------------------------------------------------
# Review round 5 — P3-1: whitespace-only yaml_text
# ---------------------------------------------------------------------------
@when("I call resolve_config_files with an actor that has whitespace-only yaml_text")
def step_resolve_with_whitespace_yaml_text(context: Any) -> None:
mock_actor = MagicMock()
mock_actor.yaml_text = " \n \t "
mock_actor.config_blob = {"name": "ws-actor", "type": "custom"}
mock_registry = MagicMock()
mock_registry.get.return_value = mock_actor
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
with patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
):
context.resolve_result = resolve_config_files("local/ws-actor", [])
@then("a temporary YAML file is created from the whitespace actor config_blob")
def step_temp_file_from_whitespace_config_blob(context: Any) -> None:
assert len(context.resolve_result) == 1
tmp_path = context.resolve_result[0]
context.add_cleanup(lambda p=tmp_path: p.unlink(missing_ok=True))
assert tmp_path.exists()
content = tmp_path.read_text(encoding="utf-8")
assert "ws-actor" in content
parsed = yaml.safe_load(content)
assert isinstance(parsed, dict)
assert parsed["name"] == "ws-actor"
# ---------------------------------------------------------------------------
# Review round 6 — m8: _cleanup_temp_files removes tracked files
# ---------------------------------------------------------------------------
@when("I call resolve_config_files and then cleanup_temp_files")
def step_resolve_then_cleanup(context: Any) -> None:
_cleanup_temp_files()
mock_actor = MagicMock()
mock_actor.yaml_text = "name: cleanup-test-actor\ntype: custom\n"
mock_actor.config_blob = None
mock_registry = MagicMock()
mock_registry.get.return_value = mock_actor
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
with patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
):
result_paths = resolve_config_files("local/cleanup-test-actor", [])
context.cleanup_tmp_path = result_paths[0]
# Verify file exists before cleanup
assert context.cleanup_tmp_path.exists()
# Verify path was tracked
assert str(context.cleanup_tmp_path) in _temp_files
_cleanup_temp_files()
@then("the temporary file should be removed by cleanup")
def step_temp_file_removed_by_cleanup(context: Any) -> None:
assert not context.cleanup_tmp_path.exists()
assert len(_temp_files) == 0
# ---------------------------------------------------------------------------
# Review round 6 — m9: actor_registry() raising an infrastructure error
# ---------------------------------------------------------------------------
@when("I call resolve_config_files and actor_registry raises an infrastructure error")
def step_resolve_with_actor_registry_error(context: Any) -> None:
mock_container = MagicMock()
mock_container.actor_registry.side_effect = InfrastructureError(
"actor registry unavailable"
)
with patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
):
try:
resolve_config_files("local/some-actor", [])
context.actor_registry_error_propagated = False
except InfrastructureError:
context.actor_registry_error_propagated = True
@then("the actor_registry infrastructure error should propagate uncaught")
def step_actor_registry_error_propagated(context: Any) -> None:
assert context.actor_registry_error_propagated is True
@@ -0,0 +1,64 @@
"""Security-oriented step definitions for actor run signature tests."""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock, patch
import click
import typer
from behave import then, when
from cleveragents.cli.commands._resolve_actor import resolve_config_files
from cleveragents.core.exceptions import NotFoundError
@when("I call resolve_config_files with a control-character actor name")
def step_resolve_with_control_character_name(context: Any) -> None:
actor_name = "local/\x1b[31mevil\x1b[0m\x00actor"
mock_registry = MagicMock()
mock_registry.get.side_effect = NotFoundError(
resource_type="actor",
resource_id=actor_name,
)
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
captured_stderr: list[str] = []
original_echo = typer.echo
def _capture_echo(message: str = "", err: bool = False, **kwargs: Any) -> None:
if err:
captured_stderr.append(message)
original_echo(message, err=err, **kwargs)
with (
patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
),
patch(
"cleveragents.cli.commands._resolve_actor.typer.echo",
side_effect=_capture_echo,
),
):
try:
resolve_config_files(actor_name, [])
context.resolve_exit_code = 0
except (SystemExit, click.exceptions.Exit) as exc:
context.resolve_exit_code = getattr(
exc, "exit_code", getattr(exc, "code", 1)
)
context.resolve_stderr = " ".join(captured_stderr)
@then("the not-found error should not include control characters")
def step_error_sanitises_control_characters(context: Any) -> None:
assert context.resolve_exit_code == 2
assert "not found in registry" in context.resolve_stderr
assert "\x1b" not in context.resolve_stderr
assert "\x00" not in context.resolve_stderr
assert "evil" in context.resolve_stderr
+49
View File
@@ -0,0 +1,49 @@
*** Settings ***
Documentation Integration tests for actor run positional NAME/PROMPT signature
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_actor_run_signature.py
*** Test Cases ***
Positional Args With Config Fallback
[Documentation] Verify that positional NAME + PROMPT work with --config fallback
${result}= Run Process ${PYTHON} ${HELPER} positional-with-config cwd=${WORKSPACE} timeout=120s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} positional-with-config-ok
Positional NAME From Actor Registry
[Documentation] Verify that NAME is resolved from the actor registry when no --config
${result}= Run Process ${PYTHON} ${HELPER} positional-from-registry cwd=${WORKSPACE} timeout=120s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} positional-from-registry-ok
Actor App Registry Resolution
[Documentation] Verify that NAME is resolved from the actor registry via actor_app
${result}= Run Process ${PYTHON} ${HELPER} actor-app-registry cwd=${WORKSPACE} timeout=120s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-app-registry-ok
Unknown Actor Name Error
[Documentation] Verify that unknown actor name exits with error code 2
${result}= Run Process ${PYTHON} ${HELPER} unknown-actor-name cwd=${WORKSPACE} timeout=120s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} unknown-actor-name-ok
Actor App Unknown Name Error
[Documentation] Verify that unknown actor name exits with error code 2 via actor_app
${result}= Run Process ${PYTHON} ${HELPER} actor-app-unknown-name cwd=${WORKSPACE} timeout=120s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-app-unknown-name-ok
+14 -14
View File
@@ -29,7 +29,7 @@ Test Delete Single Context With Yes Flag
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test message"
... test-actor "test message"
Directory Should Exist ${CONTEXT_DIR}/${context_name}
@@ -54,7 +54,7 @@ Test Delete Single Context With Short Yes Flag
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test message"
... test-actor "test message"
Directory Should Exist ${CONTEXT_DIR}/${context_name}
@@ -78,7 +78,7 @@ Test Delete Single Context With Confirmation Accept
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test message"
... test-actor "test message"
Directory Should Exist ${CONTEXT_DIR}/${context_name}
@@ -102,7 +102,7 @@ Test Delete Single Context With Confirmation Reject
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test message"
... test-actor "test message"
Directory Should Exist ${CONTEXT_DIR}/${context_name}
@@ -130,7 +130,7 @@ Test Delete All Contexts With All And Yes Flags
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
Run Process ${PYTHON} -m cleveragents actor run
... -c ${SIMPLE_CONFIG}
@@ -138,7 +138,7 @@ Test Delete All Contexts With All And Yes Flags
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
Run Process ${PYTHON} -m cleveragents actor run
... -c ${SIMPLE_CONFIG}
@@ -146,7 +146,7 @@ Test Delete All Contexts With All And Yes Flags
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
Directory Should Exist ${CONTEXT_DIR}/${ctx1}
Directory Should Exist ${CONTEXT_DIR}/${ctx2}
@@ -177,7 +177,7 @@ Test Delete All With Confirmation Accept
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
Run Process ${PYTHON} -m cleveragents actor run
... -c ${SIMPLE_CONFIG}
@@ -185,7 +185,7 @@ Test Delete All With Confirmation Accept
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
# Delete all with confirmation
${result} = Run Process ${PYTHON} -m cleveragents context delete
@@ -212,7 +212,7 @@ Test Delete All With Confirmation Reject
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
Run Process ${PYTHON} -m cleveragents actor run
... -c ${SIMPLE_CONFIG}
@@ -220,7 +220,7 @@ Test Delete All With Confirmation Reject
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
# Delete all with confirmation rejected
${result} = Run Process ${PYTHON} -m cleveragents context delete
@@ -288,7 +288,7 @@ Test Delete All Shows Context List Before Deletion
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
Run Process ${PYTHON} -m cleveragents actor run
... -c ${SIMPLE_CONFIG}
@@ -296,7 +296,7 @@ Test Delete All Shows Context List Before Deletion
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
Run Process ${PYTHON} -m cleveragents actor run
... -c ${SIMPLE_CONFIG}
@@ -304,7 +304,7 @@ Test Delete All Shows Context List Before Deletion
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
# Delete all with confirmation to see list
${result} = Run Process ${PYTHON} -m cleveragents context delete
+3 -3
View File
@@ -37,7 +37,7 @@ Test Run With Context Creates Directory
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test message"
... test-actor "test message"
Should Be Equal As Integers ${result.rc} 0
Directory Should Exist ${CONTEXT_DIR}/${context_name}
@@ -53,7 +53,7 @@ Test Context Delete
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
Directory Should Exist ${CONTEXT_DIR}/${context_name}
@@ -77,7 +77,7 @@ Test Context Clear
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
# Clear it
${result} = Run Process ${PYTHON} -m cleveragents context clear
+266
View File
@@ -0,0 +1,266 @@
"""Helper script for actor_run_signature.robot integration tests.
Tests that the ``actor run`` and ``actor-run`` CLI commands accept
positional NAME and PROMPT arguments while preserving --config fallback.
"""
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
# Ensure local source tree is importable
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
import typer # noqa: E402
from typer.testing import CliRunner # noqa: E402
from cleveragents.cli.commands.actor import app as actor_app # noqa: E402
from cleveragents.cli.commands.actor_run import app as actor_run_app # noqa: E402
runner = CliRunner()
_SIMPLE_YAML = """\
name: smoke-actor
type: custom
provider: openai
model: gpt-4
tools:
- operation: identity
"""
def _write_yaml(content: str) -> str:
fd, path = tempfile.mkstemp(suffix=".yaml")
with os.fdopen(fd, "w", encoding="utf-8") as fh:
fh.write(content)
return path
def _make_app(*, result: str) -> MagicMock:
app_exec = MagicMock()
app_exec.config = SimpleNamespace(global_context={})
app_exec.run_single_shot = AsyncMock(return_value=result)
return app_exec
def positional_with_config() -> None:
"""Test positional NAME + PROMPT with --config fallback."""
path = _write_yaml(_SIMPLE_YAML)
app_exec = _make_app(result="config response")
try:
with patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
):
result = runner.invoke(
actor_app,
["run", "--config", path, "my-actor", "hello world"],
)
if result.exit_code != 0:
print(
f"FAIL: expected exit code 0, got {result.exit_code}",
file=sys.stderr,
)
print(result.output, file=sys.stderr)
sys.exit(1)
if "config response" not in result.output:
print(
f"FAIL: expected 'config response' in output, got: {result.output}",
file=sys.stderr,
)
sys.exit(1)
print("positional-with-config-ok")
finally:
Path(path).unlink(missing_ok=True)
def positional_from_registry() -> None:
"""Test positional NAME resolved from the actor registry."""
path = _write_yaml(_SIMPLE_YAML)
app_exec = _make_app(result="registry response")
def _mock_resolve(name: str, config: list[Any]) -> list[Any]:
return [Path(path)]
try:
with (
patch(
"cleveragents.cli.commands.actor_run._resolve_config_files",
side_effect=_mock_resolve,
),
patch(
"cleveragents.cli.commands.actor_run.ReactiveCleverAgentsApp",
return_value=app_exec,
),
):
result = runner.invoke(
actor_run_app,
["local/my-actor", "hello from registry"],
)
if result.exit_code != 0:
print(
f"FAIL: expected exit code 0, got {result.exit_code}",
file=sys.stderr,
)
print(result.output, file=sys.stderr)
sys.exit(1)
if "registry response" not in result.output:
print(
f"FAIL: expected 'registry response' in output, got: {result.output}",
file=sys.stderr,
)
sys.exit(1)
print("positional-from-registry-ok")
finally:
Path(path).unlink(missing_ok=True)
def actor_app_registry_resolution() -> None:
"""Test positional NAME resolved from the actor registry via actor_app."""
path = _write_yaml(_SIMPLE_YAML)
app_exec = _make_app(result="actor-app registry response")
def _mock_resolve(name: str, config: list[Any]) -> list[Any]:
return [Path(path)]
try:
with (
patch(
"cleveragents.cli.commands.actor._resolve_config_files",
side_effect=_mock_resolve,
),
patch(
"cleveragents.cli.commands.actor.ReactiveCleverAgentsApp",
return_value=app_exec,
),
):
result = runner.invoke(
actor_app,
["run", "local/my-actor", "hello from actor_app registry"],
)
if result.exit_code != 0:
print(
f"FAIL: expected exit code 0, got {result.exit_code}",
file=sys.stderr,
)
print(result.output, file=sys.stderr)
sys.exit(1)
if "actor-app registry response" not in result.output:
print(
"FAIL: expected 'actor-app registry response' "
f"in output, got: {result.output}",
file=sys.stderr,
)
sys.exit(1)
print("actor-app-registry-ok")
finally:
Path(path).unlink(missing_ok=True)
def unknown_actor_name() -> None:
"""Test error when actor name is not found in registry."""
def _not_found_resolve(name: str, config: list[Any]) -> list[Any]:
typer.echo(
f"Error: Actor '{name}' not found in registry and no --config provided.",
err=True,
)
raise typer.Exit(code=2)
with patch(
"cleveragents.cli.commands.actor_run._resolve_config_files",
side_effect=_not_found_resolve,
):
result = runner.invoke(
actor_run_app,
["nonexistent/actor", "test prompt"],
)
if result.exit_code != 2:
print(
f"FAIL: expected exit code 2, got {result.exit_code}",
file=sys.stderr,
)
print(result.output, file=sys.stderr)
sys.exit(1)
combined = (result.output or "") + (getattr(result, "stderr", None) or "")
if "not found in registry" not in combined:
print(
"FAIL: expected 'not found in registry' in output",
file=sys.stderr,
)
print(combined, file=sys.stderr)
sys.exit(1)
print("unknown-actor-name-ok")
def actor_app_unknown_name() -> None:
"""Test error when actor name is not found in registry via actor_app."""
def _not_found_resolve(name: str, config: list[Any]) -> list[Any]:
typer.echo(
f"Error: Actor '{name}' not found in registry and no --config provided.",
err=True,
)
raise typer.Exit(code=2)
with patch(
"cleveragents.cli.commands.actor._resolve_config_files",
side_effect=_not_found_resolve,
):
result = runner.invoke(
actor_app,
["run", "nonexistent/actor", "test prompt"],
)
if result.exit_code != 2:
print(
f"FAIL: expected exit code 2, got {result.exit_code}",
file=sys.stderr,
)
print(result.output, file=sys.stderr)
sys.exit(1)
combined = (result.output or "") + (getattr(result, "stderr", None) or "")
if "not found in registry" not in combined:
print(
"FAIL: expected 'not found in registry' in output",
file=sys.stderr,
)
print(combined, file=sys.stderr)
sys.exit(1)
print("actor-app-unknown-name-ok")
# ---------------------------------------------------------------------------
# Main dispatcher
# ---------------------------------------------------------------------------
_COMMANDS: dict[str, Any] = {
"positional-with-config": positional_with_config,
"positional-from-registry": positional_from_registry,
"actor-app-registry": actor_app_registry_resolution,
"unknown-actor-name": unknown_actor_name,
"actor-app-unknown-name": actor_app_unknown_name,
}
def main() -> None:
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr)
sys.exit(2)
_COMMANDS[sys.argv[1]]()
if __name__ == "__main__":
main()
+2 -2
View File
@@ -47,7 +47,7 @@ def unknown_skill_exits_with_error() -> None:
[
"--config",
path,
"--prompt",
"test-actor",
"hello",
"--skill",
"local/nonexistent-skill",
@@ -104,7 +104,7 @@ def skill_flag_accepted() -> None:
[
"--config",
path,
"--prompt",
"test-actor",
"hello",
"--skill",
"local/smoke-skill",
+2 -2
View File
@@ -31,7 +31,7 @@ Test Next Command With Null Writing Stage
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p !next discovery
... test-actor !next discovery
... stderr=STDOUT
... timeout=120s on_timeout=kill
@@ -46,7 +46,7 @@ Test Next Command With Null Writing Stage
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p !stage
... test-actor !stage
... stderr=STDOUT
... timeout=120s on_timeout=kill
+9 -9
View File
@@ -27,7 +27,7 @@ Test Load Context Transiently With Run Command
... --load-context ${context_file}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test message"
... test-actor "test message"
Should Be Equal As Integers ${result.rc} 0
# Verify no context directory was created (transient)
@@ -47,7 +47,7 @@ Test Load Context Into Named Context
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test message"
... test-actor "test message"
Should Be Equal As Integers ${result.rc} 0
@@ -72,7 +72,7 @@ Test Load Context Replaces Existing Named Context
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "original message"
... test-actor "original message"
Should Be Equal As Integers ${result1.rc} 0
@@ -87,7 +87,7 @@ Test Load Context Replaces Existing Named Context
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "new message"
... test-actor "new message"
Should Be Equal As Integers ${result2.rc} 0
@@ -108,7 +108,7 @@ Test Load Context From Export Format
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test data for export"
... test-actor "test data for export"
Should Be Equal As Integers ${result1.rc} 0
@@ -138,7 +138,7 @@ Test Load Context From Export Format
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "verify import"
... test-actor "verify import"
Should Be Equal As Integers ${result4.rc} 0
@@ -154,7 +154,7 @@ Test Load Context With Non-Existent File
... --load-context /nonexistent/file.json
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
Should Not Be Equal As Integers ${result.rc} 0
Should Contain Any ${result.stderr} does not exist No such file not found
@@ -169,7 +169,7 @@ Test Load Context With Invalid JSON
... --load-context ${bad_json_file}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "test"
... test-actor "test"
Should Not Be Equal As Integers ${result.rc} 0
@@ -206,7 +206,7 @@ Test Load Context With All Components
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p "verify all components"
... test-actor "verify all components"
Should Be Equal As Integers ${result.rc} 0
+4 -4
View File
@@ -24,7 +24,7 @@ Routing Prefix Stripping In Application
... -c ${DISCOVERY_CONFIG}
... --unsafe
... --allow-rxpy-in-run-mode
... -p test input
... test-actor test input
... stderr=STDOUT
... timeout=120s on_timeout=kill
@@ -42,7 +42,7 @@ Multiple Routing Prefixes Are Stripped
... -c ${BRAINSTORM_CONFIG}
... --unsafe
... --allow-rxpy-in-run-mode
... -p test input
... test-actor test input
... stderr=STDOUT
... timeout=120s on_timeout=kill
@@ -58,7 +58,7 @@ Content Without Prefix Remains Unchanged
... -c ${NO_PREFIX_CONFIG}
... --unsafe
... --allow-rxpy-in-run-mode
... -p test input
... test-actor test input
... stderr=STDOUT
... timeout=120s on_timeout=kill
@@ -73,7 +73,7 @@ SET Prefix Family Stripped
... -c ${SET_TOPIC_CONFIG}
... --unsafe
... --allow-rxpy-in-run-mode
... -p test input
... test-actor test input
... stderr=STDOUT
... timeout=120s on_timeout=kill
+8 -8
View File
@@ -27,8 +27,8 @@ Test Run Command Rejects RxPY Routes
# Try to run with RxPY routes - should fail
${result} = Run Process ${PYTHON} -m cleveragents actor run
... -c ${RXPY_CONFIG}
... -p test
... --unsafe
... test-actor test
... stderr=STDOUT timeout=120s on_timeout=kill
Should Not Be Equal As Integers ${result.rc} 0 Command should have failed
@@ -49,10 +49,10 @@ Test Run Command With Context Does Not Create Context
# Try to run with --context flag
${result} = Run Process ${PYTHON} -m cleveragents actor run
... -c ${RXPY_CONFIG}
... -p test
... --unsafe
... --context ${context_name}
... --context-dir ${CONTEXT_DIR}
... test-actor test
... stderr=STDOUT timeout=120s on_timeout=kill
Should Not Be Equal As Integers ${result.rc} 0 Command should have failed
@@ -73,7 +73,7 @@ Test Run Command With LangGraph Routes Succeeds
${result} = Run Process ${PYTHON} -m cleveragents actor run
... -c ${LANGGRAPH_CONFIG}
... --context-dir ${CONTEXT_DIR}
... -p test
... test-actor test
... stderr=STDOUT timeout=120s on_timeout=kill
# For now, might fail on actual execution but shouldn't have route validation error
@@ -89,11 +89,11 @@ Test Run Command Accepts RxPY Routes When Allowed
${context_name} = Set Variable rxpy_test_ctx_${UNIQUE_ID}
${result} = Run Process ${PYTHON} -m cleveragents actor run
... -c ${RXPY_CONFIG}
... -p test
... --unsafe
... --allow-rxpy-in-run-mode
... --context ${context_name}
... --context-dir ${CONTEXT_DIR}
... test-actor test
... stderr=STDOUT
... timeout=120s on_timeout=kill
@@ -108,8 +108,8 @@ Test Error Message Content
${result} = Run Process ${PYTHON} -m cleveragents actor run
... -c ${RXPY_CONFIG}
... -p test
... --unsafe
... test-actor test
... stderr=STDOUT timeout=120s on_timeout=kill
Should Not Be Equal As Integers ${result.rc} 0
@@ -129,8 +129,8 @@ Test Run With Nested Switch Operators
${result} = Run Process ${PYTHON} -m cleveragents actor run
... -c ${TEST_DIR}/nested_switch_config.yaml
... -p test
... --unsafe
... test-actor test
... stderr=STDOUT timeout=120s on_timeout=kill
Should Not Be Equal As Integers ${result.rc} 0
@@ -145,8 +145,8 @@ Test Mixed Route Types Detection
# Should detect RxPY routes even if LangGraph routes also present
${result} = Run Process ${PYTHON} -m cleveragents actor run
... -c ${TEST_DIR}/mixed_config.yaml
... -p test
... --unsafe
... test-actor test
... stderr=STDOUT timeout=120s on_timeout=kill
Should Not Be Equal As Integers ${result.rc} 0
@@ -165,10 +165,10 @@ Test Context File Not Created On Multiple Runs
FOR ${i} IN RANGE 3
${result} = Run Process ${PYTHON} -m cleveragents actor run
... -c ${RXPY_CONFIG}
... -p test
... --unsafe
... --context ${context_name}
... --context-dir ${CONTEXT_DIR}
... test-actor test
... stderr=STDOUT timeout=120s on_timeout=kill
Should Not Be Equal As Integers ${result.rc} 0
+3 -3
View File
@@ -26,7 +26,7 @@ Scientific Paper Writer Basic Integration Test
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p Hello
... test-actor Hello
... stderr=DEVNULL
... timeout=120s on_timeout=kill
@@ -40,7 +40,7 @@ Scientific Paper Writer Basic Integration Test
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p !help
... test-actor !help
... stderr=DEVNULL
... timeout=120s on_timeout=kill
@@ -52,7 +52,7 @@ Scientific Paper Writer Basic Integration Test
... -c ${V2_CONFIG_DIR}/simple_echo_config.yaml
... --unsafe
... --allow-rxpy-in-run-mode
... -p Test message
... test-actor Test message
... stderr=DEVNULL
... timeout=120s on_timeout=kill
+9 -9
View File
@@ -103,7 +103,7 @@ Scientific Paper Writer LaTeX Generation
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p !next latex_generation
... test-actor !next latex_generation
... stderr=STDOUT
... timeout=180s
Should Be Equal As Integers ${result.rc} 0
@@ -118,7 +118,7 @@ Scientific Paper Writer LaTeX Generation
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p Generate the complete LaTeX document
... test-actor Generate the complete LaTeX document
... stderr=STDOUT
... timeout=180s
Should Be Equal As Integers ${result.rc} 0
@@ -140,7 +140,7 @@ Scientific Paper Writer LaTeX Generation
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p !context
... test-actor !context
... stderr=STDOUT
... timeout=30s
Should Contain ${result.stdout} latex_source
@@ -161,7 +161,7 @@ Scientific Paper Writer Stage Navigation
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p !stages
... test-actor !stages
... stderr=STDOUT
... timeout=20s
Should Contain ${result.stdout} intro
@@ -173,7 +173,7 @@ Scientific Paper Writer Stage Navigation
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p !next
... test-actor !next
... stderr=STDOUT
... timeout=20s
Should Be Equal As Integers ${result.rc} 0
@@ -184,7 +184,7 @@ Scientific Paper Writer Stage Navigation
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p !stage
... test-actor !stage
... stderr=STDOUT
... timeout=20s
Should Not Contain ${stage_check.stdout} intro
@@ -196,7 +196,7 @@ Scientific Paper Writer Stage Navigation
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p !help
... test-actor !help
... stderr=STDOUT
... timeout=20s
Should Contain ${result.stdout} Available Commands
@@ -210,7 +210,7 @@ Scientific Paper Writer Stage Navigation
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p !stage
... test-actor !stage
... stderr=STDOUT
... timeout=20s
Should Contain ${result.stdout} Current Stage
@@ -237,7 +237,7 @@ Run Paper Command
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p ${command}
... test-actor ${command}
... stderr=STDOUT
... timeout=${timeout}
Log Command: ${command}
+3 -3
View File
@@ -31,7 +31,7 @@ Test Context Export Import Commands
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p Hello world
... test-actor Hello world
... timeout=120s on_timeout=kill
# Export it
@@ -62,7 +62,7 @@ Test Context List Command
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p First
... test-actor First
... timeout=120s on_timeout=kill
Run Process ${PYTHON} -m cleveragents actor run
@@ -71,7 +71,7 @@ Test Context List Command
... --context-dir ${CONTEXT_DIR}
... --unsafe
... --allow-rxpy-in-run-mode
... -p Second
... test-actor Second
... timeout=120s on_timeout=kill
# List them
+1 -1
View File
@@ -50,7 +50,7 @@ System Prompt Template Variables Are Rendered With Runtime Context
# Run the agent with --load-context - the LLM should receive a system prompt with rendered values
# Since we can't easily intercept the actual LLM call, we verify the config was loaded correctly
${result}= Run Process ${PYTHON} -m cleveragents actor run -c ${TEMP_DIR}/template_test.yaml --unsafe --allow-rxpy-in-run-mode --load-context ${TEMP_DIR}/test_context.json -p "Analyze this"
${result}= Run Process ${PYTHON} -m cleveragents actor run -c ${TEMP_DIR}/template_test.yaml --unsafe --allow-rxpy-in-run-mode --load-context ${TEMP_DIR}/test_context.json test-actor "Analyze this"
... shell=False timeout=120s on_timeout=kill
# The agent should have processed successfully (even if it times out, the config should load)
@@ -0,0 +1,122 @@
"""Shared actor-config resolution for the ``actor run`` CLI commands.
Both ``actor.py`` and ``actor_run.py`` need to resolve an actor name
to one or more YAML config-file paths. This module centralises that
logic so the two CLI entry-points stay DRY.
Spec Deviation
--------------
The specification (lines 4562-4566) defines ``agents actor run`` with
no ``--config/-c`` option. The implementation preserves ``--config``
as an optional convenience for direct YAML invocation. When provided,
``--config`` takes precedence over registry-based name resolution.
Accepted in issue #901 AC: "`--config/-c` is removed or made optional
(for direct YAML invocation as a convenience)".
"""
from __future__ import annotations
import atexit
import tempfile
from pathlib import Path
import typer
import yaml
from cleveragents.application.container import get_container
from cleveragents.core.exceptions import NotFoundError
# Module-level set + single atexit handler to avoid unbounded handler
# accumulation when resolve_config_files is called multiple times in the
# same process (e.g. test suites using CliRunner).
_temp_files: set[str] = set()
def _cleanup_temp_files() -> None:
"""Remove all temporary YAML files created by resolve_config_files."""
snapshot = list(_temp_files)
_temp_files.difference_update(snapshot)
for p in snapshot:
Path(p).unlink(missing_ok=True)
atexit.register(_cleanup_temp_files)
def _sanitize_name(name: str) -> str:
"""Strip non-printable and ANSI control characters from *name*.
Prevents user-controlled CLI input from injecting terminal escape
sequences into error messages rendered in CI logs or web dashboards.
"""
return "".join(c for c in name if c.isprintable())
def resolve_config_files(name: str, config: list[Path]) -> list[Path]:
"""Return config file paths, resolving *name* from the actor registry
when *config* is empty.
When ``--config`` is provided the caller already has file paths and
*name* is ignored (backward-compatible). Otherwise *name* is looked
up in the actor registry and the YAML text stored on the actor is
written to a temporary file so the existing
:class:`ReactiveCleverAgentsApp` flow can consume it unchanged.
Raises:
typer.Exit(code=2): When the actor name is empty, the actor is
not found in the registry and no ``--config`` was provided,
or when the actor has no configuration data.
Other exceptions from ``get_container()`` or
``container.actor_registry()`` are **not** caught here so
callers can handle infrastructure failures in their own
``try`` blocks.
"""
if config:
return config
if not name or not name.strip():
typer.echo("Error: Actor name must not be empty.", err=True)
raise typer.Exit(code=2)
safe_name = _sanitize_name(name)
container = get_container()
actor_registry = container.actor_registry()
try:
actor = actor_registry.get(name)
except NotFoundError:
typer.echo(
f"Error: Actor '{safe_name}' not found in registry "
"and no --config provided.",
err=True,
)
raise typer.Exit(code=2) from None
yaml_text = (actor.yaml_text or "").strip()
if not yaml_text:
config_blob = actor.config_blob
if not config_blob:
typer.echo(
f"Error: Actor '{safe_name}' has no configuration data.",
err=True,
)
raise typer.Exit(code=2)
try:
yaml_text = yaml.safe_dump(config_blob, default_flow_style=False)
except yaml.YAMLError:
typer.echo(
f"Error: Actor '{safe_name}' config_blob could not be "
"serialised to YAML.",
err=True,
)
raise typer.Exit(code=2) from None
with tempfile.NamedTemporaryFile(
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
) as tmp:
_temp_files.add(tmp.name)
tmp.write(yaml_text)
tmp.flush()
return [Path(tmp.name)]
+21 -10
View File
@@ -5,7 +5,9 @@ import json
from pathlib import Path
from typing import Annotated, Any, cast
import click
import typer
import yaml
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
@@ -13,6 +15,9 @@ from rich.table import Table
from cleveragents.actor.config import ActorConfiguration
from cleveragents.actor.schema import actor_role_warnings
from cleveragents.application.container import get_container
from cleveragents.cli.commands._resolve_actor import (
resolve_config_files as _resolve_config_files,
)
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.core.exceptions import (
BusinessRuleViolation,
@@ -41,8 +46,16 @@ _FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)
@app.command()
def run(
name: Annotated[
str,
typer.Argument(help="Actor name to run (resolved from actor registry)"),
],
prompt: Annotated[
str,
typer.Argument(help="Prompt text to send to the actor"),
],
config: Annotated[
list[Path],
list[Path] | None,
typer.Option(
"--config",
"-c",
@@ -51,10 +64,9 @@ def run(
dir_okay=False,
readable=True,
resolve_path=True,
help="Path to one or more YAML/JSON configs",
help="YAML config paths (overrides registry-based name resolution)",
),
],
prompt: Annotated[str, typer.Option("--prompt", "-p", help="Prompt to send")],
] = None,
output: Annotated[
Path | None,
typer.Option(
@@ -112,8 +124,9 @@ def run(
) -> None:
"""Run the reactive network once with actor-first configs."""
try:
resolved_config = _resolve_config_files(name, list(config or []))
app_exec = ReactiveCleverAgentsApp(
config_files=config,
config_files=resolved_config,
verbose=verbose,
unsafe=unsafe,
temperature_override=temperature,
@@ -137,10 +150,8 @@ def run(
ctx_mgr.save_global_context(app_exec.config.global_context)
return result
if load_context:
import json as _json
with open(load_context, encoding="utf-8") as f:
data = _json.load(f)
data = json.load(f)
if data.get("global_context") and app_exec.config:
app_exec.config.global_context.update(data["global_context"])
return await app_exec.run_single_shot(
@@ -172,6 +183,8 @@ def run(
except UnsafeConfigurationError as exc:
typer.echo(f"Error: {exc}", err=True)
raise typer.Exit(code=1) from exc
except click.exceptions.Exit:
raise
except CleverAgentsError as exc:
typer.echo(f"Error: {exc}", err=True)
raise typer.Exit(code=2) from exc
@@ -210,8 +223,6 @@ def _load_config(config_path: Path | None) -> dict[str, Any] | None:
data = json.loads(text)
except json.JSONDecodeError:
try:
import yaml
data = yaml.safe_load(text)
except Exception as exc: # pragma: no cover - defensive
raise typer.BadParameter(f"Failed to parse config: {exc}") from exc
+26 -10
View File
@@ -1,12 +1,20 @@
from __future__ import annotations
import asyncio
import json
from pathlib import Path
from typing import Annotated
import click
import typer
from cleveragents.core.exceptions import CleverAgentsException, UnsafeConfigurationError
from cleveragents.cli.commands._resolve_actor import (
resolve_config_files as _resolve_config_files,
)
from cleveragents.core.exceptions import (
CleverAgentsError,
UnsafeConfigurationError,
)
from cleveragents.reactive.application import ReactiveCleverAgentsApp
from cleveragents.reactive.context_manager import ContextManager
@@ -15,8 +23,16 @@ app = typer.Typer(help="Run reactive configs with actor-first semantics.")
@app.command()
def run(
name: Annotated[
str,
typer.Argument(help="Actor name to run (resolved from actor registry)"),
],
prompt: Annotated[
str,
typer.Argument(help="Prompt text to send to the actor"),
],
config: Annotated[
list[Path],
list[Path] | None,
typer.Option(
"--config",
"-c",
@@ -25,10 +41,9 @@ def run(
dir_okay=False,
readable=True,
resolve_path=True,
help="Path to one or more YAML/JSON configs",
help="YAML config paths (overrides registry-based name resolution)",
),
],
prompt: Annotated[str, typer.Option("--prompt", "-p", help="Prompt to send")],
] = None,
output: Annotated[
Path | None,
typer.Option(
@@ -85,8 +100,9 @@ def run(
) -> None:
"""Run the reactive network once with actor-first configs."""
try:
resolved_config = _resolve_config_files(name, list(config or []))
app_exec = ReactiveCleverAgentsApp(
config_files=config,
config_files=resolved_config,
verbose=verbose,
unsafe=unsafe,
temperature_override=temperature,
@@ -110,10 +126,8 @@ def run(
ctx_mgr.save_global_context(app_exec.config.global_context)
return result
if load_context:
import json as _json
with open(load_context, encoding="utf-8") as f:
data = _json.load(f)
data = json.load(f)
if data.get("global_context") and app_exec.config:
app_exec.config.global_context.update(data["global_context"])
return await app_exec.run_single_shot(
@@ -145,7 +159,9 @@ def run(
except UnsafeConfigurationError as exc:
typer.echo(f"Error: {exc}", err=True)
raise typer.Exit(code=1) from exc
except CleverAgentsException as exc:
except click.exceptions.Exit:
raise
except CleverAgentsError as exc:
typer.echo(f"Error: {exc}", err=True)
raise typer.Exit(code=2) from exc
except Exception as exc: # pragma: no cover