|
| 1 | +"""Trial and force action modes over the actionability gate (Playwright-style). |
| 2 | +
|
| 3 | +``actionability.act_when_ready`` has one behaviour: wait for the target to be |
| 4 | +actionable, then act (or raise on timeout). Real flows need two more modes that |
| 5 | +Playwright codified: |
| 6 | +
|
| 7 | +* **trial** — run every actionability check but *don't* perform the action; just |
| 8 | + report whether it *would* have acted. The dry-run for "is this control ready?" |
| 9 | + without side effects. |
| 10 | +* **force** — skip the checks and act *now*, for the deliberate escape hatch when |
| 11 | + the gate is wrong (a control the heuristics misjudge as occluded / disabled). |
| 12 | +
|
| 13 | +``act_with_mode`` adds both alongside the default gated behaviour, over the same |
| 14 | +injectable seams as the gate, so each mode is testable without a screen. Reuses |
| 15 | +:func:`actionability.wait_actionable`. Imports no ``PySide6``. |
| 16 | +""" |
| 17 | +from typing import Any, Callable, Dict, List, Optional |
| 18 | + |
| 19 | +from je_auto_control.utils.actionability import wait_actionable |
| 20 | +from je_auto_control.utils.actionability.actionability import _center |
| 21 | + |
| 22 | +# The supported action modes. |
| 23 | +ACT_MODES = ("auto", "trial", "force") |
| 24 | + |
| 25 | + |
| 26 | +def _force_act(action: Callable[[List[int]], Any], |
| 27 | + bbox_provider: Callable[[], Any]) -> Dict[str, Any]: |
| 28 | + """Act at the target centre with no actionability checks.""" |
| 29 | + bbox = bbox_provider() |
| 30 | + if not bbox: |
| 31 | + return {"mode": "force", "acted": False, "actionable": True, |
| 32 | + "reason": "no target", "point": None, "result": None} |
| 33 | + point = _center(bbox) |
| 34 | + return {"mode": "force", "acted": True, "actionable": True, |
| 35 | + "reason": "forced", "point": point, "result": action(point)} |
| 36 | + |
| 37 | + |
| 38 | +def act_with_mode(action: Callable[[List[int]], Any], |
| 39 | + bbox_provider: Callable[[], Any], *, mode: str = "auto", |
| 40 | + region_sampler: Optional[Callable[[Any], Any]] = None, |
| 41 | + enabled_probe: Optional[Callable[[], Optional[bool]]] = None, |
| 42 | + hit_tester: Optional[Callable[[List[int]], bool]] = None, |
| 43 | + config: Optional[Any] = None) -> Dict[str, Any]: |
| 44 | + """Perform ``action`` on a target under a ``mode`` (auto / trial / force). |
| 45 | +
|
| 46 | + ``auto`` waits for the actionability gate and acts only if it passes; |
| 47 | + ``trial`` runs the gate but never acts (a dry run); ``force`` acts at once |
| 48 | + with no checks. Returns ``{mode, acted, actionable, reason, point, result}``. |
| 49 | + Raises ``ValueError`` for an unknown ``mode``. |
| 50 | + """ |
| 51 | + if mode not in ACT_MODES: |
| 52 | + raise ValueError(f"unknown act mode: {mode!r}") |
| 53 | + if mode == "force": |
| 54 | + return _force_act(action, bbox_provider) |
| 55 | + report = wait_actionable(bbox_provider, region_sampler=region_sampler, |
| 56 | + enabled_probe=enabled_probe, hit_tester=hit_tester, |
| 57 | + config=config) |
| 58 | + point = list(report.point) if report.point is not None else None |
| 59 | + base = {"mode": mode, "actionable": report.actionable, |
| 60 | + "reason": report.reason, "point": point} |
| 61 | + if mode == "trial" or not report.actionable: |
| 62 | + return {**base, "acted": False, "result": None} |
| 63 | + return {**base, "acted": True, "result": action(report.point)} |
0 commit comments