@hasna/recordings 0.2.11 → 0.2.13

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 (167) hide show
  1. package/README.md +349 -69
  2. package/bun.lock +3 -0
  3. package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
  4. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
  5. package/dist/cli/index.js +1728 -320
  6. package/dist/cli/macos-permissions.d.ts +13 -0
  7. package/dist/cli/macos-permissions.d.ts.map +1 -0
  8. package/dist/cli/options.d.ts +12 -0
  9. package/dist/cli/options.d.ts.map +1 -1
  10. package/dist/db/pg-migrations.d.ts.map +1 -1
  11. package/dist/db/recordings.d.ts +2 -1
  12. package/dist/db/recordings.d.ts.map +1 -1
  13. package/dist/db/remote-storage.d.ts +5 -1
  14. package/dist/db/remote-storage.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +628 -147
  18. package/dist/lib/bun-runtime.d.ts +11 -0
  19. package/dist/lib/bun-runtime.d.ts.map +1 -0
  20. package/dist/lib/config.d.ts +8 -1
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/enhancer.d.ts +10 -2
  23. package/dist/lib/enhancer.d.ts.map +1 -1
  24. package/dist/lib/install-maintenance.d.ts +24 -0
  25. package/dist/lib/install-maintenance.d.ts.map +1 -0
  26. package/dist/lib/machine.d.ts +2 -0
  27. package/dist/lib/machine.d.ts.map +1 -0
  28. package/dist/lib/recorder.d.ts +2 -1
  29. package/dist/lib/recorder.d.ts.map +1 -1
  30. package/dist/lib/recording-create-identity.d.ts +13 -0
  31. package/dist/lib/recording-create-identity.d.ts.map +1 -0
  32. package/dist/lib/release-install-policy.d.ts +31 -0
  33. package/dist/lib/release-install-policy.d.ts.map +1 -0
  34. package/dist/lib/transcriber.d.ts.map +1 -1
  35. package/dist/mcp/index.d.ts.map +1 -1
  36. package/dist/mcp/index.js +977 -194
  37. package/dist/sdk/index.js +5 -1
  38. package/dist/sdk/v1.generated.d.ts +4 -0
  39. package/dist/sdk/v1.generated.d.ts.map +1 -1
  40. package/dist/server/cloud-config.d.ts +9 -0
  41. package/dist/server/cloud-config.d.ts.map +1 -0
  42. package/dist/server/cloud-readiness.d.ts +10 -0
  43. package/dist/server/cloud-readiness.d.ts.map +1 -0
  44. package/dist/server/cloud.d.ts +9 -18
  45. package/dist/server/cloud.d.ts.map +1 -1
  46. package/dist/server/index.js +1654 -304
  47. package/dist/server/migrate-command.d.ts +11 -0
  48. package/dist/server/migrate-command.d.ts.map +1 -0
  49. package/dist/server/openapi.d.ts +22 -0
  50. package/dist/server/openapi.d.ts.map +1 -1
  51. package/dist/server/repo.d.ts +8 -1
  52. package/dist/server/repo.d.ts.map +1 -1
  53. package/dist/server/serve.d.ts +6 -1
  54. package/dist/server/serve.d.ts.map +1 -1
  55. package/dist/server/v1.d.ts.map +1 -1
  56. package/dist/storage.js +515 -80
  57. package/dist/store.d.ts +3 -1
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/types/index.d.ts +10 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/version.d.ts +1 -1
  62. package/package.json +7 -4
  63. package/packaging/macos/Empty.entitlements +5 -0
  64. package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
  65. package/packaging/macos/Verifier.entitlements +10 -0
  66. package/packaging/macos/artifact-verifier.sb +33 -0
  67. package/packaging/macos/build_release_pkg.sh +872 -0
  68. package/packaging/macos/managed_bootstrap.sh +623 -0
  69. package/packaging/macos/pkgutil_fingerprint.awk +37 -0
  70. package/packaging/macos/release_lifecycle.ts +463 -0
  71. package/packaging/macos/scripts/postinstall +565 -0
  72. package/packaging/macos/scripts/preinstall +297 -0
  73. package/scripts/build_companion_cli.sh +337 -0
  74. package/scripts/build_native_fs_guard.sh +59 -0
  75. package/scripts/generate-sdk.ts +19 -1
  76. package/scripts/install_macos_app.sh +1919 -98
  77. package/scripts/macos_artifact.ts +5316 -0
  78. package/scripts/migrate.ts +38 -10
  79. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  80. package/scripts/native/recordings_fs_guard.c +1167 -0
  81. package/scripts/native_fs_guard.ts +158 -0
  82. package/scripts/release-guard.ts +20 -1
  83. package/scripts/resolve_tailscale_cli.sh +389 -0
  84. package/scripts/smoke_macos_app.sh +573 -0
  85. package/src/native/Recordings/App/ContentView.swift +83 -0
  86. package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
  87. package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
  88. package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
  89. package/src/native/Recordings/App/RecordingsApp.swift +367 -44
  90. package/src/native/Recordings/App/RecordingsListView.swift +130 -0
  91. package/src/native/Recordings/App/RecordingsStore.swift +206 -0
  92. package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
  93. package/src/native/Recordings/App/SidebarView.swift +212 -0
  94. package/src/native/Recordings/App/Theme.swift +87 -0
  95. package/src/native/Recordings/Package.resolved +5 -5
  96. package/src/native/Recordings/Package.swift +62 -2
  97. package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
  98. package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
  99. package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
  101. package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
  102. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
  103. package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
  104. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
  105. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
  106. package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
  107. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
  108. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
  109. package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
  110. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
  111. package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
  112. package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
  113. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
  114. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
  115. package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
  116. package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
  117. package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
  118. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
  119. package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
  120. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
  121. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
  122. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
  123. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
  124. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
  125. package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
  126. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
  127. package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
  128. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
  129. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
  130. package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
  131. package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
  132. package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
  133. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
  134. package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
  135. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
  136. package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
  137. package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
  138. package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
  139. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
  140. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
  141. package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
  142. package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
  143. package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
  144. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
  145. package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
  146. package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
  147. package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
  148. package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
  149. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
  150. package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
  151. package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
  152. package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
  153. package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
  154. package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
  155. package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
  156. package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
  157. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
  158. package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
  159. package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
  160. package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
  161. package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
  162. package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
  163. package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
  164. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
  165. package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
  166. package/src/native/Recordings/build.sh +1973 -236
  167. package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
@@ -1,11 +1,97 @@
1
1
  import Testing
2
+ import Foundation
2
3
  @testable import RecordingsLib
3
4
 
5
+ @MainActor
6
+ private final class FakeRealtimeClock {
7
+ var nowMilliseconds: UInt64 = 0
8
+
9
+ func advance(by milliseconds: UInt64) {
10
+ nowMilliseconds += milliseconds
11
+ }
12
+ }
13
+
14
+ @MainActor
15
+ private final class RealtimeDeliveryProbe {
16
+ var deliveredText: String?
17
+ var persistedResult: RealtimeFastPathSaveResult?
18
+ }
19
+
20
+ private actor BlockingRealtimePersistence {
21
+ private let result: RealtimeFastPathSaveResult
22
+ private var started = false
23
+ private var released = false
24
+ private var startWaiters: [CheckedContinuation<Void, Never>] = []
25
+ private var releaseWaiters: [CheckedContinuation<Void, Never>] = []
26
+
27
+ init(result: RealtimeFastPathSaveResult = RealtimeFastPathSaveResult(text: "stored text", error: nil)) {
28
+ self.result = result
29
+ }
30
+
31
+ func run() async -> RealtimeFastPathSaveResult {
32
+ started = true
33
+ let waiters = startWaiters
34
+ startWaiters.removeAll()
35
+ waiters.forEach { $0.resume() }
36
+ if !released {
37
+ await withCheckedContinuation { releaseWaiters.append($0) }
38
+ }
39
+ return result
40
+ }
41
+
42
+ func waitUntilStarted() async {
43
+ if started { return }
44
+ await withCheckedContinuation { startWaiters.append($0) }
45
+ }
46
+
47
+ func hasStarted() -> Bool {
48
+ started
49
+ }
50
+
51
+ func release() {
52
+ released = true
53
+ let waiters = releaseWaiters
54
+ releaseWaiters.removeAll()
55
+ waiters.forEach { $0.resume() }
56
+ }
57
+ }
58
+
59
+ private actor BlockingRealtimeDelivery {
60
+ private var started = false
61
+ private var released = false
62
+ private var startWaiters: [CheckedContinuation<Void, Never>] = []
63
+ private var releaseWaiters: [CheckedContinuation<Void, Never>] = []
64
+
65
+ func run() async {
66
+ started = true
67
+ let waiters = startWaiters
68
+ startWaiters.removeAll()
69
+ waiters.forEach { $0.resume() }
70
+ if !released {
71
+ await withCheckedContinuation { releaseWaiters.append($0) }
72
+ }
73
+ }
74
+
75
+ func waitUntilStarted() async {
76
+ if started { return }
77
+ await withCheckedContinuation { startWaiters.append($0) }
78
+ }
79
+
80
+ func release() {
81
+ released = true
82
+ let waiters = releaseWaiters
83
+ releaseWaiters.removeAll()
84
+ waiters.forEach { $0.resume() }
85
+ }
86
+ }
87
+
4
88
  // MARK: - RealtimeTranscriptionClient Event Parsing Tests
5
89
 
6
90
  struct RealtimeTranscriptionTests {
7
91
  @Test("Model ID is set to low-latency realtime transcription model")
8
92
  func modelID() {
93
+ #expect(RealtimeTranscriptionClient.sessionModelID == "gpt-realtime")
94
+ #expect(RealtimeTranscriptionClient.transcriptionModelID == "gpt-realtime-whisper")
9
95
  #expect(RealtimeTranscriptionClient.modelID == "gpt-realtime-whisper")
10
96
  #expect(RealtimeTranscriptionClient.transcriptionDelay == "low")
11
97
  }
@@ -71,6 +157,44 @@ struct RealtimeTranscriptionTests {
71
157
  #expect(RealtimeTranscriptionClient.parseErrorTestHelper(errorJSON) == "Model not found")
72
158
  }
73
159
 
160
+ @Test("Realtime error parsing redacts credentials before returning display-safe text")
161
+ func parseErrorMessageRedactsCredentials() throws {
162
+ let keyFragment = "sk-" + "synthetic-fragment-123456"
163
+ let bearerFragment = "synthetic-bearer-123456"
164
+ let tokenFragment = "synthetic-query-token-123456"
165
+ let message = "401 Incorrect API key provided: \(keyFragment); Authorization: Bearer \(bearerFragment); request?token=\(tokenFragment)"
166
+ let event: [String: Any] = ["type": "error", "error": ["message": message, "code": 401]]
167
+ let data = try JSONSerialization.data(withJSONObject: event)
168
+ let json = try #require(String(data: data, encoding: .utf8))
169
+
170
+ let parsed = try #require(RealtimeTranscriptionClient.parseErrorTestHelper(json))
171
+
172
+ #expect(!parsed.contains(keyFragment))
173
+ #expect(!parsed.contains(bearerFragment))
174
+ #expect(!parsed.contains(tokenFragment))
175
+ #expect(parsed.contains("401 Incorrect API key provided"))
176
+ }
177
+
178
+ @Test("Realtime error events store only sanitized state and preserve ordinary errors")
179
+ @MainActor
180
+ func errorEventStateIsSanitized() throws {
181
+ let keyFragment = "sk-" + "synthetic-state-fragment-123456"
182
+ let event: [String: Any] = [
183
+ "type": "conversation.item.input_audio_transcription.failed",
184
+ "error": ["message": "401 rejected \(keyFragment)"],
185
+ ]
186
+ let data = try JSONSerialization.data(withJSONObject: event)
187
+ let json = try #require(String(data: data, encoding: .utf8))
188
+ let client = RealtimeTranscriptionClient(apiKey: "synthetic-test-value", homePath: "/tmp")
189
+
190
+ client.handleEventTestHelper(json)
191
+
192
+ #expect(client.error == "401 rejected [REDACTED]")
193
+ #expect(RealtimeTranscriptionClient.parseErrorTestHelper(
194
+ #"{"type":"error","error":{"message":"Model not found"}}"#
195
+ ) == "Model not found")
196
+ }
197
+
74
198
  @Test("Builds strict verbatim prompt with vocabulary context")
75
199
  func buildPrompt() {
76
200
  let prompt = RealtimeTranscriptionClient.buildPromptTestHelper("Alumia, Takumi")
@@ -100,8 +224,7 @@ struct RealtimeTranscriptionTests {
100
224
  #expect(transcription?["prompt"] as? String == nil)
101
225
  #expect(transcription?["language"] as? String == "en")
102
226
 
103
- let turnDetection = input?["turn_detection"] as? [String: Any]
104
- #expect(turnDetection == nil)
227
+ #expect(input?["turn_detection"] is NSNull)
105
228
 
106
229
  let include = session?["include"] as? [String]
107
230
  #expect(include == nil)
@@ -119,6 +242,710 @@ struct RealtimeTranscriptionTests {
119
242
  #expect(RealtimeTranscriptionClient.shouldManuallyCommitTestHelper(uncommittedAudioBytes: 5_760) == true)
120
243
  }
121
244
 
245
+ @Test("Realtime configuration buffering fails closed before dropping early audio")
246
+ func pendingAudioOverflowRequiresBatchFallback() {
247
+ var buffer = RealtimePendingAudioBuffer(maximumByteCount: 6)
248
+ let acceptedFirstChunk = buffer.append(Data([1, 2, 3]))
249
+ let acceptedSecondChunk = buffer.append(Data([4, 5, 6]))
250
+ let acceptedOverflowChunk = buffer.append(Data([7, 8, 9]))
251
+ let retainedChunkCount = buffer.chunkCount
252
+ let retainedByteCount = buffer.byteCount
253
+ let retainedChunks = buffer.drain()
254
+
255
+ #expect(acceptedFirstChunk)
256
+ #expect(acceptedSecondChunk)
257
+ #expect(!acceptedOverflowChunk)
258
+ #expect(retainedChunkCount == 2)
259
+ #expect(retainedByteCount == 6)
260
+ #expect(retainedChunks == [Data([1, 2, 3]), Data([4, 5, 6])])
261
+ }
262
+
263
+ @Test("Configured realtime outbound audio stays bounded without Base64 task accumulation")
264
+ func configuredOutboundAudioQueueIsBounded() {
265
+ var queue = RealtimeOutboundEventBuffer(maximumByteCount: 8, maximumEventCount: 2)
266
+
267
+ let acceptedFirst = queue.append(.audio(Data([1, 2, 3, 4])), sequence: 0)
268
+ let acceptedSecond = queue.append(.audio(Data([5, 6, 7, 8])), sequence: 1)
269
+ let acceptedOverflow = queue.append(.audio(Data([9])), sequence: 2)
270
+ #expect(acceptedFirst)
271
+ #expect(acceptedSecond)
272
+ #expect(!acceptedOverflow)
273
+ #expect(queue.byteCount == 8)
274
+ #expect(queue.eventCount == 2)
275
+
276
+ #expect(queue.popFirst()?.payload == .audio(Data([1, 2, 3, 4])))
277
+ #expect(queue.byteCount == 4)
278
+ #expect(queue.eventCount == 1)
279
+ }
280
+
281
+ @Test("A stalled configured WebSocket send reaches a bounded deadline and forces fallback")
282
+ @MainActor
283
+ func configuredOutboundSendDeadlineIsBounded() async {
284
+ let client = RealtimeTranscriptionClient(apiKey: "synthetic-test-value", homePath: "/tmp")
285
+ let clock = FakeRealtimeClock()
286
+ let sent = await client.enqueueOutboundOperationTestHelper(
287
+ timeoutMilliseconds: 50,
288
+ operation: {
289
+ try await Task.sleep(for: .seconds(60))
290
+ },
291
+ nowMilliseconds: { clock.nowMilliseconds },
292
+ sleepMilliseconds: { milliseconds in
293
+ await Task.yield()
294
+ clock.advance(by: milliseconds)
295
+ }
296
+ )
297
+
298
+ #expect(!sent)
299
+ #expect(clock.nowMilliseconds == 50)
300
+ #expect(client.error?.contains("timed out") == true)
301
+ #expect(client.settlementResolutionTestHelper() == .failed)
302
+ }
303
+
304
+ @Test("Final realtime deadline includes a stalled outbound commit chain")
305
+ @MainActor
306
+ func stalledOutboundCommitCannotExtendFinishDeadline() async {
307
+ let clock = FakeRealtimeClock()
308
+ var commitStarted = false
309
+ let waitResult = await RealtimeTranscriptionClient.waitForSettlementWhileCommitRunsTestHelper(
310
+ timeoutMilliseconds: 700,
311
+ beginCommit: {
312
+ commitStarted = true
313
+ try? await Task.sleep(for: .seconds(60))
314
+ return false
315
+ },
316
+ resolution: { .waiting },
317
+ nowMilliseconds: { clock.nowMilliseconds },
318
+ sleepMilliseconds: { milliseconds in
319
+ await Task.yield()
320
+ clock.advance(by: milliseconds)
321
+ }
322
+ )
323
+
324
+ #expect(commitStarted)
325
+ #expect(waitResult.resolution == .waiting)
326
+ #expect(waitResult.elapsedMilliseconds == 700)
327
+ }
328
+
329
+ @Test("WebSocket send failure settles immediately and never accepts partial realtime text")
330
+ @MainActor
331
+ func outboundSendFailureRequiresImmediateBatchFallback() async {
332
+ struct SyntheticSendFailure: Error {}
333
+ let client = RealtimeTranscriptionClient(apiKey: "synthetic-test-value", homePath: "/tmp")
334
+ let sent = await client.enqueueOutboundOperationTestHelper {
335
+ throw SyntheticSendFailure()
336
+ }
337
+ #expect(!sent)
338
+ #expect(client.error?.contains("Realtime send failed") == true)
339
+
340
+ let clock = FakeRealtimeClock()
341
+ let waitResult = await RealtimeTranscriptionClient.waitForSettlementTestHelper(
342
+ timeoutMilliseconds: 700,
343
+ resolution: { client.settlementResolutionTestHelper() },
344
+ nowMilliseconds: { clock.nowMilliseconds },
345
+ sleepMilliseconds: { clock.advance(by: $0) }
346
+ )
347
+ #expect(waitResult.resolution == .failed)
348
+ #expect(waitResult.elapsedMilliseconds == 0)
349
+
350
+ let failedResult = RealtimeFinishResult(
351
+ text: "optimistic partial",
352
+ settled: false,
353
+ error: client.error
354
+ )
355
+ #expect(RecordingEngine.settledRealtimeFastPathTranscript(
356
+ finishResult: failedResult,
357
+ pcmByteCount: 96_000,
358
+ language: "en"
359
+ ) == nil)
360
+ }
361
+
362
+ @Test("Realtime finish waits for the exact final item and every prior item")
363
+ func exactFinalItemSettlementHandlesOutOfOrderCompletion() {
364
+ var tracker = RealtimeCommitSettlementTracker()
365
+ tracker.queueCommit(isFinal: false)
366
+ tracker.queueCommit(isFinal: true)
367
+ tracker.complete(itemID: "item-final")
368
+ tracker.acknowledge(itemID: "item-prior")
369
+ tracker.acknowledge(itemID: "item-final")
370
+ #expect(tracker.resolution(uncommittedRealAudioBytes: 0) == .waiting)
371
+
372
+ tracker.complete(itemID: "item-prior")
373
+ #expect(tracker.finalItemID == "item-final")
374
+ #expect(tracker.resolution(uncommittedRealAudioBytes: 1) == .waiting)
375
+ #expect(tracker.resolution(uncommittedRealAudioBytes: 0) == .settled)
376
+ }
377
+
378
+ @Test("Commit settlement handles duplicate acknowledgements, prior failure, and periodic reuse")
379
+ func commitSettlementEdgeCases() {
380
+ var tracker = RealtimeCommitSettlementTracker()
381
+ let priorSequence = tracker.queueCommit(isFinal: false)
382
+ let finalSequence = tracker.queueCommit(isFinal: false)
383
+ #expect(tracker.acknowledge(itemID: "item-prior")?.sequence == priorSequence)
384
+ #expect(tracker.acknowledge(itemID: "item-prior")?.sequence == priorSequence)
385
+ #expect(tracker.acknowledge(itemID: "item-final")?.sequence == finalSequence)
386
+ let didMarkLatestCommitFinal = tracker.markLatestCommitFinal()
387
+ #expect(didMarkLatestCommitFinal)
388
+ #expect(tracker.finalItemID == "item-final")
389
+
390
+ tracker.complete(itemID: "item-final")
391
+ tracker.fail(itemID: "item-prior")
392
+ #expect(tracker.resolution(uncommittedRealAudioBytes: 0) == .failed)
393
+ }
394
+
395
+ @Test("A final completion at 690 ms settles without a batch fallback")
396
+ @MainActor
397
+ func finalCompletionNearDeadlineUsesFastPath() async {
398
+ var tracker = RealtimeCommitSettlementTracker()
399
+ tracker.queueCommit(isFinal: true)
400
+ let clock = FakeRealtimeClock()
401
+
402
+ let waitResult = await RealtimeTranscriptionClient.waitForSettlementTestHelper(
403
+ timeoutMilliseconds: 700,
404
+ resolution: {
405
+ tracker.resolution(uncommittedRealAudioBytes: 0)
406
+ },
407
+ nowMilliseconds: {
408
+ clock.nowMilliseconds
409
+ },
410
+ sleepMilliseconds: { milliseconds in
411
+ clock.advance(by: milliseconds)
412
+ if clock.nowMilliseconds == 10 {
413
+ tracker.acknowledge(itemID: "item-final")
414
+ }
415
+ if clock.nowMilliseconds >= 690 {
416
+ tracker.complete(itemID: "item-final")
417
+ }
418
+ }
419
+ )
420
+
421
+ #expect(waitResult.resolution == .settled)
422
+ #expect(waitResult.elapsedMilliseconds == 690)
423
+ let finishResult = RealtimeFinishResult(text: "final words", settled: true, error: nil)
424
+ #expect(RecordingEngine.settledRealtimeFastPathTranscript(
425
+ finishResult: finishResult,
426
+ pcmByteCount: 12_000,
427
+ language: "en"
428
+ ) == "final words")
429
+ }
430
+
431
+ @Test("Settlement deadline arithmetic saturates at UInt64 bounds")
432
+ @MainActor
433
+ func settlementDeadlineDoesNotOverflow() async {
434
+ var now = UInt64.max - 5
435
+ let waitResult = await RealtimeTranscriptionClient.waitForSettlementTestHelper(
436
+ timeoutMilliseconds: 10,
437
+ resolution: { .waiting },
438
+ nowMilliseconds: { now },
439
+ sleepMilliseconds: { milliseconds in
440
+ let (advanced, overflow) = now.addingReportingOverflow(milliseconds)
441
+ now = overflow ? UInt64.max : advanced
442
+ }
443
+ )
444
+
445
+ #expect(waitResult.resolution == .waiting)
446
+ #expect(waitResult.elapsedMilliseconds == 5)
447
+ }
448
+
449
+ @Test("Periodic realtime commits use bounded monotonic elapsed time")
450
+ func periodicCommitSchedulingIsMonotonicAndBounded() {
451
+ #expect(RecordingEngine.realtimePeriodicCommitIsDue(
452
+ nowMilliseconds: 10,
453
+ lastCommitMilliseconds: nil
454
+ ))
455
+ #expect(!RecordingEngine.realtimePeriodicCommitIsDue(
456
+ nowMilliseconds: 999,
457
+ lastCommitMilliseconds: 100
458
+ ))
459
+ #expect(RecordingEngine.realtimePeriodicCommitIsDue(
460
+ nowMilliseconds: 1_000,
461
+ lastCommitMilliseconds: 100
462
+ ))
463
+ #expect(!RecordingEngine.realtimePeriodicCommitIsDue(
464
+ nowMilliseconds: 99,
465
+ lastCommitMilliseconds: 100
466
+ ))
467
+ #expect(!RecordingEngine.realtimePeriodicCommitIsDue(
468
+ nowMilliseconds: UInt64.max,
469
+ lastCommitMilliseconds: UInt64.max - 899
470
+ ))
471
+ }
472
+
473
+ @Test("A sub-threshold final PCM tail is retained and padded only for realtime commit")
474
+ func subThresholdFinalTailIsPadded() {
475
+ let storedPCM = Data((0..<1_280).map { UInt8($0 % 251) })
476
+ let storedCopy = storedPCM
477
+ let plan = RealtimeTranscriptionClient.finalCommitPlanTestHelper(
478
+ realAudioByteCount: storedPCM.count
479
+ )
480
+
481
+ #expect(plan.realAudioByteCount == 1_280)
482
+ #expect(plan.paddingByteCount == 4_480)
483
+ #expect(plan.committedAudioByteCount == 5_760)
484
+ #expect(plan.realtimePadding.count == 4_480)
485
+ #expect(plan.realtimePadding.allSatisfy { $0 == 0 })
486
+ #expect(storedPCM == storedCopy)
487
+ #expect(RealtimeTranscriptionClient.finalCommitPlanTestHelper(
488
+ realAudioByteCount: 5_759
489
+ ).paddingByteCount == 1)
490
+ #expect(RealtimeTranscriptionClient.finalCommitPlanTestHelper(
491
+ realAudioByteCount: 5_760
492
+ ).paddingByteCount == 0)
493
+ #expect(RealtimeTranscriptionClient.finalCommitPlanTestHelper(
494
+ realAudioByteCount: 0
495
+ ).committedAudioByteCount == 0)
496
+ }
497
+
498
+ @Test("Failed and unacknowledged final items require batch fallback without partial paste")
499
+ func incompleteFinalItemsRejectFastPath() {
500
+ var failedTracker = RealtimeCommitSettlementTracker()
501
+ failedTracker.queueCommit(isFinal: true)
502
+ failedTracker.acknowledge(itemID: "item-final")
503
+ failedTracker.fail(itemID: "item-final")
504
+ failedTracker.complete(itemID: "item-final")
505
+ #expect(failedTracker.resolution(uncommittedRealAudioBytes: 0) == .failed)
506
+
507
+ var unacknowledgedTracker = RealtimeCommitSettlementTracker()
508
+ unacknowledgedTracker.queueCommit(isFinal: true)
509
+ unacknowledgedTracker.complete(itemID: "unbound-completion")
510
+ #expect(unacknowledgedTracker.resolution(uncommittedRealAudioBytes: 0) == .waiting)
511
+
512
+ let partialResult = RealtimeFinishResult(text: "optimistic partial", settled: false, error: nil)
513
+ #expect(RecordingEngine.settledRealtimeFastPathTranscript(
514
+ finishResult: partialResult,
515
+ pcmByteCount: 96_000,
516
+ language: "en"
517
+ ) == nil)
518
+ #expect(RecordingEngine.settledRealtimeFallbackTranscript(
519
+ finishResult: partialResult,
520
+ pcmByteCount: 96_000,
521
+ language: "en"
522
+ ) == nil)
523
+
524
+ let settledPartialResult = RealtimeFinishResult(text: "Hi", settled: true, error: nil)
525
+ #expect(RecordingEngine.settledRealtimeFallbackTranscript(
526
+ finishResult: settledPartialResult,
527
+ pcmByteCount: 96_000,
528
+ language: "en"
529
+ ) == nil)
530
+ }
531
+
532
+ @Test("Disabled transformation pastes before blocked persistence completes")
533
+ @MainActor
534
+ func blockedPersistenceDoesNotDelayPaste() async {
535
+ let delivery = BlockingRealtimeDelivery()
536
+ let persistence = BlockingRealtimePersistence()
537
+ let probe = RealtimeDeliveryProbe()
538
+
539
+ let persistenceTask = RecordingEngine.deliverRealtimeBeforePersistence(
540
+ text: "settled realtime text",
541
+ persist: { await persistence.run() },
542
+ deliver: {
543
+ probe.deliveredText = $0
544
+ await delivery.run()
545
+ },
546
+ persistenceCompleted: { probe.persistedResult = $0 }
547
+ )
548
+
549
+ #expect(probe.persistedResult?.text == nil)
550
+ await delivery.waitUntilStarted()
551
+ #expect(await persistence.hasStarted() == false)
552
+ await delivery.release()
553
+ await persistence.waitUntilStarted()
554
+ #expect(probe.deliveredText == "settled realtime text")
555
+
556
+ await persistence.release()
557
+ await persistenceTask.value
558
+ #expect(probe.persistedResult?.text == "stored text")
559
+ #expect(RecordingEngine.shouldPasteBeforePersistence(
560
+ postProcessingMode: PostProcessingMode.off.rawValue,
561
+ transcript: "settled realtime text",
562
+ hasSelection: false,
563
+ intentDetectionEnabled: true
564
+ ))
565
+ #expect(RecordingEngine.shouldPasteBeforePersistence(
566
+ postProcessingMode: PostProcessingMode.off.rawValue,
567
+ transcript: "rewrite this to be more formal",
568
+ hasSelection: true,
569
+ intentDetectionEnabled: true
570
+ ) == false)
571
+ #expect(RecordingEngine.shouldPasteBeforePersistence(
572
+ postProcessingMode: PostProcessingMode.always.rawValue,
573
+ transcript: "settled realtime text",
574
+ hasSelection: false,
575
+ intentDetectionEnabled: true
576
+ ) == false)
577
+ #expect(RecordingEngine.shouldLabelRewriting(
578
+ postProcessingMode: PostProcessingMode.always.rawValue
579
+ ))
580
+ #expect(RecordingEngine.shouldLabelRewriting(
581
+ postProcessingMode: PostProcessingMode.auto.rawValue
582
+ ))
583
+ #expect(RecordingEngine.shouldLabelRewriting(
584
+ postProcessingMode: PostProcessingMode.off.rawValue
585
+ ) == false)
586
+ }
587
+
588
+ @Test("Async realtime delivery prompts once before persistence and normal launch never prompts")
589
+ @MainActor
590
+ func asyncRealtimeDeliveryUsesProcessPromptGate() async {
591
+ let normalLaunch = PermissionRequestLaunchPlan(arguments: ["Recordings"])
592
+ let gate = AccessibilityPromptGate()
593
+ let persistence = BlockingRealtimePersistence()
594
+ let probe = RealtimeDeliveryProbe()
595
+ var promptCalls = 0
596
+
597
+ #expect(!normalLaunch.requestsAccessibilityPrompt)
598
+ #expect(gate.promptRequestCount == 0)
599
+
600
+ let persistenceTask = RecordingEngine.deliverRealtimeBeforePersistence(
601
+ text: "settled realtime text",
602
+ persist: { await persistence.run() },
603
+ deliver: { text in
604
+ let first = gate.trustForProtectedOperation(
605
+ isTrusted: { false },
606
+ requestPrompt: {
607
+ promptCalls += 1
608
+ return false
609
+ }
610
+ )
611
+ let repeatedCheck = gate.trustForProtectedOperation(
612
+ isTrusted: { false },
613
+ requestPrompt: {
614
+ promptCalls += 1
615
+ return false
616
+ }
617
+ )
618
+ #expect(first.didPrompt)
619
+ #expect(!repeatedCheck.didPrompt)
620
+ probe.deliveredText = text
621
+ },
622
+ persistenceCompleted: { probe.persistedResult = $0 }
623
+ )
624
+
625
+ await persistence.waitUntilStarted()
626
+ #expect(probe.deliveredText == "settled realtime text")
627
+ #expect(promptCalls == 1)
628
+ #expect(gate.promptRequestCount == 1)
629
+ #expect(probe.persistedResult == nil)
630
+
631
+ await persistence.release()
632
+ await persistenceTask.value
633
+ #expect(probe.persistedResult?.text == "stored text")
634
+ }
635
+
636
+ @Test("Async save failure starts recovery only after realtime text is delivered")
637
+ @MainActor
638
+ func asyncSaveFailureRecoversAfterDelivery() async {
639
+ let persistence = BlockingRealtimePersistence(
640
+ result: RealtimeFastPathSaveResult(text: nil, error: "synthetic save failure")
641
+ )
642
+ let probe = RealtimeDeliveryProbe()
643
+ var recoveryError: String?
644
+
645
+ let persistenceTask = RecordingEngine.deliverRealtimeBeforePersistence(
646
+ text: "settled realtime text",
647
+ persist: { await persistence.run() },
648
+ deliver: { probe.deliveredText = $0 },
649
+ persistenceCompleted: { result in
650
+ #expect(probe.deliveredText == "settled realtime text")
651
+ recoveryError = result.error
652
+ probe.persistedResult = result
653
+ }
654
+ )
655
+
656
+ await persistence.waitUntilStarted()
657
+ #expect(probe.deliveredText == "settled realtime text")
658
+ #expect(recoveryError == nil)
659
+
660
+ await persistence.release()
661
+ await persistenceTask.value
662
+ #expect(recoveryError == "synthetic save failure")
663
+ #expect(probe.persistedResult?.text == nil)
664
+ }
665
+
666
+ @Test("Persistence revision advances only for confirmed saves")
667
+ @MainActor
668
+ func persistenceRevisionTracksConfirmedSaves() {
669
+ let engine = RecordingEngine()
670
+
671
+ #expect(engine.persistedRecordingRevision == 0)
672
+ engine.recordPersistenceCompletion(savedText: nil)
673
+ #expect(engine.persistedRecordingRevision == 0)
674
+
675
+ engine.recordPersistenceCompletion(savedText: "stored realtime text")
676
+ #expect(engine.persistedRecordingRevision == 1)
677
+ engine.recordPersistenceCompletion(savedText: "recovered full-audio text")
678
+ #expect(engine.persistedRecordingRevision == 2)
679
+ }
680
+
681
+ @Test("Background recovery never overwrites a newer recording pipeline or pastes twice")
682
+ func backgroundRecoveryRespectsPipelineGeneration() {
683
+ #expect(RecordingEngine.shouldApplyBackgroundRecoveryStatus(
684
+ recoveryGeneration: 4,
685
+ currentGeneration: 4,
686
+ isRecording: false,
687
+ isTranscribing: false
688
+ ))
689
+ #expect(!RecordingEngine.shouldApplyBackgroundRecoveryStatus(
690
+ recoveryGeneration: 4,
691
+ currentGeneration: 5,
692
+ isRecording: false,
693
+ isTranscribing: false
694
+ ))
695
+ #expect(!RecordingEngine.shouldApplyBackgroundRecoveryStatus(
696
+ recoveryGeneration: 4,
697
+ currentGeneration: 4,
698
+ isRecording: true,
699
+ isTranscribing: false
700
+ ))
701
+ #expect(!RecordingEngine.shouldApplyBackgroundRecoveryStatus(
702
+ recoveryGeneration: 4,
703
+ currentGeneration: 4,
704
+ isRecording: false,
705
+ isTranscribing: true
706
+ ))
707
+
708
+ #expect(RecordingEngine.fallbackCompletionAction(
709
+ cliText: "saved full-file transcript",
710
+ cliError: nil,
711
+ realtimeText: nil,
712
+ deliverResult: false
713
+ ) == .backgroundRecovered)
714
+ #expect(RecordingEngine.fallbackCompletionAction(
715
+ cliText: nil,
716
+ cliError: "synthetic fallback failure",
717
+ realtimeText: nil,
718
+ deliverResult: false
719
+ ) == .backgroundFailed("synthetic fallback failure"))
720
+ #expect(RecordingEngine.fallbackCompletionAction(
721
+ cliText: "foreground transcript",
722
+ cliError: nil,
723
+ realtimeText: nil,
724
+ deliverResult: true
725
+ ) == .deliver("foreground transcript"))
726
+ }
727
+
728
+ @Test("Delayed delivery from recording A pastes once without overwriting recording B")
729
+ func delayedDeliveryRespectsNewerPipeline() {
730
+ var gate = PipelineDeliveryGate()
731
+ gate.registerPipeline(4)
732
+ gate.registerPipeline(5)
733
+
734
+ let firstA = gate.claimDelivery(for: 4)
735
+ let duplicateA = gate.claimDelivery(for: 4)
736
+ let firstB = gate.claimDelivery(for: 5)
737
+ #expect(firstA)
738
+ #expect(!duplicateA)
739
+ #expect(firstB)
740
+ #expect(!gate.shouldApplyStatus(
741
+ deliveryGeneration: 4,
742
+ currentGeneration: 5,
743
+ isRecording: true,
744
+ isTranscribing: false
745
+ ))
746
+ #expect(gate.shouldApplyStatus(
747
+ deliveryGeneration: 5,
748
+ currentGeneration: 5,
749
+ isRecording: false,
750
+ isTranscribing: false
751
+ ))
752
+ }
753
+
754
+ @Test("Delivery claims compact without forgetting old or out-of-order generations")
755
+ func deliveryClaimsRemainExactAfterCompaction() {
756
+ var gate = PipelineDeliveryGate()
757
+ var sequentialClaims: [Bool] = []
758
+ for generation in 1...64 {
759
+ gate.registerPipeline(UInt64(generation))
760
+ sequentialClaims.append(gate.claimDelivery(for: UInt64(generation)))
761
+ }
762
+ let duplicateOne = gate.claimDelivery(for: 1)
763
+ let duplicateThirtyTwo = gate.claimDelivery(for: 32)
764
+ #expect(sequentialClaims.allSatisfy { $0 })
765
+ #expect(!duplicateOne)
766
+ #expect(!duplicateThirtyTwo)
767
+
768
+ var outOfOrder = PipelineDeliveryGate()
769
+ outOfOrder.registerPipeline(100)
770
+ outOfOrder.registerPipeline(1)
771
+ let highClaim = outOfOrder.claimDelivery(for: 100)
772
+ let pendingLowClaim = outOfOrder.claimDelivery(for: 1)
773
+ let duplicateLowClaim = outOfOrder.claimDelivery(for: 1)
774
+ outOfOrder.registerPipeline(99)
775
+ let pendingMiddleClaim = outOfOrder.claimDelivery(for: 99)
776
+ let duplicateHighClaim = outOfOrder.claimDelivery(for: 100)
777
+ #expect(highClaim)
778
+ #expect(pendingLowClaim)
779
+ #expect(!duplicateLowClaim)
780
+ #expect(pendingMiddleClaim)
781
+ #expect(!duplicateHighClaim)
782
+
783
+ var gapped = PipelineDeliveryGate()
784
+ var gappedClaims: [Bool] = []
785
+ for generation in stride(from: 2, through: 200, by: 2) {
786
+ gapped.registerPipeline(UInt64(generation))
787
+ gappedClaims.append(gapped.claimDelivery(for: UInt64(generation)))
788
+ }
789
+ let duplicateTwo = gapped.claimDelivery(for: 2)
790
+ let duplicateOneNinetyEight = gapped.claimDelivery(for: 198)
791
+ #expect(gappedClaims.allSatisfy { $0 })
792
+ #expect(!duplicateTwo)
793
+ #expect(!duplicateOneNinetyEight)
794
+ }
795
+
796
+ @Test("Background recovery reconstructs missing WAV without replacing an existing capture")
797
+ func backgroundRecoveryAudioReconstruction() throws {
798
+ let directory = FileManager.default.temporaryDirectory
799
+ .appendingPathComponent("recordings-recovery-\(UUID().uuidString)", isDirectory: true)
800
+ let audioURL = directory.appendingPathComponent("capture.wav")
801
+ defer { try? FileManager.default.removeItem(at: directory) }
802
+
803
+ let pcm = Data([0x01, 0x02, 0x03, 0x04])
804
+ #expect(RecordingEngine.ensureBackgroundRecoveryAudio(
805
+ audioPath: audioURL.path,
806
+ pcmData: pcm
807
+ ) == audioURL.path)
808
+ let reconstructed = try Data(contentsOf: audioURL)
809
+ #expect(reconstructed.count == 44 + pcm.count)
810
+ #expect(reconstructed.suffix(pcm.count) == pcm)
811
+
812
+ #expect(RecordingEngine.ensureBackgroundRecoveryAudio(
813
+ audioPath: audioURL.path,
814
+ pcmData: Data([0xFF, 0xFF])
815
+ ) == audioURL.path)
816
+ #expect(try Data(contentsOf: audioURL) == reconstructed)
817
+ #expect(RecordingEngine.ensureBackgroundRecoveryAudio(
818
+ audioPath: directory.appendingPathComponent("empty.wav").path,
819
+ pcmData: Data()
820
+ ) == nil)
821
+ }
822
+
823
+ @Test("Background recovery retains the failed recording's processing configuration")
824
+ func backgroundRecoveryUsesCapturedConfiguration() {
825
+ let recordingA = RecordingProcessingConfiguration(
826
+ transcriptionPrompt: "Project A vocabulary",
827
+ transcriberPrompt: "Project A vocabulary",
828
+ postProcessingMode: PostProcessingMode.off.rawValue,
829
+ transcriptionLanguage: "es",
830
+ transcriptionModel: "whisper-1",
831
+ transcriberModel: "gpt-a",
832
+ enhancementModel: "gpt-a-fallback",
833
+ intentModel: "gpt-intent-a",
834
+ intentDetectionEnabled: true,
835
+ enhanceTriggersJSON: #"["rewrite a"]"#,
836
+ keywordTransformsJSON: #"{"code with":"Codewith"}"#
837
+ )
838
+ var currentProject = recordingA
839
+ let capturedForRecovery = currentProject
840
+
841
+ currentProject = RecordingProcessingConfiguration(
842
+ transcriptionPrompt: "Project B vocabulary",
843
+ transcriberPrompt: "Project B rewrite policy",
844
+ postProcessingMode: PostProcessingMode.always.rawValue,
845
+ transcriptionLanguage: "en",
846
+ transcriptionModel: "gpt-4o-transcribe",
847
+ transcriberModel: "gpt-b",
848
+ enhancementModel: "gpt-b-fallback",
849
+ intentModel: "gpt-intent-b",
850
+ intentDetectionEnabled: true,
851
+ enhanceTriggersJSON: #"["rewrite b"]"#,
852
+ keywordTransformsJSON: #"{"open ai":"OpenAI"}"#
853
+ )
854
+ let recoveryArgs = RecordingEngine.transcribeCLIArgs(
855
+ audioPath: "/tmp/recording-a.wav",
856
+ activeProjectId: "project-a",
857
+ transcriberPrompt: capturedForRecovery.transcriberPrompt,
858
+ postProcessingMode: capturedForRecovery.postProcessingMode,
859
+ language: capturedForRecovery.transcriptionLanguage,
860
+ transcriptionPrompt: capturedForRecovery.transcriptionPrompt,
861
+ transcriptionModel: capturedForRecovery.transcriptionModel,
862
+ transcriberModel: capturedForRecovery.transcriberModel,
863
+ enhancementModel: capturedForRecovery.enhancementModel,
864
+ enhanceTriggersJSON: capturedForRecovery.enhanceTriggersJSON,
865
+ keywordTransformsJSON: capturedForRecovery.keywordTransformsJSON,
866
+ recordingId: "pipeline-a"
867
+ )
868
+
869
+ #expect(capturedForRecovery != currentProject)
870
+ #expect(recoveryArgs.contains("Project A vocabulary"))
871
+ #expect(recoveryArgs.contains(PostProcessingMode.off.rawValue))
872
+ #expect(recoveryArgs.contains("es"))
873
+ #expect(recoveryArgs.contains("whisper-1"))
874
+ #expect(recoveryArgs.contains("gpt-a"))
875
+ #expect(recoveryArgs.contains(#"["rewrite a"]"#))
876
+ #expect(recoveryArgs.contains(#"{"code with":"Codewith"}"#))
877
+ #expect(recoveryArgs.contains("pipeline-a"))
878
+ #expect(!recoveryArgs.contains("Project B rewrite policy"))
879
+ #expect(!recoveryArgs.contains(PostProcessingMode.always.rawValue))
880
+ #expect(!recoveryArgs.contains("en"))
881
+ }
882
+
883
+ @Test("Command rewrite retains recording A configuration after switching to project B")
884
+ func commandRewriteUsesCapturedConfiguration() {
885
+ let recordingA = RecordingProcessingConfiguration(
886
+ transcriptionPrompt: "Project A vocabulary",
887
+ transcriberPrompt: "Project A rewrite policy",
888
+ postProcessingMode: PostProcessingMode.always.rawValue,
889
+ transcriptionLanguage: "es",
890
+ transcriptionModel: "whisper-a",
891
+ transcriberModel: "gpt-command-a",
892
+ enhancementModel: "gpt-fallback-a",
893
+ intentModel: "gpt-intent-a",
894
+ intentDetectionEnabled: true,
895
+ enhanceTriggersJSON: #"["rewrite a"]"#,
896
+ keywordTransformsJSON: #"{"code with":"Codewith A"}"#
897
+ )
898
+ let recordingB = RecordingProcessingConfiguration(
899
+ transcriptionPrompt: "Project B vocabulary",
900
+ transcriberPrompt: "Project B rewrite policy",
901
+ postProcessingMode: PostProcessingMode.off.rawValue,
902
+ transcriptionLanguage: "en",
903
+ transcriptionModel: "whisper-b",
904
+ transcriberModel: "gpt-command-b",
905
+ enhancementModel: "gpt-fallback-b",
906
+ intentModel: "gpt-intent-b",
907
+ intentDetectionEnabled: true,
908
+ enhanceTriggersJSON: #"["rewrite b"]"#,
909
+ keywordTransformsJSON: #"{"open ai":"OpenAI B"}"#
910
+ )
911
+ var mutableCurrentConfiguration = recordingA
912
+ let capturedRequest = mutableCurrentConfiguration
913
+ mutableCurrentConfiguration = recordingB
914
+
915
+ let args = RecordingEngine.rewriteCLIArgs(
916
+ selectedText: "selected A",
917
+ instruction: "instruction A",
918
+ activeProjectId: "project-a",
919
+ processingConfiguration: capturedRequest
920
+ )
921
+
922
+ #expect(capturedRequest != mutableCurrentConfiguration)
923
+ for expected in [
924
+ "selected A", "instruction A", "project-a", "Project A vocabulary",
925
+ "Project A rewrite policy", PostProcessingMode.always.rawValue, "es", "whisper-a",
926
+ "gpt-command-a", "gpt-fallback-a", #"["rewrite a"]"#,
927
+ #"{"code with":"Codewith A"}"#,
928
+ ] {
929
+ #expect(args.contains(expected))
930
+ }
931
+ for forbidden in [
932
+ "Project B vocabulary", "Project B rewrite policy", PostProcessingMode.off.rawValue,
933
+ "whisper-b", "gpt-command-b", "gpt-fallback-b", #"["rewrite b"]"#,
934
+ #"{"open ai":"OpenAI B"}"#,
935
+ ] {
936
+ #expect(!args.contains(forbidden))
937
+ }
938
+
939
+ let optionLikeTextArgs = RecordingEngine.rewriteCLIArgs(
940
+ selectedText: "--literal-selection",
941
+ instruction: "instruction A",
942
+ activeProjectId: "project-a",
943
+ processingConfiguration: capturedRequest
944
+ )
945
+ #expect(Array(optionLikeTextArgs.suffix(2)) == ["--", "--literal-selection"])
946
+ #expect(optionLikeTextArgs.firstIndex(of: "--instruction")! < optionLikeTextArgs.firstIndex(of: "--")!)
947
+ }
948
+
122
949
  @Test("Partial realtime text falls back for longer recordings")
123
950
  func partialRealtimeFallback() {
124
951
  #expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "Hi", pcmByteCount: 96_000) == true)
@@ -126,7 +953,7 @@ struct RealtimeTranscriptionTests {
126
953
  #expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "Hi", pcmByteCount: 12_000) == false)
127
954
  }
128
955
 
129
- @Test("Realtime fast path accepts useful and safely repaired text")
956
+ @Test("Realtime fast path accepts safe text and rejects lexical cleanup")
130
957
  func realtimeFastPathDecision() {
131
958
  #expect(RecordingEngine.shouldUseRealtimeFastPath(realtimeText: " this is a useful transcript ", pcmByteCount: 96_000))
132
959
  #expect(RecordingEngine.shouldUseRealtimeFastPath(realtimeText: "Hi", pcmByteCount: 12_000))
@@ -136,12 +963,12 @@ struct RealtimeTranscriptionTests {
136
963
  realtimeText: "Actually 리수 Zoom your goal",
137
964
  pcmByteCount: 96_000,
138
965
  language: "en"
139
- ) == "Actually Zoom your goal")
966
+ ) == nil)
140
967
  #expect(RecordingEngine.realtimeFastPathTranscript(
141
968
  realtimeText: "어 Okay I don't know if this This is working어 Okay I don't know if this This is working",
142
969
  pcmByteCount: 96_000,
143
970
  language: "en"
144
- ) == "Okay I don't know if this is working")
971
+ ) == nil)
145
972
  #expect(RecordingEngine.realtimeFastPathTranscript(
146
973
  realtimeText: "Actually Zoom your goal",
147
974
  pcmByteCount: 96_000,
@@ -162,11 +989,11 @@ struct RealtimeTranscriptionTests {
162
989
  #expect(cleaned == "Okay I don't know if this is working")
163
990
  }
164
991
 
165
- @Test("Realtime artifact cleanup removes CJK tokens from English-dominant text")
166
- func realtimeCJKArtifactCleanup() {
992
+ @Test("Realtime artifact cleanup preserves mixed-language lexical tokens")
993
+ func realtimeMixedLanguageCleanup() {
167
994
  let cleaned = RecordingEngine.cleanRealtimeArtifactText(
168
995
  "Actually 리수 Zoom your goal and do this work with sabi 度扫 agents actually"
169
996
  )
170
- #expect(cleaned == "Actually Zoom your goal and do this work with sabi agents actually")
997
+ #expect(cleaned == "Actually 리수 Zoom your goal and do this work with sabi 度扫 agents actually")
171
998
  }
172
999
  }