33a5adcfee
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 16s
CI / quality (pull_request) Successful in 17s
CI / build (pull_request) Successful in 22s
CI / typecheck (pull_request) Successful in 28s
CI / security (pull_request) Successful in 33s
CI / integration_tests (pull_request) Successful in 2m25s
CI / unit_tests (pull_request) Successful in 5m43s
CI / docker (pull_request) Successful in 58s
CI / benchmark-regression (pull_request) Successful in 17m48s
CI / coverage (pull_request) Successful in 19m18s
Adds CorrectionRequest, CorrectionResult, CorrectionMode, CorrectionPatch, CorrectionDryRunReport, CorrectionNotFoundError, and CorrectionConflictError domain models. Implements CorrectionService with BFS-based revert (marks decisions as rolled back and restores via inverse changes) and append mode (spawns a child correction plan). Includes request_correction() with dry-run support and dispatch_correction() convenience method. 33 Behave scenarios, 8 Robot smoke tests, ASV benchmark suite, and reference documentation. Ref: Day-14 Rebaseline – M4.2 Decision-correction flows [Jeff]
158 lines
5.4 KiB
Python
158 lines
5.4 KiB
Python
"""Helper script for correction flows Robot Framework smoke tests.
|
|
|
|
Exercises CorrectionService revert/append flows, impact analysis,
|
|
dry-run reports, and cancellation without requiring persistence.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
from cleveragents.application.services.correction_service import CorrectionService
|
|
from cleveragents.core.exceptions import ValidationError
|
|
from cleveragents.domain.models.core.correction import CorrectionMode
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared fixtures
|
|
# ---------------------------------------------------------------------------
|
|
_PLAN_ID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
_DECISION_D1 = "D1"
|
|
_DECISION_D2 = "D2"
|
|
_DECISION_D3 = "D3"
|
|
|
|
|
|
def _simple_tree() -> dict[str, list[str]]:
|
|
return {_DECISION_D1: [_DECISION_D2, _DECISION_D3]}
|
|
|
|
|
|
def _deep_tree() -> dict[str, list[str]]:
|
|
return {
|
|
_DECISION_D1: [_DECISION_D2, _DECISION_D3],
|
|
_DECISION_D2: ["D4", "D5"],
|
|
_DECISION_D3: ["D6"],
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Command handlers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _revert_simple() -> None:
|
|
svc = CorrectionService()
|
|
req = svc.request_correction(_PLAN_ID, _DECISION_D1, CorrectionMode.REVERT)
|
|
result = svc.execute_revert(req.correction_id, {_DECISION_D1: []})
|
|
assert result.status == "applied", f"Expected applied, got {result.status}"
|
|
assert _DECISION_D1 in result.reverted_decisions
|
|
print("revert-simple-ok")
|
|
|
|
|
|
def _revert_deep_subtree() -> None:
|
|
svc = CorrectionService()
|
|
req = svc.request_correction(_PLAN_ID, _DECISION_D1, CorrectionMode.REVERT)
|
|
result = svc.execute_revert(req.correction_id, _deep_tree())
|
|
assert len(result.reverted_decisions) == 6
|
|
print("revert-deep-subtree-ok")
|
|
|
|
|
|
def _append_correction() -> None:
|
|
svc = CorrectionService()
|
|
req = svc.request_correction(_PLAN_ID, _DECISION_D1, CorrectionMode.APPEND)
|
|
result = svc.execute_append(req.correction_id)
|
|
assert result.status == "applied"
|
|
assert result.spawned_child_plan_id is not None
|
|
print("append-correction-ok")
|
|
|
|
|
|
def _dry_run_report() -> None:
|
|
svc = CorrectionService()
|
|
req = svc.request_correction(_PLAN_ID, _DECISION_D1, CorrectionMode.REVERT)
|
|
report = svc.generate_dry_run_report(req.correction_id, _simple_tree())
|
|
assert report.mode == "revert"
|
|
assert len(report.decisions_to_invalidate) == 3
|
|
assert report.estimated_recompute_time_seconds > 0
|
|
print("dry-run-report-ok")
|
|
|
|
|
|
def _impact_risk_levels() -> None:
|
|
svc = CorrectionService()
|
|
# Low risk: 2 nodes
|
|
req1 = svc.request_correction(_PLAN_ID, _DECISION_D1, CorrectionMode.REVERT)
|
|
impact1 = svc.analyze_impact(req1.correction_id, {_DECISION_D1: [_DECISION_D2]})
|
|
assert impact1.risk_level == "low", f"Expected low, got {impact1.risk_level}"
|
|
|
|
# High risk: > 10 nodes (chain)
|
|
tree: dict[str, list[str]] = {}
|
|
current = "R"
|
|
for i in range(14):
|
|
child = f"R_c{i}"
|
|
tree[current] = [child]
|
|
current = child
|
|
req2 = svc.request_correction(_PLAN_ID, "R", CorrectionMode.REVERT)
|
|
impact2 = svc.analyze_impact(req2.correction_id, tree)
|
|
assert impact2.risk_level == "high", f"Expected high, got {impact2.risk_level}"
|
|
print("impact-risk-levels-ok")
|
|
|
|
|
|
def _cancel_correction() -> None:
|
|
svc = CorrectionService()
|
|
req = svc.request_correction(_PLAN_ID, _DECISION_D1, CorrectionMode.REVERT)
|
|
svc.cancel_correction(req.correction_id)
|
|
updated = svc.get_correction(req.correction_id)
|
|
assert updated.status == "cancelled"
|
|
print("cancel-correction-ok")
|
|
|
|
|
|
def _execute_cancelled_error() -> None:
|
|
svc = CorrectionService()
|
|
req = svc.request_correction(_PLAN_ID, _DECISION_D1, CorrectionMode.REVERT)
|
|
svc.cancel_correction(req.correction_id)
|
|
try:
|
|
svc.execute_revert(req.correction_id, {})
|
|
print("FAIL: expected ValidationError")
|
|
sys.exit(1)
|
|
except ValidationError:
|
|
print("execute-cancelled-error-ok")
|
|
|
|
|
|
def _list_corrections_by_plan() -> None:
|
|
svc = CorrectionService()
|
|
svc.request_correction(_PLAN_ID, _DECISION_D1, CorrectionMode.REVERT)
|
|
svc.request_correction(_PLAN_ID, _DECISION_D2, CorrectionMode.APPEND)
|
|
svc.request_correction("OTHER_PLAN", _DECISION_D3, CorrectionMode.REVERT)
|
|
result = svc.list_corrections(plan_id=_PLAN_ID)
|
|
assert len(result) == 2, f"Expected 2, got {len(result)}"
|
|
print("list-corrections-by-plan-ok")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
COMMANDS: dict[str, object] = {
|
|
"revert-simple": _revert_simple,
|
|
"revert-deep-subtree": _revert_deep_subtree,
|
|
"append-correction": _append_correction,
|
|
"dry-run-report": _dry_run_report,
|
|
"impact-risk-levels": _impact_risk_levels,
|
|
"cancel-correction": _cancel_correction,
|
|
"execute-cancelled-error": _execute_cancelled_error,
|
|
"list-corrections-by-plan": _list_corrections_by_plan,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
"""Dispatch command from sys.argv."""
|
|
if len(sys.argv) < 2:
|
|
raise SystemExit("Expected command argument")
|
|
command = sys.argv[1]
|
|
if command not in COMMANDS:
|
|
raise SystemExit(f"Unknown command: {command}")
|
|
func = COMMANDS[command]
|
|
if callable(func):
|
|
func()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|