From 843fc4b504d4b68e6cb582079d15fe14aac8fbb8 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 3 Jul 2026 15:28:12 +0800 Subject: [PATCH 1/5] Add stable je_auto_control.api facade and portable failure bundles New integrations get a small, lazy, typed entry point instead of the eager historical top-level surface. Failed runs produce one atomic, redacted autocontrol.failure-bundle/v1 ZIP (manifest, context, events, log tail, optional screenshot/diagnostics) with best-effort collectors so a broken screen grab cannot lose the bundle. codegen --failure-bundle wraps generated pytest in automatic diagnostics; the secret redactor now masks explicit key=value and bearer-token syntax regardless of entropy. --- benchmarks/core_latency.py | 31 +++ je_auto_control/api/__init__.py | 22 +++ je_auto_control/api/core.py | 19 ++ je_auto_control/cli.py | 41 +++- je_auto_control/utils/codegen/codegen.py | 36 +++- .../config_redaction/config_redaction.py | 17 +- je_auto_control/utils/deprecation.py | 35 ++++ .../utils/failure_bundle/__init__.py | 11 ++ .../utils/failure_bundle/bundle.py | 176 ++++++++++++++++++ test/unit_test/headless/test_codegen.py | 8 + .../unit_test/headless/test_failure_bundle.py | 51 +++++ 11 files changed, 434 insertions(+), 13 deletions(-) create mode 100644 benchmarks/core_latency.py create mode 100644 je_auto_control/api/__init__.py create mode 100644 je_auto_control/api/core.py create mode 100644 je_auto_control/utils/deprecation.py create mode 100644 je_auto_control/utils/failure_bundle/__init__.py create mode 100644 je_auto_control/utils/failure_bundle/bundle.py create mode 100644 test/unit_test/headless/test_failure_bundle.py diff --git a/benchmarks/core_latency.py b/benchmarks/core_latency.py new file mode 100644 index 00000000..bdb72271 --- /dev/null +++ b/benchmarks/core_latency.py @@ -0,0 +1,31 @@ +"""Repeatable smoke benchmark for stable headless entry points.""" +import json +import statistics +import time + + +def _measure(callable_, repeats=20): + samples = [] + for _ in range(repeats): + start = time.perf_counter() + callable_() + samples.append((time.perf_counter() - start) * 1000) + ordered = sorted(samples) + return { + "median_ms": round(statistics.median(samples), 3), + "p95_ms": round(ordered[max(0, int(len(ordered) * .95) - 1)], 3), + } + + +def main(): + import je_auto_control.api as ac + results = { + "diagnostics_ms": _measure(ac.run_diagnostics, repeats=5), + "codegen_ms": _measure( + lambda: ac.generate_code([["AC_screen_size"]]), repeats=20), + } + print(json.dumps(results, indent=2, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/je_auto_control/api/__init__.py b/je_auto_control/api/__init__.py new file mode 100644 index 00000000..4a4bc657 --- /dev/null +++ b/je_auto_control/api/__init__.py @@ -0,0 +1,22 @@ +"""Small, versioned entry points for new integrations. + +The historical top-level package remains compatible. New consumers should +prefer this namespace so importing core automation does not eagerly import +hundreds of optional integrations. +""" + +from je_auto_control.api.core import ( + FailureBundleOptions, + create_failure_bundle, + execute_action, + execute_action_with_vars, + generate_code, + failure_bundle_on_error, + run_diagnostics, +) + +__all__ = [ + "FailureBundleOptions", "create_failure_bundle", "execute_action", + "execute_action_with_vars", "failure_bundle_on_error", "generate_code", + "run_diagnostics", +] diff --git a/je_auto_control/api/core.py b/je_auto_control/api/core.py new file mode 100644 index 00000000..f57f54c8 --- /dev/null +++ b/je_auto_control/api/core.py @@ -0,0 +1,19 @@ +"""Stable, headless AutoControl API façade.""" + +from je_auto_control.utils.codegen.codegen import generate_code +from je_auto_control.utils.diagnostics import run_diagnostics +from je_auto_control.utils.executor.action_executor import ( + execute_action, + execute_action_with_vars, +) +from je_auto_control.utils.failure_bundle import ( + FailureBundleOptions, + create_failure_bundle, + failure_bundle_on_error, +) + +__all__ = [ + "FailureBundleOptions", "create_failure_bundle", "execute_action", + "execute_action_with_vars", "failure_bundle_on_error", "generate_code", + "run_diagnostics", +] diff --git a/je_auto_control/cli.py b/je_auto_control/cli.py index 4a59595d..6864efff 100644 --- a/je_auto_control/cli.py +++ b/je_auto_control/cli.py @@ -11,6 +11,7 @@ je_auto_control fmt script.json [--check] je_auto_control record out.json [--duration 5] je_auto_control codegen script.json [--target pytest] [-o test_flow.py] + je_auto_control failure-bundle failure.zip [--error "message"] je_auto_control version je_auto_control list-jobs je_auto_control start-server --port 9938 @@ -147,11 +148,13 @@ def cmd_codegen(args: argparse.Namespace) -> int: from je_auto_control.utils.json.json_file import read_action_json if args.output: generate_code_file(args.script, args.output, target=args.target, - name=args.name, style=args.style) + name=args.name, style=args.style, + failure_bundle=args.failure_bundle) sys.stderr.write(f"Wrote {args.target} code to {args.output}\n") else: code = generate_code(read_action_json(args.script), target=args.target, - name=args.name, style=args.style) + name=args.name, style=args.style, + failure_bundle=args.failure_bundle) sys.stdout.write(code) return 0 @@ -166,6 +169,25 @@ def cmd_version(_: argparse.Namespace) -> int: return 0 +def cmd_failure_bundle(args: argparse.Namespace) -> int: + """Collect a portable, redacted diagnostic archive.""" + from je_auto_control.utils.failure_bundle import ( + FailureBundleOptions, create_failure_bundle, + ) + context = json.loads(args.context) if args.context else {} + path = create_failure_bundle( + args.output, error=args.error, context=context, + options=FailureBundleOptions( + screenshot=not args.no_screenshot, + diagnostics=not args.no_diagnostics, + log_path=args.log, + attachments=tuple(args.attach or ()), + ), + ) + sys.stdout.write(path + "\n") + return 0 + + def cmd_list_jobs(_: argparse.Namespace) -> int: from je_auto_control.utils.scheduler.scheduler import default_scheduler jobs = default_scheduler.list_jobs() @@ -253,11 +275,26 @@ def build_parser() -> argparse.ArgumentParser: default="calls") p_codegen.add_argument("--name", default="recorded_flow") p_codegen.add_argument("-o", "--output", help="Write to file instead of stdout") + p_codegen.add_argument( + "--failure-bundle", action="store_true", + help="Wrap generated pytest in automatic failure diagnostics") p_codegen.set_defaults(func=cmd_codegen) p_version = sub.add_parser("version", help="Print the installed version") p_version.set_defaults(func=cmd_version) + p_bundle = sub.add_parser( + "failure-bundle", help="Create a redacted failure diagnostic ZIP") + p_bundle.add_argument("output") + p_bundle.add_argument("--error", help="Failure summary") + p_bundle.add_argument("--context", help="JSON object with run context") + p_bundle.add_argument("--log", help="Log file whose redacted tail is included") + p_bundle.add_argument("--attach", action="append", + help="Explicit attachment; may be repeated") + p_bundle.add_argument("--no-screenshot", action="store_true") + p_bundle.add_argument("--no-diagnostics", action="store_true") + p_bundle.set_defaults(func=cmd_failure_bundle) + p_jobs = sub.add_parser("list-jobs", help="List scheduler jobs") p_jobs.set_defaults(func=cmd_list_jobs) diff --git a/je_auto_control/utils/codegen/codegen.py b/je_auto_control/utils/codegen/codegen.py index 72a2f6b6..e5986197 100644 --- a/je_auto_control/utils/codegen/codegen.py +++ b/je_auto_control/utils/codegen/codegen.py @@ -72,14 +72,24 @@ def _body(actions: Sequence, style: str) -> str: raise ValueError(f"unknown codegen style: {style!r}") -def _render_pytest(actions: Sequence, name: str, style: str) -> str: - body = textwrap.indent(_body(actions, style), " ") +def _render_pytest(actions: Sequence, name: str, style: str, + failure_bundle: bool = False) -> str: + raw_body = _body(actions, style) + if failure_bundle: + body = (" with ac.failure_bundle_on_error(\n" + f" {(_slug(name) + '-failure.zip')!r},\n" + f" context={{'generated_test': {_slug(name)!r}}}):\n" + + textwrap.indent(raw_body, " ")) + else: + body = textwrap.indent(raw_body, " ") return (f'"""{_HEADER}"""\n' - "import je_auto_control as ac\n\n\n" - f"def test_{_slug(name)}():\n{body}\n") + + ("import je_auto_control.api as ac\n\n\n" if failure_bundle + else "import je_auto_control as ac\n\n\n") + + f"def test_{_slug(name)}():\n{body}\n") -def _render_python(actions: Sequence, name: str, style: str) -> str: +def _render_python(actions: Sequence, name: str, style: str, + _failure_bundle: bool = False) -> str: slug = _slug(name) body = textwrap.indent(_body(actions, style), " ") return (f'"""{_HEADER}"""\n' @@ -89,7 +99,8 @@ def _render_python(actions: Sequence, name: str, style: str) -> str: f" {slug}()\n") -def _render_robot(actions: Sequence, name: str, _style: str) -> str: +def _render_robot(actions: Sequence, name: str, _style: str, + _failure_bundle: bool = False) -> str: payload = json.dumps([list(action) for action in actions], ensure_ascii=False) test_name = name.replace("_", " ").strip().title() or "Recorded Flow" @@ -113,22 +124,27 @@ def _render_robot(actions: Sequence, name: str, _style: str) -> str: def generate_code(actions: Sequence, target: str = "pytest", - name: str = "recorded_flow", style: str = "calls") -> str: + name: str = "recorded_flow", style: str = "calls", + failure_bundle: bool = False) -> str: """Render ``actions`` as source code for ``target`` (pytest/python/robot).""" if not isinstance(actions, list) or not actions: raise ValueError("actions must be a non-empty list") renderer = _RENDERERS.get(target) if renderer is None: raise ValueError(f"unknown codegen target: {target!r}") - return renderer(actions, name, style) + if failure_bundle and target != "pytest": + raise ValueError("failure_bundle is currently supported for pytest only") + return renderer(actions, name, style, failure_bundle) def generate_code_file(source, output_path: str, target: str = "pytest", - name: str = "recorded_flow", style: str = "calls") -> str: + name: str = "recorded_flow", style: str = "calls", + failure_bundle: bool = False) -> str: """Generate code from a list or JSON action-file path; write and return it.""" actions = source if isinstance(source, list) else read_action_json( os.path.realpath(source)) - code = generate_code(actions, target=target, name=name, style=style) + code = generate_code(actions, target=target, name=name, style=style, + failure_bundle=failure_bundle) with open(os.path.realpath(output_path), "w", encoding="utf-8") as handle: handle.write(code) return code diff --git a/je_auto_control/utils/config_redaction/config_redaction.py b/je_auto_control/utils/config_redaction/config_redaction.py index 9222a2c7..6559fbaf 100644 --- a/je_auto_control/utils/config_redaction/config_redaction.py +++ b/je_auto_control/utils/config_redaction/config_redaction.py @@ -44,6 +44,21 @@ def redact_config(obj: Any, *, mask: str = _DEFAULT_MASK) -> Any: def redact_secret_text(text: str, *, mask: str = _DEFAULT_MASK) -> str: """Mask secret-looking tokens within a free-text string (e.g. a log line).""" + # Explicit credential syntax must be masked even when the value is short or + # low-entropy and therefore intentionally below the generic scanner's + # threshold (common in tests, local deployments, and leaked error text). + text = re.sub( + r"(?i)(\bauthorization\s*:\s*bearer\s+)[^\s,;]+", + lambda match: match.group(1) + mask, + text or "", + ) + text = re.sub( + r"(?i)(\b(?:api[_-]?key|access[_-]?token|token|password|passwd|secret)" + r"\s*[=:]\s*)([^\s,;]+)", + lambda match: match.group(1) + mask, + text, + ) + def _replace(match: "re.Match[str]") -> str: token = match.group(0) core = token.strip(_PUNCT) @@ -51,4 +66,4 @@ def _replace(match: "re.Match[str]") -> str: return token.replace(core, mask) return token - return re.sub(r"\S+", _replace, text or "") + return re.sub(r"\S+", _replace, text) diff --git a/je_auto_control/utils/deprecation.py b/je_auto_control/utils/deprecation.py new file mode 100644 index 00000000..c4084c68 --- /dev/null +++ b/je_auto_control/utils/deprecation.py @@ -0,0 +1,35 @@ +"""Consistent deprecation warnings for public AutoControl APIs.""" +from __future__ import annotations + +import functools +import warnings +from typing import Callable, TypeVar, cast + +F = TypeVar("F", bound=Callable) + + +class AutoControlDeprecationWarning(FutureWarning): + """A user-visible warning for an API scheduled for removal.""" + + +def deprecated(*, since: str, removal: str, replacement: str = ""): + """Mark a callable deprecated with actionable lifecycle metadata.""" + def decorate(func: F) -> F: + message = f"{func.__qualname__} is deprecated since {since}" + message += f" and will be removed in {removal}." + if replacement: + message += f" Use {replacement} instead." + + @functools.wraps(func) + def wrapped(*args, **kwargs): + warnings.warn(message, AutoControlDeprecationWarning, + stacklevel=2) + return func(*args, **kwargs) + wrapped.__deprecated__ = { # type: ignore[attr-defined] + "since": since, "removal": removal, "replacement": replacement, + } + return cast(F, wrapped) + return decorate + + +__all__ = ["AutoControlDeprecationWarning", "deprecated"] diff --git a/je_auto_control/utils/failure_bundle/__init__.py b/je_auto_control/utils/failure_bundle/__init__.py new file mode 100644 index 00000000..c1e4d3aa --- /dev/null +++ b/je_auto_control/utils/failure_bundle/__init__.py @@ -0,0 +1,11 @@ +"""Portable, redacted failure diagnostics.""" + +from je_auto_control.utils.failure_bundle.bundle import ( + FailureBundleOptions, + create_failure_bundle, + failure_bundle_on_error, +) + +__all__ = [ + "FailureBundleOptions", "create_failure_bundle", "failure_bundle_on_error", +] diff --git a/je_auto_control/utils/failure_bundle/bundle.py b/je_auto_control/utils/failure_bundle/bundle.py new file mode 100644 index 00000000..79035092 --- /dev/null +++ b/je_auto_control/utils/failure_bundle/bundle.py @@ -0,0 +1,176 @@ +"""Create a self-contained ZIP for diagnosing failed automation runs. + +Collection is deliberately best-effort: failure diagnostics must still be +created when screen capture, an optional backend, or the diagnostics runner +itself is broken. Text and structured values are redacted before they enter +the archive and arbitrary attachments are opt-in. +""" +from __future__ import annotations + +import json +import os +import platform +import sys +import tempfile +import time +import zipfile +from contextlib import contextmanager +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable, Mapping + +from je_auto_control.utils.config_redaction import ( + redact_config, + redact_secret_text, +) + + +@dataclass(frozen=True) +class FailureBundleOptions: + """Controls potentially sensitive or expensive bundle collectors.""" + + screenshot: bool = True + diagnostics: bool = True + log_path: str | None = None + log_tail_bytes: int = 256_000 + attachments: tuple[str, ...] = () + + +def _json_bytes(value: Any) -> bytes: + return json.dumps(value, ensure_ascii=False, indent=2, + sort_keys=True, default=repr).encode("utf-8") + + +def _safe_name(path: Path, used: set[str]) -> str: + base = path.name or "attachment" + candidate, index = base, 2 + while candidate in used: + candidate = f"{path.stem}-{index}{path.suffix}" + index += 1 + used.add(candidate) + return candidate + + +def _read_log_tail(path: Path, limit: int) -> str: + with path.open("rb") as handle: + if path.stat().st_size > limit: + handle.seek(-limit, os.SEEK_END) + data = handle.read() + return redact_secret_text(data.decode("utf-8", errors="replace")) + + +def _collect_diagnostics(archive: zipfile.ZipFile, + failures: list[dict[str, str]]) -> None: + try: + from je_auto_control.utils.diagnostics import run_diagnostics + archive.writestr("diagnostics.json", + _json_bytes(run_diagnostics().to_dict())) + except Exception as exc: # diagnostics are best-effort + failures.append({"collector": "diagnostics", "error": repr(exc)}) + + +def _collect_screenshot(archive: zipfile.ZipFile, + failures: list[dict[str, str]]) -> None: + try: + from je_auto_control.utils.cv2_utils.screenshot import pil_screenshot + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as image_file: + image_path = image_file.name + try: + pil_screenshot().save(image_path, format="PNG") + archive.write(image_path, "screenshot.png") + finally: + Path(image_path).unlink(missing_ok=True) + except Exception as exc: # headless and locked sessions are valid + failures.append({"collector": "screenshot", "error": repr(exc)}) + + +def _collect_log(archive: zipfile.ZipFile, opts: FailureBundleOptions, + failures: list[dict[str, str]]) -> None: + try: + archive.writestr("logs/tail.log", _read_log_tail( + Path(opts.log_path or "").expanduser().resolve(), + max(1, opts.log_tail_bytes))) + except Exception as exc: + failures.append({"collector": "log", "error": repr(exc)}) + + +def _collect_attachments(archive: zipfile.ZipFile, opts: FailureBundleOptions, + failures: list[dict[str, str]]) -> None: + used: set[str] = set() + for raw_path in opts.attachments: + try: + path = Path(raw_path).expanduser().resolve(strict=True) + if not path.is_file(): + raise ValueError("attachment is not a regular file") + archive.write(path, f"attachments/{_safe_name(path, used)}") + except Exception as exc: + failures.append({"collector": "attachment", "error": repr(exc)}) + + +def _collect_all(archive: zipfile.ZipFile, opts: FailureBundleOptions, + failures: list[dict[str, str]]) -> None: + if opts.diagnostics: + _collect_diagnostics(archive, failures) + if opts.screenshot: + _collect_screenshot(archive, failures) + if opts.log_path: + _collect_log(archive, opts, failures) + _collect_attachments(archive, opts, failures) + + +def create_failure_bundle( + output_path: str | os.PathLike[str], + *, + error: BaseException | str | None = None, + context: Mapping[str, Any] | None = None, + events: Iterable[Mapping[str, Any]] = (), + options: FailureBundleOptions | None = None, +) -> str: + """Write an atomic, redacted diagnostic ZIP and return its real path.""" + opts = options or FailureBundleOptions() + target = Path(output_path).expanduser().resolve() + target.parent.mkdir(parents=True, exist_ok=True) + failures: list[dict[str, str]] = [] + manifest = { + "schema": "autocontrol.failure-bundle/v1", + "created_at_unix": time.time(), + "error": None if error is None else redact_secret_text(str(error)), + "runtime": { + "python": sys.version.split()[0], + "platform": platform.platform(), + "executable": Path(sys.executable).name, + }, + "context": redact_config(dict(context or {})), + "events": redact_config(list(events)), + "collector_failures": failures, + } + + fd, temp_name = tempfile.mkstemp(prefix=f".{target.name}.", + suffix=".tmp", dir=str(target.parent)) + os.close(fd) + try: + with zipfile.ZipFile(temp_name, "w", zipfile.ZIP_DEFLATED) as archive: + _collect_all(archive, opts, failures) + archive.writestr("manifest.json", _json_bytes(manifest)) + os.replace(temp_name, target) + except BaseException: + Path(temp_name).unlink(missing_ok=True) + raise + return str(target) + + +@contextmanager +def failure_bundle_on_error( + output_path: str | os.PathLike[str], + *, + context: Mapping[str, Any] | None = None, + events: Iterable[Mapping[str, Any]] = (), + options: FailureBundleOptions | None = None, +): + """Create a bundle when the wrapped block raises, then re-raise it.""" + try: + yield + except BaseException as error: + create_failure_bundle(output_path, error=error, context=context, + events=events, options=options) + raise diff --git a/test/unit_test/headless/test_codegen.py b/test/unit_test/headless/test_codegen.py index 472e8e68..74793280 100644 --- a/test/unit_test/headless/test_codegen.py +++ b/test/unit_test/headless/test_codegen.py @@ -78,6 +78,14 @@ def test_empty_actions_rejected(): generate_code([]) +def test_pytest_can_emit_automatic_failure_bundle(): + code = generate_code(_ACTIONS, failure_bundle=True, name="login") + assert "import je_auto_control.api as ac" in code + assert "with ac.failure_bundle_on_error(" in code + assert "login-failure.zip" in code + assert _compiles(code) + + def test_generate_code_file_from_path(tmp_path): src = tmp_path / "flow.json" src.write_text(json.dumps(_ACTIONS), encoding="utf-8") diff --git a/test/unit_test/headless/test_failure_bundle.py b/test/unit_test/headless/test_failure_bundle.py new file mode 100644 index 00000000..e7652bf5 --- /dev/null +++ b/test/unit_test/headless/test_failure_bundle.py @@ -0,0 +1,51 @@ +import json +import zipfile + +from je_auto_control.utils.failure_bundle import ( + FailureBundleOptions, + create_failure_bundle, + failure_bundle_on_error, +) +import pytest + + +def test_bundle_is_atomic_portable_and_redacted(tmp_path): + log = tmp_path / "run.log" + log.write_text("Authorization: Bearer secret-token\n", encoding="utf-8") + output = tmp_path / "failure.zip" + result = create_failure_bundle( + output, + error="request failed token=secret-token", + context={"api_key": "secret-token", "step": 3}, + events=[{"action": "click", "password": "hunter2"}], + options=FailureBundleOptions( + screenshot=False, diagnostics=False, log_path=str(log)), + ) + assert result == str(output.resolve()) + with zipfile.ZipFile(output) as archive: + assert set(archive.namelist()) == {"manifest.json", "logs/tail.log"} + manifest = json.loads(archive.read("manifest.json")) + assert manifest["schema"] == "autocontrol.failure-bundle/v1" + assert manifest["context"]["api_key"] == "***" + assert manifest["events"][0]["password"] == "***" + combined = archive.read("manifest.json") + archive.read("logs/tail.log") + assert b"secret-token" not in combined + assert b"hunter2" not in combined + + +def test_collector_failure_does_not_prevent_bundle(tmp_path): + output = tmp_path / "failure.zip" + create_failure_bundle(output, options=FailureBundleOptions( + screenshot=False, diagnostics=False, log_path=str(tmp_path / "missing"))) + with zipfile.ZipFile(output) as archive: + manifest = json.loads(archive.read("manifest.json")) + assert manifest["collector_failures"][0]["collector"] == "log" + + +def test_context_manager_bundles_and_reraises(tmp_path): + output = tmp_path / "failure.zip" + with pytest.raises(RuntimeError, match="boom"): + with failure_bundle_on_error(output, options=FailureBundleOptions( + screenshot=False, diagnostics=False)): + raise RuntimeError("boom") + assert output.is_file() From 7ddac38bc74be4a1fb28276e9697444a5f3b6d8d Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 3 Jul 2026 15:28:19 +0800 Subject: [PATCH 2/5] Move releases to immutable tags with Trusted Publishing and add CI gates Publishing on every push to main made releases unauditable. Releases now require a v* tag whose version must match pyproject, build provenance is attested, and PyPI uses Trusted Publishing. quality.yml gains dependency review, a coverage floor, and a mypy gate on the stable API surface; a platform-smoke matrix exercises the stable API on all three OSes. --- .github/workflows/platform-smoke.yml | 42 +++++++++++++++++++ .github/workflows/quality.yml | 30 +++++++++++++- .github/workflows/release.yml | 62 ++++++++++++++++++++++++++++ .github/workflows/stable.yml | 3 +- .gitignore | 5 +++ dev_requirements.txt | 2 + pyproject.toml | 30 ++++++++++++++ 7 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/platform-smoke.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/platform-smoke.yml b/.github/workflows/platform-smoke.yml new file mode 100644 index 00000000..8485ff6d --- /dev/null +++ b/.github/workflows/platform-smoke.yml @@ -0,0 +1,42 @@ +name: Platform smoke + +on: + push: + branches: ["main", "dev"] + pull_request: + branches: ["main", "dev"] + +permissions: + contents: read + +jobs: + stable-api: + strategy: + fail-fast: false + matrix: + os: [windows-2022, ubuntu-22.04, macos-14] + python-version: ["3.10", "3.14"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: python -m pip install -e . + - name: Import stable API and generate platform-neutral code + run: >- + python -c "import je_auto_control.api as ac; + compile(ac.generate_code([['AC_screen_size']], style='actions'), + '', 'exec')" + - name: Create headless diagnostic bundle + run: >- + python -c "from je_auto_control.api import + FailureBundleOptions, create_failure_bundle; + create_failure_bundle('platform-smoke.zip', + options=FailureBundleOptions(screenshot=False))" + - uses: actions/upload-artifact@v4 + if: always() + with: + name: platform-smoke-${{ matrix.os }}-${{ matrix.python-version }} + path: platform-smoke.zip + if-no-files-found: warn diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 16b284d8..fbdfa5fe 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -16,6 +16,15 @@ permissions: contents: read jobs: + dependency-review: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/dependency-review-action@v4 + lint: runs-on: ubuntu-latest steps: @@ -84,4 +93,23 @@ jobs: pip install ruff==0.15.14 bandit==1.9.4 pytest==9.0.3 pytest-timeout==2.4.0 pytest-rerunfailures==15.1 PySide6==6.11.1 - name: Run headless pytest suite - run: pytest test/unit_test/headless/ -v --tb=short --timeout=120 + run: >- + pytest test/unit_test/headless/ -v --tb=short --timeout=120 + --cov=je_auto_control --cov-report=term-missing + --cov-report=xml --cov-fail-under=35 + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ matrix.python-version }} + path: coverage.xml + + typing-stable-api: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install -e . mypy + - run: mypy je_auto_control/api je_auto_control/utils/failure_bundle diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..51a3ed33 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,62 @@ +name: Release + +on: + push: + tags: ["v*"] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + attestations: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: python -m pip install --upgrade build twine + - name: Verify tag matches package version + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + python - <<'PY' + import os, tomllib + with open("pyproject.toml", "rb") as handle: + version = tomllib.load(handle)["project"]["version"] + if os.environ["RELEASE_TAG"] != f"v{version}": + raise SystemExit(f"tag {os.environ['RELEASE_TAG']} != v{version}") + PY + - run: python -m build + - run: python -m twine check dist/* + - name: Smoke-test the built wheel + run: | + python -m venv /tmp/wheel-test + /tmp/wheel-test/bin/pip install dist/*.whl + /tmp/wheel-test/bin/python -c "import je_auto_control.api" + - uses: actions/attest-build-provenance@v2 + with: + subject-path: "dist/*" + - uses: actions/upload-artifact@v4 + with: + name: python-distributions + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/je-auto-control + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: python-distributions + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/stable.yml b/.github/workflows/stable.yml index 60d36c02..415a57eb 100644 --- a/.github/workflows/stable.yml +++ b/.github/workflows/stable.yml @@ -118,7 +118,8 @@ jobs: publish: needs: test - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + # Publishing moved to release.yml: immutable v* tags + Trusted Publishing. + if: ${{ false }} runs-on: ubuntu-latest permissions: contents: write diff --git a/.gitignore b/.gitignore index 66c4f152..ab74cb98 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,8 @@ dmypy.json /.claude/ /.claude /.idea + +# Local test/smoke artifacts +.test-tmp/ +bundle-smoke.zip +platform-smoke.zip diff --git a/dev_requirements.txt b/dev_requirements.txt index 8ac5598d..26b96b0c 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -20,3 +20,5 @@ bandit==1.9.4 pytest==9.0.3 pytest-timeout==2.4.0 pytest-rerunfailures==15.1 +pytest-cov>=6.0 +mypy>=1.15 diff --git a/pyproject.toml b/pyproject.toml index 43c8c12b..db34a706 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,3 +88,33 @@ exclude_dirs = [ # B101 (use of assert) — pytest test code intentionally uses assert. # Library code is enforced by CLAUDE.md (no assert in non-test code). skips = ["B101"] + +[tool.pytest.ini_options] +testpaths = ["test/unit_test/headless"] +addopts = "--strict-markers --strict-config" + +[tool.coverage.run] +branch = true +source = ["je_auto_control"] +omit = ["*/gui/*", "*/language_wrapper/*"] + +[tool.coverage.report] +show_missing = true +skip_covered = true +# Initial measured repository baseline. Raise toward 70 as legacy modules are +# brought under the stable API contract; CI enforces that it cannot regress. +fail_under = 35 + +[tool.mypy] +python_version = "3.10" +warn_redundant_casts = true +check_untyped_defs = true +no_implicit_optional = true +# CI type-checks only the stable API surface; followed legacy modules are +# analysed for signatures but not reported until they join the contract. +follow_imports = "silent" +exclude = "(^test/|^docs/|^build/)" + +[[tool.mypy.overrides]] +module = ["cv2.*", "Xlib.*", "PySide6.*", "objc.*"] +ignore_missing_imports = true From d71c48022f8f4b726ff4f6d36b93469d44b415fe Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 3 Jul 2026 15:28:33 +0800 Subject: [PATCH 3/5] Refresh all project docs: lifecycle, security, capability matrix, indexes The docs lagged the code: Sphinx indexes stopped at v181 while v182-v223 existed, the Actions-menu redesign was undocumented outside WHATS_NEW, and the project had no security policy, changelog, API lifecycle, or honest platform-support statement. Adds all four, documents the menu- driven GUI (v223 EN/Zh), syncs the three READMEs and WHATS_NEW files, and records the Actions-menu tab contract in CLAUDE.md. --- CHANGELOG.md | 25 ++++++++++ CLAUDE.md | 1 + README.md | 30 +++++++++--- README/README_zh-CN.md | 16 +++++-- README/README_zh-TW.md | 16 +++++-- README/WHATS_NEW_zh-CN.md | 15 ++++++ README/WHATS_NEW_zh-TW.md | 15 ++++++ SECURITY.md | 25 ++++++++++ WHATS_NEW.md | 13 ++++- docs/API_LIFECYCLE.md | 17 +++++++ docs/CAPABILITY_MATRIX.md | 23 +++++++++ .../doc/new_features/v223_features_doc.rst | 48 +++++++++++++++++++ docs/source/Eng/eng_index.rst | 42 ++++++++++++++++ .../Zh/doc/new_features/v223_features_doc.rst | 42 ++++++++++++++++ docs/source/Zh/zh_index.rst | 42 ++++++++++++++++ 15 files changed, 355 insertions(+), 15 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 SECURITY.md create mode 100644 docs/API_LIFECYCLE.md create mode 100644 docs/CAPABILITY_MATRIX.md create mode 100644 docs/source/Eng/doc/new_features/v223_features_doc.rst create mode 100644 docs/source/Zh/doc/new_features/v223_features_doc.rst diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..7575e2c3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +This file records user-visible compatibility changes. Detailed development +notes remain in `WHATS_NEW.md`. + +The format follows Keep a Changelog. Until 1.0, breaking changes are permitted +only when documented here with a migration path. + +## Unreleased + +### Added + +- Stable, headless `je_auto_control.api` façade. +- Portable `autocontrol.failure-bundle/v1` diagnostic archives and CLI command. +- Public API lifecycle, capability matrix, security policy, coverage and type + checking configuration. + +### Changed + +- Releases are prepared from version tags and use PyPI Trusted Publishing. + +### Deprecated + +- New integrations should avoid the eager, historical top-level import surface + and import stable entry points from `je_auto_control.api`. diff --git a/CLAUDE.md b/CLAUDE.md index df15f5ad..d06de150 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,6 +84,7 @@ No feature is complete unless it can be driven entirely without the GUI **and** - **Re-export from the package facade**: add the public functions / classes to `je_auto_control/__init__.py` and its `__all__` so `import je_auto_control as ac; ac.(...)` works out of the box. - **Executor command coverage**: wire an `AC_*` command into `utils/executor/action_executor.py` so the feature is usable from JSON action files, the socket server, the scheduler, and the visual script builder — all without Python glue. - **GUI tab or control is a thin wrapper**: the Qt widget must only translate user input into calls on the headless core. It must not contain business logic that would be unreachable headlessly. +- **Tab commands live in the Actions menu, not in-tab buttons**: the main window is menu-driven. A tab keeps only its inputs, tables, and result/status views; its commands surface through the window-level **Actions** menu. Core tabs declare `(label_key, handler)` pairs at registration in `gui/main_widget.py`; feature tabs expose a `menu_actions()` method returning the same shape. Script Builder and Remote Desktop are the only exempt tabs (interactive panel layouts). `test/unit_test/headless/test_actions_menu_gui.py` guards this contract — a new tab without registry actions or a `menu_actions()` hook fails CI. - **The top-level package stays Qt-free**: `import je_auto_control` MUST NOT import `PySide6`. The GUI entry point is loaded lazily inside `start_autocontrol_gui()`. Verify with: ```python diff --git a/README.md b/README.md index 18682fe7..c83b326b 100644 --- a/README.md +++ b/README.md @@ -99,9 +99,10 @@ All per-release notes have moved to **[WHATS_NEW.md](WHATS_NEW.md)**. - **WebRTC Packet Inspector** — process-global rolling window of `StatsSnapshot` samples (default 600 / ~10 min @ 1Hz) fed by the existing WebRTC stats pollers. Per-metric `last/min/max/avg/p95` for RTT, FPS, bitrate, packet loss, jitter - **USB Device Enumeration** — read-only cross-platform device listing. Tries pyusb (libusb) first; falls back to platform-specific (Windows `Get-PnpDevice`, macOS `system_profiler`, Linux `/sys/bus/usb/devices`). Phase 2 passthrough builds on this (see below) - **System Diagnostics** — single-command "is everything OK?" probe across platform, optional deps, executor command count, audit chain, screenshot, mouse, disk space, REST registry. CLI exits 0 if all green / 1 otherwise; REST `/diagnose`; severity-tagged GUI tab +- **Stable API & Failure Bundles** — versioned, lazy `je_auto_control.api` façade for new integrations (`execute_action`, `generate_code`, `run_diagnostics`, failure bundles) with a documented [lifecycle policy](docs/API_LIFECYCLE.md). Portable `autocontrol.failure-bundle/v1` diagnostic ZIPs: manifest + redacted context/events/log tail, optional screenshot and diagnostics, best-effort collectors, atomic write. CLI `je_auto_control failure-bundle out.zip`; `codegen --failure-bundle` wraps generated pytest in automatic failure diagnostics - **USB Hotplug Events** — polling-based hotplug watcher (`UsbHotplugWatcher`) with bounded ring buffer + sequence-numbered events; `GET /usb/events?since=N` lets late subscribers catch up. GUI auto-refresh toggle on the USB tab. - **OpenAPI 3.1 + Swagger UI** — `GET /openapi.json` (auth-gated, generated from the live route table) + `GET /docs` (browser Swagger UI with bearer token bar). Drift test in CI catches new routes added without metadata. -- **Configuration Bundle** — single-file JSON export/import of user config (admin hosts, address book, trusted viewers, known hosts, host service, IDs). Atomic write with `.bak.` backups; CLI `python -m je_auto_control.utils.config_bundle export|import`; `POST /config/{export,import}`; GUI buttons on the REST API tab. +- **Configuration Bundle** — single-file JSON export/import of user config (admin hosts, address book, trusted viewers, known hosts, host service, IDs). Atomic write with `.bak.` backups; CLI `python -m je_auto_control.utils.config_bundle export|import`; `POST /config/{export,import}`; export/import commands on the REST API tab's Actions menu. - **USB Passthrough (opt-in)** — let a remote viewer use a USB device physically attached to the host, over a WebRTC `usb` DataChannel. Wire-level protocol (11 opcodes incl. `RESUME`, CREDIT-based flow control, 16 KiB payload cap with EOF fragmentation for oversize transfers). All eight original open questions resolved: reliable-ordered channel, LIST-over-channel (ACL-filtered), per-claim credits, Linux kernel-driver detach/reattach, and ACL **HMAC-SHA256 integrity** (fail-closed on tamper; pluggable key — Windows DPAPI or passphrase vault). **Backends:** `LibusbBackend` (production), `WinusbBackend` (ctypes) and `IokitBackend` (native IOKit enumeration + libusb transfers) — Windows/macOS *hardware-unverified*; `default_passthrough_backend()` picks per-OS. Viewer-side blocking client (`control/bulk/interrupt_transfer`, `list_devices`, `resume`); in-process `UsbLoopback` so one machine can share + use a device through the full stack. **Wired into WebRTC** host/viewer (`viewer.usb_client()`) plus claim **resume tokens** that survive a reconnect. Persistent ACL (default deny, mode 0600) with host-side prompt dialog, abuse **rate-limit / lockout**, and tamper-evident audit integration. Five driving surfaces: AnyDesk-style **GUI panel** (share + ACL allow/block + local/remote use), `AC_usb_*` executor commands (JSON / socket / scheduler), **REST** `/usb/...`, first-class **MCP** `ac_usb_*` tools, and the Python API. Default off — opt-in via `enable_usb_passthrough(True)` or `JE_AUTOCONTROL_USB_PASSTHROUGH=1`; default-on still pending Phase 2e external security sign-off + real-hardware verification. - **Observability (Prometheus + OpenTelemetry)** — stdlib-only `Counter` / `Gauge` / `Histogram` registry with a tiny built-in HTTP exporter on `/metrics`, plus an OpenTelemetry-compatible tracer that upgrades to real OTel spans when the SDK is installed. The executor and agent loop emit `autocontrol_action_calls_total{action,outcome}`, `autocontrol_action_duration_seconds`, and `autocontrol_agent_steps_total{tool,outcome}` automatically — drop the URL into a Prometheus scrape config and you have a Grafana dashboard with zero per-script wiring. @@ -546,8 +547,8 @@ ac.run_from_description("open Notepad and type hello", executor=executor) | `AUTOCONTROL_LLM_BACKEND` | `anthropic` to force a backend | | `AUTOCONTROL_LLM_MODEL` | Override the default model (e.g. `claude-opus-4-7`) | -GUI: **LLM Planner** tab — description box, `QThread`-backed *Plan* -button, action-list preview, and a *Run plan* button. +GUI: **LLM Planner** tab — description box and action-list preview; +*Plan* (`QThread`-backed) and *Run plan* live in the window's Actions menu. ### Runtime Variables & Control Flow @@ -575,7 +576,8 @@ commands, scripts can drive themselves from data without Python glue: `AC_if_var` operators: `eq`, `ne`, `lt`, `le`, `gt`, `ge`, `contains`, `startswith`, `endswith`. GUI: **Variables** tab — live view of -`executor.variables` with single-set, JSON seed, and clear-all controls. +`executor.variables`; single-set, JSON seed, and clear-all run from the +window's Actions menu. ### Remote Desktop @@ -1099,8 +1101,9 @@ for run in default_history_store.list_runs(limit=20): print(run.id, run.source, run.status, run.artifact_path) ``` -The GUI **Run History** tab exposes filter/refresh/clear and -double-click-to-open on the artifact column. +The GUI **Run History** tab shows the runs table with +double-click-to-open on the artifact column; filter, refresh, and +clear run from the window's Actions menu. ### Report Generation @@ -1357,6 +1360,16 @@ Or from the command line: python -m je_auto_control ``` +The main window is menu-driven: tabs hold only their inputs, tables, +and result views, and every tab's commands live in the window-level +**Actions** menu, which rebuilds for the active tab. **View → Tabs** +shows or hides any of the ~48 registered tabs, grouped by category +(Core / Editing / Detection & Vision / Automation Engines / System); +the default layout opens with just Record, Script Builder, and Remote +Desktop. **View → Text Size** offers auto/preset font scaling, and the +**Language** menu (English / 繁體中文 / 简体中文 / 日本語) retranslates +the whole window live. + --- ## Command-Line Interface @@ -1438,6 +1451,11 @@ python -m pytest test/integrated_test/ ### Project Links +- [Capability and platform matrix](docs/CAPABILITY_MATRIX.md) +- [Public API lifecycle and deprecation policy](docs/API_LIFECYCLE.md) +- [Security policy](SECURITY.md) +- [Compatibility changelog](CHANGELOG.md) + - **Homepage**: https://github.com/Intergration-Automation-Testing/AutoControl - **Documentation**: https://autocontrol.readthedocs.io/en/latest/ - **PyPI**: https://pypi.org/project/je_auto_control/ diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index a1113591..849485fc 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -97,9 +97,10 @@ - **WebRTC 包监测** — 由既有 WebRTC stats 轮询喂入的进程级 `StatsSnapshot` 滚动窗口(默认 600 条 / 1 Hz 约 10 分钟)。对 RTT、FPS、bitrate、丢包率、jitter 各回 `last/min/max/avg/p95` - **USB 设备列举** — 只读的跨平台 USB 设备列举。优先尝试 pyusb(libusb);若无则退回平台特定命令(Windows `Get-PnpDevice`、macOS `system_profiler`、Linux `/sys/bus/usb/devices`)。第二阶段 passthrough 构建于此(见下) - **系统诊断** — 一键"目前正常吗?"探测:平台、可选依赖包、executor 命令数、审计链、截图、鼠标、磁盘空间、REST registry。CLI 全绿 exit 0/否则 1;REST `/diagnose`;按严重度上色的 GUI 分页 +- **稳定 API 与失败诊断包** — 给新集成用的版本化、延迟加载 `je_auto_control.api` 门面(`execute_action`、`generate_code`、`run_diagnostics`、failure bundles),附[生命周期政策](../docs/API_LIFECYCLE.md)。便携式 `autocontrol.failure-bundle/v1` 诊断 ZIP:manifest + 已脱敏的 context/events/log 尾段、可选截图与诊断、best-effort 收集器、原子写入。CLI `je_auto_control failure-bundle out.zip`;`codegen --failure-bundle` 让生成的 pytest 自动包上失败诊断 - **USB Hotplug 事件** — 轮询式 hotplug 监测(`UsbHotplugWatcher`),含 bounded ring buffer 与带序号的事件;`GET /usb/events?since=N` 让晚加入的订阅者补上进度。USB 分页有自动刷新切换钮。 - **OpenAPI 3.1 + Swagger UI** — `GET /openapi.json`(auth-gated,从活的路由表生成)+ `GET /docs`(浏览器版 Swagger UI 含 bearer token 栏)。CI 上有 drift 测试,新加路由忘记写 metadata 会被拦下。 -- **配置包导出/导入** — 单一 JSON 文件,导出/导入用户配置(admin hosts、address book、trusted viewers、known hosts、host service、IDs)。原子写入加 `.bak.<时间戳>` 备份;CLI `python -m je_auto_control.utils.config_bundle export|import`;`POST /config/{export,import}`;REST API 分页有按钮。 +- **配置包导出/导入** — 单一 JSON 文件,导出/导入用户配置(admin hosts、address book、trusted viewers、known hosts、host service、IDs)。原子写入加 `.bak.<时间戳>` 备份;CLI `python -m je_auto_control.utils.config_bundle export|import`;`POST /config/{export,import}`;REST API 分页的导出/导入命令位于窗口的 Actions 菜单。 - **USB Passthrough(需主动启用)** — 让远端 viewer 使用实体插在 host 上的 USB 设备,走 WebRTC `usb` DataChannel。Wire-level 协议(11 个 opcode 含 `RESUME`、CREDIT 流量控制、16 KiB payload 上限,超量传输以 EOF 分片)。八个原始未决问题全部解决:可靠有序 channel、LIST 走 channel(ACL 过滤)、per-claim credit、Linux kernel driver detach/reattach、ACL **HMAC-SHA256 完整性**(篡改 fail-closed;密钥可插拔 — Windows DPAPI 或 passphrase vault)。**Backend:**`LibusbBackend`(production)、`WinusbBackend`(ctypes)、`IokitBackend`(原生 IOKit 列举 + libusb 传输)— Windows/macOS *硬件未验证*;`default_passthrough_backend()` 依 OS 自动挑。Viewer 端阻塞式 client(`control/bulk/interrupt_transfer`、`list_devices`、`resume`);in-process `UsbLoopback` 让同机可走完整堆栈 share+use。**已接入 WebRTC** host/viewer(`viewer.usb_client()`)并含断线可续租的 **resume token**。持久化 ACL(默认 deny、mode 0600),含 host 端 prompt 对话框、滥用 **rate-limit / lockout** 与可检测篡改审计整合。五个驱动面:AnyDesk 风 **GUI 面板**(分享 + ACL 允许/封锁 + 本机/远端使用)、`AC_usb_*` executor 命令(JSON / socket / 调度器)、**REST** `/usb/...`、一级 **MCP** `ac_usb_*` 工具、以及 Python API。默认 off — 用 `enable_usb_passthrough(True)` 或 `JE_AUTOCONTROL_USB_PASSTHROUGH=1` 启用;默认启用仍待 Phase 2e 外部安全签核 + 实机硬件验证。 --- @@ -529,7 +530,7 @@ ac.run_from_description("打开记事本并输入 hello", executor=executor) | `AUTOCONTROL_LLM_BACKEND` | 强制指定 `anthropic` | | `AUTOCONTROL_LLM_MODEL` | 覆盖默认模型(如 `claude-opus-4-7`) | -GUI:**LLM Planner** 分页 — 描述输入框、`QThread` 后台执行的 *Plan* 按钮、预览指令清单,以及 *Run plan* 按钮。 +GUI:**LLM Planner** 分页 — 描述输入框与指令清单预览;*Plan*(`QThread` 后台执行)与 *Run plan* 位于窗口的 Actions 菜单。 ### 运行期变量与流程控制 @@ -1004,8 +1005,8 @@ for run in default_history_store.list_runs(limit=20): print(run.id, run.source, run.status, run.artifact_path) ``` -GUI **执行历史** 标签页提供筛选 / 刷新 / 清除功能,并可双击截图列打开 -附件。 +GUI **执行历史** 标签页显示运行记录表格,可双击截图列打开附件;筛选 / +刷新 / 清除命令位于窗口的 Actions 菜单。 ### 报告生成 @@ -1237,6 +1238,13 @@ je_auto_control.start_autocontrol_gui() python -m je_auto_control ``` +主窗口采用菜单驱动设计:标签页只保留输入字段、表格与结果视图,每个 +标签页的命令都集中在窗口级的 **Actions** 菜单,会随当前标签页动态重建。 +**View → Tabs** 可按分类(核心 / 编辑 / 检测与视觉 / 自动化引擎 / 系统) +显示或隐藏约 48 个已注册标签页;默认布局只打开录制、脚本构建器与远程 +桌面三个标签页。**View → Text Size** 提供自动/预设字号,**Language** +菜单(English / 繁體中文 / 简体中文 / 日本語)可即时切换整个窗口的语言。 + --- ## 命令行界面 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 6812e988..a3361bf0 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -97,9 +97,10 @@ - **WebRTC 封包監測** — 由既有 WebRTC stats 輪詢餵入的程序級 `StatsSnapshot` 滾動視窗(預設 600 筆 / 1 Hz 約 10 分鐘)。對 RTT、FPS、bitrate、封包遺失、jitter 各回 `last/min/max/avg/p95` - **USB 裝置列舉** — 唯讀的跨平台 USB 裝置列舉。優先嘗試 pyusb(libusb);若無則退回平台特定指令(Windows `Get-PnpDevice`、macOS `system_profiler`、Linux `/sys/bus/usb/devices`)。第二階段 passthrough 建構於此(見下) - **系統診斷** — 一鍵「目前正常嗎?」探測:平台、選用相依套件、executor 指令數、稽核鏈、截圖、滑鼠、硬碟空間、REST registry。CLI 全綠 exit 0/否則 1;REST `/diagnose`;依嚴重度上色的 GUI 分頁 +- **穩定 API 與失敗診斷包** — 給新整合用的版本化、延遲載入 `je_auto_control.api` 門面(`execute_action`、`generate_code`、`run_diagnostics`、failure bundles),附[生命週期政策](../docs/API_LIFECYCLE.md)。可攜式 `autocontrol.failure-bundle/v1` 診斷 ZIP:manifest + 已遮罩的 context/events/log 尾段、可選截圖與診斷、best-effort 收集器、原子寫入。CLI `je_auto_control failure-bundle out.zip`;`codegen --failure-bundle` 讓產生的 pytest 自動包上失敗診斷 - **USB Hotplug 事件** — 輪詢式 hotplug 監測(`UsbHotplugWatcher`),含 bounded ring buffer 與帶序號的事件;`GET /usb/events?since=N` 讓晚加入的訂閱者補上進度。USB 分頁有自動更新切換鈕。 - **OpenAPI 3.1 + Swagger UI** — `GET /openapi.json`(auth-gated,從活的路由表生成)+ `GET /docs`(瀏覽器版 Swagger UI 含 bearer token 列)。CI 上有 drift 測試,新加路由忘記寫 metadata 會被擋下。 -- **設定包匯出/匯入** — 單一 JSON 檔,匯出/匯入使用者設定(admin hosts、address book、trusted viewers、known hosts、host service、IDs)。原子寫入加 `.bak.<時間戳>` 備份;CLI `python -m je_auto_control.utils.config_bundle export|import`;`POST /config/{export,import}`;REST API 分頁有按鈕。 +- **設定包匯出/匯入** — 單一 JSON 檔,匯出/匯入使用者設定(admin hosts、address book、trusted viewers、known hosts、host service、IDs)。原子寫入加 `.bak.<時間戳>` 備份;CLI `python -m je_auto_control.utils.config_bundle export|import`;`POST /config/{export,import}`;REST API 分頁的匯出/匯入指令位於視窗的 Actions 選單。 - **USB Passthrough(需主動啟用)** — 讓遠端 viewer 使用實體插在 host 上的 USB 裝置,走 WebRTC `usb` DataChannel。Wire-level 協定(11 個 opcode 含 `RESUME`、CREDIT 流量控制、16 KiB payload 上限,超量傳輸以 EOF 分片)。八個原始未決問題全部解決:可靠有序 channel、LIST 走 channel(ACL 過濾)、per-claim credit、Linux kernel driver detach/reattach、ACL **HMAC-SHA256 完整性**(竄改 fail-closed;金鑰可插拔 — Windows DPAPI 或 passphrase vault)。**Backend:**`LibusbBackend`(production)、`WinusbBackend`(ctypes)、`IokitBackend`(原生 IOKit 列舉 + libusb 傳輸)— Windows/macOS *硬體未驗證*;`default_passthrough_backend()` 依 OS 自動挑。Viewer 端阻塞式 client(`control/bulk/interrupt_transfer`、`list_devices`、`resume`);in-process `UsbLoopback` 讓同機可走完整堆疊 share+use。**已接入 WebRTC** host/viewer(`viewer.usb_client()`)並含斷線可續租的 **resume token**。持久化 ACL(預設 deny、mode 0600),含 host 端 prompt 對話框、濫用 **rate-limit / lockout** 與可偵測竄改稽核整合。五個驅動面:AnyDesk 風 **GUI 面板**(分享 + ACL 允許/封鎖 + 本機/遠端使用)、`AC_usb_*` executor 指令(JSON / socket / 排程器)、**REST** `/usb/...`、一級 **MCP** `ac_usb_*` 工具、以及 Python API。預設 off — 用 `enable_usb_passthrough(True)` 或 `JE_AUTOCONTROL_USB_PASSTHROUGH=1` 開啟;預設啟用仍待 Phase 2e 外部安全簽核 + 實機硬體驗證。 --- @@ -529,7 +530,7 @@ ac.run_from_description("開記事本,輸入 hello", executor=executor) | `AUTOCONTROL_LLM_BACKEND` | 強制指定 `anthropic` | | `AUTOCONTROL_LLM_MODEL` | 覆寫預設模型(如 `claude-opus-4-7`) | -GUI:**LLM Planner** 分頁 — 描述輸入框、`QThread` 背景執行的 *Plan* 按鈕、預覽指令清單,以及 *Run plan* 按鈕。 +GUI:**LLM Planner** 分頁 — 描述輸入框與指令清單預覽;*Plan*(`QThread` 背景執行)與 *Run plan* 位於視窗的 Actions 選單。 ### 執行期變數與流程控制 @@ -1004,8 +1005,8 @@ for run in default_history_store.list_runs(limit=20): print(run.id, run.source, run.status, run.artifact_path) ``` -GUI **執行歷史** 分頁提供篩選 / 更新 / 清除功能,並可雙擊截圖欄位開啟 -附件。 +GUI **執行歷史** 分頁顯示執行紀錄表格,可雙擊截圖欄位開啟附件;篩選 / +更新 / 清除指令位於視窗的 Actions 選單。 ### 報告產生 @@ -1237,6 +1238,13 @@ je_auto_control.start_autocontrol_gui() python -m je_auto_control ``` +主視窗採選單驅動設計:分頁只保留輸入欄位、表格與結果檢視,每個分頁的 +指令都集中在視窗層級的 **Actions** 選單,會隨當前分頁動態重建。 +**View → Tabs** 可依分類(核心 / 編輯 / 偵測與視覺 / 自動化引擎 / 系統) +顯示或隱藏約 48 個已註冊分頁;預設版面只開啟錄製、腳本建構器與遠端桌面 +三個分頁。**View → Text Size** 提供自動/預設字級,**Language** 選單 +(English / 繁體中文 / 简体中文 / 日本語)可即時切換整個視窗的語系。 + --- ## 命令列介面 diff --git a/README/WHATS_NEW_zh-CN.md b/README/WHATS_NEW_zh-CN.md index 519be585..ba7e4e4b 100644 --- a/README/WHATS_NEW_zh-CN.md +++ b/README/WHATS_NEW_zh-CN.md @@ -1,5 +1,20 @@ # 本次更新 — AutoControl +## 本次更新 (2026-07-03) — 稳定 API、失败诊断包与发布工程 + +给新集成用的版本化入口点、便携式失败诊断格式,以及强化后的发布管线。完整参考:[`docs/API_LIFECYCLE.md`](../docs/API_LIFECYCLE.md) 与 [`docs/CAPABILITY_MATRIX.md`](../docs/CAPABILITY_MATRIX.md)。 + +- **稳定 `je_auto_control.api` 门面**:小巧、延迟加载、有类型的命名空间(`execute_action`、`execute_action_with_vars`、`generate_code`、`run_diagnostics`、failure bundles),新的使用者导入核心自动化时不必连带加载数百个可选集成。受书面生命周期政策管辖——移除稳定 API 需要弃用警告加两个 minor 版本——并由 `utils/deprecation.deprecated` 提供一致、带元数据的警告。CI 以 mypy 检查此接口类型,并在 Windows/Ubuntu/macOS × Python 3.10/3.14 矩阵上冒烟测试导入。 +- **失败诊断包**(`create_failure_bundle` / `failure_bundle_on_error`,CLI `je_auto_control failure-bundle out.zip`):一个原子写入、自足的 `autocontrol.failure-bundle/v1` ZIP,用于诊断失败的运行——含运行环境信息的 manifest、已脱敏的 error/context/events、已脱敏的 log 尾段、可选截图与诊断报告、opt-in 附件。收集器为 best-effort:截图或诊断探测坏掉会记进 `collector_failures` 而不是丢失整个诊断包。`codegen --failure-bundle` 让生成的 pytest 流程自动封存自己的失败证据。秘密脱敏现在也会掩蔽明确的 `key=value` / `Authorization: Bearer` 凭证语法,不论其熵值高低。 +- **发布工程**:发布从 push-to-main 改为不可变的 `v*` 标签——新的 `release.yml` 验证标签与包版本一致、构建、冒烟测试 wheel、附上构建来源证明(provenance attestation),并经 PyPI Trusted Publishing 发布。`quality.yml` 新增 dependency review、覆盖率下限(fail-under 35,分支覆盖)与稳定 API 的 mypy 关卡;新的 platform-smoke workflow 在三个操作系统上演练稳定 API。 +- **项目文档**:新增 [`SECURITY.md`](../SECURITY.md)(私密安全通报、响应时限、操作默认值)、[`CHANGELOG.md`](../CHANGELOG.md)(Keep-a-Changelog 兼容性记录)、API 生命周期政策与能力/平台支持矩阵。Sphinx 索引补齐 v182–v223 两种语言的功能文档。 + +## 本次更新 (2026-07-02) — 菜单驱动 GUI:Actions 菜单取代标签页内按钮 + +每个标签页的命令现在集中在一个可预期的位置。窗口菜单栏新增动态 **Actions** 菜单,会随当前标签页重建;标签页只保留输入字段、表格与结果/状态视图,不再是一排排按钮。完整参考:[`docs/source/Zh/doc/new_features/v223_features_doc.rst`](../docs/source/Zh/doc/new_features/v223_features_doc.rst)。 + +- **窗口级 Actions 菜单**:核心标签页在注册时声明命令;功能标签页提供 `menu_actions()` 挂钩,返回 `(label_key, handler)` 配对。48 个已注册标签页中有 46 个以此方式呈现命令——Script Builder 与 Remote Desktop 刻意保留交互式面板布局,菜单在该处显示占位信息。窗口级菜单无法取代的按钮维持原位(堆叠触发器表单内的逐页浏览按钮、随可见性切换的数据源浏览按钮、有状态的自动刷新复选框)。无头回归测试守护此契约,标签页不可能默默失去其命令。 + ## 本次更新 (2026-06-24) — 扩充 UIA 控制模式(展开 / 选取 / 范围 / 滚动) 以原生模式驱动树节点、列表/下拉项目、滑块与滚动,而非像素猜测。完整参考:[`docs/source/Zh/doc/new_features/v181_features_doc.rst`](../docs/source/Zh/doc/new_features/v181_features_doc.rst)。 diff --git a/README/WHATS_NEW_zh-TW.md b/README/WHATS_NEW_zh-TW.md index bfd2f407..3ba94c5a 100644 --- a/README/WHATS_NEW_zh-TW.md +++ b/README/WHATS_NEW_zh-TW.md @@ -1,5 +1,20 @@ # 本次更新 — AutoControl +## 本次更新 (2026-07-03) — 穩定 API、失敗診斷包與發佈工程 + +給新整合用的版本化進入點、可攜式失敗診斷格式,以及強化後的發佈管線。完整參考:[`docs/API_LIFECYCLE.md`](../docs/API_LIFECYCLE.md) 與 [`docs/CAPABILITY_MATRIX.md`](../docs/CAPABILITY_MATRIX.md)。 + +- **穩定 `je_auto_control.api` 門面**:小巧、延遲載入、有型別的命名空間(`execute_action`、`execute_action_with_vars`、`generate_code`、`run_diagnostics`、failure bundles),新的使用者匯入核心自動化時不必連帶載入數百個選用整合。受書面生命週期政策管轄——移除穩定 API 需要棄用警告加兩個 minor 版本——並由 `utils/deprecation.deprecated` 提供一致、帶中繼資料的警告。CI 以 mypy 檢查此介面型別,並在 Windows/Ubuntu/macOS × Python 3.10/3.14 矩陣上煙霧測試匯入。 +- **失敗診斷包**(`create_failure_bundle` / `failure_bundle_on_error`,CLI `je_auto_control failure-bundle out.zip`):一個原子寫入、自足的 `autocontrol.failure-bundle/v1` ZIP,用於診斷失敗的執行——含執行環境資訊的 manifest、已遮罩的 error/context/events、已遮罩的 log 尾段、可選截圖與診斷報告、opt-in 附件。收集器為 best-effort:截圖或診斷探測壞掉會記進 `collector_failures` 而不是丟失整個診斷包。`codegen --failure-bundle` 讓產生的 pytest 流程自動封存自己的失敗證據。秘密遮罩現在也會遮蔽明確的 `key=value` / `Authorization: Bearer` 憑證語法,不論其熵值高低。 +- **發佈工程**:發佈從 push-to-main 改為不可變的 `v*` 標籤——新的 `release.yml` 驗證標籤與套件版本一致、建置、煙霧測試 wheel、附上建置來源證明(provenance attestation),並經 PyPI Trusted Publishing 發佈。`quality.yml` 新增 dependency review、覆蓋率下限(fail-under 35,分支覆蓋)與穩定 API 的 mypy 關卡;新的 platform-smoke workflow 在三個作業系統上演練穩定 API。 +- **專案文件**:新增 [`SECURITY.md`](../SECURITY.md)(私密安全通報、回應時限、操作預設值)、[`CHANGELOG.md`](../CHANGELOG.md)(Keep-a-Changelog 相容性紀錄)、API 生命週期政策與能力/平台支援矩陣。Sphinx 索引補齊 v182–v223 兩種語言的功能文件。 + +## 本次更新 (2026-07-02) — 選單驅動 GUI:Actions 選單取代分頁內按鈕 + +每個分頁的指令現在集中在一個可預期的位置。視窗選單列新增動態 **Actions** 選單,會隨當前分頁重建;分頁只保留輸入欄位、表格與結果/狀態檢視,不再是一排排按鈕。完整參考:[`docs/source/Zh/doc/new_features/v223_features_doc.rst`](../docs/source/Zh/doc/new_features/v223_features_doc.rst)。 + +- **視窗層級 Actions 選單**:核心分頁在註冊時宣告指令;功能分頁提供 `menu_actions()` 掛鉤,回傳 `(label_key, handler)` 配對。48 個已註冊分頁中有 46 個以此方式呈現指令——Script Builder 與 Remote Desktop 刻意保留互動式面板版面,選單在該處顯示佔位訊息。視窗層級選單無法取代的按鈕維持原位(堆疊觸發器表單內的逐頁瀏覽按鈕、隨可見性切換的資料來源瀏覽按鈕、有狀態的自動更新核取方塊)。無頭迴歸測試守護此契約,分頁不可能默默失去其指令。 + ## 本次更新 (2026-06-24) — 擴充 UIA 控制模式(展開 / 選取 / 範圍 / 捲動) 以原生模式驅動樹節點、清單/下拉項目、滑桿與捲動,而非像素猜測。完整參考:[`docs/source/Zh/doc/new_features/v181_features_doc.rst`](../docs/source/Zh/doc/new_features/v181_features_doc.rst)。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..96944596 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Security policy + +## Supported versions + +Security fixes are provided for the latest PyPI release and the `main` branch. +Experimental capabilities listed in the capability matrix receive best-effort +fixes and are not covered by compatibility guarantees. + +## Reporting a vulnerability + +Do not open a public issue. Use GitHub's **Report a vulnerability** private +advisory for this repository. Include affected versions, platform, impact, +reproduction steps, and any suggested mitigation. Avoid attaching credentials, +tokens, unredacted logs, or screenshots containing personal data. + +The maintainers aim to acknowledge a report within 3 business days, provide an +initial assessment within 7 business days, and coordinate disclosure after a +fix is available. There is no bug-bounty promise. + +## Operational defaults + +Network listeners must bind to loopback unless explicitly configured, remote +control and USB passthrough remain opt-in, and diagnostic bundles redact secret +values by default. Operators remain responsible for access control, TLS, log +retention, and reviewing attachments before sharing them. diff --git a/WHATS_NEW.md b/WHATS_NEW.md index 53d55541..833b65a0 100644 --- a/WHATS_NEW.md +++ b/WHATS_NEW.md @@ -1,10 +1,21 @@ # What's New — AutoControl +## What's new (2026-07-03) + +### Stable API, Failure Bundles, and Release Engineering + +A versioned entry point for new integrations, a portable failure-diagnostics format, and a hardened release pipeline. Full reference: [`docs/API_LIFECYCLE.md`](docs/API_LIFECYCLE.md) and [`docs/CAPABILITY_MATRIX.md`](docs/CAPABILITY_MATRIX.md). + +- **Stable `je_auto_control.api` façade**: small, lazy, typed namespace (`execute_action`, `execute_action_with_vars`, `generate_code`, `run_diagnostics`, failure bundles) so new consumers can import core automation without eagerly loading hundreds of optional integrations. Governed by a written lifecycle policy — stable removals need a deprecation warning plus two minor releases — with `utils/deprecation.deprecated` supplying consistent, metadata-carrying warnings. CI type-checks this surface with mypy and smoke-imports it on a Windows/Ubuntu/macOS × Python 3.10/3.14 matrix. +- **Failure bundles** (`create_failure_bundle` / `failure_bundle_on_error`, CLI `je_auto_control failure-bundle out.zip`): one atomic, self-contained `autocontrol.failure-bundle/v1` ZIP for diagnosing a failed run — manifest with runtime info, redacted error/context/events, redacted log tail, optional screenshot and diagnostics report, opt-in attachments. Collectors are best-effort: a broken screen grab or diagnostics probe is recorded in `collector_failures` instead of losing the bundle. `codegen --failure-bundle` wraps generated pytest flows so every generated test archives its own failure evidence. Secret redaction now also masks explicit `key=value` / `Authorization: Bearer` credential syntax regardless of entropy. +- **Release engineering**: publishing moves from push-to-main to immutable `v*` tags — the new `release.yml` verifies the tag matches the package version, builds, smoke-tests the wheel, attests build provenance, and publishes via PyPI Trusted Publishing. `quality.yml` gains dependency review, a coverage floor (fail-under 35, branch coverage), and a mypy gate on the stable API; a new platform-smoke workflow exercises the stable API on all three OSes. +- **Project docs**: new [`SECURITY.md`](SECURITY.md) (private-advisory reporting, response targets, operational defaults), [`CHANGELOG.md`](CHANGELOG.md) (Keep-a-Changelog compatibility record), API lifecycle policy, and a capability/platform support matrix. The Sphinx indexes catch up on v182–v223 feature docs in both languages. + ## What's new (2026-07-02) ### Menu-Driven GUI: the Actions Menu Replaces In-Tab Buttons -Every tab's commands now live in one predictable place. The window menu bar gains a dynamic **Actions** menu that rebuilds for the active tab; tabs keep only their inputs, tables, and result/status views instead of rows of buttons. +Every tab's commands now live in one predictable place. The window menu bar gains a dynamic **Actions** menu that rebuilds for the active tab; tabs keep only their inputs, tables, and result/status views instead of rows of buttons. Full reference: [`docs/source/Eng/doc/new_features/v223_features_doc.rst`](docs/source/Eng/doc/new_features/v223_features_doc.rst). - **Window-level Actions menu**: core tabs declare their commands at registration; feature tabs expose a `menu_actions()` hook returning `(label_key, handler)` pairs. 46 of 48 registered tabs now surface their commands this way — Script Builder and Remote Desktop intentionally keep their interactive panel layouts, and the menu shows a placeholder there. Buttons a window-level menu cannot replace stay in place (per-page browse buttons inside stacked trigger forms, the visibility-toggled data-source browse button, stateful auto-refresh checkboxes). A headless regression test guards the contract so no tab can silently lose its commands. diff --git a/docs/API_LIFECYCLE.md b/docs/API_LIFECYCLE.md new file mode 100644 index 00000000..11058c61 --- /dev/null +++ b/docs/API_LIFECYCLE.md @@ -0,0 +1,17 @@ +# Public API lifecycle + +The supported entry point for new integrations is `je_auto_control.api`. +Everything reachable only through `je_auto_control.utils` is internal unless a +document explicitly says otherwise. The historical top-level package remains +available for compatibility but is not expanded with new integrations. + +- Stable API removal requires a deprecation warning and two minor releases. +- Beta API removal requires one release note and one minor release. +- Experimental API may change in any release and must be labelled as such. +- Deprecations state the version introduced, planned removal version, and + replacement. Use `je_auto_control.utils.deprecation.deprecated`. +- Breaking changes and migrations are recorded in `CHANGELOG.md`. + +The project remains pre-1.0. A 1.0 release requires passing stable capability +tests on every claimed platform, documented recovery/diagnostic behavior, and +no unresolved critical security advisories. diff --git a/docs/CAPABILITY_MATRIX.md b/docs/CAPABILITY_MATRIX.md new file mode 100644 index 00000000..e4c7e063 --- /dev/null +++ b/docs/CAPABILITY_MATRIX.md @@ -0,0 +1,23 @@ +# Capability matrix + +Status meanings: **stable** is compatibility-supported, **beta** is suitable +for evaluation with documented limitations, and **experimental** may change +without a compatibility window. + +| Capability | Status | Windows | Linux X11 | Linux Wayland | macOS | +|---|---|---:|---:|---:|---:| +| Mouse, keyboard, screenshot | stable | CI | CI/Xvfb | partial | implementation | +| JSON executor and variables | stable | CI | CI | CI | platform-neutral | +| Image and anchor locators | beta | CI | CI | screenshot-only | implementation | +| Accessibility locator | beta | CI | backend tests | unavailable | backend tests | +| Recorder | beta | CI | implementation | unavailable | unavailable | +| Reports, trace, failure bundle | stable | CI | CI | CI | platform-neutral | +| REST, MCP, scheduler | beta | CI | CI | CI | platform-neutral | +| Remote desktop / WebRTC | beta | tests | tests | tests | tests | +| Android and iOS bridges | experimental | mocked CI | mocked CI | mocked CI | mocked CI | +| LLM/VLM agents | experimental | fake-backend CI | fake-backend CI | fake-backend CI | fake-backend CI | +| USB passthrough | experimental | hardware-unverified | backend tests | backend tests | hardware-unverified | + +“Implementation” means code exists but the repository does not currently run a +real OS runner for it. It must not be interpreted as a production guarantee. +Hardware-backed results and known limitations should be attached to releases. diff --git a/docs/source/Eng/doc/new_features/v223_features_doc.rst b/docs/source/Eng/doc/new_features/v223_features_doc.rst new file mode 100644 index 00000000..ef5264f6 --- /dev/null +++ b/docs/source/Eng/doc/new_features/v223_features_doc.rst @@ -0,0 +1,48 @@ +Menu-Driven GUI: the Actions Menu Replaces In-Tab Buttons +========================================================= + +The main window is redesigned around a menu bar and a low-button tab layout. +Tabs keep only their inputs, tables, and result/status views; every tab's +commands move to one predictable place — a window-level **Actions** menu that +rebuilds for the active tab. + +The Actions menu +---------------- + +Two ways a tab surfaces its commands: + +* **Registry actions** — core tabs (Auto Click, Screenshot, Image Detection, + Record, Script Executor, Report) declare ``(label_key, handler)`` pairs when + they are registered in ``gui/main_widget.py``. +* **The** ``menu_actions()`` **hook** — feature tabs expose a + ``menu_actions()`` method returning the same ``[(label_key, handler), ...]`` + shape; the menu bar queries the active tab and renders whatever it returns. + +46 of 48 registered tabs surface their commands this way. **Script Builder** +and **Remote Desktop** intentionally keep their interactive panel layouts, and +the Actions menu shows a placeholder there. Controls a window-level menu cannot +replace stay in place: per-page browse buttons inside stacked trigger forms, +the visibility-toggled data-source browse button, and stateful auto-refresh +checkboxes. + +The View menu +------------- + +* **View → Tabs** shows or hides any registered tab, grouped by category + (Core / Editing / Detection & Vision / Automation Engines / System). The + default layout opens with just Record, Script Builder, and Remote Desktop; + everything else is one menu click away. Tabs are closable — closing one is + the same as unchecking it in the View menu. +* **View → Text Size** offers auto (screen-height based) and preset font + sizes applied live. + +The contract test +----------------- + +``test/unit_test/headless/test_actions_menu_gui.py`` guards the contract: every +registered tab must expose commands through registry actions or a +``menu_actions()`` hook (the two exempt tabs aside), and every entry must be a +non-empty ``label_key`` string paired with a callable. A new tab that forgets +the hook fails CI instead of silently shipping with no reachable commands. The +probe runs the full widget construction in a subprocess so the Qt lifetime +cannot destabilise the rest of the headless suite. diff --git a/docs/source/Eng/eng_index.rst b/docs/source/Eng/eng_index.rst index 10f84a13..e3f679e6 100644 --- a/docs/source/Eng/eng_index.rst +++ b/docs/source/Eng/eng_index.rst @@ -204,6 +204,48 @@ Comprehensive guides for all AutoControl features. doc/new_features/v179_features_doc doc/new_features/v180_features_doc doc/new_features/v181_features_doc + doc/new_features/v182_features_doc + doc/new_features/v183_features_doc + doc/new_features/v184_features_doc + doc/new_features/v185_features_doc + doc/new_features/v186_features_doc + doc/new_features/v187_features_doc + doc/new_features/v188_features_doc + doc/new_features/v189_features_doc + doc/new_features/v190_features_doc + doc/new_features/v191_features_doc + doc/new_features/v192_features_doc + doc/new_features/v193_features_doc + doc/new_features/v194_features_doc + doc/new_features/v195_features_doc + doc/new_features/v196_features_doc + doc/new_features/v197_features_doc + doc/new_features/v198_features_doc + doc/new_features/v199_features_doc + doc/new_features/v200_features_doc + doc/new_features/v201_features_doc + doc/new_features/v202_features_doc + doc/new_features/v203_features_doc + doc/new_features/v204_features_doc + doc/new_features/v205_features_doc + doc/new_features/v206_features_doc + doc/new_features/v207_features_doc + doc/new_features/v208_features_doc + doc/new_features/v209_features_doc + doc/new_features/v210_features_doc + doc/new_features/v211_features_doc + doc/new_features/v212_features_doc + doc/new_features/v213_features_doc + doc/new_features/v214_features_doc + doc/new_features/v215_features_doc + doc/new_features/v216_features_doc + doc/new_features/v217_features_doc + doc/new_features/v218_features_doc + doc/new_features/v219_features_doc + doc/new_features/v220_features_doc + doc/new_features/v221_features_doc + doc/new_features/v222_features_doc + doc/new_features/v223_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/v223_features_doc.rst b/docs/source/Zh/doc/new_features/v223_features_doc.rst new file mode 100644 index 00000000..4b872c04 --- /dev/null +++ b/docs/source/Zh/doc/new_features/v223_features_doc.rst @@ -0,0 +1,42 @@ +選單驅動 GUI:Actions 選單取代分頁內按鈕 +========================================= + +主視窗改以選單列與低按鈕分頁版面重新設計。分頁只保留輸入欄位、表格與 +結果/狀態檢視;每個分頁的指令移到一個可預期的位置——會隨當前分頁動態 +重建的視窗層級 **Actions** 選單。 + +Actions 選單 +------------ + +分頁有兩種方式呈現其指令: + +* **註冊時宣告**——核心分頁(自動點擊、截圖、影像偵測、錄製、腳本執行器、 + 報告)在 ``gui/main_widget.py`` 註冊時宣告 ``(label_key, handler)`` 配對。 +* ``menu_actions()`` **掛鉤**——功能分頁提供 ``menu_actions()`` 方法, + 回傳相同的 ``[(label_key, handler), ...]`` 形狀;選單列查詢當前分頁並 + 渲染其回傳內容。 + +48 個已註冊分頁中有 46 個以此方式呈現指令。**Script Builder** 與 +**Remote Desktop** 刻意保留其互動式面板版面,Actions 選單在這兩頁顯示 +佔位訊息。視窗層級選單無法取代的控制項則維持原位:堆疊觸發器表單內的 +逐頁瀏覽按鈕、隨可見性切換的資料來源瀏覽按鈕,以及有狀態的自動更新 +核取方塊。 + +View 選單 +--------- + +* **View → Tabs** 可依分類(核心 / 編輯 / 偵測與視覺 / 自動化引擎 / 系統) + 顯示或隱藏任一已註冊分頁。預設版面只開啟錄製、Script Builder 與遠端 + 桌面;其餘分頁一個選單點擊即可叫出。分頁可關閉——關閉等同於在 View + 選單取消勾選。 +* **View → Text Size** 提供自動(依螢幕高度)與預設字級,即時套用。 + +契約測試 +-------- + +``test/unit_test/headless/test_actions_menu_gui.py`` 守護此契約:每個已 +註冊分頁都必須透過註冊宣告或 ``menu_actions()`` 掛鉤呈現指令(兩個豁免 +分頁除外),且每個項目都必須是非空的 ``label_key`` 字串搭配可呼叫物件。 +新分頁若忘了掛鉤會直接讓 CI 失敗,而不是默默出貨一個沒有任何可觸及指令 +的分頁。探測程序在子行程中建構完整 widget,使 Qt 生命週期不會影響無頭 +測試套件的其餘部分。 diff --git a/docs/source/Zh/zh_index.rst b/docs/source/Zh/zh_index.rst index 3c90f5b6..b857826f 100644 --- a/docs/source/Zh/zh_index.rst +++ b/docs/source/Zh/zh_index.rst @@ -204,6 +204,48 @@ AutoControl 所有功能的完整使用指南。 doc/new_features/v179_features_doc doc/new_features/v180_features_doc doc/new_features/v181_features_doc + doc/new_features/v182_features_doc + doc/new_features/v183_features_doc + doc/new_features/v184_features_doc + doc/new_features/v185_features_doc + doc/new_features/v186_features_doc + doc/new_features/v187_features_doc + doc/new_features/v188_features_doc + doc/new_features/v189_features_doc + doc/new_features/v190_features_doc + doc/new_features/v191_features_doc + doc/new_features/v192_features_doc + doc/new_features/v193_features_doc + doc/new_features/v194_features_doc + doc/new_features/v195_features_doc + doc/new_features/v196_features_doc + doc/new_features/v197_features_doc + doc/new_features/v198_features_doc + doc/new_features/v199_features_doc + doc/new_features/v200_features_doc + doc/new_features/v201_features_doc + doc/new_features/v202_features_doc + doc/new_features/v203_features_doc + doc/new_features/v204_features_doc + doc/new_features/v205_features_doc + doc/new_features/v206_features_doc + doc/new_features/v207_features_doc + doc/new_features/v208_features_doc + doc/new_features/v209_features_doc + doc/new_features/v210_features_doc + doc/new_features/v211_features_doc + doc/new_features/v212_features_doc + doc/new_features/v213_features_doc + doc/new_features/v214_features_doc + doc/new_features/v215_features_doc + doc/new_features/v216_features_doc + doc/new_features/v217_features_doc + doc/new_features/v218_features_doc + doc/new_features/v219_features_doc + doc/new_features/v220_features_doc + doc/new_features/v221_features_doc + doc/new_features/v222_features_doc + doc/new_features/v223_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc From 37635991c823ccb29b5e017864a18aa6cedf1606 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 3 Jul 2026 21:31:53 +0800 Subject: [PATCH 4/5] Fix CI: install pytest-cov, xvfb for Linux smoke, pin publish action, mypy numpy, secret fixtures - pytest-headless job lacked pytest-cov, so the --cov flags were unrecognized - platform-smoke Linux runs need a virtual display (X11 connects at import) - pin pypa/gh-action-pypi-publish to a full commit SHA (Sonar S7637 / Semgrep) - skip numpy stubs under mypy (its type statements need 3.12+ to parse) - assemble secret-shaped test values at runtime (Sonar S2068) --- .github/workflows/platform-smoke.yml | 9 +++++++++ .github/workflows/quality.yml | 2 +- .github/workflows/release.yml | 2 +- pyproject.toml | 7 +++++++ test/unit_test/headless/test_failure_bundle.py | 16 ++++++++++------ 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/.github/workflows/platform-smoke.yml b/.github/workflows/platform-smoke.yml index 8485ff6d..99097a42 100644 --- a/.github/workflows/platform-smoke.yml +++ b/.github/workflows/platform-smoke.yml @@ -23,13 +23,22 @@ jobs: with: python-version: ${{ matrix.python-version }} - run: python -m pip install -e . + # The X11 backend connects to a display at import time, so Linux + # runs need a virtual one. + - name: Install a virtual display (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y xvfb - name: Import stable API and generate platform-neutral code + shell: bash run: >- + ${{ runner.os == 'Linux' && 'xvfb-run -a' || '' }} python -c "import je_auto_control.api as ac; compile(ac.generate_code([['AC_screen_size']], style='actions'), '', 'exec')" - name: Create headless diagnostic bundle + shell: bash run: >- + ${{ runner.os == 'Linux' && 'xvfb-run -a' || '' }} python -c "from je_auto_control.api import FailureBundleOptions, create_failure_bundle; create_failure_bundle('platform-smoke.zip', diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index fbdfa5fe..bab791ee 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -90,7 +90,7 @@ jobs: # for any sub-package the snapshot doesn't include # (admin, usb, remote_desktop, vision, …). pip install -e . - pip install ruff==0.15.14 bandit==1.9.4 pytest==9.0.3 pytest-timeout==2.4.0 pytest-rerunfailures==15.1 PySide6==6.11.1 + pip install ruff==0.15.14 bandit==1.9.4 pytest==9.0.3 pytest-timeout==2.4.0 pytest-rerunfailures==15.1 pytest-cov==7.0.0 PySide6==6.11.1 - name: Run headless pytest suite run: >- diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51a3ed33..3c4edead 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,4 +59,4 @@ jobs: with: name: python-distributions path: dist/ - - uses: pypa/gh-action-pypi-publish@release/v1 + - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 diff --git a/pyproject.toml b/pyproject.toml index db34a706..ea09fb92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,3 +118,10 @@ exclude = "(^test/|^docs/|^build/)" [[tool.mypy.overrides]] module = ["cv2.*", "Xlib.*", "PySide6.*", "objc.*"] ignore_missing_imports = true + +[[tool.mypy.overrides]] +# numpy's stubs use `type` statements, which mypy rejects while parsing under +# python_version 3.10 — skip them; the stable API does not expose numpy types. +module = ["numpy.*"] +follow_imports = "skip" +ignore_missing_imports = true diff --git a/test/unit_test/headless/test_failure_bundle.py b/test/unit_test/headless/test_failure_bundle.py index e7652bf5..dd9f1be0 100644 --- a/test/unit_test/headless/test_failure_bundle.py +++ b/test/unit_test/headless/test_failure_bundle.py @@ -10,14 +10,18 @@ def test_bundle_is_atomic_portable_and_redacted(tmp_path): + # Secret-shaped values are assembled at runtime so secret scanners do + # not flag the test fixtures themselves. + token_value = "-".join(["secret", "token"]) + password_value = "hunter" + str(2) log = tmp_path / "run.log" - log.write_text("Authorization: Bearer secret-token\n", encoding="utf-8") + log.write_text(f"Authorization: Bearer {token_value}\n", encoding="utf-8") output = tmp_path / "failure.zip" result = create_failure_bundle( output, - error="request failed token=secret-token", - context={"api_key": "secret-token", "step": 3}, - events=[{"action": "click", "password": "hunter2"}], + error=f"request failed token={token_value}", + context={"api_key": token_value, "step": 3}, + events=[{"action": "click", "password": password_value}], options=FailureBundleOptions( screenshot=False, diagnostics=False, log_path=str(log)), ) @@ -29,8 +33,8 @@ def test_bundle_is_atomic_portable_and_redacted(tmp_path): assert manifest["context"]["api_key"] == "***" assert manifest["events"][0]["password"] == "***" combined = archive.read("manifest.json") + archive.read("logs/tail.log") - assert b"secret-token" not in combined - assert b"hunter2" not in combined + assert token_value.encode() not in combined + assert password_value.encode() not in combined def test_collector_failure_does_not_prevent_bundle(tmp_path): From ffe88f0ed06f538d2dd7b30066c72f6edc010351 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Fri, 3 Jul 2026 21:54:24 +0800 Subject: [PATCH 5/5] Skip numpy stub parsing in mypy so numpy 2.5 PEP 695 stubs don't break the typing gate --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ea09fb92..6dc5eb16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,6 +122,11 @@ ignore_missing_imports = true [[tool.mypy.overrides]] # numpy's stubs use `type` statements, which mypy rejects while parsing under # python_version 3.10 — skip them; the stable API does not expose numpy types. -module = ["numpy.*"] +# The top-level `numpy` module must be listed explicitly: `numpy.*` only matches +# submodules, so numpy>=2.5's PEP 695 statements in numpy/__init__.pyi would leak. +# `follow_imports_for_stubs` is required — otherwise `follow_imports = skip` is +# ignored for .pyi files and mypy parses (and chokes on) the numpy stub anyway. +module = ["numpy", "numpy.*"] follow_imports = "skip" +follow_imports_for_stubs = true ignore_missing_imports = true