Files
Brennan Benson 66dfdc456f feat(computer-use): support macOS middle click and stop the silent left-click fallback (#14721)
* feat(computer-use): support macOS middle click and gate the AX click path

`--mouse-button middle` already validated end-to-end through the CLI, the
zod schema, and the provider validator, and both the Windows and Linux
providers honored it. Only the macOS provider rejected it outright with
"middle-click is not yet supported", so the flag was a dead end on the one
platform that has no fallback.

Two changes:

- Add `.middle` to the macOS button mapping. macOS has no dedicated middle
  event family, so it rides `otherMouseDown`/`otherMouseUp` with the button
  number carried by `mouseButton: .center`; that constructor argument is
  honored for exactly the `otherMouse*` types, so no extra field write is
  needed.
- Validate the requested button before the accessibility fast path, and skip
  that path for buttons it cannot express. Previously the raw string was read
  unvalidated, and `performClickAction` only special-cased `right`, so
  `click --mouse-button middle --element-index N` (no modifiers, count 1) fell
  through to `AXPress` — a left click — and reported success with
  `path: "accessibility"`. Any unrecognized button string did the same. This
  matches guards the Windows and Linux providers already had.

The button enum moves into `OrcaComputerUseMacOSCore` so it is unit-testable;
`main.swift` keeps only the CoreGraphics mapping.

Also documents `--mouse-button` in the computer-use skill guide, which never
mentioned the flag, so agents on Windows and Linux had no way to discover it.

* test(computer-use): cover macOS middle click in the real-desktop e2e suite

* test(computer-use): prove macOS middle-click delivery
2026-08-15 00:41:45 -07:00

96 lines
3.7 KiB
Swift

import OrcaComputerUseMacOSCore
import XCTest
final class ActionArgumentValidationTests: XCTestCase {
func testPositiveIntegerAcceptsPositiveValuesAndDefaults() {
XCTAssertEqual(
try ActionArgumentValidation.positiveInteger(nil, defaultValue: 1, name: "clickCount").get(),
1
)
XCTAssertEqual(
try ActionArgumentValidation.positiveInteger(2, defaultValue: 1, name: "clickCount").get(),
2
)
}
func testPositiveIntegerRejectsZeroNegativeAndNonFiniteValues() {
XCTAssertEqual(
failureMessage(ActionArgumentValidation.positiveInteger(0, defaultValue: 1, name: "clickCount")),
"clickCount must be a positive integer"
)
XCTAssertEqual(
failureMessage(ActionArgumentValidation.positiveInteger(-1, defaultValue: 1, name: "clickCount")),
"clickCount must be a positive integer"
)
XCTAssertEqual(
failureMessage(ActionArgumentValidation.positiveInteger(.infinity, defaultValue: 1, name: "clickCount")),
"clickCount must be a positive integer"
)
}
func testPositiveNumberAcceptsPositiveValuesAndDefaults() {
XCTAssertEqual(
try ActionArgumentValidation.positiveNumber(nil, defaultValue: 1, name: "pages").get(),
1
)
XCTAssertEqual(
try ActionArgumentValidation.positiveNumber(0.5, defaultValue: 1, name: "pages").get(),
0.5
)
}
func testPositiveNumberRejectsZeroNegativeAndNonFiniteValues() {
XCTAssertEqual(
failureMessage(ActionArgumentValidation.positiveNumber(0, defaultValue: 1, name: "pages")),
"pages must be a positive number"
)
XCTAssertEqual(
failureMessage(ActionArgumentValidation.positiveNumber(-0.5, defaultValue: 1, name: "pages")),
"pages must be a positive number"
)
XCTAssertEqual(
failureMessage(ActionArgumentValidation.positiveNumber(.nan, defaultValue: 1, name: "pages")),
"pages must be a positive number"
)
}
func testMouseButtonDefaultsToLeftAndAcceptsEveryButton() {
XCTAssertEqual(try ActionArgumentValidation.mouseButton(nil).get(), .left)
XCTAssertEqual(try ActionArgumentValidation.mouseButton("left").get(), .left)
XCTAssertEqual(try ActionArgumentValidation.mouseButton("right").get(), .right)
XCTAssertEqual(try ActionArgumentValidation.mouseButton("middle").get(), .middle)
}
func testMouseButtonRejectsUnknownButtons() {
XCTAssertEqual(
failureMessage(ActionArgumentValidation.mouseButton("primary")),
"unsupported mouse button 'primary'"
)
XCTAssertEqual(
failureMessage(ActionArgumentValidation.mouseButton("")),
"unsupported mouse button ''"
)
}
func testOnlyMiddleButtonLacksAnAccessibilityAction() {
XCTAssertTrue(MouseButtonSelection.left.hasAccessibilityAction)
XCTAssertTrue(MouseButtonSelection.right.hasAccessibilityAction)
XCTAssertFalse(MouseButtonSelection.middle.hasAccessibilityAction)
}
func testScrollDirectionRejectsUnknownDirections() {
XCTAssertEqual(try ActionArgumentValidation.scrollDirection("down").get(), "down")
XCTAssertEqual(
failureMessage(ActionArgumentValidation.scrollDirection("diagonal")),
"unsupported scroll direction: diagonal"
)
}
private func failureMessage<T>(_ result: Result<T, ActionArgumentValidationError>) -> String? {
if case let .failure(error) = result {
return error.message
}
return nil
}
}