diff --git a/packages/experimental/cua-macos/native/README.i18n.yaml b/packages/experimental/cua-macos/native/README.i18n.yaml index 98dc70f933..6637bab31e 100644 --- a/packages/experimental/cua-macos/native/README.i18n.yaml +++ b/packages/experimental/cua-macos/native/README.i18n.yaml @@ -24,8 +24,8 @@ en: c527eedc3a0ee968 zh: ce8cc12d5a62e8ae /dsh-cua-native-helper/verification: - en: a40f5a47ea65d04b - zh: deb40dceac90d311 + en: b4be018ae95b449d + zh: e604f17c8c81047b /dsh-cua-native-helper/further-exploration: en: 265e1927952ce6d7 zh: 7c35d9fd98b6e6c8 diff --git a/packages/experimental/cua-macos/native/README.md b/packages/experimental/cua-macos/native/README.md index 32dd41ea2e..7c48bf3634 100644 --- a/packages/experimental/cua-macos/native/README.md +++ b/packages/experimental/cua-macos/native/README.md @@ -105,7 +105,7 @@ Use `python3 packages/experimental/cua-macos/native/tests/test_swift.py` for off Run `python3 packages/experimental/cua-macos/native/tests/shared_helper_e2e.py --helper /absolute/helper` to verify concurrent startup, a shared service PID, independent handles, same-application handoff, request-local screenshot deadlines, connection cleanup and signal shutdown with an owned AppKit fixture. Existing Accessibility and Screen Recording grants are required. `--without-desktop` limits the check to protocol and process lifetime. -Run `python3 packages/experimental/cua-macos/native/tests/application_consent_e2e.py --helper /absolute/helper` in an unlocked macOS GUI session with the Apple Swift compiler available. It creates and cleans up its own AppKit bundles, rejects changed bundle metadata before launch, checks cold/running identity agreement, runs two same-ID copies together, and confirms that an application without a bundle identifier can use persistent path consent. It neither reads AX nor captures screens or sends input, so it does not require Accessibility or Screen Recording grants. +Run `python3 packages/experimental/cua-macos/native/tests/application_consent_e2e.py --helper /absolute/helper` in an unlocked macOS GUI session with the Apple Swift compiler available. It owns and cleans up its AppKit bundles and standalone GUI processes, checks metadata replacement, cold and direct launches, shared bundle IDs, and persistent path identity across standalone restarts. The default identity check requires no Accessibility or Screen Recording grants. Add `--desktop` to read each fixture’s controls, verify an AX button press in its state file, and capture its window; this requires existing grants for both permissions. Run `python3 packages/experimental/cua-macos/native/tests/launcher_identity_e2e.py --helper /absolute/helper` in a GUI login session to verify a Launch Services application that starts a nested GUI launcher and then the helper. The helper identifies the outer responsible application and retains its name and icon after the launchers exit. This check owns both applications and the helper, cleans up its Launch Services registrations, and does not request Accessibility or Screen Recording access. diff --git a/packages/experimental/cua-macos/native/README.zh.md b/packages/experimental/cua-macos/native/README.zh.md index 19868ed988..88864bf323 100644 --- a/packages/experimental/cua-macos/native/README.zh.md +++ b/packages/experimental/cua-macos/native/README.zh.md @@ -105,7 +105,7 @@ AppKit 在主事件循环上运行。阻塞式 socket 接收与握手使用后 运行 `python3 packages/experimental/cua-macos/native/tests/shared_helper_e2e.py --helper /absolute/helper`,通过测试自建的 AppKit fixture 核实并发启动、共享服务 PID、独立句柄、同一应用交接、请求级截图期限、连接清理和信号退出。需要已有辅助功能和屏幕录制权限。`--without-desktop` 将检查限定为协议与进程生命期。 -在已解锁的 macOS GUI 会话中运行 `python3 packages/experimental/cua-macos/native/tests/application_consent_e2e.py --helper /absolute/helper`,需要可用的 Apple Swift 编译器。它创建并清理自己的 AppKit 应用,验证启动前拒绝已改变的应用元数据、启动前后身份一致,同 ID 的两个副本同时运行,以及缺少 Bundle ID 的应用可以获得持久路径许可。测试不读取 AX、不截图、不发送输入,因此不需要辅助功能或屏幕录制权限。 +在已解锁的 macOS GUI 会话中运行 `python3 packages/experimental/cua-macos/native/tests/application_consent_e2e.py --helper /absolute/helper`,需要可用的 Apple Swift 编译器。它创建并清理自己的 AppKit 应用和独立 GUI 进程,检查元数据替换、冷启动与直接启动、共用 Bundle ID,以及独立程序重启后的持久路径身份。默认身份检查不需要辅助功能或屏幕录制权限。添加 `--desktop` 可读取各测试窗口的控件,通过状态文件验证 AX 按钮点击,并截取该窗口;此模式要求已有两项系统权限。 在 GUI 登录会话中运行 `python3 packages/experimental/cua-macos/native/tests/launcher_identity_e2e.py --helper /absolute/helper`,验证由 Launch Services 启动的应用先启动另一个 GUI 启动器,再启动 helper。helper 识别外层责任应用,并在启动器退出后保留其名称和图标。检查只使用测试自建的两个应用和 helper,结束后清理对应的 Launch Services 注册,不申请辅助功能或屏幕录制权限。 diff --git a/packages/experimental/cua-macos/native/tests/CapabilitiesFixture.swift b/packages/experimental/cua-macos/native/tests/CapabilitiesFixture.swift index c10d14fa31..8e621fd014 100644 --- a/packages/experimental/cua-macos/native/tests/CapabilitiesFixture.swift +++ b/packages/experimental/cua-macos/native/tests/CapabilitiesFixture.swift @@ -87,7 +87,7 @@ final class CapabilitiesFixture: NSObject, NSApplicationDelegate, NSWindowDelega self.persist() } window.contentView!.addSubview(pad) - if Bundle.main.object(forInfoDictionaryKey: "DSHFixtureBackground") as? Bool == true { + if Bundle.main.object(forInfoDictionaryKey: "DSHFixtureBackground") as? Bool == true || CommandLine.arguments.contains("--background") { window.orderFront(nil) } else { window.makeKeyAndOrderFront(nil) diff --git a/packages/experimental/cua-macos/native/tests/application_consent_e2e.py b/packages/experimental/cua-macos/native/tests/application_consent_e2e.py index 9aec4a2e36..4cab4cf516 100644 --- a/packages/experimental/cua-macos/native/tests/application_consent_e2e.py +++ b/packages/experimental/cua-macos/native/tests/application_consent_e2e.py @@ -1,5 +1,6 @@ -"""Verify bundle consent across cold and running application copies without desktop input.""" +"""Verify bundle and executable-path consent, optionally with fixture-only GUI operations.""" import argparse +import base64 from contextlib import ExitStack import hashlib import json @@ -11,12 +12,13 @@ import tempfile import uuid from capabilities_e2e import stop_application, wait_for_state -from native_test_support import BINARY, ROOT, NativeHelper +from native_test_support import BINARY, ROOT, NativeHelper, stop def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--helper", type=Path, default=BINARY) + parser.add_argument("--desktop", action="store_true", help="Read, click and capture owned fixture windows; requires existing system grants") args = parser.parse_args() transcript = [] with tempfile.TemporaryDirectory(prefix="dsh-cua-consent-") as temporary, ExitStack() as cleanup: @@ -28,6 +30,21 @@ def main(): expected = hashlib.sha256(("macos:bundle-id:" + identifier).encode()).hexdigest() helper = NativeHelper(transcript, args.helper) cleanup.callback(helper.stop) + if args.desktop: + assert helper.call("getCapabilities", {})["permissions"] == {"accessibility": True, "screenRecording": True} + + def check_window(app, state_path): + if not args.desktop: + return + state = wait_for_state(state_path, lambda value: value["pid"] == app["pid"] and value["windowId"] > 0) + owner = {"appHandle": app["appHandle"], "windowId": state["windowId"]} + observed = helper.call("getState", {**owner, "maxElements": 2000, "maxDepth": 25}) + pad = next(item for item in observed["elements"] if item["role"] == "AXButton" and item["title"] == "Input receiver") + helper.call("click", {"appHandle": app["appHandle"], "snapshotId": observed["snapshotId"], "elementIndex": pad["index"]}) + wait_for_state(state_path, lambda value: value["axPresses"] == state["axPresses"] + 1) + captured = helper.call("screenshot", owner) + assert base64.b64decode(captured["data"], validate=True).startswith(b"\x89PNG\r\n\x1a\n") + register = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister" def fixture(name, bundle_id): app = root / (name + ".app") @@ -93,9 +110,37 @@ def main(): authorization = running["authorization"] expected_path = hashlib.sha256(("macos:executable-path:" + authorization["executablePath"]).encode()).hexdigest() assert authorization["id"] == expected_path, authorization + check_window(running, state) assert identities[0] == identities[1] == expected, identities assert identities[2] != expected, identities - print("PASS: changed bundles cannot launch; cold/running identities agree; copies share consent; other IDs stay separate; missing IDs use persistent paths") + + app, executable, state, _ = fixture("Direct bundle", identifier) + pending = helper.call("getApp", {"selector": str(app)}) + process = subprocess.Popen([str(executable)], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + cleanup.callback(stop, process) + wait_for_state(state, lambda value: value["pid"] == process.pid and value["windowId"] > 0) + running = helper.call("getApp", {"selector": str(process.pid)}) + assert running["authorization"]["id"] == pending["authorization"]["id"] == expected, running["authorization"] + check_window(running, state) + + standalone_ids = [] + for index in range(2): + state = root / f"standalone-{index}.json" + process = subprocess.Popen([str(binary), str(state), "--background"], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + cleanup.callback(stop, process) + try: + wait_for_state(state, lambda value: value["pid"] == process.pid and value["windowId"] > 0) + running = helper.call("getApp", {"selector": str(process.pid)}) + authorization = running["authorization"] + assert running.get("bundleId") is None and authorization["persistable"] is True, authorization + assert Path(authorization["executablePath"]).samefile(binary), authorization + standalone_ids.append(authorization["id"]) + check_window(running, state) + finally: + stop(process) + assert standalone_ids[0] == standalone_ids[1], standalone_ids + print("PASS: changed bundles cannot launch; cold/running and direct-bundle identities agree; copies share consent; other IDs stay separate; missing IDs and standalone GUIs use persistent paths across restarts" + + ("; all seven GUI instances expose controls, receive an AX click and produce a PNG" if args.desktop else "")) if __name__ == "__main__":