Files
temp/features/steps/m3_decision_validation_smoke_steps.py
Brent E. Edwards 5e625b22e1 fix(test): convert M1-M6 E2E suites to real subprocess CLI invocations (closes #658)
Replace CliRunner + unittest.mock.patch with subprocess.run for all
21 CLI-facing test functions across the M1-M6 E2E verification helpers.

Application code fixes:
- action.py: _get_lifecycle_service() uses container.plan_lifecycle_service()
- plan.py: _get_lifecycle_service() uses container.plan_lifecycle_service()
- plan.py: three container.resolve(DecisionService) → container.decision_service()

Test infrastructure:
- New robot/helper_e2e_common.py with shared subprocess utilities
  (run_cli, setup_workspace with DB migrations, cleanup_workspace)
- M1-M4, M6 helpers refactored to use run_cli() with real SQLite DB
- M5 unchanged (0 CLI tests, all domain-level)
- TDD detection updated to recognise run_cli() as subprocess invocation
- Remove @tdd_expected_fail from TDD feature + robot tags
- Update 8 Behave step files that mocked container.resolve() to use
  container.decision_service() / container.plan_lifecycle_service()
2026-03-12 20:42:14 +00:00

757 lines
28 KiB
Python

"""Step definitions for M3 decision tree, validation, and invariant smoke tests.
All step names are prefixed with ``m3 smoke`` to avoid ``AmbiguousStep``
conflicts with existing steps.
"""
from __future__ import annotations
import json
import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner
from cleveragents.cli.commands.invariant import app as invariant_app
from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.cli.commands.validation import app as validation_app
from cleveragents.domain.models.core.correction import (
CorrectionMode,
CorrectionRequest,
CorrectionStatus,
)
from cleveragents.domain.models.core.invariant import Invariant, InvariantScope
_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "m3"
_PLAN_ULID = "01M3SM0KE00000000000000001"
_DECISION_ULID = "01M3DEC1S10N00000000000001"
_CORRECTION_ULID = "01M3C0RRECT10N000000000001"
_ATTACHMENT_ULID = "01M3ATTACH0000000000000001"
_INVARIANT_ULID = "01M3JNVAR1ANT0000000000001"
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a m3 smoke test runner")
def step_m3_smoke_runner(context: Context) -> None:
"""Set up the CLI runner for M3 smoke tests."""
context.runner = CliRunner()
@given("a m3 smoke mocked environment")
def step_m3_smoke_mock_env(context: Context) -> None:
"""Set up the mocked services for M3 smoke tests."""
context.mock_invariant_service = MagicMock()
context.mock_tool_registry_service = MagicMock()
context.mock_correction_service = MagicMock()
context.mock_lifecycle_service = MagicMock()
context.invariant_patcher = patch(
"cleveragents.cli.commands.invariant._get_service",
return_value=context.mock_invariant_service,
)
context.validation_patcher = patch(
"cleveragents.cli.commands.validation._get_tool_registry_service",
return_value=context.mock_tool_registry_service,
)
context.plan_patcher = patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=context.mock_lifecycle_service,
)
context.invariant_patcher.start()
context.add_cleanup(context.invariant_patcher.stop)
context.validation_patcher.start()
context.add_cleanup(context.validation_patcher.stop)
context.plan_patcher.start()
context.add_cleanup(context.plan_patcher.stop)
context.last_result = None
context._m3_known_invariant_id = None
context._m3_known_attachment_id = None
# ---------------------------------------------------------------------------
# Decision tree fixture loading
# ---------------------------------------------------------------------------
@when("I m3 smoke load the decision tree fixture")
def step_m3_load_decision_tree(context: Context) -> None:
"""Load the decision tree outputs fixture JSON."""
fixture_path = _FIXTURES_DIR / "decision_tree_outputs.json"
with open(fixture_path) as f:
context.decision_tree_fixtures = json.load(f)
@then("the m3 smoke decision tree fixture should have a minimal tree entry")
def step_m3_has_minimal_tree(context: Context) -> None:
"""Verify fixture has a minimal_decision_tree entry."""
fixtures = context.decision_tree_fixtures["fixtures"]
names = [f["name"] for f in fixtures]
assert "minimal_decision_tree" in names, (
f"Expected 'minimal_decision_tree' in {names}"
)
@then("the m3 smoke minimal tree should contain {count:d} decisions")
def step_m3_minimal_tree_count(context: Context, count: int) -> None:
"""Verify the minimal tree has the expected number of decisions."""
fixtures = context.decision_tree_fixtures["fixtures"]
minimal = next(f for f in fixtures if f["name"] == "minimal_decision_tree")
actual = len(minimal["decisions"])
assert actual == count, f"Expected {count} decisions, got {actual}"
@then("the m3 smoke decision tree fixture should have an invariant tree entry")
def step_m3_has_invariant_tree(context: Context) -> None:
"""Verify fixture has a tree_with_invariant entry."""
fixtures = context.decision_tree_fixtures["fixtures"]
names = [f["name"] for f in fixtures]
assert "tree_with_invariant" in names, f"Expected 'tree_with_invariant' in {names}"
@then("the m3 smoke invariant tree should contain an invariant_enforced decision")
def step_m3_invariant_tree_has_enforced(context: Context) -> None:
"""Verify the invariant tree has an invariant_enforced decision."""
fixtures = context.decision_tree_fixtures["fixtures"]
tree = next(f for f in fixtures if f["name"] == "tree_with_invariant")
types = [d["decision_type"] for d in tree["decisions"]]
assert "invariant_enforced" in types, f"Expected 'invariant_enforced' in {types}"
@then("the m3 smoke decision tree fixture should have a correction tree entry")
def step_m3_has_correction_tree(context: Context) -> None:
"""Verify fixture has a tree_with_correction entry."""
fixtures = context.decision_tree_fixtures["fixtures"]
names = [f["name"] for f in fixtures]
assert "tree_with_correction" in names, (
f"Expected 'tree_with_correction' in {names}"
)
@then("the m3 smoke correction tree should contain a superseded decision")
def step_m3_correction_tree_has_superseded(context: Context) -> None:
"""Verify the correction tree has a superseded decision."""
fixtures = context.decision_tree_fixtures["fixtures"]
tree = next(f for f in fixtures if f["name"] == "tree_with_correction")
superseded = [d for d in tree["decisions"] if d.get("is_superseded")]
assert len(superseded) > 0, "Expected at least one superseded decision"
@then("the m3 smoke correction tree should contain a correction decision")
def step_m3_correction_tree_has_correction(context: Context) -> None:
"""Verify the correction tree has a correction decision."""
fixtures = context.decision_tree_fixtures["fixtures"]
tree = next(f for f in fixtures if f["name"] == "tree_with_correction")
corrections = [d for d in tree["decisions"] if d.get("is_correction")]
assert len(corrections) > 0, "Expected at least one correction decision"
# ---------------------------------------------------------------------------
# Validation fixture loading
# ---------------------------------------------------------------------------
@when("I m3 smoke load the validation attachment fixture")
def step_m3_load_validation_fixtures(context: Context) -> None:
"""Load the validation attachments fixture JSON."""
fixture_path = _FIXTURES_DIR / "validation_attachments.json"
with open(fixture_path) as f:
context.validation_fixtures = json.load(f)
@then("the m3 smoke validation fixture should have a coverage entry")
def step_m3_has_coverage_validation(context: Context) -> None:
"""Verify fixture has a coverage_validation entry."""
fixtures = context.validation_fixtures["fixtures"]
names = [f["name"] for f in fixtures]
assert "coverage_validation" in names, f"Expected 'coverage_validation' in {names}"
@then('the m3 smoke coverage validation should have mode "{mode}"')
def step_m3_coverage_mode(context: Context, mode: str) -> None:
"""Verify coverage validation has expected mode."""
fixtures = context.validation_fixtures["fixtures"]
entry = next(f for f in fixtures if f["name"] == "coverage_validation")
actual = entry["validation"]["mode"]
assert actual == mode, f"Expected mode '{mode}', got '{actual}'"
@then("the m3 smoke validation fixture should have a lint entry")
def step_m3_has_lint_validation(context: Context) -> None:
"""Verify fixture has a lint_validation entry."""
fixtures = context.validation_fixtures["fixtures"]
names = [f["name"] for f in fixtures]
assert "lint_validation" in names, f"Expected 'lint_validation' in {names}"
@then('the m3 smoke lint validation should have mode "{mode}"')
def step_m3_lint_mode(context: Context, mode: str) -> None:
"""Verify lint validation has expected mode."""
fixtures = context.validation_fixtures["fixtures"]
entry = next(f for f in fixtures if f["name"] == "lint_validation")
actual = entry["validation"]["mode"]
assert actual == mode, f"Expected mode '{mode}', got '{actual}'"
# ---------------------------------------------------------------------------
# Invariant fixture loading
# ---------------------------------------------------------------------------
@when("I m3 smoke load the invariant config fixture")
def step_m3_load_invariant_fixtures(context: Context) -> None:
"""Load the invariant configs fixture JSON."""
fixture_path = _FIXTURES_DIR / "invariant_configs.json"
with open(fixture_path) as f:
context.invariant_fixtures = json.load(f)
@then("the m3 smoke invariant fixture should have a global entry")
def step_m3_has_global_invariant(context: Context) -> None:
"""Verify fixture has a global_invariant entry."""
fixtures = context.invariant_fixtures["fixtures"]
names = [f["name"] for f in fixtures]
assert "global_invariant" in names, f"Expected 'global_invariant' in {names}"
@then('the m3 smoke global invariant text should be "{text}"')
def step_m3_global_invariant_text(context: Context, text: str) -> None:
"""Verify global invariant has expected text."""
fixtures = context.invariant_fixtures["fixtures"]
entry = next(f for f in fixtures if f["name"] == "global_invariant")
actual = entry["invariant"]["text"]
assert actual == text, f"Expected text '{text}', got '{actual}'"
@then("the m3 smoke invariant fixture should have a merge set entry")
def step_m3_has_merge_set(context: Context) -> None:
"""Verify fixture has an invariant_set_merge entry."""
fixtures = context.invariant_fixtures["fixtures"]
names = [f["name"] for f in fixtures]
assert "invariant_set_merge" in names, f"Expected 'invariant_set_merge' in {names}"
@then("the m3 smoke merge set should define expected merged count {count:d}")
def step_m3_merge_set_count(context: Context, count: int) -> None:
"""Verify merge set defines expected merged count."""
fixtures = context.invariant_fixtures["fixtures"]
entry = next(f for f in fixtures if f["name"] == "invariant_set_merge")
actual = entry["expected_merged_count"]
assert actual == count, f"Expected merged count {count}, got {actual}"
# ---------------------------------------------------------------------------
# Invariant CLI: add
# ---------------------------------------------------------------------------
def _make_m3_invariant(
*,
text: str = "Never delete production data",
scope: InvariantScope = InvariantScope.GLOBAL,
source_name: str = "system",
) -> Invariant:
"""Create an Invariant instance for M3 smoke tests."""
return Invariant(
id=_INVARIANT_ULID,
text=text,
scope=scope,
source_name=source_name,
)
@when('I m3 smoke invoke invariant add with text "{text}" and global scope')
def step_m3_invariant_add_global(context: Context, text: str) -> None:
"""Invoke invariant add with global scope."""
context.mock_invariant_service.add_invariant.return_value = _make_m3_invariant(
text=text
)
result = context.runner.invoke(
invariant_app,
["add", "--global", text, "--format", "plain"],
)
context.last_result = result
@when('I m3 smoke invoke invariant add with text "{text}" and project "{project}"')
def step_m3_invariant_add_project(context: Context, text: str, project: str) -> None:
"""Invoke invariant add with project scope."""
context.mock_invariant_service.add_invariant.return_value = _make_m3_invariant(
text=text,
scope=InvariantScope.PROJECT,
source_name=project,
)
result = context.runner.invoke(
invariant_app,
["add", "--project", project, text, "--format", "plain"],
)
context.last_result = result
@then("the m3 smoke invariant add should succeed")
def step_m3_invariant_add_ok(context: Context) -> None:
"""Verify invariant add succeeded."""
assert context.last_result is not None
assert context.last_result.exit_code == 0, (
f"Expected exit 0, got {context.last_result.exit_code}. "
f"Output: {context.last_result.output}"
)
@then('the m3 smoke invariant output should contain "{text}"')
def step_m3_invariant_output_contains(context: Context, text: str) -> None:
"""Verify invariant output contains expected text."""
output = context.last_result.output
assert text in output, f"Expected '{text}' in: {output}"
# ---------------------------------------------------------------------------
# Invariant CLI: list
# ---------------------------------------------------------------------------
@given("m3 smoke invariants have been added")
def step_m3_invariants_added(context: Context) -> None:
"""Set up mock invariant service with pre-added invariants."""
inv1 = _make_m3_invariant()
inv2 = _make_m3_invariant(
text="All API changes need tests",
scope=InvariantScope.PROJECT,
source_name="local/m3-smoke-proj",
)
context.mock_invariant_service.list_invariants.return_value = [inv1, inv2]
context._m3_known_invariant_id = _INVARIANT_ULID
@when("I m3 smoke invoke invariant list")
def step_m3_invariant_list(context: Context) -> None:
"""Invoke invariant list."""
result = context.runner.invoke(
invariant_app,
["list", "--format", "plain"],
)
context.last_result = result
@when('I m3 smoke invoke invariant list with project filter "{project}"')
def step_m3_invariant_list_project(context: Context, project: str) -> None:
"""Invoke invariant list with project filter."""
result = context.runner.invoke(
invariant_app,
["list", "--project", project, "--format", "plain"],
)
context.last_result = result
@then("the m3 smoke invariant list should succeed")
def step_m3_invariant_list_ok(context: Context) -> None:
"""Verify invariant list succeeded."""
assert context.last_result is not None
assert context.last_result.exit_code == 0, (
f"Expected exit 0, got {context.last_result.exit_code}. "
f"Output: {context.last_result.output}"
)
@then('the m3 smoke invariant list output should contain "{text}"')
def step_m3_invariant_list_contains(context: Context, text: str) -> None:
"""Verify invariant list output contains expected text."""
output = context.last_result.output
assert text in output, f"Expected '{text}' in: {output}"
# ---------------------------------------------------------------------------
# Invariant CLI: remove
# ---------------------------------------------------------------------------
@when("I m3 smoke invoke invariant remove with a known id")
def step_m3_invariant_remove_known(context: Context) -> None:
"""Invoke invariant remove with a known invariant ID."""
inv_id = context._m3_known_invariant_id or _INVARIANT_ULID
context.mock_invariant_service.remove_invariant.return_value = _make_m3_invariant()
result = context.runner.invoke(
invariant_app,
["remove", "--yes", inv_id, "--format", "plain"],
)
context.last_result = result
@when('I m3 smoke invoke invariant remove with id "{inv_id}"')
def step_m3_invariant_remove_by_id(context: Context, inv_id: str) -> None:
"""Invoke invariant remove with specific id."""
from cleveragents.core.exceptions import ResourceNotFoundError
context.mock_invariant_service.remove_invariant.side_effect = ResourceNotFoundError(
message=f"Invariant not found: {inv_id}",
resource_type="invariant",
resource_id=inv_id,
)
result = context.runner.invoke(
invariant_app,
["remove", "--yes", inv_id, "--format", "plain"],
)
context.last_result = result
@then("the m3 smoke invariant remove should succeed")
def step_m3_invariant_remove_ok(context: Context) -> None:
"""Verify invariant remove succeeded."""
assert context.last_result is not None
assert context.last_result.exit_code == 0, (
f"Expected exit 0, got {context.last_result.exit_code}. "
f"Output: {context.last_result.output}"
)
@then("the m3 smoke invariant remove should fail")
def step_m3_invariant_remove_fail(context: Context) -> None:
"""Verify invariant remove failed."""
assert context.last_result is not None
assert context.last_result.exit_code != 0
# ---------------------------------------------------------------------------
# Validation CLI: add
# ---------------------------------------------------------------------------
@given("a m3 smoke temporary validation config file")
def step_m3_temp_validation_config(context: Context) -> None:
"""Create a temp YAML config file for validation add."""
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)
context.m3_validation_config = tmp.name
context.add_cleanup(lambda: os.unlink(context.m3_validation_config))
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",
}
context.mock_tool_registry_service.register_tool.return_value = mock_validation
@when("I m3 smoke invoke validation add with the config")
def step_m3_validation_add(context: Context) -> None:
"""Invoke validation add CLI with config file."""
result = context.runner.invoke(
validation_app,
["add", "--config", context.m3_validation_config, "--format", "plain"],
)
context.last_result = result
@then("the m3 smoke validation add should succeed")
def step_m3_validation_add_ok(context: Context) -> None:
"""Verify validation add succeeded."""
assert context.last_result is not None
assert context.last_result.exit_code == 0, (
f"Expected exit 0, got {context.last_result.exit_code}. "
f"Output: {context.last_result.output}"
)
@then('the m3 smoke validation output should contain "{text}"')
def step_m3_validation_output_contains(context: Context, text: str) -> None:
"""Verify validation output contains expected text."""
output = context.last_result.output
assert text in output, f"Expected '{text}' in: {output}"
# ---------------------------------------------------------------------------
# Validation CLI: attach
# ---------------------------------------------------------------------------
@given('a m3 smoke validation "{name}" is registered')
def step_m3_validation_registered(context: Context, name: str) -> None:
"""Set up a registered validation for attach tests."""
mock_attachment = MagicMock()
mock_attachment.attachment_id = _ATTACHMENT_ULID
mock_attachment.validation_name = name
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"
context.mock_tool_registry_service.attach_validation.return_value = mock_attachment
context._m3_known_attachment_id = _ATTACHMENT_ULID
@when('I m3 smoke invoke validation attach "{name}" to "{resource}"')
def step_m3_validation_attach(context: Context, name: str, resource: str) -> None:
"""Invoke validation attach CLI."""
result = context.runner.invoke(
validation_app,
["attach", resource, name, "--format", "plain"],
)
context.last_result = result
@then("the m3 smoke validation attach should succeed")
def step_m3_validation_attach_ok(context: Context) -> None:
"""Verify validation attach succeeded."""
assert context.last_result is not None
assert context.last_result.exit_code == 0, (
f"Expected exit 0, got {context.last_result.exit_code}. "
f"Output: {context.last_result.output}"
)
# ---------------------------------------------------------------------------
# Validation CLI: detach
# ---------------------------------------------------------------------------
@given("a m3 smoke validation attachment exists")
def step_m3_attachment_exists(context: Context) -> None:
"""Set up an existing validation attachment for detach tests."""
context.mock_tool_registry_service.detach_validation.return_value = True
context._m3_known_attachment_id = _ATTACHMENT_ULID
@when("I m3 smoke invoke validation detach with the attachment id")
def step_m3_validation_detach(context: Context) -> None:
"""Invoke validation detach CLI."""
att_id = context._m3_known_attachment_id or _ATTACHMENT_ULID
result = context.runner.invoke(
validation_app,
["detach", "--yes", att_id, "--format", "plain"],
)
context.last_result = result
@then("the m3 smoke validation detach should succeed")
def step_m3_validation_detach_ok(context: Context) -> None:
"""Verify validation detach succeeded."""
assert context.last_result is not None
assert context.last_result.exit_code == 0, (
f"Expected exit 0, got {context.last_result.exit_code}. "
f"Output: {context.last_result.output}"
)
# ---------------------------------------------------------------------------
# Plan correct CLI
# ---------------------------------------------------------------------------
@given("a m3 smoke plan with decisions exists")
def step_m3_plan_with_decisions(context: Context) -> None:
"""Set up mock correction service for plan correct tests."""
mock_request = MagicMock(spec=CorrectionRequest)
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_result = MagicMock()
mock_result.correction_id = _CORRECTION_ULID
mock_result.status = CorrectionStatus.APPLIED
mock_result.new_decisions = ["01M3NEWDEC000000000000001"]
mock_result.reverted_decisions = [_DECISION_ULID]
context.mock_correction_service.request_correction.return_value = mock_request
context.mock_correction_service.analyze_impact.return_value = mock_impact
context.mock_correction_service.execute_correction.return_value = mock_result
context.correction_patcher = patch(
"cleveragents.application.services.correction_service.CorrectionService",
return_value=context.mock_correction_service,
)
context.correction_patcher.start()
context.add_cleanup(context.correction_patcher.stop)
# Mock DecisionService resolved via DI container (issue #606 fix)
mock_decision_svc = MagicMock()
mock_decision_svc.list_decisions.return_value = []
mock_decision_svc.get_influence_edges.return_value = {}
mock_container = MagicMock()
mock_container.decision_service.return_value = mock_decision_svc
context.m3_container_patcher = patch(
"cleveragents.application.container.get_container",
return_value=mock_container,
)
context.m3_container_patcher.start()
context.add_cleanup(context.m3_container_patcher.stop)
@when("I m3 smoke invoke plan correct in dry-run mode")
def step_m3_plan_correct_dry_run(context: Context) -> None:
"""Invoke plan correct with --dry-run."""
result = context.runner.invoke(
plan_app,
[
"correct",
_DECISION_ULID,
"--mode",
"revert",
"--guidance",
"Use FastAPI instead",
"--dry-run",
"--plan",
_PLAN_ULID,
"--format",
"plain",
],
)
context.last_result = result
@when("I m3 smoke invoke plan correct in revert mode")
def step_m3_plan_correct_revert(context: Context) -> None:
"""Invoke plan correct with revert mode."""
result = context.runner.invoke(
plan_app,
[
"correct",
_DECISION_ULID,
"--mode",
"revert",
"--guidance",
"Use FastAPI instead",
"--yes",
"--plan",
_PLAN_ULID,
"--format",
"plain",
],
)
context.last_result = result
@when("I m3 smoke invoke plan correct in append mode")
def step_m3_plan_correct_append(context: Context) -> None:
"""Invoke plan correct with append mode."""
context.mock_correction_service.request_correction.return_value.mode = (
CorrectionMode.APPEND
)
result = context.runner.invoke(
plan_app,
[
"correct",
_DECISION_ULID,
"--mode",
"append",
"--guidance",
"Add caching layer",
"--yes",
"--plan",
_PLAN_ULID,
"--format",
"plain",
],
)
context.last_result = result
@then("the m3 smoke plan correct dry run should succeed")
def step_m3_correct_dry_run_ok(context: Context) -> None:
"""Verify plan correct dry run succeeded."""
assert context.last_result is not None
assert context.last_result.exit_code == 0, (
f"Expected exit 0, got {context.last_result.exit_code}. "
f"Output: {context.last_result.output}"
)
@then('the m3 smoke correction output should contain "{text}"')
def step_m3_correction_output_contains(context: Context, text: str) -> None:
"""Verify correction output contains expected text."""
output = context.last_result.output
assert text in output, f"Expected '{text}' in: {output}"
@then("the m3 smoke plan correct should succeed")
def step_m3_correct_ok(context: Context) -> None:
"""Verify plan correct succeeded."""
assert context.last_result is not None
assert context.last_result.exit_code == 0, (
f"Expected exit 0, got {context.last_result.exit_code}. "
f"Output: {context.last_result.output}"
)
# ---------------------------------------------------------------------------
# Negative cases
# ---------------------------------------------------------------------------
@when("I m3 smoke invoke plan correct with empty guidance")
def step_m3_correct_empty_guidance(context: Context) -> None:
"""Invoke plan correct with empty guidance string."""
result = context.runner.invoke(
plan_app,
[
"correct",
_DECISION_ULID,
"--mode",
"revert",
"--guidance",
"",
"--yes",
"--plan",
_PLAN_ULID,
],
)
context.last_result = result
@when('I m3 smoke invoke plan correct with invalid mode "{mode}"')
def step_m3_correct_invalid_mode(context: Context, mode: str) -> None:
"""Invoke plan correct with an invalid mode."""
result = context.runner.invoke(
plan_app,
[
"correct",
_DECISION_ULID,
"--mode",
mode,
"--guidance",
"Some guidance",
"--yes",
"--plan",
_PLAN_ULID,
],
)
context.last_result = result
@then("the m3 smoke plan correct should fail")
def step_m3_correct_fail(context: Context) -> None:
"""Verify plan correct failed."""
assert context.last_result is not None
assert context.last_result.exit_code != 0