test(e2e): verify M2 success criteria — actor compiler and tool routing
CI / lint (pull_request) Successful in 16s
CI / benchmark-publish (pull_request) Has been skipped
CI / security (pull_request) Successful in 39s
CI / quality (pull_request) Successful in 38s
CI / typecheck (pull_request) Successful in 56s
CI / build (pull_request) Successful in 52s
CI / integration_tests (pull_request) Successful in 4m17s
CI / unit_tests (pull_request) Successful in 22m35s
CI / docker (pull_request) Successful in 1m0s
CI / benchmark-regression (pull_request) Successful in 22m49s
CI / coverage (pull_request) Successful in 41m16s
CI / lint (pull_request) Successful in 16s
CI / benchmark-publish (pull_request) Has been skipped
CI / security (pull_request) Successful in 39s
CI / quality (pull_request) Successful in 38s
CI / typecheck (pull_request) Successful in 56s
CI / build (pull_request) Successful in 52s
CI / integration_tests (pull_request) Successful in 4m17s
CI / unit_tests (pull_request) Successful in 22m35s
CI / docker (pull_request) Successful in 1m0s
CI / benchmark-regression (pull_request) Successful in 22m49s
CI / coverage (pull_request) Successful in 41m16s
This commit is contained in:
@@ -405,7 +405,11 @@ def step_exact_count(context: Context, count: int) -> None:
|
||||
|
||||
@then('the first entry should have plan_id "{expected}"')
|
||||
def step_first_plan_id(context: Context, expected: str) -> None:
|
||||
assert context.entries[0].plan_id == expected
|
||||
actual = context.entries[0].plan_id
|
||||
assert actual == expected, (
|
||||
f"Expected first entry plan_id={expected!r}, got {actual!r} "
|
||||
f"(created_at={context.entries[0].created_at!r})"
|
||||
)
|
||||
|
||||
|
||||
@then('the fetched entry should have event_type "{expected}"')
|
||||
|
||||
@@ -0,0 +1,659 @@
|
||||
"""Robot Framework helper for M2 E2E verification tests.
|
||||
|
||||
Exercises the complete M2 success criteria sequence:
|
||||
1. Actor YAML creation and loading
|
||||
2. Actor compilation to LangGraph StateGraphs
|
||||
3. Tool router external tool resolution
|
||||
4. Validation runner with required/informational modes
|
||||
5. Multi-file generation producing a correct ChangeSet
|
||||
|
||||
Each subcommand prints a sentinel string on success and exits 0.
|
||||
On failure it prints a diagnostic and exits 1.
|
||||
|
||||
Usage:
|
||||
python robot/helper_m2_e2e_verification.py <command> [args...]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# Ensure src is importable when run from workspace root
|
||||
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from typer.testing import CliRunner # noqa: E402
|
||||
|
||||
from cleveragents.actor.compiler import ( # noqa: E402
|
||||
ActorCompilationError,
|
||||
CompiledActor,
|
||||
compile_actor,
|
||||
)
|
||||
from cleveragents.actor.loader import ActorLoader # noqa: E402
|
||||
from cleveragents.actor.schema import ( # noqa: E402
|
||||
ActorConfigSchema,
|
||||
ActorType,
|
||||
)
|
||||
from cleveragents.cli.commands.action import app as action_app # noqa: E402
|
||||
from cleveragents.cli.commands.actor import app as actor_app # noqa: E402
|
||||
from cleveragents.cli.commands.plan import app as plan_app # noqa: E402
|
||||
from cleveragents.domain.models.core.action import ( # noqa: E402
|
||||
Action,
|
||||
ActionState,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import ( # noqa: E402
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
PlanTimestamps,
|
||||
ProcessingState,
|
||||
ProjectLink,
|
||||
)
|
||||
from cleveragents.domain.models.core.tool import ( # noqa: E402
|
||||
Validation,
|
||||
ValidationMode,
|
||||
)
|
||||
from cleveragents.tool.builtins.changeset import ( # noqa: E402
|
||||
ChangeSet,
|
||||
ChangeSetCapture,
|
||||
)
|
||||
from cleveragents.tool.builtins.file_tools import FILE_WRITE_SPEC # noqa: E402
|
||||
from cleveragents.tool.registry import ToolRegistry # noqa: E402
|
||||
from cleveragents.tool.router import ( # noqa: E402
|
||||
ProviderFormat,
|
||||
ToolCallRouter,
|
||||
detect_provider_format,
|
||||
normalize_tool_call,
|
||||
)
|
||||
from cleveragents.tool.runner import ToolRunner # noqa: E402
|
||||
from cleveragents.tool.runtime import ToolSpec # noqa: E402
|
||||
|
||||
cli_runner = CliRunner()
|
||||
|
||||
_PLAN_ULID = "01KHDE6WWS2171PWW3GJEBXZ8S"
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
def _mock_plan() -> Plan:
|
||||
now = datetime.now()
|
||||
return Plan(
|
||||
identity=PlanIdentity(plan_id=_PLAN_ULID),
|
||||
namespaced_name=NamespacedName.parse("local/m2-test-plan"),
|
||||
description="M2 verification plan",
|
||||
definition_of_done="All M2 criteria pass",
|
||||
action_name="local/m2-test-action",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
project_links=[ProjectLink(project_name="my-project")],
|
||||
arguments={},
|
||||
arguments_order=[],
|
||||
automation_profile=None,
|
||||
invariants=[],
|
||||
strategy_actor="openai/gpt-4",
|
||||
execution_actor="openai/gpt-4",
|
||||
timestamps=PlanTimestamps(created_at=now, updated_at=now),
|
||||
created_by="m2-test",
|
||||
reusable=True,
|
||||
read_only=False,
|
||||
)
|
||||
|
||||
|
||||
def _mock_action() -> Action:
|
||||
return Action(
|
||||
namespaced_name=NamespacedName.parse("local/m2-test-action"),
|
||||
description="M2 test action",
|
||||
long_description="M2 test action long description",
|
||||
definition_of_done="All tests pass",
|
||||
strategy_actor="openai/gpt-4",
|
||||
execution_actor="openai/gpt-4",
|
||||
state=ActionState.AVAILABLE,
|
||||
reusable=True,
|
||||
read_only=False,
|
||||
created_by="m2-test",
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
|
||||
def _echo(inputs: dict[str, Any]) -> dict[str, Any]:
|
||||
return dict(inputs)
|
||||
|
||||
|
||||
def _validation_pass(inputs: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"passed": True, "message": "validation passed"}
|
||||
|
||||
|
||||
def _validation_fail(inputs: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"passed": False, "message": "validation failed"}
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Subcommand: actor-yaml-create-load
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
def actor_yaml_create_load() -> None:
|
||||
"""Create a temporary actor YAML and load it via ActorLoader."""
|
||||
tmp = Path(tempfile.mkdtemp(prefix="m2_actor_"))
|
||||
yaml_content = (
|
||||
"name: local/m2-test-actor\n"
|
||||
"type: graph\n"
|
||||
"description: M2 E2E test actor\n"
|
||||
'version: "1.0"\n'
|
||||
"model: gpt-4\n"
|
||||
"route:\n"
|
||||
" nodes:\n"
|
||||
" - id: planner\n"
|
||||
" type: agent\n"
|
||||
" name: Planner\n"
|
||||
" description: Plans the task\n"
|
||||
" config:\n"
|
||||
" model: gpt-4\n"
|
||||
" prompt: Plan the task\n"
|
||||
" - id: executor\n"
|
||||
" type: tool\n"
|
||||
" name: Executor\n"
|
||||
" description: Executes the plan\n"
|
||||
" config:\n"
|
||||
" tool_name: builtins/echo\n"
|
||||
" edges:\n"
|
||||
" - from_node: planner\n"
|
||||
" to_node: executor\n"
|
||||
" entry_node: planner\n"
|
||||
" exit_nodes:\n"
|
||||
" - executor\n"
|
||||
)
|
||||
yaml_path = tmp / "m2_actor.yaml"
|
||||
yaml_path.write_text(yaml_content)
|
||||
|
||||
loader = ActorLoader(search_roots=[tmp])
|
||||
actors = loader.discover()
|
||||
assert len(actors) == 1, f"Expected 1 actor, got {len(actors)}"
|
||||
actor = actors[0]
|
||||
assert actor.name == "local/m2-test-actor", f"Wrong name: {actor.name}"
|
||||
assert actor.type == ActorType.GRAPH, f"Wrong type: {actor.type}"
|
||||
print("m2-actor-yaml-create-load-ok")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Subcommand: actor-add-config-cli
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
def actor_add_config_cli() -> None:
|
||||
"""Create actor YAML and load via ``agents actor add --config``."""
|
||||
tmp = Path(tempfile.mkdtemp(prefix="m2_actor_cli_"))
|
||||
yaml_path = tmp / "actor.yaml"
|
||||
yaml_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
"options": {"temperature": 0.5},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
mock_svc = MagicMock()
|
||||
mock_registry = MagicMock()
|
||||
mock_actor = MagicMock()
|
||||
mock_actor.name = "local/m2-cli-actor"
|
||||
mock_actor.provider = "openai"
|
||||
mock_actor.model = "gpt-4"
|
||||
mock_actor.unsafe = False
|
||||
mock_actor.is_default = False
|
||||
mock_actor.is_built_in = False
|
||||
mock_actor.config_hash = "abc123"
|
||||
mock_actor.schema_version = "1.0"
|
||||
mock_actor.updated_at = datetime.now()
|
||||
mock_actor.graph_descriptor = None
|
||||
mock_actor.config_blob = {}
|
||||
mock_registry.upsert_actor.return_value = mock_actor
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.actor._get_services",
|
||||
return_value=(mock_svc, mock_registry),
|
||||
):
|
||||
result = cli_runner.invoke(
|
||||
actor_app,
|
||||
[
|
||||
"add",
|
||||
"local/m2-cli-actor",
|
||||
"--config",
|
||||
str(yaml_path),
|
||||
"--format",
|
||||
"plain",
|
||||
],
|
||||
)
|
||||
if result.exit_code == 0:
|
||||
print("m2-actor-add-config-cli-ok")
|
||||
else:
|
||||
print(f"FAIL: exit={result.exit_code} output={result.output}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Subcommand: action-create
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
def action_create() -> None:
|
||||
"""Create an action referencing the custom actor via CLI."""
|
||||
import yaml as _yaml
|
||||
|
||||
tmp = Path(tempfile.mkdtemp(prefix="m2_action_"))
|
||||
action_config = {
|
||||
"name": "local/m2-test-action",
|
||||
"description": "M2 test action",
|
||||
"definition_of_done": "All tests pass",
|
||||
"strategy_actor": "openai/gpt-4",
|
||||
"execution_actor": "openai/gpt-4",
|
||||
}
|
||||
config_path = tmp / "action.yaml"
|
||||
config_path.write_text(_yaml.dump(action_config))
|
||||
|
||||
mock_svc = MagicMock()
|
||||
mock_action = _mock_action()
|
||||
mock_svc.create_action.return_value = mock_action
|
||||
mock_svc.get_action_by_name.return_value = mock_action
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.action._get_lifecycle_service",
|
||||
return_value=mock_svc,
|
||||
):
|
||||
result = cli_runner.invoke(
|
||||
action_app,
|
||||
["create", "--config", str(config_path)],
|
||||
)
|
||||
if result.exit_code == 0:
|
||||
print("m2-action-create-ok")
|
||||
else:
|
||||
print(f"FAIL: exit={result.exit_code} output={result.output}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Subcommand: plan-use-execute
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
def plan_use_execute() -> None:
|
||||
"""Run plan use and plan execute with the custom actor."""
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.get_action_by_name.return_value = _mock_action()
|
||||
plan = _mock_plan()
|
||||
mock_svc.use_action.return_value = plan
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_svc,
|
||||
):
|
||||
result = cli_runner.invoke(
|
||||
plan_app,
|
||||
[
|
||||
"use",
|
||||
"local/m2-test-action",
|
||||
"--strategy-actor",
|
||||
"openai/gpt-4",
|
||||
"--format",
|
||||
"plain",
|
||||
],
|
||||
)
|
||||
if result.exit_code != 0:
|
||||
print(f"FAIL plan use: exit={result.exit_code} output={result.output}")
|
||||
sys.exit(1)
|
||||
|
||||
# Execute phase
|
||||
exec_plan = _mock_plan()
|
||||
exec_plan = exec_plan.model_copy(
|
||||
update={
|
||||
"phase": PlanPhase.EXECUTE,
|
||||
"processing_state": ProcessingState.QUEUED,
|
||||
}
|
||||
)
|
||||
mock_svc.execute_plan.return_value = exec_plan
|
||||
mock_svc.get_plan.return_value = plan
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_svc,
|
||||
):
|
||||
result = cli_runner.invoke(
|
||||
plan_app,
|
||||
["execute", _PLAN_ULID, "--format", "plain"],
|
||||
)
|
||||
if result.exit_code == 0:
|
||||
print("m2-plan-use-execute-ok")
|
||||
else:
|
||||
print(f"FAIL plan execute: exit={result.exit_code} output={result.output}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Subcommand: actor-yaml-parse-validate
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
def actor_yaml_parse_validate() -> None:
|
||||
"""Verify actor YAML files parse and validate correctly."""
|
||||
# Test valid graph YAML
|
||||
tmp = Path(tempfile.mkdtemp(prefix="m2_parse_"))
|
||||
valid_yaml = (
|
||||
"name: local/valid-actor\n"
|
||||
"type: graph\n"
|
||||
"description: Valid actor\n"
|
||||
'version: "1.0"\n'
|
||||
"model: gpt-4\n"
|
||||
"route:\n"
|
||||
" nodes:\n"
|
||||
" - id: start\n"
|
||||
" type: agent\n"
|
||||
" name: Start\n"
|
||||
" description: Start node\n"
|
||||
" config:\n"
|
||||
" model: gpt-4\n"
|
||||
" prompt: Begin\n"
|
||||
" edges: []\n"
|
||||
" entry_node: start\n"
|
||||
" exit_nodes:\n"
|
||||
" - start\n"
|
||||
)
|
||||
(tmp / "valid.yaml").write_text(valid_yaml)
|
||||
config = ActorConfigSchema.from_yaml_file(str(tmp / "valid.yaml"))
|
||||
assert config.name == "local/valid-actor"
|
||||
assert config.type == ActorType.GRAPH
|
||||
assert config.route is not None
|
||||
assert len(config.route.nodes) == 1
|
||||
|
||||
# Test valid LLM YAML
|
||||
llm_yaml = (
|
||||
"name: local/llm-actor\n"
|
||||
"type: llm\n"
|
||||
"description: LLM actor\n"
|
||||
'version: "1.0"\n'
|
||||
"model: gpt-4\n"
|
||||
)
|
||||
(tmp / "llm.yaml").write_text(llm_yaml)
|
||||
llm_config = ActorConfigSchema.from_yaml_file(str(tmp / "llm.yaml"))
|
||||
assert llm_config.type == ActorType.LLM
|
||||
|
||||
# Test invalid YAML is rejected
|
||||
bad_yaml = "name: [\ninvalid yaml\n"
|
||||
(tmp / "bad.yaml").write_text(bad_yaml)
|
||||
try:
|
||||
ActorConfigSchema.from_yaml_file(str(tmp / "bad.yaml"))
|
||||
print("FAIL: bad YAML not rejected")
|
||||
sys.exit(1)
|
||||
except Exception:
|
||||
pass # Expected
|
||||
|
||||
print("m2-actor-yaml-parse-validate-ok")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Subcommand: actor-compile-stategraph
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
def actor_compile_stategraph() -> None:
|
||||
"""Verify actors compile to LangGraph StateGraphs."""
|
||||
project_root = Path(__file__).resolve().parents[1]
|
||||
yaml_path = project_root / "examples" / "actors" / "simple_graph.yaml"
|
||||
config = ActorConfigSchema.from_yaml_file(str(yaml_path))
|
||||
compiled = compile_actor(config)
|
||||
|
||||
assert isinstance(compiled, CompiledActor)
|
||||
assert compiled.name == config.name
|
||||
assert len(compiled.nodes) > 0, "Expected at least one node"
|
||||
assert len(compiled.edges) > 0, "Expected at least one edge"
|
||||
assert compiled.entry_point != "", "Entry point must not be empty"
|
||||
assert len(compiled.metadata.node_ids) > 0
|
||||
assert compiled.metadata.entry_node != ""
|
||||
|
||||
# Verify LLM actors are rejected
|
||||
tmp = Path(tempfile.mkdtemp(prefix="m2_compile_"))
|
||||
llm_yaml = (
|
||||
"name: local/llm-only\n"
|
||||
"type: llm\n"
|
||||
"description: LLM only actor\n"
|
||||
'version: "1.0"\n'
|
||||
"model: gpt-4\n"
|
||||
)
|
||||
(tmp / "llm.yaml").write_text(llm_yaml)
|
||||
llm_config = ActorConfigSchema.from_yaml_file(str(tmp / "llm.yaml"))
|
||||
try:
|
||||
compile_actor(llm_config)
|
||||
print("FAIL: LLM actor should not compile")
|
||||
sys.exit(1)
|
||||
except ActorCompilationError:
|
||||
pass # Expected
|
||||
|
||||
print("m2-actor-compile-stategraph-ok")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Subcommand: tool-router-resolve-external
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
def tool_router_resolve_external() -> None:
|
||||
"""Verify tool router can resolve external tools."""
|
||||
registry = ToolRegistry()
|
||||
ext_spec = ToolSpec(
|
||||
name="external/custom-tool",
|
||||
description="External custom tool",
|
||||
handler=_echo,
|
||||
source="custom",
|
||||
)
|
||||
registry.register(ext_spec)
|
||||
|
||||
tool_runner_inst = ToolRunner(registry)
|
||||
router = ToolCallRouter(
|
||||
registry=registry, runner=tool_runner_inst, plan_id="m2-plan-1"
|
||||
)
|
||||
|
||||
# Route an OpenAI-format call to the external tool
|
||||
result = router.route(
|
||||
{
|
||||
"name": "external/custom-tool",
|
||||
"arguments": json.dumps({"key": "value"}),
|
||||
}
|
||||
)
|
||||
assert result.result.success, f"Expected success, got error: {result.result.error}"
|
||||
assert result.provider_format == ProviderFormat.OPENAI
|
||||
|
||||
# Route an Anthropic-format call
|
||||
result2 = router.route(
|
||||
{
|
||||
"name": "external/custom-tool",
|
||||
"input": {"key": "value"},
|
||||
}
|
||||
)
|
||||
assert result2.result.success
|
||||
assert result2.provider_format == ProviderFormat.ANTHROPIC
|
||||
|
||||
# Verify detection and normalization
|
||||
fmt = detect_provider_format({"name": "x/y", "arguments": '{"a": 1}'})
|
||||
assert fmt == ProviderFormat.OPENAI
|
||||
req = normalize_tool_call(
|
||||
{"name": "external/custom-tool", "arguments": '{"k":"v"}'}
|
||||
)
|
||||
assert req.tool_name == "external/custom-tool"
|
||||
|
||||
print("m2-tool-router-resolve-external-ok")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Subcommand: validation-runner-execute
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
def validation_runner_execute() -> None:
|
||||
"""Verify validation runner executes required/informational validations."""
|
||||
# Create a required validation
|
||||
required_val = Validation.from_config(
|
||||
{
|
||||
"name": "local/coverage-check",
|
||||
"description": "Check code coverage",
|
||||
"source": "custom",
|
||||
"mode": "required",
|
||||
"code": (
|
||||
"def run(inputs): return {'passed': inputs.get('coverage', 0) >= 80}"
|
||||
),
|
||||
}
|
||||
)
|
||||
assert required_val.mode == ValidationMode.REQUIRED
|
||||
|
||||
# Create an informational validation
|
||||
info_val = Validation.from_config(
|
||||
{
|
||||
"name": "local/lint-check",
|
||||
"description": "Lint check",
|
||||
"source": "custom",
|
||||
"mode": "informational",
|
||||
"code": (
|
||||
"def run(inputs): return {'passed': False, 'message': 'lint warnings'}"
|
||||
),
|
||||
}
|
||||
)
|
||||
assert info_val.mode == ValidationMode.INFORMATIONAL
|
||||
|
||||
# Register validation tools in registry and execute through runner
|
||||
registry = ToolRegistry()
|
||||
required_spec = ToolSpec(
|
||||
name="local/coverage-check",
|
||||
description="Coverage validation",
|
||||
handler=_validation_pass,
|
||||
)
|
||||
info_spec = ToolSpec(
|
||||
name="local/lint-check",
|
||||
description="Lint validation",
|
||||
handler=_validation_fail,
|
||||
)
|
||||
registry.register(required_spec)
|
||||
registry.register(info_spec)
|
||||
tool_runner_inst = ToolRunner(registry)
|
||||
|
||||
# Execute required validation (pass)
|
||||
req_result = tool_runner_inst.execute("local/coverage-check", {"coverage": 95})
|
||||
assert req_result.success, f"Required validation failed: {req_result.error}"
|
||||
assert req_result.output.get("passed") is True
|
||||
|
||||
# Execute informational validation (fail -- should not block)
|
||||
info_result = tool_runner_inst.execute("local/lint-check", {"code": "x"})
|
||||
assert info_result.success, (
|
||||
f"Informational validation execution failed: {info_result.error}"
|
||||
)
|
||||
assert info_result.output.get("passed") is False
|
||||
|
||||
# Verify as_cli_dict renders mode
|
||||
cli_dict = required_val.as_cli_dict()
|
||||
assert cli_dict["mode"] == "required"
|
||||
info_cli = info_val.as_cli_dict()
|
||||
assert info_cli["mode"] == "informational"
|
||||
|
||||
print("m2-validation-runner-execute-ok")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Subcommand: changeset-multifile
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
|
||||
def changeset_multifile() -> None:
|
||||
"""Verify multi-file generation produces correct ChangeSet."""
|
||||
tmpdir = tempfile.mkdtemp(prefix="m2_changeset_")
|
||||
capture = ChangeSetCapture(
|
||||
plan_id="m2-plan-cs",
|
||||
resource_id="res-m2",
|
||||
sandbox_root=tmpdir,
|
||||
)
|
||||
wrapped = capture.wrap_tool(FILE_WRITE_SPEC)
|
||||
|
||||
# Write multiple files
|
||||
wrapped.handler(
|
||||
{
|
||||
"path": "file_a.py",
|
||||
"content": "# File A\nprint('hello')\n",
|
||||
"sandbox_root": tmpdir,
|
||||
}
|
||||
)
|
||||
wrapped.handler(
|
||||
{
|
||||
"path": "file_b.py",
|
||||
"content": "# File B\nprint('world')\n",
|
||||
"sandbox_root": tmpdir,
|
||||
}
|
||||
)
|
||||
wrapped.handler(
|
||||
{
|
||||
"path": "README.md",
|
||||
"content": "# Readme\nSample project\n",
|
||||
"sandbox_root": tmpdir,
|
||||
}
|
||||
)
|
||||
|
||||
cs = capture.get_changeset()
|
||||
assert isinstance(cs, ChangeSet)
|
||||
assert len(cs.entries) == 3, f"Expected 3 entries, got {len(cs.entries)}"
|
||||
|
||||
# Verify each entry
|
||||
paths = [e.path for e in cs.entries]
|
||||
assert "file_a.py" in paths
|
||||
assert "file_b.py" in paths
|
||||
assert "README.md" in paths
|
||||
|
||||
# All operations should be 'create'
|
||||
for entry in cs.entries:
|
||||
assert entry.operation == "create", (
|
||||
f"Expected 'create', got '{entry.operation}' for {entry.path}"
|
||||
)
|
||||
|
||||
# Verify summary is a non-empty string describing 3 changes
|
||||
summary_str = cs.summary()
|
||||
assert "3" in summary_str, f"Expected '3' in summary: {summary_str}"
|
||||
assert "create" in summary_str, f"Expected 'create' in summary: {summary_str}"
|
||||
|
||||
# Also verify via the domain-level SpecChangeSet summary (returns dict)
|
||||
spec_cs = capture.to_spec_changeset()
|
||||
spec_summary = spec_cs.summary()
|
||||
assert spec_summary["total"] == 3
|
||||
assert spec_summary["creates"] == 3
|
||||
|
||||
print("m2-changeset-multifile-ok")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Dispatcher
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
_COMMANDS: dict[str, object] = {
|
||||
"actor-yaml-create-load": actor_yaml_create_load,
|
||||
"actor-add-config-cli": actor_add_config_cli,
|
||||
"action-create": action_create,
|
||||
"plan-use-execute": plan_use_execute,
|
||||
"actor-yaml-parse-validate": actor_yaml_parse_validate,
|
||||
"actor-compile-stategraph": actor_compile_stategraph,
|
||||
"tool-router-resolve-external": tool_router_resolve_external,
|
||||
"validation-runner-execute": validation_runner_execute,
|
||||
"changeset-multifile": changeset_multifile,
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
|
||||
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>")
|
||||
sys.exit(1)
|
||||
fn = _COMMANDS[sys.argv[1]]
|
||||
fn() # type: ignore[operator]
|
||||
@@ -0,0 +1,103 @@
|
||||
*** Settings ***
|
||||
Documentation End-to-end verification of M2 success criteria:
|
||||
... actor compiler, tool routing, validation runner,
|
||||
... and multi-file ChangeSet generation.
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} ${CURDIR}/helper_m2_e2e_verification.py
|
||||
|
||||
*** Test Cases ***
|
||||
Actor YAML Create And Load Via Loader
|
||||
[Documentation] Create an actor YAML file and load it via ActorLoader.
|
||||
... Verifies that the YAML parses correctly and the actor
|
||||
... is discovered with the expected name and type.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} actor-yaml-create-load cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m2-actor-yaml-create-load-ok
|
||||
|
||||
Actor Add Via Config CLI
|
||||
[Documentation] Create an actor YAML config and load it via the
|
||||
... ``agents actor add --config`` CLI command. Verifies
|
||||
... the actor is registered with the expected attributes.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} actor-add-config-cli cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m2-actor-add-config-cli-ok
|
||||
|
||||
Action Create From Config Via CLI
|
||||
[Documentation] Create an action referencing a custom actor via
|
||||
... ``agents action create --config``. Verifies the
|
||||
... action is created with the correct name and actors.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} action-create cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m2-action-create-ok
|
||||
|
||||
Plan Use And Execute With Custom Actor
|
||||
[Documentation] Run ``plan use`` and ``plan execute`` with a custom
|
||||
... actor. Verifies the plan transitions through the
|
||||
... expected phases (strategize -> execute).
|
||||
${result}= Run Process ${PYTHON} ${HELPER} plan-use-execute cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m2-plan-use-execute-ok
|
||||
|
||||
Actor YAML Parse And Validate
|
||||
[Documentation] Verify actor YAML files parse and validate correctly.
|
||||
... Tests valid graph/LLM YAML and confirms invalid YAML
|
||||
... is rejected with appropriate errors.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} actor-yaml-parse-validate cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m2-actor-yaml-parse-validate-ok
|
||||
|
||||
Actor Compile To LangGraph StateGraph
|
||||
[Documentation] Verify actors compile to LangGraph StateGraphs.
|
||||
... Compiles a graph actor from examples and asserts
|
||||
... nodes, edges, entry point, and metadata. Also
|
||||
... confirms LLM actors are rejected by the compiler.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} actor-compile-stategraph cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m2-actor-compile-stategraph-ok
|
||||
|
||||
Tool Router Resolves External Tools
|
||||
[Documentation] Verify the tool router can resolve external tools
|
||||
... across OpenAI and Anthropic provider formats.
|
||||
... Tests format detection, normalization, and routing.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} tool-router-resolve-external cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m2-tool-router-resolve-external-ok
|
||||
|
||||
Validation Runner Executes Required And Informational
|
||||
[Documentation] Verify the validation runner executes both required
|
||||
... and informational validations correctly. Required
|
||||
... validations must pass; informational failures are
|
||||
... reported but do not block execution.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} validation-runner-execute cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m2-validation-runner-execute-ok
|
||||
|
||||
Multi File Generation Produces Correct ChangeSet
|
||||
[Documentation] Verify multi-file generation produces a correct
|
||||
... ChangeSet with the expected number of entries,
|
||||
... operations, and summary counts.
|
||||
${result}= Run Process ${PYTHON} ${HELPER} changeset-multifile cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} m2-changeset-multifile-ok
|
||||
@@ -38,12 +38,10 @@ VALID_EVENT_TYPES: frozenset[str] = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
# Timestamp format matching the spec: ``strftime('%Y-%m-%dT%H:%M:%f')``
|
||||
# which produces ``YYYY-MM-DDTHH:MM:ffffff`` (no seconds field, microseconds
|
||||
# immediately after minutes). All timestamps must use this format so that
|
||||
# lexicographic ``>=`` / ``<`` comparisons on the ``created_at`` TEXT column
|
||||
# remain correct.
|
||||
_TIMESTAMP_FMT = "%Y-%m-%dT%H:%M:%f"
|
||||
# ISO-8601-ish timestamp with microsecond precision. Produces
|
||||
# ``YYYY-MM-DDTHH:MM:SS.ffffff`` so that lexicographic ``>=`` / ``<``
|
||||
# comparisons on the ``created_at`` TEXT column remain correct.
|
||||
_TIMESTAMP_FMT = "%Y-%m-%dT%H:%M:%S.%f"
|
||||
|
||||
|
||||
# ── Domain data class ────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user