fix(computer-use): load cursor resources from packaged apps

Resolve optional click animation assets from the running helper bundle.
Avoid SwiftPM Bundle.module's fatal fallback to a build-machine path so
visible clicks can return successfully and keep the daemon alive.

Keep procedural feedback when optional frames are absent or unreadable.
Add an input-free resource probe and run it against relocated release and
staged helper apps. Cover standard Resources copying and reject probes
that load from an external build directory.

Validation: 524 XCTest and 15 Swift Testing cases passed, including eight
new resource regressions. Build fixtures, compiled sidecar smoke, Electron
checks, local packaging and final relocated-package probes passed.
This commit is contained in:
程序员阿江(Relakkes)
2026-09-11 15:09:50 +08:00
parent 17f194df40
commit a0a8fd4b45
6 changed files with 425 additions and 42 deletions
+58 -14
View File
@@ -1,10 +1,11 @@
// @vitest-environment node
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
import { execFile, spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { copyFile, mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises'
import { copyFile, cp, mkdir, mkdtemp, realpath, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path, { join as joinPath } from 'node:path'
import { promisify } from 'node:util'
import { describe, expect, it } from 'vitest'
import {
@@ -676,21 +677,64 @@ describe('build-sidecars cu-helper macOS gating', () => {
expect(source).toContain('env: createCuHelperBuildEnv(targetTriple, process.env)')
})
it('copies the cu-helper binary and its resource bundle into the binaries dir', () => {
it('preserves the helper app signature while staging the complete signed bundle', () => {
const source = readBuildScript()
// Both the bare binary AND the SwiftPM resource bundle must be copied, or the
// LensSequence overlay assets fail to load at runtime via Bundle.module.
expect(source).toContain('cu-helper_cc-haha-computer-use.bundle')
expect(source).toMatch(/binariesDir,\s*'cc-haha-computer-use'/)
const start = source.indexOf('async function buildCuHelper(')
expect(start).toBeGreaterThanOrEqual(0)
const end = source.indexOf('\nasync function copyPreserving(', start)
expect(end).toBeGreaterThan(start)
const buildCuHelperBody = source.slice(start, end)
expect(buildCuHelperBody).toContain('await copyPreserving(builtBinary, destApp)')
expect(buildCuHelperBody).not.toContain('signMacBinary(')
expect(buildCuHelperBody).not.toContain('codesign')
})
})
it('does NOT ad-hoc re-sign cu-helper (would rotate its stable TCC identity)', () => {
const source = readBuildScript()
// adHocSignMacBinary must only be applied to the bun-compiled sidecar, never
// to cu-helper (build.sh already signs it with a stable hardened-runtime identity).
const buildCuHelperBody = source.slice(source.indexOf('async function buildCuHelper()'))
expect(buildCuHelperBody).not.toContain('adHocSignMacBinary')
})
describe.skipIf(!compiledSidecarSmokeEnabled || process.platform !== 'darwin')('staged cu-helper resource smoke', () => {
it('loads optional cursor resources from a relocated staged app instead of the build tree', async context => {
const exec = promisify(execFile)
const sourceApp = path.resolve(import.meta.dirname, '../src-tauri/binaries/cc-haha-computer-use.app')
const inner = path.join('Contents', 'MacOS', 'cc-haha-computer-use')
const { stdout: architectures } = await exec('/usr/bin/lipo', ['-archs', path.join(sourceApp, inner)])
const hostArch = process.arch === 'arm64' ? 'arm64' : 'x86_64'
if (!architectures.trim().split(/\s+/).includes(hostArch)) {
console.warn(`[cu-helper resource smoke] skipped execution: target ${architectures.trim()}, host ${hostArch}`)
context.skip()
return
}
const fixtureRoot = await mkdtemp(path.join(tmpdir(), 'cu-helper-staged-resources-'))
try {
const app = path.join(fixtureRoot, 'cc-haha-computer-use.app')
await cp(sourceApp, app, { recursive: true, verbatimSymlinks: true })
const home = path.join(fixtureRoot, 'home')
const config = path.join(fixtureRoot, 'config')
const temp = path.join(fixtureRoot, 'tmp')
await Promise.all([home, config, temp].map(directory => mkdir(directory)))
const { stdout } = await exec(path.join(app, inner), ['--probe-cursor-resources'], {
cwd: fixtureRoot,
env: {
PATH: '/usr/bin:/bin:/usr/sbin:/sbin', HOME: home,
CFFIXED_USER_HOME: home, CLAUDE_CONFIG_DIR: config, TMPDIR: `${temp}/`,
},
timeout: 10_000,
maxBuffer: 1024 * 1024,
})
const report = JSON.parse(stdout) as {
resourceDirectory: string | null
frameCount: number
proceduralFallback: boolean
}
expect(report.resourceDirectory).not.toBeNull()
expect(await realpath(report.resourceDirectory!)).toBe(await realpath(path.join(
app, 'Contents', 'Resources', 'cu-helper_cc-haha-computer-use.bundle', 'LensSequence',
)))
expect(Number.isInteger(report.frameCount)).toBe(true)
expect(report.frameCount).toBeGreaterThanOrEqual(0)
expect(report.proceduralFallback).toBe(report.frameCount === 0)
} finally {
await rm(fixtureRoot, { recursive: true, force: true })
}
}, 20_000)
})
/**
@@ -279,7 +279,7 @@ public final class VirtualCursor {
private var glideLastTick: CFTimeInterval = 0
// Lazily-loaded raster ripple frames (nil => not bundled => procedural).
private static let rasterFrames: [CGImage]? = VirtualCursor.loadLensSequence()
private static let rasterFrames: [CGImage]? = VirtualCursor.loadLensSequence().frames
// MARK: - Geometry helpers
@@ -853,22 +853,28 @@ public final class VirtualCursor {
/// Load optional click-ripple PNG frames bundled under
/// `Resources/LensSequence`. Frames are sorted by filename so `frame_000`,
/// `frame_001`, … animate in order. Returns nil when no bundle / no frames,
/// in which case `showClick` falls back to the procedural ring.
private static func loadLensSequence() -> [CGImage]? {
/// `frame_001`, … animate in order. Missing or unreadable optional frames
/// leave `frames` nil, so `showClick` uses the procedural ring. The resource
/// probe calls this same loader without creating a cursor or posting input.
static func loadLensSequence(from bundle: Bundle = .main) -> (directory: URL?, frames: [CGImage]?) {
let fm = FileManager.default
// Resolve the LensSequence directory from the SwiftPM resource bundle.
// `Bundle.module` is synthesized because Package.swift declares
// `.copy("Resources/LensSequence")`. Fall back to scanning next to the
// executable for robustness in case the resource layout differs.
var directory: URL?
if let bundleURL = Bundle.module.url(forResource: "LensSequence", withExtension: nil) {
directory = bundleURL
} else if let resourceURL = Bundle.module.resourceURL?
.appendingPathComponent("LensSequence", isDirectory: true),
fm.fileExists(atPath: resourceURL.path) {
directory = resourceURL
// SwiftPM's generated Bundle.module accessor traps when its bundle is
// absent from the app root and the absolute build-machine path. Our
// signed app instead puts resources in Contents/Resources. Never invoke
// that accessor for optional visuals: discover only runtime-relative
// locations, including the sibling bundle of a bare SwiftPM executable.
let moduleName = "cu-helper_cc-haha-computer-use.bundle"
var candidates = [bundle.resourceURL, bundle.bundleURL, bundle.executableURL?.deletingLastPathComponent()]
.compactMap { $0 }
.map { $0.appendingPathComponent(moduleName, isDirectory: true)
.appendingPathComponent("LensSequence", isDirectory: true) }
if let resources = bundle.resourceURL {
candidates.append(resources.appendingPathComponent("LensSequence", isDirectory: true))
}
let directory = candidates.first { candidate in
var isDirectory: ObjCBool = false
return fm.fileExists(atPath: candidate.path, isDirectory: &isDirectory) && isDirectory.boolValue
}
guard let dir = directory,
@@ -877,13 +883,13 @@ public final class VirtualCursor {
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles]
)
else { return nil }
else { return (directory, nil) }
let pngs = entries
.filter { $0.pathExtension.lowercased() == "png" }
.sorted { $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending }
guard !pngs.isEmpty else { return nil }
guard !pngs.isEmpty else { return (directory, nil) }
var images: [CGImage] = []
images.reserveCapacity(pngs.count)
@@ -893,7 +899,7 @@ public final class VirtualCursor {
guard let cg = nsImage.cgImage(forProposedRect: &rect, context: nil, hints: nil) else { continue }
images.append(cg)
}
return images.isEmpty ? nil : images
return (directory, images.isEmpty ? nil : images)
}
}
@@ -335,6 +335,26 @@ let argv = CommandLine.arguments
let entryCommand = argv.count >= 2 ? argv[1] : nil
let isDaemonMode = entryCommand == "daemon"
// Public, input-free package diagnostic. Resolve and decode exactly the same
// optional assets used by the first visible click, before TCC disclaim, client
// authorization, NSApplication creation, runtime files, or injection cleanup.
// This permits testing a relocated release app with no user permissions/state.
if entryCommand == "--probe-cursor-resources" {
let resources = VirtualCursor.loadLensSequence()
let report = JSONValue.object([
"resourceDirectory": resources.directory.map { .string($0.path) } ?? .null,
"frameCount": .int(resources.frames?.count ?? 0),
"proceduralFallback": .bool(resources.frames == nil),
])
do {
emitLine(try JSONEncoder().encode(report))
exit(0)
} catch {
emitLine(Data("{\"error\":\"cursor resource diagnostic encoding failed\"}".utf8))
exit(1)
}
}
// disclaim re-exec — for the BARE-EXEC'd modes only (one-shot CLI + onboarding
// card, which the Electron app spawns directly). It re-execs us once with
// responsibility_spawnattrs_setdisclaim so the process becomes its OWN TCC
@@ -0,0 +1,170 @@
import AppKit
import ImageIO
import UniformTypeIdentifiers
import XCTest
@testable import cc_haha_computer_use
final class VirtualCursorResourceTests: XCTestCase {
@MainActor
func testPackagedApplicationLoadsFramesFromContentsResourcesInNaturalOrder() throws {
let fixture = try ResourceFixture()
defer { fixture.remove() }
let sequence = try fixture.sequence(at: "Contents/Resources/cu-helper_cc-haha-computer-use.bundle")
try fixture.png(width: 10, to: sequence.appendingPathComponent("frame_10.png"))
try fixture.png(width: 2, to: sequence.appendingPathComponent("frame_2.png"))
let resources = VirtualCursor.loadLensSequence(from: fixture.bundle)
let frames = try XCTUnwrap(resources.frames)
XCTAssertEqual(resources.directory?.standardizedFileURL.path, sequence.standardizedFileURL.path)
XCTAssertEqual(frames.map(\.width), [2, 10])
}
@MainActor
func testMissingOptionalBundleFallsBackWithoutLoadingAnyBuildTreeResources() throws {
let fixture = try ResourceFixture()
defer { fixture.remove() }
let resources = VirtualCursor.loadLensSequence(from: fixture.bundle)
XCTAssertNil(resources.directory)
XCTAssertNil(resources.frames)
}
@MainActor
func testReadmeOnlySequenceFallsBackToProceduralRipple() throws {
let fixture = try ResourceFixture()
defer { fixture.remove() }
let sequence = try fixture.sequence(at: "Contents/Resources/cu-helper_cc-haha-computer-use.bundle")
try Data("Optional animation frames are not installed.".utf8)
.write(to: sequence.appendingPathComponent("README.md"))
let resources = VirtualCursor.loadLensSequence(from: fixture.bundle)
XCTAssertEqual(resources.directory?.standardizedFileURL.path, sequence.standardizedFileURL.path)
XCTAssertNil(resources.frames)
}
@MainActor
func testInvalidFramesDoNotHideLaterValidFrames() throws {
let fixture = try ResourceFixture()
defer { fixture.remove() }
let sequence = try fixture.sequence(at: "Contents/Resources/cu-helper_cc-haha-computer-use.bundle")
try Data("not a PNG".utf8).write(to: sequence.appendingPathComponent("frame_1.png"))
try fixture.png(width: 4, to: sequence.appendingPathComponent("frame_2.PNG"))
let frames = try XCTUnwrap(VirtualCursor.loadLensSequence(from: fixture.bundle).frames)
XCTAssertEqual(frames.map(\.width), [4])
}
@MainActor
func testExecutableSiblingModuleBundleRemainsSupported() throws {
let fixture = try ResourceFixture()
defer { fixture.remove() }
let sequence = try fixture.sequence(at: "Contents/MacOS/cu-helper_cc-haha-computer-use.bundle")
try fixture.png(width: 3, to: sequence.appendingPathComponent("frame_1.png"))
let resources = VirtualCursor.loadLensSequence(from: fixture.bundle)
XCTAssertEqual(resources.directory?.standardizedFileURL.path, sequence.standardizedFileURL.path)
XCTAssertEqual(resources.frames?.map(\.width), [3])
}
@MainActor
func testPackagedSequenceTakesPrecedenceOverLegacyLocationsEvenWithoutFrames() throws {
let fixture = try ResourceFixture()
defer { fixture.remove() }
let packaged = try fixture.sequence(at: "Contents/Resources/cu-helper_cc-haha-computer-use.bundle")
let sibling = try fixture.sequence(at: "Contents/MacOS/cu-helper_cc-haha-computer-use.bundle")
try fixture.png(width: 3, to: sibling.appendingPathComponent("frame_1.png"))
let resources = VirtualCursor.loadLensSequence(from: fixture.bundle)
XCTAssertEqual(resources.directory?.standardizedFileURL.path, packaged.standardizedFileURL.path)
XCTAssertNil(resources.frames, "An intentionally empty deployed sequence must not borrow other frames")
}
@MainActor
func testAllInvalidFramesFallBackToProceduralRipple() throws {
let fixture = try ResourceFixture()
defer { fixture.remove() }
let sequence = try fixture.sequence(at: "Contents/Resources/cu-helper_cc-haha-computer-use.bundle")
try Data("not a PNG".utf8).write(to: sequence.appendingPathComponent("frame_1.png"))
let resources = VirtualCursor.loadLensSequence(from: fixture.bundle)
XCTAssertEqual(resources.directory?.standardizedFileURL.path, sequence.standardizedFileURL.path)
XCTAssertNil(resources.frames)
}
@MainActor
func testResourcePathThatIsAFileDoesNotCrash() throws {
let fixture = try ResourceFixture()
defer { fixture.remove() }
try Data("not a directory".utf8).write(to: fixture.application
.appendingPathComponent("Contents/Resources/cu-helper_cc-haha-computer-use.bundle"))
let resources = VirtualCursor.loadLensSequence(from: fixture.bundle)
XCTAssertNil(resources.directory)
XCTAssertNil(resources.frames)
}
}
private struct ResourceFixture {
let root: URL
let application: URL
let bundle: Bundle
init() throws {
root = FileManager.default.temporaryDirectory
.appendingPathComponent("cu-cursor-resources-\(UUID().uuidString)", isDirectory: true)
application = root.appendingPathComponent("Moved Helper's App.app", isDirectory: true)
let contents = application.appendingPathComponent("Contents", isDirectory: true)
try FileManager.default.createDirectory(
at: contents.appendingPathComponent("MacOS", isDirectory: true),
withIntermediateDirectories: true
)
try FileManager.default.createDirectory(
at: contents.appendingPathComponent("Resources", isDirectory: true),
withIntermediateDirectories: true
)
let info: [String: Any] = [
"CFBundleIdentifier": "dev.cchaha.tests.cursor-resources.\(UUID().uuidString)",
"CFBundleName": "Cursor Resource Fixture",
"CFBundlePackageType": "APPL",
"CFBundleExecutable": "fixture-helper",
]
try PropertyListSerialization.data(fromPropertyList: info, format: .xml, options: 0)
.write(to: contents.appendingPathComponent("Info.plist"))
try Data().write(to: contents.appendingPathComponent("MacOS/fixture-helper"))
bundle = try XCTUnwrap(Bundle(url: application))
}
func sequence(at relativeRoot: String) throws -> URL {
let directory = application.appendingPathComponent(relativeRoot, isDirectory: true)
.appendingPathComponent("LensSequence", isDirectory: true)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
return directory
}
func png(width: Int, to url: URL) throws {
let context = try XCTUnwrap(CGContext(
data: nil, width: width, height: 2, bitsPerComponent: 8,
bytesPerRow: width * 4, space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
))
context.setFillColor(CGColor(gray: 1, alpha: 1))
context.fill(CGRect(x: 0, y: 0, width: width, height: 2))
let image = try XCTUnwrap(context.makeImage())
let destination = try XCTUnwrap(CGImageDestinationCreateWithURL(
url as CFURL, UTType.png.identifier as CFString, 1, nil
))
CGImageDestinationAddImage(destination, image, nil)
XCTAssertTrue(CGImageDestinationFinalize(destination))
}
func remove() {
try? FileManager.default.removeItem(at: root)
}
}
+56 -6
View File
@@ -427,6 +427,14 @@ verify() {
# APP_PATH comment). Contents/Info.plist (CFBundleIdentifier == $BUNDLE_ID)
# makes the inner binary's TCC identity a proper app bundle.
# ---------------------------------------------------------------------------
copy_cursor_resources() {
local res_bundle="$1"
local destination_app="$2"
[ -d "$res_bundle/LensSequence" ] || die "Cursor resource bundle not found at $res_bundle (SwiftPM must produce the declared LensSequence directory)."
mkdir -p "$destination_app/Contents/Resources"
cp -R "$res_bundle" "$destination_app/Contents/Resources/"
}
wrap_app() {
[ -s "$APP_ICON_PATH" ] || die "App icon not found at $APP_ICON_PATH (needed for the Privacy lists)."
log ""
@@ -440,14 +448,13 @@ wrap_app() {
cp "$PKG_DIR/Info.plist" "$APP_PATH/Contents/Info.plist"
cp "$APP_ICON_PATH" "$APP_PATH/Contents/Resources/icon.icns"
# SwiftPM resource bundle (LensSequence overlay), loaded via Bundle.module.
# Standard .app location is Contents/Resources/ (Bundle.main.resourceURL). Do
# SwiftPM resource bundle (LensSequence overlay). Standard .app location is
# Contents/Resources/ (Bundle.main.resourceURL). Do
# NOT also put it in MacOS/ — a nested .bundle there breaks codesign with an
# "In subcomponent" error. Overlay degrades to a procedural ring if unresolved.
# "In subcomponent" error. The optional sequence may contain only its README;
# missing PNGs are supported, a missing declared build resource is not.
local res_bundle="${RESOURCE_BUNDLE_PATH:-$BUILD_DIR/$BUILD_CONFIG/cu-helper_cc-haha-computer-use.bundle}"
if [ -d "$res_bundle" ]; then
cp -R "$res_bundle" "$APP_PATH/Contents/Resources/"
fi
copy_cursor_resources "$res_bundle" "$APP_PATH"
# Sign the WHOLE bundle with the SAME stable identity + hardened runtime.
codesign \
@@ -473,6 +480,48 @@ wrap_app() {
log "verified: .app bundle Identifier=$app_id (stable)"
}
# Run the production loader from an independent .app location. Checking only a
# source-tree build can silently succeed through SwiftPM's absolute buildPath.
# The probe performs no input, GUI startup, permission checks, or user-state IO.
verify_relocated_cursor_resources() (
local host_arch
host_arch="$(uname -m)"
if [ "$ARCH" != "$host_arch" ]; then
log "skipped: cursor resource execution probe (target $ARCH, host $host_arch); package structure was verified before signing"
return 0
fi
# The subshell isolates this variable. Bash 3 unwinds function-local variables
# before an EXIT trap after die(), so it must remain available for cleanup.
probe_root="$(mktemp -d "${TMPDIR:-/tmp}/cc-haha-cursor-probe.XXXXXX")"
trap 'rm -rf "$probe_root"' EXIT
local probe_app="$probe_root/cc-haha-computer-use.app"
local report="$probe_root/resources.json"
cp -R "$APP_PATH" "$probe_app"
mkdir -p "$probe_root/home" "$probe_root/config" "$probe_root/tmp"
if ! env -i \
PATH=/usr/bin:/bin:/usr/sbin:/sbin \
HOME="$probe_root/home" \
CFFIXED_USER_HOME="$probe_root/home" \
CLAUDE_CONFIG_DIR="$probe_root/config" \
TMPDIR="$probe_root/tmp/" \
"$probe_app/Contents/MacOS/cc-haha-computer-use" --probe-cursor-resources >"$report"; then
die "Cursor resource probe failed for relocated helper $probe_app"
fi
local resource_directory
resource_directory="$(/usr/bin/plutil -extract resourceDirectory raw -o - "$report" 2>/dev/null)" \
|| die "Cursor resource probe did not report a resourceDirectory"
resource_directory="$(cd "$resource_directory" 2>/dev/null && pwd -P)" \
|| die "Cursor resource probe reported an unreadable resourceDirectory"
local expected_directory="$probe_app/Contents/Resources/cu-helper_cc-haha-computer-use.bundle/LensSequence"
[ -d "$expected_directory" ] || die "Cursor resource probe package is missing $expected_directory"
expected_directory="$(cd "$expected_directory" && pwd -P)"
[ "$resource_directory" = "$expected_directory" ] \
|| die "Cursor resource probe loaded '$resource_directory' instead of relocated package '$expected_directory'"
log "verified: relocated cursor resources ($resource_directory)"
)
# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------
@@ -483,6 +532,7 @@ main() {
sign
verify
wrap_app
verify_relocated_cursor_resources
# The ONE machine-readable line on STDOUT — the .app BUNDLE path. The caller
# (build-sidecars.ts) copies the whole .app; the runtime resolver
+97 -4
View File
@@ -1,11 +1,12 @@
import { afterEach, describe, expect, test } from 'bun:test'
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
const buildScript = path.resolve(import.meta.dirname, 'build.sh')
const productIcon = path.resolve(import.meta.dirname, '../../desktop/src-tauri/icons/icon.icns')
const fixtureDirectories: string[] = []
const resourceBundleName = 'cu-helper_cc-haha-computer-use.bundle'
function resolveArchitectureSpecificBuildPaths(arch: 'arm64' | 'x86_64') {
const directory = mkdtempSync(path.join(tmpdir(), 'cu-helper-build-path-'))
@@ -47,10 +48,15 @@ afterEach(() => {
}
})
function wrapFixtureApp(missingIcon = false) {
function wrapFixtureApp(options: { missingIcon?: boolean, missingResources?: boolean } = {}) {
const directory = mkdtempSync(path.join(tmpdir(), 'cu-helper-app-icon-'))
fixtureDirectories.push(directory)
writeFileSync(path.join(directory, 'fixture-binary'), 'fixture executable')
const resourceBundle = path.join(directory, resourceBundleName)
if (!options.missingResources) {
mkdirSync(path.join(resourceBundle, 'LensSequence'), { recursive: true })
writeFileSync(path.join(resourceBundle, 'LensSequence', 'README.md'), 'optional frames fixture')
}
const result = Bun.spawnSync([
'bash',
@@ -60,6 +66,7 @@ source "$1"
TEST_BUNDLE_DIR="$2"
BUILD_DIR="$TEST_BUNDLE_DIR/build"
BIN_PATH="$TEST_BUNDLE_DIR/fixture-binary"
RESOURCE_BUNDLE_PATH="$TEST_BUNDLE_DIR/cu-helper_cc-haha-computer-use.bundle"
APP_PATH="$TEST_BUNDLE_DIR/cc-haha-computer-use.app"
BUNDLE_ID="dev.cchaha.cu-helper"
SIGN_IDENTITY="fixture-only"
@@ -78,7 +85,7 @@ wrap_app
'cu-helper-app-icon-test',
buildScript,
directory,
missingIcon ? 'missing' : 'present',
options.missingIcon ? 'missing' : 'present',
], { cwd: directory })
return {
@@ -89,6 +96,49 @@ wrap_app
}
}
function probeFixtureApp(mode: 'packaged' | 'build-path' | 'invalid-json' | 'crash', crossArch = false) {
const directory = mkdtempSync(path.join(tmpdir(), 'cu-helper-resource-probe-'))
fixtureDirectories.push(directory)
const app = path.join(directory, 'source.app')
const binary = path.join(app, 'Contents', 'MacOS', 'cc-haha-computer-use')
mkdirSync(path.dirname(binary), { recursive: true })
mkdirSync(path.join(app, 'Contents', 'Resources', resourceBundleName, 'LensSequence'), { recursive: true })
writeFileSync(path.join(app, 'Contents', 'Resources', resourceBundleName, 'LensSequence', 'README.md'), 'optional frames fixture')
const buildTreeResources = path.join(directory, 'build-tree', resourceBundleName, 'LensSequence')
mkdirSync(buildTreeResources, { recursive: true })
writeFileSync(path.join(app, 'Contents', 'Resources', 'outside-resource-path'), buildTreeResources)
writeFileSync(binary, `#!/bin/bash
set -eu
[ "$1" = "--probe-cursor-resources" ] || exit 40
case '${mode}' in
crash) exit 41 ;;
invalid-json) printf 'not-json'; exit 0 ;;
build-path) resource_dir="$(cat "$(dirname "$0")/../Resources/outside-resource-path")" ;;
*) resource_dir="$(cd "$(dirname "$0")/../Resources/${resourceBundleName}/LensSequence" && pwd -P)" ;;
esac
printf '{"resourceDirectory":"%s","frameCount":0,"proceduralFallback":true}\\n' "$resource_dir"
`)
chmodSync(binary, 0o755)
const result = Bun.spawnSync([
'bash', '-c', `
source "$1"
APP_PATH="$2"
ARCH="$3"
uname() { printf 'arm64\\n'; }
verify_relocated_cursor_resources
`, 'cu-helper-resource-probe-test', buildScript, app,
crossArch ? 'x86_64' : 'arm64',
], {
env: { PATH: process.env.PATH, HOME: directory, TMPDIR: directory },
cwd: directory,
})
return {
exitCode: result.exitCode,
stderr: result.stderr.toString(),
leftovers: readdirSync(directory).filter(name => name.startsWith('cc-haha-cursor-probe.')),
}
}
function resolveTimestampArgument(identity: string, mode = 'auto') {
const result = Bun.spawnSync([
'bash',
@@ -208,10 +258,53 @@ describe.skipIf(process.platform !== 'darwin')('cu-helper permission-list app ic
})
test('refuses to sign an app when the required product icon is missing', () => {
const result = wrapFixtureApp(true)
const result = wrapFixtureApp({ missingIcon: true })
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toContain('App icon not found')
expect(existsSync(path.join(result.directory, 'contents-at-sign'))).toBe(false)
})
})
describe('cu-helper packaged cursor resources', () => {
test('copies the complete SwiftPM resource directory into standard Resources before signing', () => {
const result = wrapFixtureApp()
expect(result.exitCode).toBe(0)
const relative = path.join('Resources', resourceBundleName, 'LensSequence', 'README.md')
expect(readFileSync(path.join(result.contents, relative), 'utf8')).toBe('optional frames fixture')
expect(readFileSync(path.join(result.directory, 'contents-at-sign', relative), 'utf8')).toBe('optional frames fixture')
expect(existsSync(path.join(result.contents, 'MacOS', resourceBundleName))).toBe(false)
})
test('refuses to sign when the declared SwiftPM resource bundle was not produced', () => {
const result = wrapFixtureApp({ missingResources: true })
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toContain('Cursor resource bundle not found')
expect(existsSync(path.join(result.directory, 'contents-at-sign'))).toBe(false)
})
})
describe.skipIf(process.platform !== 'darwin')('cu-helper relocated resource probe', () => {
test('loads from the relocated app even when the optional frame directory has no PNGs', () => {
const result = probeFixtureApp('packaged')
expect(result.exitCode).toBe(0)
expect(result.stderr).toContain('verified: relocated cursor resources')
expect(result.leftovers).toEqual([])
})
test.each(['build-path', 'invalid-json', 'crash'] as const)('rejects %s instead of accepting a false resource-load success', mode => {
const result = probeFixtureApp(mode)
expect(result.exitCode).not.toBe(0)
expect(result.stderr).toContain('Cursor resource probe')
if (mode === 'build-path') expect(result.stderr).toContain('instead of relocated package')
expect(result.leftovers).toEqual([])
})
test('reports cross-architecture execution as skipped without running the probe', () => {
const result = probeFixtureApp('crash', true)
expect(result.exitCode).toBe(0)
expect(result.stderr).toContain('skipped: cursor resource execution probe (target x86_64, host arm64)')
expect(result.stderr).not.toContain('verified: relocated cursor resources')
expect(result.leftovers).toEqual([])
})
})