Files
temp/robot/helper_actor_cli_show.py
freemo b26b37947e fix(cli): wrap format_output() in spec-required JSON/YAML envelope across all CLI commands
Implements the spec-required JSON/YAML output envelope for all CLI commands
that use format_output(). The envelope structure is:

  {
    "command": "<command that was run>",
    "status": "ok" | "warn" | "error",
    "exit_code": 0,
    "data": { ... command-specific payload ... },
    "timing": { "duration_ms": 123 },
    "messages": [{ "level": "ok", "text": "..." }]
  }

Changes:
- Add _build_envelope() helper to construct the spec-required envelope
- Add optional command, status, exit_code, messages parameters to format_output()
- Wrap json/yaml output in the envelope; plain/table/rich/color unchanged
- Add timing measurement (duration_ms) to all json/yaml outputs
- Add new BDD feature file (cli_json_envelope.feature) with 14 scenarios
  testing envelope field presence, values, and data payload
- Update 14 existing step files to unwrap the envelope when checking
  specific data keys (backward-compatible via _unwrap_envelope() helper)

Closes #3431
2026-04-05 21:18:11 +00:00

145 lines
4.4 KiB
Python

"""Helper script for actor CLI show output fields Robot test."""
from __future__ import annotations
import json
import sys
from typing import Any
from unittest.mock import MagicMock, patch
from typer.testing import CliRunner
from cleveragents.cli.commands.actor import app as actor_app
from cleveragents.domain.models.core.actor import Actor
_ENVELOPE_KEYS = frozenset(
{"command", "status", "exit_code", "data", "timing", "messages"}
)
def _unwrap_envelope(parsed: Any) -> Any:
"""Return the ``data`` field if *parsed* is a spec envelope, else *parsed* as-is."""
if isinstance(parsed, dict) and _ENVELOPE_KEYS.issubset(parsed.keys()):
return parsed["data"]
return parsed
def _make_actor(
*,
name: str = "local/robot-actor",
provider: str = "openai",
model: str = "gpt-4",
config: dict[str, Any] | None = None,
unsafe: bool = False,
is_default: bool = False,
is_built_in: bool = False,
) -> Actor:
blob = config or {"provider": provider, "model": model}
return Actor(
id=1,
name=name,
provider=provider,
model=model,
config_blob=blob,
config_hash=Actor.compute_hash(blob),
unsafe=unsafe,
is_built_in=is_built_in,
is_default=is_default,
)
def test_show_json_fields() -> None:
"""Verify show --format json contains expected fields."""
runner = CliRunner()
actor = _make_actor()
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
registry.get_actor.return_value = actor
mock_svc.return_value = (MagicMock(), registry)
result = runner.invoke(actor_app, ["show", actor.name, "--format", "json"])
assert result.exit_code == 0, (
f"exit_code={result.exit_code}, output={result.output}"
)
parsed = json.loads(result.output.strip())
# Output is wrapped in spec-required envelope; unwrap the data field
data = _unwrap_envelope(parsed)
assert isinstance(data, dict), f"Expected dict, got {type(data)}"
required_fields = [
"name",
"provider",
"model",
"unsafe",
"is_default",
"is_built_in",
"config_hash",
"schema_version",
"updated_at",
]
missing = [f for f in required_fields if f not in data]
assert not missing, f"Missing fields: {missing}"
assert data["name"] == "local/robot-actor"
assert data["provider"] == "openai"
assert data["model"] == "gpt-4"
print("actor-cli-show-json-fields-ok")
def test_show_yaml_output() -> None:
"""Verify show --format yaml produces valid YAML."""
runner = CliRunner()
actor = _make_actor()
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
registry.get_actor.return_value = actor
mock_svc.return_value = (MagicMock(), registry)
result = runner.invoke(actor_app, ["show", actor.name, "--format", "yaml"])
assert result.exit_code == 0
assert "name:" in result.output
assert "local/robot-actor" in result.output
print("actor-cli-show-yaml-output-ok")
def test_list_json_format() -> None:
"""Verify list --format json returns array of actors."""
runner = CliRunner()
actors = [
_make_actor(name="local/a1", provider="p1", model="m1"),
_make_actor(name="local/a2", provider="p2", model="m2"),
]
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
registry.list_actors.return_value = actors
mock_svc.return_value = (MagicMock(), registry)
result = runner.invoke(actor_app, ["list", "--format", "json"])
assert result.exit_code == 0
parsed = json.loads(result.output.strip())
# Output is wrapped in spec-required envelope; unwrap the data field
data = _unwrap_envelope(parsed)
assert isinstance(data, list)
assert len(data) == 2
print("actor-cli-list-json-format-ok")
def main() -> None:
command = sys.argv[1] if len(sys.argv) > 1 else "show-json-fields"
dispatch = {
"show-json-fields": test_show_json_fields,
"show-yaml-output": test_show_yaml_output,
"list-json-format": test_list_json_format,
}
fn = dispatch.get(command)
if fn is None:
print(f"Unknown command: {command}", file=sys.stderr)
sys.exit(1)
fn()
if __name__ == "__main__":
main()