forked from HAL9000/cleveragents-core
5e625b22e1
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()
757 lines
28 KiB
Python
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
|