"""Helper script for m3_decision_validation_smoke.robot E2E tests. Each subcommand is a self-contained check that prints a sentinel on success. Uses ``--format plain`` where possible to stabilise assertion strings. """ from __future__ import annotations import os import sys import tempfile from collections.abc import Callable from pathlib import Path from unittest.mock import MagicMock, patch # Ensure local source tree is importable _SRC = str(Path(__file__).resolve().parents[1] / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) from helpers_common import reset_global_state # noqa: E402 from typer.testing import CliRunner # noqa: E402 from cleveragents.application.container import Container # noqa: E402 from cleveragents.cli.commands.invariant import app as invariant_app # noqa: E402 from cleveragents.cli.commands.plan import app as plan_app # noqa: E402 from cleveragents.cli.commands.validation import app as validation_app # noqa: E402 from cleveragents.domain.models.core.correction import ( # noqa: E402 CorrectionMode, CorrectionStatus, ) from cleveragents.domain.models.core.invariant import ( # noqa: E402 Invariant, InvariantScope, ) runner = CliRunner() _PLAN_ULID = "01M3SM0KE00000000000000001" _DECISION_ULID = "01M3DEC1S10N00000000000001" _CORRECTION_ULID = "01M3C0RRECT10N000000000001" _ATTACHMENT_ULID = "01M3ATTACH0000000000000001" _INVARIANT_ULID = "01M3JNVAR1ANT0000000000001" def _mock_invariant( *, text: str = "Never delete production data", scope: InvariantScope = InvariantScope.GLOBAL, source_name: str = "system", ) -> Invariant: """Create an Invariant for M3 smoke tests.""" return Invariant( id=_INVARIANT_ULID, text=text, scope=scope, source_name=source_name, ) # --------------------------------------------------------------------------- # Subcommands # --------------------------------------------------------------------------- def invariant_add_global() -> None: """Add a global invariant constraint.""" mock_svc = MagicMock() mock_svc.add_invariant.return_value = _mock_invariant() with patch( "cleveragents.cli.commands.invariant._get_service", return_value=mock_svc, ): result = runner.invoke( invariant_app, ["add", "--global", "Never delete production data", "--format", "plain"], ) if result.exit_code == 0: print("m3-invariant-add-global-ok") else: print(f"FAIL: exit={result.exit_code} output={result.output}") sys.exit(1) def invariant_add_project() -> None: """Add a project-scoped invariant constraint.""" mock_svc = MagicMock() mock_svc.add_invariant.return_value = _mock_invariant( text="All API changes need tests", scope=InvariantScope.PROJECT, source_name="local/m3-smoke-proj", ) with patch( "cleveragents.cli.commands.invariant._get_service", return_value=mock_svc, ): result = runner.invoke( invariant_app, [ "add", "--project", "local/m3-smoke-proj", "All API changes need tests", "--format", "plain", ], ) if result.exit_code == 0: print("m3-invariant-add-project-ok") else: print(f"FAIL: exit={result.exit_code} output={result.output}") sys.exit(1) def invariant_list() -> None: """List invariants.""" mock_svc = MagicMock() mock_svc.list_invariants.return_value = [ _mock_invariant(), _mock_invariant( text="All API changes need tests", scope=InvariantScope.PROJECT, source_name="local/m3-smoke-proj", ), ] with patch( "cleveragents.cli.commands.invariant._get_service", return_value=mock_svc, ): result = runner.invoke( invariant_app, ["list", "--format", "plain"], ) if result.exit_code == 0: print("m3-invariant-list-ok") else: print(f"FAIL: exit={result.exit_code} output={result.output}") sys.exit(1) def invariant_remove() -> None: """Remove an invariant by ID.""" mock_svc = MagicMock() mock_svc.remove_invariant.return_value = _mock_invariant() with patch( "cleveragents.cli.commands.invariant._get_service", return_value=mock_svc, ): result = runner.invoke( invariant_app, ["remove", "--yes", _INVARIANT_ULID, "--format", "plain"], ) if result.exit_code == 0: print("m3-invariant-remove-ok") else: print(f"FAIL: exit={result.exit_code} output={result.output}") sys.exit(1) def validation_add() -> None: """Register a validation from YAML config.""" config_content = ( "name: local/coverage-check\n" "description: Check code coverage meets threshold\n" "source: custom\n" "mode: required\n" "code: |\n" " def run(inputs):\n" " return {'passed': inputs['coverage'] >= 80}\n" ) with tempfile.NamedTemporaryFile( mode="w", suffix=".yaml", delete=False, ) as tmp: tmp.write(config_content) tmp_name = tmp.name try: mock_svc = MagicMock() mock_validation = MagicMock() mock_validation.name = "local/coverage-check" mock_validation.as_cli_dict.return_value = { "name": "local/coverage-check", "description": "Check code coverage meets threshold", "source": "custom", "mode": "required", } mock_svc.register_tool.return_value = mock_validation with patch( "cleveragents.cli.commands.validation._get_tool_registry_service", return_value=mock_svc, ): result = runner.invoke( validation_app, ["add", "--config", tmp_name, "--format", "plain"], ) if result.exit_code == 0 and "coverage-check" in result.output: print("m3-validation-add-ok") else: print(f"FAIL: exit={result.exit_code} output={result.output}") sys.exit(1) finally: os.unlink(tmp_name) def validation_attach() -> None: """Attach a validation to a resource.""" mock_svc = MagicMock() mock_attachment = MagicMock() mock_attachment.attachment_id = _ATTACHMENT_ULID mock_attachment.validation_name = "local/coverage-check" mock_attachment.resource_id = "git-checkout/my-repo" mock_attachment.mode = "required" mock_attachment.project_name = None mock_attachment.plan_id = None mock_attachment.created_at = "2026-01-01T00:00:00" mock_svc.attach_validation.return_value = mock_attachment with patch( "cleveragents.cli.commands.validation._get_tool_registry_service", return_value=mock_svc, ): result = runner.invoke( validation_app, [ "attach", "git-checkout/my-repo", "local/coverage-check", "--format", "plain", ], ) if result.exit_code == 0: print("m3-validation-attach-ok") else: print(f"FAIL: exit={result.exit_code} output={result.output}") sys.exit(1) def validation_detach() -> None: """Detach a validation from a resource.""" mock_svc = MagicMock() mock_svc.detach_validation.return_value = True with patch( "cleveragents.cli.commands.validation._get_tool_registry_service", return_value=mock_svc, ): result = runner.invoke( validation_app, ["detach", "--yes", _ATTACHMENT_ULID, "--format", "plain"], ) if result.exit_code == 0: print("m3-validation-detach-ok") else: print(f"FAIL: exit={result.exit_code} output={result.output}") sys.exit(1) def plan_correct_dry_run() -> None: """Run plan correct in dry-run mode.""" mock_correction_svc = MagicMock() mock_request = MagicMock() mock_request.correction_id = _CORRECTION_ULID mock_request.mode = CorrectionMode.REVERT mock_request.target_decision_id = _DECISION_ULID mock_request.guidance = "Use FastAPI instead" mock_request.status = CorrectionStatus.PENDING mock_impact = MagicMock() mock_impact.affected_decisions = [_DECISION_ULID] mock_impact.affected_files = ["src/main.py"] mock_impact.estimated_cost = "low" mock_impact.risk_level = "low" mock_correction_svc.request_correction.return_value = mock_request mock_correction_svc.analyze_impact.return_value = mock_impact # Mock DecisionService provider on DI container (issue #606 / #647 fix) mock_decision_svc = MagicMock() mock_decision_svc.list_decisions.return_value = [] mock_decision_svc.get_influence_edges.return_value = {} mock_container = MagicMock(spec=Container) mock_container.decision_service.return_value = mock_decision_svc with ( patch( "cleveragents.application.services.correction_service.CorrectionService", return_value=mock_correction_svc, ), patch( "cleveragents.application.container.get_container", return_value=mock_container, ), ): result = runner.invoke( plan_app, [ "correct", _DECISION_ULID, "--mode", "revert", "--guidance", "Use FastAPI instead", "--dry-run", "--plan", _PLAN_ULID, "--format", "plain", ], ) if result.exit_code == 0: print("m3-plan-correct-dry-run-ok") else: print(f"FAIL: exit={result.exit_code} output={result.output}") sys.exit(1) # --------------------------------------------------------------------------- # Dispatcher # --------------------------------------------------------------------------- _COMMANDS: dict[str, Callable[[], None]] = { "invariant-add-global": invariant_add_global, "invariant-add-project": invariant_add_project, "invariant-list": invariant_list, "invariant-remove": invariant_remove, "validation-add": validation_add, "validation-attach": validation_attach, "validation-detach": validation_detach, "plan-correct-dry-run": plan_correct_dry_run, } 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) reset_global_state() fn = _COMMANDS[sys.argv[1]] fn()