diff --git a/README.md b/README.md index ee4add40..e6ab0c1f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ ## Table of Contents +- [What's new (2026-06-19) — Checkpoint & Resume](#whats-new-2026-06-19--checkpoint--resume) - [What's new (2026-06-19) — i18n / l10n Testing](#whats-new-2026-06-19--i18n--l10n-testing) - [What's new (2026-06-19) — Data Quality](#whats-new-2026-06-19--data-quality) - [What's new (2026-06-19) — SBOM & Suite Sharding](#whats-new-2026-06-19--sbom--suite-sharding) @@ -73,6 +74,13 @@ --- +## What's new (2026-06-19) — Checkpoint & Resume + +Durable execution for long flows + a `py.typed` marker, full stack. Full reference: [`docs/source/Eng/doc/new_features/v21_features_doc.rst`](docs/source/Eng/doc/new_features/v21_features_doc.rst). + +- **Flow checkpoint & resume** — `run_resumable(actions, run_id=..., store=...)` / `CheckpointStore` (`AC_run_resumable` / `AC_checkpoint_status` / `AC_checkpoint_clear`, `ac_*`): persist step-index + variables after each step; on re-run with the same `run_id`, fast-forward past completed steps and rehydrate variables — a flow that crashes at step 400 resumes at 400, not 0. Pluggable (SQLite default), cleared on completion. +- **`py.typed` marker** — ships the PEP 561 marker so Mypy/Pyright/Pylance honor AutoControl's inline type hints in downstream code (the repo's typed API was previously invisible to type checkers). + ## What's new (2026-06-19) — i18n / l10n Testing Three pure-stdlib internationalization/localization testing helpers that compound, full stack. Full reference: [`docs/source/Eng/doc/new_features/v20_features_doc.rst`](docs/source/Eng/doc/new_features/v20_features_doc.rst). diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 3a58c6a9..95630b48 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -12,6 +12,7 @@ ## 目录 +- [本次更新 (2026-06-19) — 检查点与续跑](#本次更新-2026-06-19--检查点与续跑) - [本次更新 (2026-06-19) — i18n / l10n 测试](#本次更新-2026-06-19--i18n--l10n-测试) - [本次更新 (2026-06-19) — 数据质量](#本次更新-2026-06-19--数据质量) - [本次更新 (2026-06-19) — SBOM 与测试分片](#本次更新-2026-06-19--sbom-与测试分片) @@ -72,6 +73,13 @@ --- +## 本次更新 (2026-06-19) — 检查点与续跑 + +长流程的耐久执行 + `py.typed` 标记,走完整五层。完整参考:[`docs/source/Zh/doc/new_features/v21_features_doc.rst`](../docs/source/Zh/doc/new_features/v21_features_doc.rst)。 + +- **流程检查点与续跑** — `run_resumable(actions, run_id=..., store=...)` / `CheckpointStore`(`AC_run_resumable` / `AC_checkpoint_status` / `AC_checkpoint_clear`、`ac_*`):每步后持久化 step-index + 变量;以相同 `run_id` 再执行时快进略过已完成步骤并还原变量——在第 400 步崩溃的流程会从 400 续跑,而非从 0。可抽换(默认 SQLite),完成后清除。 +- **`py.typed` 标记** — 附带 PEP 561 标记,让 Mypy/Pyright/Pylance 在下游代码采用 AutoControl 的内嵌类型注解(此前类型化 API 对类型检查器是隐形的)。 + ## 本次更新 (2026-06-19) — i18n / l10n 测试 三项可互相搭配的纯标准库国际化/本地化测试辅助工具,走完整五层。完整参考:[`docs/source/Zh/doc/new_features/v20_features_doc.rst`](../docs/source/Zh/doc/new_features/v20_features_doc.rst)。 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 98666165..d8993bbe 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -12,6 +12,7 @@ ## 目錄 +- [本次更新 (2026-06-19) — 檢查點與續跑](#本次更新-2026-06-19--檢查點與續跑) - [本次更新 (2026-06-19) — i18n / l10n 測試](#本次更新-2026-06-19--i18n--l10n-測試) - [本次更新 (2026-06-19) — 資料品質](#本次更新-2026-06-19--資料品質) - [本次更新 (2026-06-19) — SBOM 與測試分片](#本次更新-2026-06-19--sbom-與測試分片) @@ -72,6 +73,13 @@ --- +## 本次更新 (2026-06-19) — 檢查點與續跑 + +長流程的耐久執行 + `py.typed` 標記,走完整五層。完整參考:[`docs/source/Zh/doc/new_features/v21_features_doc.rst`](../docs/source/Zh/doc/new_features/v21_features_doc.rst)。 + +- **流程檢查點與續跑** — `run_resumable(actions, run_id=..., store=...)` / `CheckpointStore`(`AC_run_resumable` / `AC_checkpoint_status` / `AC_checkpoint_clear`、`ac_*`):每步後持久化 step-index + 變數;以相同 `run_id` 再執行時快轉略過已完成步驟並還原變數——在第 400 步當掉的流程會從 400 續跑,而非從 0。可抽換(預設 SQLite),完成後清除。 +- **`py.typed` 標記** — 附帶 PEP 561 標記,讓 Mypy/Pyright/Pylance 在下游程式碼採用 AutoControl 的內嵌型別註記(此前型別化 API 對型別檢查器是隱形的)。 + ## 本次更新 (2026-06-19) — i18n / l10n 測試 三項可互相搭配的純標準庫國際化/在地化測試輔助工具,走完整五層。完整參考:[`docs/source/Zh/doc/new_features/v20_features_doc.rst`](../docs/source/Zh/doc/new_features/v20_features_doc.rst)。 diff --git a/docs/source/Eng/doc/new_features/v21_features_doc.rst b/docs/source/Eng/doc/new_features/v21_features_doc.rst new file mode 100644 index 00000000..eaa16bef --- /dev/null +++ b/docs/source/Eng/doc/new_features/v21_features_doc.rst @@ -0,0 +1,51 @@ +================================================== +New Features (2026-06-19) — Checkpoint & Resume +================================================== + +Durable execution for long action lists, plus a ``py.typed`` marker so the +package's inline type hints are honored by type checkers. Pure standard +library; wired through the full stack (facade, ``AC_*`` executor commands, +MCP tools, Script Builder). + +.. contents:: + :local: + :depth: 2 + + +Flow checkpoint & resume +======================= + +A multi-hour unattended flow that dies at step 400 should not restart from +zero. :func:`run_resumable` persists ``{run_id, step_index, variables}`` +after each executed step to a pluggable store; on a later run with the same +``run_id`` it fast-forwards past completed steps and rehydrates the script +variables:: + + from je_auto_control import run_resumable, CheckpointStore + + store = CheckpointStore("runs.db") + result = run_resumable(actions, run_id="nightly-invoices", store=store) + result["resumed_from"] # 0 on a fresh run, N when resuming after a crash + +On normal completion the checkpoint is cleared. The store is injectable, so +resume is unit-tested deterministically without a real crash: +``CheckpointStore.save`` / ``load`` / ``clear``. + +Executor / MCP commands: + +* ``AC_run_resumable`` — run ``actions`` with checkpoint/resume keyed by + ``run_id`` (persisted to ``db``). +* ``AC_checkpoint_status`` — the saved checkpoint for a run (or null). +* ``AC_checkpoint_clear`` — delete a run's checkpoint. + +(and the matching ``ac_run_resumable`` / ``ac_checkpoint_status`` / +``ac_checkpoint_clear`` MCP tools). + + +``py.typed`` marker +================== + +The package now ships a PEP 561 ``py.typed`` marker, so Mypy / Pyright / +Pylance honor AutoControl's inline type annotations in downstream code — +completing the value of the typed public API. No code change for callers; +just better editor autocompletion and type checking out of the box. diff --git a/docs/source/Eng/eng_index.rst b/docs/source/Eng/eng_index.rst index a6e6e985..48989a48 100644 --- a/docs/source/Eng/eng_index.rst +++ b/docs/source/Eng/eng_index.rst @@ -43,6 +43,7 @@ Comprehensive guides for all AutoControl features. doc/new_features/v18_features_doc doc/new_features/v19_features_doc doc/new_features/v20_features_doc + doc/new_features/v21_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/docs/source/Zh/doc/new_features/v21_features_doc.rst b/docs/source/Zh/doc/new_features/v21_features_doc.rst new file mode 100644 index 00000000..533aca52 --- /dev/null +++ b/docs/source/Zh/doc/new_features/v21_features_doc.rst @@ -0,0 +1,47 @@ +========================================== +新功能 (2026-06-19) — 檢查點與續跑 +========================================== + +長動作清單的耐久執行(durable execution),並新增 ``py.typed`` 標記讓 +型別檢查器採用套件的內嵌型別註記。純標準庫;走完整五層(facade、 +``AC_*`` 執行器指令、MCP 工具、Script Builder)。 + +.. contents:: + :local: + :depth: 2 + + +流程檢查點與續跑 +================ + +跑了數小時、卻在第 400 步當掉的無人值守流程,不該從頭重來。 +:func:`run_resumable` 在每執行完一步後,把 ``{run_id, step_index, +variables}`` 存入可抽換的儲存後端;之後以相同 ``run_id`` 再執行時,會 +快轉略過已完成的步驟並還原腳本變數:: + + from je_auto_control import run_resumable, CheckpointStore + + store = CheckpointStore("runs.db") + result = run_resumable(actions, run_id="nightly-invoices", store=store) + result["resumed_from"] # 全新執行為 0;當機後續跑則為 N + +正常完成後檢查點會被清除。儲存後端可注入,因此續跑邏輯可在不真的當機的 +情況下做決定性單元測試:``CheckpointStore.save`` / ``load`` / ``clear``。 + +執行器 / MCP 指令: + +* ``AC_run_resumable`` — 以 ``run_id`` 為鍵,帶檢查點/續跑執行 ``actions`` + (存到 ``db``)。 +* ``AC_checkpoint_status`` — 某次執行已存的檢查點(或 null)。 +* ``AC_checkpoint_clear`` — 刪除某次執行的檢查點。 + +(以及對應的 ``ac_run_resumable`` / ``ac_checkpoint_status`` / +``ac_checkpoint_clear`` MCP 工具)。 + + +``py.typed`` 標記 +================= + +套件現在附帶 PEP 561 的 ``py.typed`` 標記,讓 Mypy / Pyright / Pylance +在下游程式碼中採用 AutoControl 的內嵌型別註記——讓型別化的公開 API 真正 +發揮價值。呼叫端無需改動;開箱即享更好的編輯器自動完成與型別檢查。 diff --git a/docs/source/Zh/zh_index.rst b/docs/source/Zh/zh_index.rst index bdc9d1ca..cf3cb61b 100644 --- a/docs/source/Zh/zh_index.rst +++ b/docs/source/Zh/zh_index.rst @@ -43,6 +43,7 @@ AutoControl 所有功能的完整使用指南。 doc/new_features/v18_features_doc doc/new_features/v19_features_doc doc/new_features/v20_features_doc + doc/new_features/v21_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/je_auto_control/__init__.py b/je_auto_control/__init__.py index 36b2ce4c..11abc90b 100644 --- a/je_auto_control/__init__.py +++ b/je_auto_control/__init__.py @@ -161,6 +161,10 @@ from je_auto_control.utils.i18n_test import ( check_catalog, check_overflow, pseudo_localize, pseudo_localize_catalog, ) +# Flow checkpoint & resume (durable execution for long action lists) +from je_auto_control.utils.checkpoint import ( + Checkpoint, CheckpointStore, run_resumable, +) # Background popup/interrupt watchdog (unattended automation) from je_auto_control.utils.watchdog import ( PopupWatchdog, WatchdogRule, default_popup_watchdog, @@ -583,6 +587,7 @@ def start_autocontrol_gui(*args, **kwargs): "extract_fields", "mask_rows", "validate_rows", "check_catalog", "check_overflow", "pseudo_localize", "pseudo_localize_catalog", + "Checkpoint", "CheckpointStore", "run_resumable", # MCP server "AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt", "MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool", diff --git a/je_auto_control/gui/script_builder/command_schema.py b/je_auto_control/gui/script_builder/command_schema.py index a1bcc0b7..0c51ad84 100644 --- a/je_auto_control/gui/script_builder/command_schema.py +++ b/je_auto_control/gui/script_builder/command_schema.py @@ -660,6 +660,28 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None: _add_memory_specs(specs) _add_data_quality_specs(specs) _add_i18n_specs(specs) + _add_checkpoint_specs(specs) + + +def _add_checkpoint_specs(specs: List[CommandSpec]) -> None: + run_id = FieldSpec("run_id", FieldType.STRING) + db = FieldSpec("db", FieldType.FILE_PATH) + specs.append(CommandSpec( + "AC_run_resumable", "Flow", "Run Resumable (checkpoint)", + fields=(run_id, db), + description="Run 'actions' (JSON view) with checkpoint/resume keyed " + "by run_id; resumes past completed steps after a crash.", + )) + specs.append(CommandSpec( + "AC_checkpoint_status", "Flow", "Checkpoint: Status", + fields=(run_id, db), + description="Return the saved checkpoint for a run (step + variables).", + )) + specs.append(CommandSpec( + "AC_checkpoint_clear", "Flow", "Checkpoint: Clear", + fields=(run_id, db), + description="Delete a run's checkpoint.", + )) specs.append(CommandSpec( "AC_wcag_audit", "Accessibility", "WCAG 2.2 Conformance Audit", fields=( diff --git a/je_auto_control/py.typed b/je_auto_control/py.typed new file mode 100644 index 00000000..afc0ba63 --- /dev/null +++ b/je_auto_control/py.typed @@ -0,0 +1 @@ +# PEP 561 marker: this package ships inline type information. diff --git a/je_auto_control/utils/checkpoint/__init__.py b/je_auto_control/utils/checkpoint/__init__.py new file mode 100644 index 00000000..72195328 --- /dev/null +++ b/je_auto_control/utils/checkpoint/__init__.py @@ -0,0 +1,6 @@ +"""Flow checkpoint & resume — durable execution for long action lists.""" +from je_auto_control.utils.checkpoint.checkpoint import ( + Checkpoint, CheckpointStore, run_resumable, +) + +__all__ = ["Checkpoint", "CheckpointStore", "run_resumable"] diff --git a/je_auto_control/utils/checkpoint/checkpoint.py b/je_auto_control/utils/checkpoint/checkpoint.py new file mode 100644 index 00000000..9bad332e --- /dev/null +++ b/je_auto_control/utils/checkpoint/checkpoint.py @@ -0,0 +1,109 @@ +"""Flow checkpoint & resume — durable execution for long action lists. + +A multi-hour unattended flow that dies at step 400 should not restart from +zero. After each executed step this persists ``{run_id, step_index, +variables}`` to a pluggable store (SQLite by default); on a later run with +the same ``run_id`` it fast-forwards past completed steps and rehydrates the +script variables, so execution resumes where it stopped. + +Pure standard library (``sqlite3`` / ``json``); imports no ``PySide6``. The +store is injectable, so resume logic is unit-tested deterministically +without a real crash. +""" +import json +import sqlite3 +import time +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + + +@dataclass +class Checkpoint: + """A persisted run position: next step to execute + variable snapshot.""" + run_id: str + step_index: int + variables: Dict[str, Any] = field(default_factory=dict) + updated: float = 0.0 + + +class CheckpointStore: + """SQLite-backed store of one checkpoint per ``run_id``.""" + + def __init__(self, db_path: str) -> None: + self._db_path = db_path + self._ensure_schema() + + def _connect(self) -> sqlite3.Connection: + conn = sqlite3.connect(self._db_path, timeout=30.0, + isolation_level=None) + conn.row_factory = sqlite3.Row + return conn + + def _ensure_schema(self) -> None: + with self._connect() as conn: + conn.execute( + "CREATE TABLE IF NOT EXISTS checkpoints (" + "run_id TEXT PRIMARY KEY, step_index INTEGER NOT NULL, " + "variables TEXT NOT NULL, updated REAL NOT NULL)") + + def save(self, run_id: str, step_index: int, + variables: Dict[str, Any]) -> None: + """Persist (or overwrite) the checkpoint for ``run_id``.""" + with self._connect() as conn: + conn.execute( + "INSERT INTO checkpoints (run_id, step_index, variables, " + "updated) VALUES (?, ?, ?, ?) ON CONFLICT(run_id) DO UPDATE " + "SET step_index=excluded.step_index, " + "variables=excluded.variables, updated=excluded.updated", + (str(run_id), int(step_index), json.dumps(variables), + time.time())) + + def load(self, run_id: str) -> Optional[Checkpoint]: + """Return the checkpoint for ``run_id`` or ``None``.""" + with self._connect() as conn: + row = conn.execute( + "SELECT * FROM checkpoints WHERE run_id=?", + (str(run_id),)).fetchone() + if row is None: + return None + return Checkpoint(run_id=row["run_id"], step_index=row["step_index"], + variables=json.loads(row["variables"]), + updated=row["updated"]) + + def clear(self, run_id: str) -> bool: + """Delete the checkpoint for ``run_id``; return whether it existed.""" + with self._connect() as conn: + cur = conn.execute("DELETE FROM checkpoints WHERE run_id=?", + (str(run_id),)) + return cur.rowcount > 0 + + +def _new_executor() -> Any: + from je_auto_control.utils.executor.action_executor import Executor + return Executor() + + +def run_resumable(actions: List[Any], *, run_id: str, store: CheckpointStore, + executor: Any = None, + variables: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Run ``actions``, checkpointing after each step; resume if interrupted. + + On entry, any saved checkpoint for ``run_id`` fast-forwards past + completed steps and rehydrates variables. On normal completion the + checkpoint is cleared. Returns ``{completed, total, resumed_from, + record}``. + """ + runner = executor or _new_executor() + existing = store.load(run_id) + start = existing.step_index if existing else 0 + if existing: + runner.variables.update_many(existing.variables) + elif variables: + runner.variables.update_many(variables) + record: Dict[str, Any] = {} + for index in range(start, len(actions)): + record.update(runner.execute_action([actions[index]])) + store.save(run_id, index + 1, runner.variables.as_dict()) + store.clear(run_id) + return {"completed": True, "total": len(actions), + "resumed_from": start, "record": record} diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index a3129ef0..d8fb2c98 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -2717,6 +2717,32 @@ def _check_catalog(base: Dict[str, Any], return check_catalog(base, target) +def _run_resumable(actions: List[Any], run_id: str, db: str, + variables: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Adapter: run actions with checkpoint/resume keyed by run_id.""" + from je_auto_control.utils.checkpoint import CheckpointStore, run_resumable + return run_resumable(actions, run_id=run_id, + store=CheckpointStore(db), variables=variables) + + +def _checkpoint_status(run_id: str, db: str) -> Dict[str, Any]: + """Adapter: return the saved checkpoint for a run (or null).""" + from je_auto_control.utils.checkpoint import CheckpointStore + checkpoint = CheckpointStore(db).load(run_id) + if checkpoint is None: + return {"checkpoint": None} + return {"checkpoint": {"run_id": checkpoint.run_id, + "step_index": checkpoint.step_index, + "variables": checkpoint.variables}} + + +def _checkpoint_clear(run_id: str, db: str) -> Dict[str, Any]: + """Adapter: delete a run's checkpoint.""" + from je_auto_control.utils.checkpoint import CheckpointStore + return {"cleared": CheckpointStore(db).clear(run_id)} + + class Executor: """ Executor @@ -2924,6 +2950,9 @@ def __init__(self): "AC_pseudo_localize": _pseudo_localize, "AC_check_overflow": _check_overflow, "AC_check_catalog": _check_catalog, + "AC_run_resumable": _run_resumable, + "AC_checkpoint_status": _checkpoint_status, + "AC_checkpoint_clear": _checkpoint_clear, "AC_a11y_record_start": _a11y_record_start, "AC_a11y_record_stop": _a11y_record_stop, "AC_a11y_record_events": _a11y_record_events, diff --git a/je_auto_control/utils/mcp_server/tools/_factories.py b/je_auto_control/utils/mcp_server/tools/_factories.py index 0b3905fb..2c88ff6e 100644 --- a/je_auto_control/utils/mcp_server/tools/_factories.py +++ b/je_auto_control/utils/mcp_server/tools/_factories.py @@ -2269,6 +2269,40 @@ def i18n_tools() -> List[MCPTool]: ] +def checkpoint_tools() -> List[MCPTool]: + _R = {"run_id": {"type": "string"}, "db": {"type": "string"}} + return [ + MCPTool( + name="ac_run_resumable", + description=("Run an action list with checkpoint/resume keyed by " + "'run_id': persists step-index+variables after each " + "step to 'db' and, on re-run, resumes past completed " + "steps. Durable execution for long flows."), + input_schema=schema({ + "actions": {"type": "array"}, + "variables": {"type": "object"}, **_R}, + required=["actions", "run_id", "db"]), + handler=h.run_resumable, + annotations=SIDE_EFFECT_ONLY, + ), + MCPTool( + name="ac_checkpoint_status", + description=("Return the saved checkpoint for a run_id " + "({step_index, variables}) or null."), + input_schema=schema(dict(_R), required=["run_id", "db"]), + handler=h.checkpoint_status, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_checkpoint_clear", + description="Delete a run's checkpoint; returns {cleared}.", + input_schema=schema(dict(_R), required=["run_id", "db"]), + handler=h.checkpoint_clear, + annotations=SIDE_EFFECT_ONLY, + ), + ] + + def unattended_tools() -> List[MCPTool]: return [ MCPTool( @@ -3323,6 +3357,7 @@ def media_assert_tools() -> List[MCPTool]: skill_library_tools, guardrail_tools, a2a_tools, office_tools, agent_memory_tools, determinism_tools, observer_tools, sbom_tools, sharding_tools, data_quality_tools, i18n_tools, + checkpoint_tools, screen_record_tools, process_and_shell_tools, remote_desktop_tools, gamepad_tools, usb_passthrough_tools, assertion_tools, data_source_tools, diff --git a/je_auto_control/utils/mcp_server/tools/_handlers.py b/je_auto_control/utils/mcp_server/tools/_handlers.py index e4f298ad..ab3e4328 100644 --- a/je_auto_control/utils/mcp_server/tools/_handlers.py +++ b/je_auto_control/utils/mcp_server/tools/_handlers.py @@ -1112,6 +1112,27 @@ def check_catalog(base, target): return _cc(base, target) +def run_resumable(actions, run_id, db, variables=None): + from je_auto_control.utils.checkpoint import ( + CheckpointStore, run_resumable as _run) + return _run(actions, run_id=run_id, store=CheckpointStore(db), + variables=variables) + + +def checkpoint_status(run_id, db): + from je_auto_control.utils.checkpoint import CheckpointStore + cp = CheckpointStore(db).load(run_id) + if cp is None: + return {"checkpoint": None} + return {"checkpoint": {"run_id": cp.run_id, "step_index": cp.step_index, + "variables": cp.variables}} + + +def checkpoint_clear(run_id, db): + from je_auto_control.utils.checkpoint import CheckpointStore + return {"cleared": CheckpointStore(db).clear(run_id)} + + def vlm_locate(description: str, screen_region: Optional[List[int]] = None, model: Optional[str] = None) -> Optional[List[int]]: diff --git a/pyproject.toml b/pyproject.toml index 2db13ee3..e35619e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ content-type = "text/markdown" find = { namespaces = false } [tool.setuptools.package-data] +"je_auto_control" = ["py.typed"] "je_auto_control.utils.remote_desktop" = [ "web_viewer/*.html", "web_viewer/*.js", diff --git a/test/unit_test/headless/test_checkpoint_batch.py b/test/unit_test/headless/test_checkpoint_batch.py new file mode 100644 index 00000000..b48ef0b2 --- /dev/null +++ b/test/unit_test/headless/test_checkpoint_batch.py @@ -0,0 +1,94 @@ +"""Headless tests for flow checkpoint & resume (durable execution) and the +py.typed marker. Pure stdlib; no Qt imports.""" +import je_auto_control as ac +from je_auto_control.utils.checkpoint import ( + CheckpointStore, run_resumable) + + +def _program(): + return [["AC_set_var", {"name": "a", "value": 1}], + ["AC_set_var", {"name": "b", "value": 2}], + ["AC_inc_var", {"name": "a", "by": 10}]] + + +def test_store_save_load_clear(tmp_path): + store = CheckpointStore(str(tmp_path / "c.db")) + assert store.load("r1") is None + store.save("r1", 2, {"x": 5}) + cp = store.load("r1") + assert cp.step_index == 2 and cp.variables == {"x": 5} + store.save("r1", 3, {"x": 6}) # upsert + assert store.load("r1").step_index == 3 + assert store.clear("r1") is True + assert store.clear("r1") is False + assert store.load("r1") is None + + +def test_run_resumable_full_run(tmp_path): + store = CheckpointStore(str(tmp_path / "c.db")) + result = run_resumable(_program(), run_id="run", store=store) + assert result["completed"] is True + assert result["resumed_from"] == 0 and result["total"] == 3 + assert store.load("run") is None # cleared on completion + + +def test_run_resumable_resumes_from_checkpoint(tmp_path): + store = CheckpointStore(str(tmp_path / "c.db")) + # Simulate a crash after step 0: step 1 is next, var 'a' already set. + store.save("run", 1, {"a": 1}) + result = run_resumable(_program(), run_id="run", store=store) + assert result["resumed_from"] == 1 + # only steps 1 and 2 ran; the record reflects two executed actions + assert sum("execute:" in k for k in result["record"]) == 2 + + +def test_run_resumable_rehydrates_variables(tmp_path): + store = CheckpointStore(str(tmp_path / "c.db")) + store.save("run", 2, {"a": 100, "b": 2}) # resume at the inc step + actions = _program() + executor = ac.executor.__class__() # fresh isolated executor + run_resumable(actions, run_id="run", store=store, executor=executor) + assert executor.variables.get_value("a") == 110 # 100 + 10 + + +# --- py.typed marker ------------------------------------------------------ + +def test_py_typed_marker_present(): + import os + import je_auto_control + pkg_dir = os.path.dirname(je_auto_control.__file__) + assert os.path.isfile(os.path.join(pkg_dir, "py.typed")) + + +# --- wiring --------------------------------------------------------------- + +def test_executor_wiring(tmp_path): + db = str(tmp_path / "e.db") + rec = ac.execute_action([["AC_run_resumable", { + "actions": [["AC_set_var", {"name": "z", "value": 9}]], + "run_id": "w", "db": db}]]) + assert any("'completed': True" in str(v) for v in rec.values()) + status = ac.execute_action([["AC_checkpoint_status", + {"run_id": "w", "db": db}]]) + assert any("None" in str(v) for v in status.values()) # cleared + known = ac.executor.known_commands() + assert {"AC_run_resumable", "AC_checkpoint_status", + "AC_checkpoint_clear"} <= known + + +def test_mcp_and_builder_wiring(): + from je_auto_control.utils.mcp_server.tools import ( + build_default_tool_registry) + names = {t.name for t in build_default_tool_registry()} + assert {"ac_run_resumable", "ac_checkpoint_status", + "ac_checkpoint_clear"} <= names + from je_auto_control.gui.script_builder.command_schema import _build_specs + cmds = {s.command for s in _build_specs()} + assert {"AC_run_resumable", "AC_checkpoint_status", + "AC_checkpoint_clear"} <= cmds + + +def test_facade_exports(): + for attr in ("Checkpoint", "CheckpointStore", "run_resumable"): + assert hasattr(ac, attr) + assert attr in ac.__all__