From 9a562aa6ca1e05aefb033de394c538586d1aa284 Mon Sep 17 00:00:00 2001 From: creatixchu Date: Tue, 29 Sep 2026 18:16:58 +0800 Subject: [PATCH] fix(cua): distinguish disabled paste commands and verify fixture focus --- .../cua/native/Sources/CuaNative/AXAccess.swift | 4 ++-- .../cua/native/tests/CapabilitiesFixture.swift | 9 ++++++++- .../computer-use/cua/native/tests/capabilities_e2e.py | 7 +++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/computer-use/cua/native/Sources/CuaNative/AXAccess.swift b/packages/computer-use/cua/native/Sources/CuaNative/AXAccess.swift index 451aa8e9a7..472d8b79bf 100644 --- a/packages/computer-use/cua/native/Sources/CuaNative/AXAccess.swift +++ b/packages/computer-use/cua/native/Sources/CuaNative/AXAccess.swift @@ -164,14 +164,14 @@ final class AXAccess { if try string(node, "AXRole") == "AXMenuItem", try string(node, "AXMenuItemCmdChar")?.lowercased() == "v" { let raw = try attribute(node, "AXMenuItemCmdModifiers") if let raw, CFGetTypeID(raw) == CFNumberGetTypeID(), let number = raw as? NSNumber, number.intValue == 0, - try actions(node).contains("AXPress"), try boolean(node, "AXEnabled") != false { + try actions(node).contains("AXPress") { guard found == nil else { throw NativeFailure(code: "UNSUPPORTED", message: "The application exposes ambiguous Paste commands") } found = node } } for child in try children(node) { pending.append((child, depth + 1)) } } - guard let found else { throw NativeFailure(code: "UNSUPPORTED", message: "No enabled standard Paste command is available") } + guard let found else { throw NativeFailure(code: "UNSUPPORTED", message: "No standard Paste command is available") } try beforeDispatch() try perform(found, "AXPress") } diff --git a/packages/computer-use/cua/native/tests/CapabilitiesFixture.swift b/packages/computer-use/cua/native/tests/CapabilitiesFixture.swift index 1d74a66547..707aea035f 100644 --- a/packages/computer-use/cua/native/tests/CapabilitiesFixture.swift +++ b/packages/computer-use/cua/native/tests/CapabilitiesFixture.swift @@ -25,7 +25,7 @@ final class CapabilityPad: NSView { } } -final class CapabilitiesFixture: NSObject, NSApplicationDelegate, NSTextViewDelegate { +final class CapabilitiesFixture: NSObject, NSApplicationDelegate, NSWindowDelegate, NSTextViewDelegate { let statePath: String var window: NSWindow! var text: NSTextView! @@ -44,6 +44,7 @@ final class CapabilitiesFixture: NSObject, NSApplicationDelegate, NSTextViewDele NSApplication.shared.mainMenu = bar window = NSWindow(contentRect: NSRect(x: 140, y: 160, width: 640, height: 500), styleMask: [.titled, .closable], backing: .buffered, defer: false) + window.delegate = self window.title = "DSH CUA capability checks" window.isReleasedWhenClosed = false window.animationBehavior = .none @@ -74,6 +75,10 @@ final class CapabilitiesFixture: NSObject, NSApplicationDelegate, NSTextViewDele NSApplication.shared.activate(ignoringOtherApps: true) persist() } + func applicationDidBecomeActive(_ notification: Notification) { persist() } + func applicationDidResignActive(_ notification: Notification) { persist() } + func windowDidBecomeKey(_ notification: Notification) { persist() } + func windowDidResignKey(_ notification: Notification) { persist() } func textDidChange(_ notification: Notification) { persist() } func textViewDidChangeSelection(_ notification: Notification) { if pad != nil { persist() } } func persist() { @@ -91,6 +96,8 @@ final class CapabilitiesFixture: NSObject, NSApplicationDelegate, NSTextViewDele let frame = pad.convert(pad.bounds, to: nil) let selection = text.selectedRange() let result: [String: Any] = ["pid": ProcessInfo.processInfo.processIdentifier, "windowId": window.windowNumber, + "active": NSApplication.shared.isActive, "keyWindow": window.isKeyWindow, + "textFocused": window.firstResponder === text, "text": text.string, "selection": [selection.location, selection.length], "runs": runs, "events": events, "pad": [frame.minX, window.frame.height - frame.maxY, frame.width, frame.height]] try! JSONSerialization.data(withJSONObject: result).write(to: URL(fileURLWithPath: statePath), options: .atomic) diff --git a/packages/computer-use/cua/native/tests/capabilities_e2e.py b/packages/computer-use/cua/native/tests/capabilities_e2e.py index 51e0c9acf9..b7cc23e4ac 100644 --- a/packages/computer-use/cua/native/tests/capabilities_e2e.py +++ b/packages/computer-use/cua/native/tests/capabilities_e2e.py @@ -78,7 +78,8 @@ def main(): # The Host consent barrier is separately exercised by provider-lifecycle.spec.ts. app = call("launchApp", owner) pid = app["pid"] - state = wait_for_state(state_file, lambda value: value["pid"] == pid) + call("activate", owner) + state = wait_for_state(state_file, lambda value: value["pid"] == pid and value["active"] and value["keyWindow"]) assert call("findApps", {"query": str(application), "maxAppMatches": 20})[0]["pid"] == pid observed = call("getState", {**owner, "maxElements": 2000, "maxDepth": 25}) field = next(item for item in observed["elements"] if item["role"] == "AXTextArea") @@ -120,8 +121,10 @@ def main(): pid = json.loads(state_file.read_text())["pid"] if args.artifacts: args.artifacts.mkdir(parents=True, exist_ok=True) + if state_file.exists(): + (args.artifacts / "state-final.json").write_bytes(state_file.read_bytes()) (args.artifacts / "transcript.json").write_text(json.dumps(transcript, ensure_ascii=False, indent=2)) - run_cleanup(*([helper.close] if helper else []), *([lambda: stop_application(pid)] if pid else [])) + run_cleanup(*([helper.stop] if helper else []), *([lambda: stop_application(pid)] if pid else [])) if __name__ == "__main__":