mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
b8a90626ce
Rebuilt against main so the branch carries the Computer Use work and no other divergence. Three unrelated efforts had been sitting uncommitted in this worktree and were swept into an earlier commit; they are preserved on cu-worktree-full-backup and belong on their own branches — adapter control credentials, Electron asar sealing, and the sidecar code-loading audit. Every file outside Computer Use now matches main exactly. The engine A Swift helper drives apps through the accessibility tree, with coordinate actuation for the Chromium and Electron apps whose tree is a bare window frame. Ten primitives matching the shape Codex uses, so an app's guidance and the model's habits transfer. Coordinate actions resolve their target window once and refuse when none can be named. The unbound event they used to fall back to is discarded by custom renderers, so a minimized target produced a whole session of "Action completed" with nothing behind it. Input acceptance is established for typing and key presses as well as clicks: each MCP call is seconds apart, so the keyboard cannot inherit the focus a click established. The synthetic focus notification is gated on the target not already being active — sent unconditionally it names window 0 at an app that already owns a key window, and nine window-bound clicks were discarded with the traffic lights fully lit. State the model can trust An off-screen target says so, and says which tools still reach it: element actions need no on-screen geometry, so an app with a real tree can still be driven from the Dock. A fully covered window is recovered once, then left alone — burying it again is the user wanting their screen back. A repeated capture is reported with the cause that actually applies rather than both, because coverage is something we compute. Signing The helper is signed under a stable identity before electron-builder sees it, and excluded from re-signing: macOS ties Accessibility and Screen Recording grants to the signing identity, so rotating it drops both on every update. Discoverability The desktop slash menu falls back to a directory scan while a session's CLI has not started, which is when the menu is first opened. Built-ins and bundled skills live in the binary, so /computer-use was absent until after the first message.
145 lines
5.9 KiB
Swift
145 lines
5.9 KiB
Swift
import CoreGraphics
|
|
import XCTest
|
|
|
|
@testable import cc_haha_computer_use
|
|
|
|
/// A minimized app produced a real screenshot, a real accessibility tree, and
|
|
/// "Action completed" for every click and keystroke of an entire session, while
|
|
/// nothing whatsoever happened. Two separate defects combined to hide it:
|
|
/// capture accepts off-screen windows, actuation cannot use them, and the gap
|
|
/// between the two was a silent fallback to an unbound event that custom
|
|
/// renderers discard.
|
|
///
|
|
/// These tests hold both halves of the fix in place: the refusal, and the fact
|
|
/// that the model is told why.
|
|
final class OffScreenTargetTests: XCTestCase {
|
|
private let point = CGPoint(x: 410, y: 50)
|
|
|
|
private func windowList(
|
|
pid: pid_t,
|
|
bounds: CGRect,
|
|
layer: Int = 0
|
|
) -> () -> [[CFString: Any]]? {
|
|
{
|
|
[[
|
|
kCGWindowLayer: layer,
|
|
kCGWindowNumber: 4242,
|
|
kCGWindowOwnerPID: pid,
|
|
kCGWindowBounds: [
|
|
"X": bounds.minX,
|
|
"Y": bounds.minY,
|
|
"Width": bounds.width,
|
|
"Height": bounds.height,
|
|
] as [String: CGFloat],
|
|
]]
|
|
}
|
|
}
|
|
|
|
// MARK: - The distinction that was missing
|
|
|
|
func testAnAppWithNoOnScreenWindowIsReportedAsOffScreen() {
|
|
// The whole failure: the window server lists on-screen windows only, so
|
|
// a minimized target simply is not there. Reported as "your coordinate
|
|
// missed", the model retries other coordinates forever.
|
|
let result = WindowGeometry.binding(at: point, pid: 501, windowList: { [] })
|
|
XCTAssertEqual(result, .failure(.noWindowOnScreen))
|
|
}
|
|
|
|
func testAMissedCoordinateIsNotReportedAsOffScreen() {
|
|
// The app IS on screen; the point just falls outside its window. Here
|
|
// re-reading coordinates is exactly the right move, so the two cases
|
|
// must not collapse into one message.
|
|
let result = WindowGeometry.binding(
|
|
at: point,
|
|
pid: 501,
|
|
windowList: windowList(pid: 501, bounds: CGRect(x: 800, y: 400, width: 300, height: 200))
|
|
)
|
|
XCTAssertEqual(result, .failure(.pointOutsideWindows))
|
|
}
|
|
|
|
func testAWindowUnderThePointBinds() {
|
|
let result = WindowGeometry.binding(
|
|
at: point,
|
|
pid: 501,
|
|
windowList: windowList(pid: 501, bounds: CGRect(x: 0, y: 0, width: 1000, height: 800))
|
|
)
|
|
XCTAssertEqual(try? result.get().id, 4242)
|
|
}
|
|
|
|
func testAnotherAppsWindowUnderThePointDoesNotCount() {
|
|
// Binding an event to a window the target does not own addresses it to
|
|
// the wrong process, which is worse than refusing.
|
|
let result = WindowGeometry.binding(
|
|
at: point,
|
|
pid: 501,
|
|
windowList: windowList(pid: 999, bounds: CGRect(x: 0, y: 0, width: 1000, height: 800))
|
|
)
|
|
XCTAssertEqual(result, .failure(.noWindowOnScreen))
|
|
}
|
|
|
|
func testOverlayLayersDoNotMakeAnAppLookReachable() {
|
|
// Layer 0 only. An app whose only surface is a panel or overlay cannot
|
|
// receive an app-directed click, so it is off screen for our purposes.
|
|
XCTAssertFalse(
|
|
WindowGeometry.hasWindowOnScreen(
|
|
pid: 501,
|
|
windowList: windowList(
|
|
pid: 501,
|
|
bounds: CGRect(x: 0, y: 0, width: 100, height: 100),
|
|
layer: 25
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
func testAZeroSizedWindowDoesNotCountAsOnScreen() {
|
|
XCTAssertFalse(
|
|
WindowGeometry.hasWindowOnScreen(
|
|
pid: 501,
|
|
windowList: windowList(pid: 501, bounds: .zero)
|
|
)
|
|
)
|
|
}
|
|
|
|
// MARK: - What the model is told
|
|
|
|
func testTheModelIsToldWhenTheTargetCannotBeActedOn() {
|
|
let notice = OffScreenTargetAdvice.noticeIfUnreachable(hasWindowOnScreen: false)
|
|
XCTAssertNotNil(notice)
|
|
// The model's instinct on a dead click is to try different coordinates.
|
|
// Against an off-screen window that is an infinite loop, so the notice
|
|
// has to close it explicitly rather than merely describe the state.
|
|
XCTAssertTrue(notice!.contains("no window on screen"))
|
|
XCTAssertTrue(notice!.contains("Do not retry with different coordinates"))
|
|
// And it must name the way out, or the model has nothing to do next.
|
|
XCTAssertTrue(notice!.contains("ask the user to bring it"))
|
|
}
|
|
|
|
func testTheNoticeKeepsElementActionsOpen() {
|
|
// Only coordinate actuation needs on-screen geometry — it is the only
|
|
// half that hit-tests. Element actions address a node directly and work
|
|
// on a window in the Dock, which IS the background automation this
|
|
// feature exists to provide.
|
|
//
|
|
// The first draft of this notice said "NOTHING can be clicked or typed".
|
|
// That reads as a tidy warning and is a capability switch: the model
|
|
// would stop at the exact moment it should reach for element_index.
|
|
let notice = try! XCTUnwrap(
|
|
OffScreenTargetAdvice.noticeIfUnreachable(hasWindowOnScreen: false)
|
|
)
|
|
XCTAssertTrue(notice.contains("Element actions still work"))
|
|
for tool in ["element_index", "set_value", "select_text", "perform_secondary_action"] {
|
|
XCTAssertTrue(notice.contains(tool), "\(tool) must be named as still usable")
|
|
}
|
|
// And it must not overclaim in the other direction either: a shell tree
|
|
// has no elements to address, so that case needs its own sentence.
|
|
XCTAssertTrue(notice.contains("bare shell"))
|
|
}
|
|
|
|
func testNothingIsSaidWhenTheTargetIsReachable() {
|
|
// Every get_app_state carries this check; a notice on the normal path
|
|
// would be noise in every single turn.
|
|
XCTAssertNil(OffScreenTargetAdvice.noticeIfUnreachable(hasWindowOnScreen: true))
|
|
}
|
|
}
|