@kaeawc/auto-mobile 0.0.44 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +3 -3
  2. package/README.md.backup +3 -3
  3. package/dist/ios/screen-capture/Package.swift +35 -0
  4. package/dist/ios/screen-capture/Sources/ScreenCaptureCore/AudioPcm16Encoder.swift +22 -0
  5. package/dist/ios/screen-capture/Sources/ScreenCaptureCore/CommandLineOptions.swift +152 -0
  6. package/dist/ios/screen-capture/Sources/ScreenCaptureCore/DeviceInfo.swift +25 -0
  7. package/dist/ios/screen-capture/Sources/ScreenCaptureCore/FrameProtocol.swift +113 -0
  8. package/dist/ios/screen-capture/Sources/ScreenCaptureCore/FrameWriter.swift +62 -0
  9. package/dist/ios/screen-capture/Sources/ScreenCaptureCore/SimulatorWindowInfo.swift +51 -0
  10. package/dist/ios/screen-capture/Sources/ScreenCaptureHelper/AudioSampleBuffer.swift +41 -0
  11. package/dist/ios/screen-capture/Sources/ScreenCaptureHelper/CMIOSystem.swift +26 -0
  12. package/dist/ios/screen-capture/Sources/ScreenCaptureHelper/DeviceCaptureSession.swift +117 -0
  13. package/dist/ios/screen-capture/Sources/ScreenCaptureHelper/DeviceDiscovery.swift +29 -0
  14. package/dist/ios/screen-capture/Sources/ScreenCaptureHelper/FrameWriter+PixelBuffer.swift +25 -0
  15. package/dist/ios/screen-capture/Sources/ScreenCaptureHelper/SimulatorCaptureSession.swift +159 -0
  16. package/dist/ios/screen-capture/Sources/ScreenCaptureHelper/SimulatorWindowDiscovery.swift +37 -0
  17. package/dist/ios/screen-capture/Sources/ScreenCaptureHelper/main.swift +255 -0
  18. package/dist/ios/screen-capture/Tests/ScreenCaptureCoreTests/AudioPcm16EncoderTests.swift +17 -0
  19. package/dist/ios/screen-capture/Tests/ScreenCaptureCoreTests/CommandLineOptionsTests.swift +181 -0
  20. package/dist/ios/screen-capture/Tests/ScreenCaptureCoreTests/DeviceInfoTests.swift +35 -0
  21. package/dist/ios/screen-capture/Tests/ScreenCaptureCoreTests/FrameProtocolTests.swift +123 -0
  22. package/dist/ios/screen-capture/Tests/ScreenCaptureCoreTests/FrameWriterTests.swift +62 -0
  23. package/dist/ios/screen-capture/Tests/ScreenCaptureCoreTests/SimulatorAudioCaptureAvailabilityTests.swift +27 -0
  24. package/dist/ios/screen-capture/Tests/ScreenCaptureCoreTests/SimulatorWindowInfoTests.swift +40 -0
  25. package/dist/schemas/tool-definitions.json +1274 -849
  26. package/dist/src/db/migrations/2026_07_03_000_repair_datetime_now_defaults.ts +11 -2
  27. package/dist/src/index.js +670 -407
  28. package/dist/src/index.js.map +1 -1
  29. package/package.json +25 -6
  30. package/schemas/tool-definitions.json +1274 -849
@@ -0,0 +1,181 @@
1
+ import XCTest
2
+ @testable import ScreenCaptureCore
3
+
4
+ final class CommandLineOptionsTests: XCTestCase {
5
+ func testParsesAudioForSimulatorCapture() throws {
6
+ let options = try CommandLineOptions.parse([
7
+ "screen-capture-helper", "--simulator-window", "42", "--audio"
8
+ ])
9
+
10
+ XCTAssertEqual(options.mode, .captureSimulator(windowID: 42, fps: 5, audio: true))
11
+ }
12
+
13
+ func testDefaultsToCaptureWithoutDeviceID() throws {
14
+ let opts = try CommandLineOptions.parse(["screen-capture-helper"])
15
+ XCTAssertEqual(opts.mode, .capture(deviceID: nil))
16
+ }
17
+
18
+ func testParsesDeviceID() throws {
19
+ let opts = try CommandLineOptions.parse([
20
+ "screen-capture-helper", "--device-id", "ABC123"
21
+ ])
22
+ XCTAssertEqual(opts.mode, .capture(deviceID: "ABC123"))
23
+ }
24
+
25
+ func testParsesListDevices() throws {
26
+ let opts = try CommandLineOptions.parse([
27
+ "screen-capture-helper", "--list-devices"
28
+ ])
29
+ XCTAssertEqual(opts.mode, .listDevices)
30
+ }
31
+
32
+ func testParsesHelpFlag() throws {
33
+ let opts = try CommandLineOptions.parse([
34
+ "screen-capture-helper", "--help"
35
+ ])
36
+ XCTAssertEqual(opts.mode, .help)
37
+ }
38
+
39
+ func testMissingDeviceIDValueThrows() {
40
+ XCTAssertThrowsError(try CommandLineOptions.parse([
41
+ "screen-capture-helper", "--device-id"
42
+ ])) { error in
43
+ XCTAssertEqual(
44
+ error as? CommandLineOptions.ParseError,
45
+ .missingValue(flag: "--device-id")
46
+ )
47
+ }
48
+ }
49
+
50
+ func testUnknownArgumentThrows() {
51
+ XCTAssertThrowsError(try CommandLineOptions.parse([
52
+ "screen-capture-helper", "--bogus"
53
+ ])) { error in
54
+ XCTAssertEqual(
55
+ error as? CommandLineOptions.ParseError,
56
+ .unknownArgument("--bogus")
57
+ )
58
+ }
59
+ }
60
+
61
+ func testParsesListSimulators() throws {
62
+ let opts = try CommandLineOptions.parse([
63
+ "screen-capture-helper", "--list-simulators"
64
+ ])
65
+ XCTAssertEqual(opts.mode, .listSimulators)
66
+ }
67
+
68
+ func testParsesSimulatorWindowWithDefaultFPS() throws {
69
+ let opts = try CommandLineOptions.parse([
70
+ "screen-capture-helper", "--simulator-window", "98765"
71
+ ])
72
+ XCTAssertEqual(
73
+ opts.mode,
74
+ .captureSimulator(
75
+ windowID: 98765,
76
+ fps: CommandLineOptions.defaultSimulatorFPS,
77
+ audio: false
78
+ )
79
+ )
80
+ }
81
+
82
+ func testParsesSimulatorFPSWithinRange() throws {
83
+ let opts = try CommandLineOptions.parse([
84
+ "screen-capture-helper",
85
+ "--simulator-window", "1",
86
+ "--simulator-fps", "30",
87
+ ])
88
+ XCTAssertEqual(opts.mode, .captureSimulator(windowID: 1, fps: 30, audio: false))
89
+ }
90
+
91
+ func testRejectsSimulatorFPSBelowRange() {
92
+ XCTAssertThrowsError(try CommandLineOptions.parse([
93
+ "screen-capture-helper",
94
+ "--simulator-window", "1",
95
+ "--simulator-fps", "4",
96
+ ])) { error in
97
+ XCTAssertEqual(
98
+ error as? CommandLineOptions.ParseError,
99
+ .invalidValue(flag: "--simulator-fps", value: "4")
100
+ )
101
+ }
102
+ }
103
+
104
+ func testRejectsSimulatorFPSAboveRange() {
105
+ XCTAssertThrowsError(try CommandLineOptions.parse([
106
+ "screen-capture-helper",
107
+ "--simulator-window", "1",
108
+ "--simulator-fps", "61",
109
+ ])) { error in
110
+ XCTAssertEqual(
111
+ error as? CommandLineOptions.ParseError,
112
+ .invalidValue(flag: "--simulator-fps", value: "61")
113
+ )
114
+ }
115
+ }
116
+
117
+ func testRejectsSimulatorFPSWithoutWindow() {
118
+ XCTAssertThrowsError(try CommandLineOptions.parse([
119
+ "screen-capture-helper", "--simulator-fps", "15",
120
+ ])) { error in
121
+ guard case CommandLineOptions.ParseError.conflictingFlags = error else {
122
+ XCTFail("expected conflictingFlags, got \(error)")
123
+ return
124
+ }
125
+ }
126
+ }
127
+
128
+ func testDefaultSimulatorFPSConstantIs5() {
129
+ XCTAssertEqual(CommandLineOptions.defaultSimulatorFPS, 5)
130
+ XCTAssertEqual(CommandLineOptions.minSimulatorFPS, 5)
131
+ XCTAssertEqual(CommandLineOptions.maxSimulatorFPS, 60)
132
+ }
133
+
134
+ func testInvalidSimulatorWindowValueThrows() {
135
+ XCTAssertThrowsError(try CommandLineOptions.parse([
136
+ "screen-capture-helper", "--simulator-window", "not-a-number"
137
+ ])) { error in
138
+ XCTAssertEqual(
139
+ error as? CommandLineOptions.ParseError,
140
+ .invalidValue(flag: "--simulator-window", value: "not-a-number")
141
+ )
142
+ }
143
+ }
144
+
145
+ func testMissingSimulatorWindowValueThrows() {
146
+ XCTAssertThrowsError(try CommandLineOptions.parse([
147
+ "screen-capture-helper", "--simulator-window"
148
+ ])) { error in
149
+ XCTAssertEqual(
150
+ error as? CommandLineOptions.ParseError,
151
+ .missingValue(flag: "--simulator-window")
152
+ )
153
+ }
154
+ }
155
+
156
+ func testConflictingDeviceAndSimulatorFlagsThrow() {
157
+ XCTAssertThrowsError(try CommandLineOptions.parse([
158
+ "screen-capture-helper",
159
+ "--device-id", "abc",
160
+ "--simulator-window", "1",
161
+ ])) { error in
162
+ guard case CommandLineOptions.ParseError.conflictingFlags = error else {
163
+ XCTFail("expected conflictingFlags, got \(error)")
164
+ return
165
+ }
166
+ }
167
+ }
168
+
169
+ func testConflictingListFlagsThrow() {
170
+ XCTAssertThrowsError(try CommandLineOptions.parse([
171
+ "screen-capture-helper",
172
+ "--list-devices",
173
+ "--list-simulators",
174
+ ])) { error in
175
+ guard case CommandLineOptions.ParseError.conflictingFlags = error else {
176
+ XCTFail("expected conflictingFlags, got \(error)")
177
+ return
178
+ }
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,35 @@
1
+ import XCTest
2
+ @testable import ScreenCaptureCore
3
+
4
+ final class DeviceInfoTests: XCTestCase {
5
+ func testRoundTripsThroughJSON() throws {
6
+ let original = DeviceListResponse(devices: [
7
+ DeviceInfo(
8
+ uniqueID: "00008140-001A2B3C0AE2401E",
9
+ localizedName: "iPhone",
10
+ modelID: "iPhone15,2",
11
+ manufacturer: "Apple"
12
+ )
13
+ ])
14
+ let data = try JSONEncoder().encode(original)
15
+ let decoded = try JSONDecoder().decode(DeviceListResponse.self, from: data)
16
+ XCTAssertEqual(decoded, original)
17
+ }
18
+
19
+ func testJSONUsesExpectedKeys() throws {
20
+ let device = DeviceInfo(
21
+ uniqueID: "id",
22
+ localizedName: "name",
23
+ modelID: "model",
24
+ manufacturer: "vendor"
25
+ )
26
+ let data = try JSONEncoder().encode(device)
27
+ let json = try XCTUnwrap(
28
+ JSONSerialization.jsonObject(with: data) as? [String: String]
29
+ )
30
+ XCTAssertEqual(json["uniqueID"], "id")
31
+ XCTAssertEqual(json["localizedName"], "name")
32
+ XCTAssertEqual(json["modelID"], "model")
33
+ XCTAssertEqual(json["manufacturer"], "vendor")
34
+ }
35
+ }
@@ -0,0 +1,123 @@
1
+ // swiftlint:disable force_unwrapping
2
+ // Force-unwrap is idiomatic in test fixtures (fail fast on bad setup); disabled file-wide.
3
+
4
+ import XCTest
5
+ @testable import ScreenCaptureCore
6
+
7
+ final class FrameProtocolTests: XCTestCase {
8
+ func testEncodeAudioHeaderUsesReservedZeroWidthMarker() {
9
+ let data = FrameProtocol.encodeAudioHeader(payloadLength: 320)
10
+
11
+ XCTAssertEqual(data.count, FrameProtocol.headerSize)
12
+ // Marker "AMF1" on the wire.
13
+ XCTAssertEqual(Array(data.prefix(4)), [0x41, 0x4D, 0x46, 0x31])
14
+ // Field bytes (offset 8): width=0, height=8000 (0x1F40), bytesPerRow=1, timestampMs=320 (0x140).
15
+ XCTAssertEqual(Array(data[8..<24]), [
16
+ 0, 0, 0, 0,
17
+ 0x40, 0x1F, 0, 0,
18
+ 1, 0, 0, 0,
19
+ 0x40, 1, 0, 0,
20
+ ])
21
+ XCTAssertEqual(
22
+ FrameProtocol.decodeHeader(data),
23
+ FrameProtocol.Header(width: 0, height: 8_000, bytesPerRow: 1, timestampMs: 320)
24
+ )
25
+ }
26
+
27
+ func testEncodeHeaderPlacesMarkerChecksumAndLittleEndianFields() {
28
+ let header = FrameProtocol.Header(
29
+ width: 0x01020304,
30
+ height: 0x05060708,
31
+ bytesPerRow: 0x090A0B0C,
32
+ timestampMs: 0x0D0E0F10
33
+ )
34
+ let data = FrameProtocol.encodeHeader(header)
35
+ XCTAssertEqual(data.count, FrameProtocol.headerSize)
36
+ XCTAssertEqual(data.withUnsafeBytes { $0.loadUnaligned(as: UInt32.self).littleEndian }, FrameProtocol.magic)
37
+ XCTAssertEqual(Array(data[8..<24]), [
38
+ 0x04, 0x03, 0x02, 0x01,
39
+ 0x08, 0x07, 0x06, 0x05,
40
+ 0x0C, 0x0B, 0x0A, 0x09,
41
+ 0x10, 0x0F, 0x0E, 0x0D,
42
+ ])
43
+ // The stored checksum matches the CRC-32 of the field bytes.
44
+ let storedChecksum = data.dropFirst(4).withUnsafeBytes { $0.loadUnaligned(as: UInt32.self).littleEndian }
45
+ XCTAssertEqual(storedChecksum, FrameProtocol.crc32(data.subdata(in: 8..<24)))
46
+ XCTAssertEqual(FrameProtocol.decodeHeader(data), header)
47
+ }
48
+
49
+ func testCrc32MatchesStandardCheckVector() {
50
+ // The canonical CRC-32 check value for the ASCII string "123456789",
51
+ // shared with the TypeScript decoder so the two agree byte for byte.
52
+ XCTAssertEqual(FrameProtocol.crc32(Data("123456789".utf8)), 0xCBF4_3926)
53
+ }
54
+
55
+ func testDecodeRejectsWrongMarker() {
56
+ var data = FrameProtocol.encodeHeader(
57
+ FrameProtocol.Header(width: 2, height: 2, bytesPerRow: 8, timestampMs: 1)
58
+ )
59
+ data[0] ^= 0xFF // corrupt the marker
60
+ XCTAssertNil(FrameProtocol.decodeHeader(data))
61
+ }
62
+
63
+ func testDecodeRejectsWrongChecksum() {
64
+ var data = FrameProtocol.encodeHeader(
65
+ FrameProtocol.Header(width: 2, height: 2, bytesPerRow: 8, timestampMs: 1)
66
+ )
67
+ data[8] ^= 0xFF // corrupt a field without fixing the checksum
68
+ XCTAssertNil(FrameProtocol.decodeHeader(data))
69
+ }
70
+
71
+ func testDecodeHeaderRoundTrip() {
72
+ let header = FrameProtocol.Header(
73
+ width: 1170,
74
+ height: 2532,
75
+ bytesPerRow: 4680,
76
+ timestampMs: 1_234_567
77
+ )
78
+ let data = FrameProtocol.encodeHeader(header)
79
+ XCTAssertEqual(FrameProtocol.decodeHeader(data), header)
80
+ }
81
+
82
+ func testDecodeRejectsShortBuffer() {
83
+ let truncated = Data(repeating: 0, count: FrameProtocol.headerSize - 1)
84
+ XCTAssertNil(FrameProtocol.decodeHeader(truncated))
85
+ }
86
+
87
+ /// Decoding a header from an unaligned buffer must not trap. The pre-fix
88
+ /// `load(as:)` requires 4-byte alignment; feeding it a header at an odd
89
+ /// address traps in debug (issue #3627). `loadUnaligned` decodes correctly.
90
+ func testDecodeHeaderHandlesUnalignedBuffer() {
91
+ let header = FrameProtocol.Header(
92
+ width: 0x1122_3344,
93
+ height: 0x5566_7788,
94
+ bytesPerRow: 0x99AA_BBCC,
95
+ timestampMs: 0xDDEE_FF00
96
+ )
97
+ let encoded = FrameProtocol.encodeHeader(header)
98
+
99
+ // Place the 16 header bytes at an odd (unaligned) offset in a raw buffer.
100
+ let raw = UnsafeMutableRawPointer.allocate(
101
+ byteCount: FrameProtocol.headerSize + 1,
102
+ alignment: 1
103
+ )
104
+ defer { raw.deallocate() }
105
+ encoded.withUnsafeBytes { src in
106
+ raw.advanced(by: 1).copyMemory(from: src.baseAddress!, byteCount: FrameProtocol.headerSize)
107
+ }
108
+ let unaligned = Data(
109
+ bytesNoCopy: raw.advanced(by: 1),
110
+ count: FrameProtocol.headerSize,
111
+ deallocator: .none
112
+ )
113
+
114
+ XCTAssertEqual(FrameProtocol.decodeHeader(unaligned), header)
115
+ }
116
+
117
+ func testEncodeDecodeRoundTrip() {
118
+ let header = FrameProtocol.Header(
119
+ width: 1290, height: 2796, bytesPerRow: 5160, timestampMs: 123_456
120
+ )
121
+ XCTAssertEqual(FrameProtocol.decodeHeader(FrameProtocol.encodeHeader(header)), header)
122
+ }
123
+ }
@@ -0,0 +1,62 @@
1
+ // swiftlint:disable force_unwrapping
2
+ // Force-unwrap is idiomatic in test fixtures (fail fast on bad setup); disabled file-wide.
3
+
4
+ import XCTest
5
+ @testable import ScreenCaptureCore
6
+
7
+ final class BufferSink: FrameSink {
8
+ var data = Data()
9
+ func write(_ chunk: Data) { data.append(chunk) }
10
+ }
11
+
12
+ final class FrameWriterTests: XCTestCase {
13
+ func testWritesHeaderFollowedByPayload() {
14
+ let sink = BufferSink()
15
+ let start = Date(timeIntervalSince1970: 0)
16
+ let writer = FrameWriter(sink: sink, startTime: start)
17
+
18
+ let width = 2
19
+ let height = 3
20
+ let bytesPerRow = 8
21
+ // 24 bytes of BGRA: 2px × 3 rows × 4 bytes
22
+ let payload = Array(0..<UInt8(bytesPerRow * height))
23
+
24
+ payload.withUnsafeBufferPointer { ptr in
25
+ writer.write(
26
+ width: width,
27
+ height: height,
28
+ bytesPerRow: bytesPerRow,
29
+ baseAddress: UnsafeRawPointer(ptr.baseAddress!),
30
+ timestamp: Date(timeIntervalSince1970: 0.5)
31
+ )
32
+ }
33
+
34
+ XCTAssertEqual(sink.data.count, FrameProtocol.headerSize + bytesPerRow * height)
35
+ let header = FrameProtocol.decodeHeader(sink.data.prefix(FrameProtocol.headerSize))
36
+ XCTAssertEqual(header?.width, UInt32(width))
37
+ XCTAssertEqual(header?.height, UInt32(height))
38
+ XCTAssertEqual(header?.bytesPerRow, UInt32(bytesPerRow))
39
+ XCTAssertEqual(header?.timestampMs, 500)
40
+ XCTAssertEqual(Array(sink.data.dropFirst(FrameProtocol.headerSize)), payload)
41
+ }
42
+
43
+ func testTimestampClampedToZeroForPriorStartTime() {
44
+ let sink = BufferSink()
45
+ let writer = FrameWriter(
46
+ sink: sink,
47
+ startTime: Date(timeIntervalSince1970: 100)
48
+ )
49
+ let payload: [UInt8] = [0, 1, 2, 3]
50
+ payload.withUnsafeBufferPointer { ptr in
51
+ writer.write(
52
+ width: 1,
53
+ height: 1,
54
+ bytesPerRow: 4,
55
+ baseAddress: UnsafeRawPointer(ptr.baseAddress!),
56
+ timestamp: Date(timeIntervalSince1970: 0)
57
+ )
58
+ }
59
+ let header = FrameProtocol.decodeHeader(sink.data.prefix(FrameProtocol.headerSize))
60
+ XCTAssertEqual(header?.timestampMs, 0)
61
+ }
62
+ }
@@ -0,0 +1,27 @@
1
+ import XCTest
2
+ @testable import ScreenCaptureCore
3
+
4
+ final class SimulatorAudioCaptureAvailabilityTests: XCTestCase {
5
+ func testAllowsAudioWhenExactlyOneSimulatorWindowIsVisible() {
6
+ XCTAssertNil(SimulatorAudioCaptureAvailability.errorMessage(for: [window(id: 1)]))
7
+ }
8
+
9
+ func testRejectsAudioWhenMultipleSimulatorWindowsAreVisible() {
10
+ XCTAssertEqual(
11
+ SimulatorAudioCaptureAvailability.errorMessage(for: [window(id: 1), window(id: 2)]),
12
+ "iOS Simulator audio capture requires exactly one visible Simulator window because ScreenCaptureKit cannot isolate audio to a selected Simulator window. Close other Simulator windows and try again."
13
+ )
14
+ }
15
+
16
+ private func window(id: UInt32) -> SimulatorWindowInfo {
17
+ SimulatorWindowInfo(
18
+ windowID: id,
19
+ title: "iPhone",
20
+ applicationName: "Simulator",
21
+ bundleIdentifier: simulatorBundleIdentifier,
22
+ processID: 1,
23
+ width: 390,
24
+ height: 844
25
+ )
26
+ }
27
+ }
@@ -0,0 +1,40 @@
1
+ import XCTest
2
+ @testable import ScreenCaptureCore
3
+
4
+ final class SimulatorWindowInfoTests: XCTestCase {
5
+ func testRoundTripsThroughJSON() throws {
6
+ let original = SimulatorWindowListResponse(windows: [
7
+ SimulatorWindowInfo(
8
+ windowID: 12345,
9
+ title: "iPhone 15 Pro — iOS 17.4",
10
+ applicationName: "Simulator",
11
+ bundleIdentifier: simulatorBundleIdentifier,
12
+ processID: 4242,
13
+ width: 1170,
14
+ height: 2532
15
+ )
16
+ ])
17
+ let data = try JSONEncoder().encode(original)
18
+ let decoded = try JSONDecoder().decode(SimulatorWindowListResponse.self, from: data)
19
+ XCTAssertEqual(decoded, original)
20
+ }
21
+
22
+ func testTitleMayBeNil() throws {
23
+ let info = SimulatorWindowInfo(
24
+ windowID: 1,
25
+ title: nil,
26
+ applicationName: "Simulator",
27
+ bundleIdentifier: simulatorBundleIdentifier,
28
+ processID: 1,
29
+ width: 0,
30
+ height: 0
31
+ )
32
+ let data = try JSONEncoder().encode(info)
33
+ let decoded = try JSONDecoder().decode(SimulatorWindowInfo.self, from: data)
34
+ XCTAssertNil(decoded.title)
35
+ }
36
+
37
+ func testSimulatorBundleIdentifierConstant() {
38
+ XCTAssertEqual(simulatorBundleIdentifier, "com.apple.iphonesimulator")
39
+ }
40
+ }