# pyright: reportRedeclaration=false """Step definitions for actor context remove/export/import commands.""" from __future__ import annotations import json import shutil import tempfile from pathlib import Path from typing import Any from behave import given, then, when from typer.testing import CliRunner from cleveragents.cli.commands.actor_context import app as actor_context_app from cleveragents.reactive.context_manager import ContextManager _ENVELOPE_KEYS = {"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 # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("a temporary context directory for actor context tests") def step_temp_context_dir(context): context.tmp_dir = Path(tempfile.mkdtemp()) context.context_dir = context.tmp_dir / "contexts" context.context_dir.mkdir(parents=True, exist_ok=True) context.runner = CliRunner() context._cleanup_handlers: list = getattr(context, "_cleanup_handlers", []) context._cleanup_handlers.append( lambda: shutil.rmtree(context.tmp_dir, ignore_errors=True) ) # Will store original messages for round-trip checks context.original_messages = None context.export_file = None # --------------------------------------------------------------------------- # Givens — context creation helpers # --------------------------------------------------------------------------- @given('an actor context named "{name}" exists') def step_context_exists(context, name): mgr = ContextManager(name, context.context_dir) mgr.add_message("system", f"Initial message for {name}") @given('an actor context named "{name}" exists with messages') def step_context_exists_with_messages(context, name): mgr = ContextManager(name, context.context_dir) mgr.add_message("user", "Hello, how are you?") mgr.add_message("assistant", "I'm doing well, thanks for asking!") mgr.add_message("user", "Great, let's get started.") context.original_messages = list(mgr.messages) @given('a valid context JSON file named "{filename}"') def step_create_json_file(context, filename): data = { "context_name": filename.replace(".json", ""), "messages": [ { "role": "user", "content": "test message", "timestamp": "2026-01-01T00:00:00", "metadata": {}, }, ], "metadata": {"context_name": filename.replace(".json", "")}, "state": {}, "global_context": {}, } context.import_file = context.tmp_dir / filename context.import_file.write_text(json.dumps(data, indent=2), encoding="utf-8") @given('a valid context JSON file with context_name "{name}"') def step_create_json_file_with_name(context, name): data = { "context_name": name, "messages": [ { "role": "user", "content": "auto-named test", "timestamp": "2026-01-01T00:00:00", "metadata": {}, }, ], "metadata": {"context_name": name}, "state": {}, "global_context": {}, } context.import_file = context.tmp_dir / f"{name}.json" context.import_file.write_text(json.dumps(data, indent=2), encoding="utf-8") # --------------------------------------------------------------------------- # When — remove # --------------------------------------------------------------------------- @when('I run actor context remove "{name}" with --yes') def step_remove_named_yes(context, name): context.result = context.runner.invoke( actor_context_app, ["remove", name, "--yes", "--context-dir", str(context.context_dir)], ) @when("I run actor context remove --all with --yes") def step_remove_all_yes(context): context.result = context.runner.invoke( actor_context_app, ["remove", "--all", "--yes", "--context-dir", str(context.context_dir)], ) @when('I run actor context remove "{name}" with --yes and format "{fmt}"') def step_remove_named_format(context, name, fmt): context.result = context.runner.invoke( actor_context_app, [ "remove", name, "--yes", "--context-dir", str(context.context_dir), "--format", fmt, ], ) @when("I run actor context remove without name or all") def step_remove_no_args(context): context.result = context.runner.invoke( actor_context_app, ["remove", "--context-dir", str(context.context_dir)], ) @when('I run actor context remove "{name}" with --all') def step_remove_name_and_all(context, name): context.result = context.runner.invoke( actor_context_app, ["remove", name, "--all", "--context-dir", str(context.context_dir)], ) # --------------------------------------------------------------------------- # When — export # --------------------------------------------------------------------------- @when('I run actor context export "{name}" to a JSON file') def step_export_json(context, name): context.export_file = context.tmp_dir / f"{name}-export.json" context.result = context.runner.invoke( actor_context_app, [ "export", name, "--output", str(context.export_file), "--context-dir", str(context.context_dir), ], ) @when('I run actor context export "{name}" to a YAML file') def step_export_yaml(context, name): context.export_file = context.tmp_dir / f"{name}-export.yaml" context.result = context.runner.invoke( actor_context_app, [ "export", name, "--output", str(context.export_file), "--context-dir", str(context.context_dir), ], ) @when('I run actor context export "{name}" to a JSON file with format "{fmt}"') def step_export_json_format(context, name, fmt): context.export_file = context.tmp_dir / f"{name}-export.json" context.result = context.runner.invoke( actor_context_app, [ "export", name, "--output", str(context.export_file), "--context-dir", str(context.context_dir), "--format", fmt, ], ) # --------------------------------------------------------------------------- # When — import # --------------------------------------------------------------------------- @when('I run actor context import from that file as "{name}"') def step_import_named(context, name): context.result = context.runner.invoke( actor_context_app, [ "import", name, "--input", str(context.import_file), "--context-dir", str(context.context_dir), ], ) @when("I run actor context import from that file without a name") def step_import_no_name(context): context.result = context.runner.invoke( actor_context_app, [ "import", "--input", str(context.import_file), "--context-dir", str(context.context_dir), ], ) @when('I run actor context import from that file as "{name}" without update') def step_import_no_update(context, name): context.result = context.runner.invoke( actor_context_app, [ "import", name, "--input", str(context.import_file), "--context-dir", str(context.context_dir), ], ) @when('I run actor context import from that file as "{name}" with --update') def step_import_with_update(context, name): context.result = context.runner.invoke( actor_context_app, [ "import", name, "--input", str(context.import_file), "--update", "--context-dir", str(context.context_dir), ], ) # --------------------------------------------------------------------------- # When — round-trip helpers # --------------------------------------------------------------------------- @when('I export the context "{name}" to a JSON file') def step_roundtrip_export(context, name): context.export_file = context.tmp_dir / f"{name}-roundtrip.json" context.result = context.runner.invoke( actor_context_app, [ "export", name, "--output", str(context.export_file), "--context-dir", str(context.context_dir), ], ) assert context.result.exit_code == 0, context.result.output @when('I remove the context "{name}"') def step_roundtrip_remove(context, name): context.result = context.runner.invoke( actor_context_app, ["remove", name, "--yes", "--context-dir", str(context.context_dir)], ) assert context.result.exit_code == 0, context.result.output @when('I import the context from that JSON file as "{name}"') def step_roundtrip_import(context, name): context.result = context.runner.invoke( actor_context_app, [ "import", name, "--input", str(context.export_file), "--context-dir", str(context.context_dir), ], ) assert context.result.exit_code == 0, context.result.output # --------------------------------------------------------------------------- # Then — success / failure # --------------------------------------------------------------------------- @then("the actor context remove command should succeed") def step_remove_success(context): assert context.result.exit_code == 0, ( f"Expected exit 0, got {context.result.exit_code}.\n" f"stdout: {context.result.output}\n" f"stderr: {getattr(context.result, 'stderr', '')}" ) @then("the actor context export command should succeed") def step_export_success(context): assert context.result.exit_code == 0, ( f"Expected exit 0, got {context.result.exit_code}.\n" f"stdout: {context.result.output}\n" f"stderr: {getattr(context.result, 'stderr', '')}" ) @then("the actor context import command should succeed") def step_import_success(context): assert context.result.exit_code == 0, ( f"Expected exit 0, got {context.result.exit_code}.\n" f"stdout: {context.result.output}\n" f"stderr: {getattr(context.result, 'stderr', '')}" ) @then("the actor context remove command should fail with exit code 1") def step_remove_fail(context): assert context.result.exit_code == 1, ( f"Expected exit 1, got {context.result.exit_code}.\n" f"stdout: {context.result.output}\n" f"stderr: {getattr(context.result, 'stderr', '')}" ) @then("the actor context export command should fail with exit code 1") def step_export_fail(context): assert context.result.exit_code == 1, ( f"Expected exit 1, got {context.result.exit_code}.\n" f"stdout: {context.result.output}\n" f"stderr: {getattr(context.result, 'stderr', '')}" ) @then("the actor context import command should fail with exit code 1") def step_import_fail(context): assert context.result.exit_code == 1, ( f"Expected exit 1, got {context.result.exit_code}.\n" f"stdout: {context.result.output}\n" f"stderr: {getattr(context.result, 'stderr', '')}" ) # --------------------------------------------------------------------------- # Then — state assertions # --------------------------------------------------------------------------- @then('the context "{name}" should no longer exist') def step_context_gone(context, name): ctx_path = context.context_dir / name assert not ctx_path.exists(), f"Context dir {ctx_path} still exists" @then("no actor contexts should remain") def step_no_contexts(context): remaining = [d for d in context.context_dir.iterdir() if d.is_dir()] assert len(remaining) == 0, ( f"Expected 0 contexts, found {len(remaining)}: {remaining}" ) @then('the context "{name}" should exist') def step_context_exists_check(context, name): mgr = ContextManager(name, context.context_dir) assert mgr.exists(), f"Context '{name}' does not exist at {mgr.context_dir}" # --------------------------------------------------------------------------- # Then — output assertions # --------------------------------------------------------------------------- @then('the output should contain valid JSON with key "{key}"') def step_output_json_key(context, key): output = context.result.output parsed = json.loads(output) data = _unwrap_envelope(parsed) assert key in data, f"Key '{key}' not found in JSON output: {data.keys()}" @then("the exported file should exist and contain valid JSON") def step_exported_json_valid(context): assert context.export_file.exists(), f"Export file {context.export_file} not found" data = json.loads(context.export_file.read_text(encoding="utf-8")) assert isinstance(data, dict) @then('the exported JSON should contain key "{key}"') def step_exported_json_key(context, key): data = json.loads(context.export_file.read_text(encoding="utf-8")) assert key in data, f"Key '{key}' not found in exported JSON: {data.keys()}" @then("the exported YAML file should exist and be valid") def step_exported_yaml_valid(context): import yaml assert context.export_file.exists(), f"Export file {context.export_file} not found" data = yaml.safe_load(context.export_file.read_text(encoding="utf-8")) assert isinstance(data, dict) @then("the imported context should have the same messages as the original") def step_roundtrip_messages(context): assert context.original_messages is not None, "No original messages saved" mgr = ContextManager("roundtrip", context.context_dir) imported = mgr.messages # Compare content and role, ignoring timestamp differences assert len(imported) == len(context.original_messages), ( f"Message count mismatch: {len(imported)} vs {len(context.original_messages)}" ) for orig, imp in zip(context.original_messages, imported, strict=True): assert orig["role"] == imp["role"], ( f"Role mismatch: {orig['role']} vs {imp['role']}" ) assert orig["content"] == imp["content"], ( f"Content mismatch: {orig['content']!r} vs {imp['content']!r}" )