Files
cleveragents-core/robot/helper_m3_decision_validation_smoke.py
T
hurui200320 2651e15854
CI / benchmark-publish (pull_request) Has been skipped
CI / security (pull_request) Successful in 59s
CI / quality (pull_request) Successful in 37s
CI / lint (pull_request) Successful in 3m35s
CI / build (pull_request) Successful in 22s
CI / helm (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 4m25s
CI / unit_tests (pull_request) Successful in 4m19s
CI / integration_tests (pull_request) Successful in 4m1s
CI / docker (pull_request) Successful in 11s
CI / e2e_tests (pull_request) Successful in 10m42s
CI / coverage (pull_request) Successful in 9m42s
CI / status-check (pull_request) Successful in 2s
CI / lint (push) Successful in 23s
CI / typecheck (push) Successful in 50s
CI / quality (push) Successful in 53s
CI / security (push) Successful in 1m2s
CI / benchmark-regression (push) Has been skipped
CI / build (push) Successful in 40s
CI / helm (push) Successful in 43s
CI / integration_tests (push) Successful in 4m29s
CI / unit_tests (push) Successful in 5m7s
CI / docker (push) Successful in 1m20s
CI / e2e_tests (push) Successful in 11m58s
CI / coverage (push) Successful in 14m35s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Successful in 33m38s
CI / benchmark-regression (pull_request) Successful in 1h2m19s
fix(cli): replace non-existent Container.resolve() with named provider calls
The three plan.py call sites (tree, explain, correct) that originally used
container.resolve(DecisionService) were already fixed in master. This commit
completes the fix by aligning the Robot Framework test mocks to match the
corrected production code:

- robot/helper_m3_decision_validation_smoke.py: mock_container.resolve →
  mock_container.decision_service (plan correct dry-run path)
- robot/helper_m4_correction_subplan_smoke.py: container.resolve →
  container.decision_service (correction subplan path)

Both helpers previously passed silently because MagicMock auto-creates any
attribute, masking the mismatch between mock setup and actual code. Added
spec=Container to both mock containers so that accessing non-existent
attributes raises AttributeError, matching real container behavior.

Updated comment wording in features/container_resolve_crash.feature to
clarify that the bug is fixed and the scenarios serve as permanent
regression guards. The @tdd_expected_fail tag was already removed by the
TDD branch before this fix branch was created; the permanent @tdd_issue
and @tdd_issue_647 tags remain per TDD tag lifecycle rules.

Added CHANGELOG.md entry describing the fix.

ISSUES CLOSED: #647
2026-03-30 04:45:56 +00:00

341 lines
11 KiB
Python

"""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()