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

This commit is contained in:
2026-02-25 01:48:49 +00:00
parent 7ec06d5db8
commit 479c275c9d
4 changed files with 771 additions and 7 deletions
+5 -1
View File
@@ -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}"')
+659
View File
@@ -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]
+103
View File
@@ -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 ────────────────────────────────────────────