diff --git a/features/steps/security_audit_steps.py b/features/steps/security_audit_steps.py index e4fe4d8ca..8f5fbd346 100644 --- a/features/steps/security_audit_steps.py +++ b/features/steps/security_audit_steps.py @@ -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}"') diff --git a/robot/helper_m2_e2e_verification.py b/robot/helper_m2_e2e_verification.py new file mode 100644 index 000000000..a624a3ae2 --- /dev/null +++ b/robot/helper_m2_e2e_verification.py @@ -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 [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] diff --git a/robot/m2_e2e_verification.robot b/robot/m2_e2e_verification.robot new file mode 100644 index 000000000..9c086b9a8 --- /dev/null +++ b/robot/m2_e2e_verification.robot @@ -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 diff --git a/src/cleveragents/application/services/audit_service.py b/src/cleveragents/application/services/audit_service.py index 38e96df47..e0d021799 100644 --- a/src/cleveragents/application/services/audit_service.py +++ b/src/cleveragents/application/services/audit_service.py @@ -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 ────────────────────────────────────────────