mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
8336cd8cc9
Route coordinate clicks through the exact AX hit, reuse render-local attributes and keep keyboard macros within validated native boundaries. Preserve lossless capture evidence while returning bounded model images, and propagate cancellation through daemon and lock acquisition. Remove legacy per-app restrictions after feature-wide consent on both platforms while retaining global disablement and target validation. Include native helpers, regression tests and a stable receiver fixture. Validation: 224 focused TypeScript tests, 516 XCTest cases and 15 Swift Testing cases passed. Local sidecar and Electron builds, development package creation and package smoke checks passed.
233 lines
8.6 KiB
Swift
233 lines
8.6 KiB
Swift
import XCTest
|
|
@testable import cc_haha_computer_use
|
|
|
|
final class ResolvedTargetAuthorizationTests: XCTestCase {
|
|
private let terminalIdentity = AXTreeProcessIdentity(
|
|
bundleID: "com.apple.Terminal",
|
|
executablePath: "/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal",
|
|
launchTime: 100
|
|
)
|
|
private let calculatorIdentity = AXTreeProcessIdentity(
|
|
bundleID: "com.apple.calculator",
|
|
executablePath: "/System/Applications/Calculator.app/Contents/MacOS/Calculator",
|
|
launchTime: 200
|
|
)
|
|
private let chromeIdentity = AXTreeProcessIdentity(
|
|
bundleID: "com.google.Chrome",
|
|
executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
launchTime: 300
|
|
)
|
|
|
|
func testChromePIDBundleNameAndPathSelectorsPreserveExactProvenIdentity() throws {
|
|
let chrome = AppTargetCandidate(
|
|
pid: 43,
|
|
bundleIdentifier: "com.google.Chrome",
|
|
bundleURL: URL(fileURLWithPath: "/Applications/Google Chrome.app"),
|
|
localizedName: "Google Chrome",
|
|
executableName: "Google Chrome"
|
|
)
|
|
let selectors: [AppTargetSelector] = [
|
|
.pid(43),
|
|
.bundleIdentifier("com.google.Chrome"),
|
|
.app("com.google.Chrome"),
|
|
.app("Google Chrome"),
|
|
.app("Google Chrome.app"),
|
|
.app("/Applications/Google Chrome.app"),
|
|
]
|
|
|
|
for selector in selectors {
|
|
let resolved = try XCTUnwrap(
|
|
AppTargetResolver.resolve(selector: selector, candidates: [chrome])
|
|
)
|
|
let target = try ResolvedTargetAuthorization.authorize(
|
|
resolved: resolved,
|
|
currentIdentity: chromeIdentity
|
|
)
|
|
XCTAssertEqual(target.pid, chrome.pid)
|
|
XCTAssertEqual(target.identity, chromeIdentity)
|
|
}
|
|
}
|
|
|
|
func testAllowingChromeStillRejectsUnprovenOrMismatchedIdentity() {
|
|
let unproven = AXTreeProcessIdentity(
|
|
bundleID: chromeIdentity.bundleID,
|
|
executablePath: chromeIdentity.executablePath,
|
|
launchTime: nil
|
|
)
|
|
XCTAssertThrowsError(try ResolvedTargetAuthorization.authorize(
|
|
pid: 43, identity: unproven, expectedBundleID: "com.google.Chrome"
|
|
)) {
|
|
XCTAssertEqual(($0 as? CUError)?.code, "app_denied")
|
|
}
|
|
XCTAssertThrowsError(try ResolvedTargetAuthorization.authorize(
|
|
pid: 43, identity: terminalIdentity, expectedBundleID: "com.google.Chrome"
|
|
)) {
|
|
XCTAssertEqual(($0 as? CUError)?.code, "stale_process")
|
|
}
|
|
}
|
|
|
|
func testNumericPIDAllowsTerminalAfterGlobalEnablement() throws {
|
|
let target = try ResolvedTargetAuthorization.authorize(
|
|
pid: 41,
|
|
identity: terminalIdentity,
|
|
expectedBundleID: nil
|
|
)
|
|
|
|
XCTAssertEqual(target.pid, 41)
|
|
XCTAssertEqual(target.identity, terminalIdentity)
|
|
}
|
|
|
|
func testEveryAppCategoryAndHostAreAllowedWithProvenProcessIdentity() throws {
|
|
let bundleIDs = [
|
|
"com.google.Chrome",
|
|
"com.apple.Safari",
|
|
"com.apple.Terminal",
|
|
"com.microsoft.VSCode",
|
|
"com.apple.shortcuts",
|
|
"com.webull.desktop.v1",
|
|
"com.binance.BinanceDesktop",
|
|
"com.ledger.live",
|
|
"com.spotify.client",
|
|
"com.apple.Music",
|
|
"com.amazon.Kindle",
|
|
"com.claude-code-haha.desktop",
|
|
"dev.cchaha.cu-helper",
|
|
"com.example.custom-host",
|
|
"com.example.new-app",
|
|
]
|
|
for bundleID in bundleIDs {
|
|
let identity = AXTreeProcessIdentity(
|
|
bundleID: bundleID,
|
|
executablePath: "/Applications/Fixture.app/Contents/MacOS/Fixture",
|
|
launchTime: 100
|
|
)
|
|
let target = try ResolvedTargetAuthorization.authorize(
|
|
pid: 41,
|
|
identity: identity,
|
|
expectedBundleID: bundleID
|
|
)
|
|
XCTAssertEqual(target.pid, 41, bundleID)
|
|
XCTAssertEqual(target.identity, identity, bundleID)
|
|
}
|
|
}
|
|
|
|
func testPIDBundleAndLocalizedNameSelectorsAllAuthorizeActualResolvedBundle() throws {
|
|
let terminal = AppTargetCandidate(
|
|
pid: 41,
|
|
bundleIdentifier: "com.apple.Terminal",
|
|
bundleURL: URL(fileURLWithPath: "/System/Applications/Utilities/Terminal.app"),
|
|
localizedName: "终端",
|
|
executableName: "Terminal"
|
|
)
|
|
let selectors: [AppTargetSelector] = [
|
|
.pid(41),
|
|
.bundleIdentifier("com.apple.Terminal"),
|
|
.app("终端"),
|
|
]
|
|
|
|
for selector in selectors {
|
|
let resolved = try XCTUnwrap(
|
|
AppTargetResolver.resolve(selector: selector, candidates: [terminal])
|
|
)
|
|
let target = try ResolvedTargetAuthorization.authorize(
|
|
resolved: resolved,
|
|
currentIdentity: terminalIdentity
|
|
)
|
|
XCTAssertEqual(target.pid, 41)
|
|
XCTAssertEqual(target.identity, terminalIdentity)
|
|
}
|
|
}
|
|
|
|
func testWorktreeHostPathResolutionAuthorizesExactProcess() throws {
|
|
let installed = AppTargetCandidate(
|
|
pid: 100,
|
|
bundleIdentifier: "com.claude-code-haha.desktop",
|
|
bundleURL: URL(fileURLWithPath: "/Applications/Claude Code Haha.app"),
|
|
localizedName: "Claude Code Haha",
|
|
executableName: "Claude Code Haha"
|
|
)
|
|
let worktree = AppTargetCandidate(
|
|
pid: 200,
|
|
bundleIdentifier: installed.bundleIdentifier,
|
|
bundleURL: URL(fileURLWithPath: "/Users/test/worktree/desktop/build-artifacts/macos-arm64/Claude Code Haha.app"),
|
|
localizedName: installed.localizedName,
|
|
executableName: installed.executableName
|
|
)
|
|
let resolved = try AppTargetResolver.match(
|
|
identifier: worktree.bundleURL!.path,
|
|
candidates: [installed, worktree]
|
|
)
|
|
let identity = AXTreeProcessIdentity(
|
|
bundleID: worktree.bundleIdentifier,
|
|
executablePath: worktree.bundleURL!.appendingPathComponent("Contents/MacOS/Claude Code Haha").path,
|
|
launchTime: 300
|
|
)
|
|
|
|
XCTAssertEqual(resolved.pid, worktree.pid)
|
|
let target = try ResolvedTargetAuthorization.authorize(
|
|
resolved: resolved,
|
|
currentIdentity: identity
|
|
)
|
|
XCTAssertEqual(target.pid, worktree.pid)
|
|
XCTAssertEqual(target.identity, identity)
|
|
}
|
|
|
|
func testOmittedFrontmostAndLaunchedTargetsUseSameActualBundlePolicy() throws {
|
|
// Both paths ultimately produce this same resolved target shape. The
|
|
// authorizer intentionally has no selector-specific bypass.
|
|
let resolved = ResolvedAppTarget(
|
|
pid: 41,
|
|
bundleIdentifier: "com.apple.Terminal",
|
|
bundleURL: URL(fileURLWithPath: "/System/Applications/Utilities/Terminal.app")
|
|
)
|
|
|
|
for _ in ["omitted-frontmost", "launched-get-app-state"] {
|
|
let target = try ResolvedTargetAuthorization.authorize(
|
|
resolved: resolved,
|
|
currentIdentity: terminalIdentity
|
|
)
|
|
XCTAssertEqual(target.pid, 41)
|
|
XCTAssertEqual(target.identity, terminalIdentity)
|
|
}
|
|
}
|
|
|
|
func testAllowedResolvedTargetReturnsExactProvenProcessLifetime() throws {
|
|
let target = try ResolvedTargetAuthorization.authorize(
|
|
pid: 42,
|
|
identity: calculatorIdentity,
|
|
expectedBundleID: "com.apple.calculator"
|
|
)
|
|
|
|
XCTAssertEqual(target.pid, 42)
|
|
XCTAssertEqual(target.identity, calculatorIdentity)
|
|
}
|
|
|
|
func testMissingBundleAndResolverIdentityMismatchFailClosed() {
|
|
let missingBundle = AXTreeProcessIdentity(
|
|
bundleID: nil,
|
|
executablePath: "/Applications/Unknown.app/Contents/MacOS/Unknown",
|
|
launchTime: 100
|
|
)
|
|
|
|
XCTAssertThrowsError(
|
|
try ResolvedTargetAuthorization.authorize(
|
|
pid: 42,
|
|
identity: missingBundle,
|
|
expectedBundleID: nil
|
|
)
|
|
) {
|
|
XCTAssertEqual(($0 as? CUError)?.code, "app_denied")
|
|
}
|
|
|
|
XCTAssertThrowsError(
|
|
try ResolvedTargetAuthorization.authorize(
|
|
pid: 42,
|
|
identity: calculatorIdentity,
|
|
expectedBundleID: "com.example.reused"
|
|
)
|
|
) {
|
|
XCTAssertEqual(($0 as? CUError)?.code, "stale_process")
|
|
}
|
|
}
|
|
}
|