@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,7 +1,1253 @@
1
+ import Foundation
2
+ import Darwin
1
3
  import Testing
2
4
  @testable import RecordingsLib
3
5
 
4
6
  struct CLIRunnerTests {
7
+ @Test("bundled CLI takes precedence over a stale user installation")
8
+ func bundledCLIIsPreferred() throws {
9
+ let root = FileManager.default.temporaryDirectory
10
+ .appendingPathComponent("recordings-bundled-cli-\(UUID().uuidString)")
11
+ let app = root.appendingPathComponent("Recordings.app")
12
+ let helper = app.appendingPathComponent("Contents/Helpers/recordings")
13
+ let home = root.appendingPathComponent("home")
14
+ let external = home.appendingPathComponent(".bun/bin/recordings")
15
+ try FileManager.default.createDirectory(
16
+ at: helper.deletingLastPathComponent(),
17
+ withIntermediateDirectories: true
18
+ )
19
+ try FileManager.default.createDirectory(
20
+ at: external.deletingLastPathComponent(),
21
+ withIntermediateDirectories: true
22
+ )
23
+ try "bundled".write(to: helper, atomically: true, encoding: .utf8)
24
+ try "stale".write(to: external, atomically: true, encoding: .utf8)
25
+ defer { try? FileManager.default.removeItem(at: root) }
26
+
27
+ let command = CLIRunner.resolveCommand(home: home.path, bundleURL: app)
28
+
29
+ #expect(command.executable == helper.path)
30
+ #expect(command.argumentsPrefix.isEmpty)
31
+ }
32
+
33
+ @Test("packaged app never falls back when its bundled CLI is missing")
34
+ func packagedAppDoesNotUseGlobalFallback() throws {
35
+ let root = FileManager.default.temporaryDirectory
36
+ .appendingPathComponent("recordings-missing-companion-\(UUID().uuidString)")
37
+ let app = root.appendingPathComponent("Recordings.app")
38
+ let home = root.appendingPathComponent("home")
39
+ let external = home.appendingPathComponent(".bun/bin/recordings")
40
+ try FileManager.default.createDirectory(at: app, withIntermediateDirectories: true)
41
+ try FileManager.default.createDirectory(
42
+ at: external.deletingLastPathComponent(),
43
+ withIntermediateDirectories: true
44
+ )
45
+ try "stale".write(to: external, atomically: true, encoding: .utf8)
46
+ defer { try? FileManager.default.removeItem(at: root) }
47
+
48
+ let command = CLIRunner.resolveCommand(home: home.path, bundleURL: app)
49
+
50
+ #expect(command.executable == app.appendingPathComponent("Contents/Helpers/recordings").path)
51
+ #expect(command.executable != external.path)
52
+ }
53
+
54
+ @Test("process runner drains more than one MiB from stdout and stderr without blocking")
55
+ func drainsLargeConcurrentOutput() throws {
56
+ let command = """
57
+ (dd if=/dev/zero bs=1048576 count=2 2>/dev/null | tr '\\0' o) &
58
+ (dd if=/dev/zero bs=1048576 count=2 2>/dev/null | tr '\\0' e >&2) &
59
+ wait
60
+ """
61
+ let result = try CLIRunner.runExecutable("/bin/sh", arguments: ["-c", command])
62
+ #expect(result.terminationStatus == 0)
63
+ #expect(result.stdout.utf8.count == 2 * 1_048_576)
64
+ #expect(result.stderr.utf8.count == 2 * 1_048_576)
65
+ #expect(result.stdout.first == "o")
66
+ #expect(result.stderr.first == "e")
67
+ }
68
+
69
+ @Test("process runner observes immediate exits without false timeouts")
70
+ func observesImmediateExits() throws {
71
+ for _ in 0..<25 {
72
+ let result = try CLIRunner.runExecutable(
73
+ "/usr/bin/true",
74
+ arguments: [],
75
+ executionTimeout: 0.1,
76
+ terminationGracePeriod: 0,
77
+ forceKillGracePeriod: 0.1,
78
+ pipeDrainTimeout: 0.1
79
+ )
80
+ #expect(result.terminationStatus == 0)
81
+ }
82
+ }
83
+
84
+ @Test("process runner terminates an entire command process group after its deadline")
85
+ func forceKillsTimedOutProcessGroup() throws {
86
+ let root = FileManager.default.temporaryDirectory
87
+ .appendingPathComponent("recordings-cli-timeout-\(UUID().uuidString)")
88
+ let script = root.appendingPathComponent("ignore-term-parent.sh")
89
+ let childScript = root.appendingPathComponent("ignore-term-child.sh")
90
+ let parentPidFile = root.appendingPathComponent("parent-pid")
91
+ let childPidFile = root.appendingPathComponent("child-pid")
92
+ let termFile = root.appendingPathComponent("term-received")
93
+ let sideEffectFile = root.appendingPathComponent("server-side-effect")
94
+ let readinessFIFO = root.appendingPathComponent("ready")
95
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
96
+ try #require(Darwin.mkfifo(readinessFIFO.path, 0o600) == 0)
97
+ defer {
98
+ let runnerProcessGroup = Darwin.getpgrp()
99
+ if let parentPid = Self.readPID(from: parentPidFile) {
100
+ let childProcessGroup = Darwin.getpgid(parentPid)
101
+ if childProcessGroup == parentPid, childProcessGroup != runnerProcessGroup {
102
+ _ = Darwin.kill(-childProcessGroup, SIGKILL)
103
+ } else {
104
+ _ = Darwin.kill(parentPid, SIGKILL)
105
+ }
106
+ }
107
+ if let childPid = Self.readPID(from: childPidFile) {
108
+ _ = Darwin.kill(childPid, SIGKILL)
109
+ }
110
+ try? FileManager.default.removeItem(at: root)
111
+ }
112
+ try """
113
+ #!/bin/sh
114
+ trap 'printf term > "$2"' TERM
115
+ printf '%s' "$$" > "$1"
116
+ "$4" "$5" "$6" &
117
+ while [ ! -s "$5" ] || [ ! -s "$6" ]; do /bin/sleep 0.01; done
118
+ printf parent-output
119
+ printf parent-error >&2
120
+ printf ready > "$3"
121
+ while :; do /bin/sleep 0.05; done
122
+ """.write(to: script, atomically: true, encoding: .utf8)
123
+ try """
124
+ #!/bin/sh
125
+ trap '' TERM HUP
126
+ printf '%s' "$$" > "$1"
127
+ printf child-output
128
+ printf child-error >&2
129
+ while :; do
130
+ printf tick >> "$2"
131
+ /bin/sleep 0.02
132
+ done
133
+ """.write(to: childScript, atomically: true, encoding: .utf8)
134
+ try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: script.path)
135
+ try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: childScript.path)
136
+
137
+ var executionDeadlineStartedAt: ContinuousClock.Instant?
138
+ var observedReadiness = false
139
+ var lifecycleEvents: [CLIRunner.ProcessLifecycleEvent] = []
140
+ do {
141
+ _ = try CLIRunner.runExecutable(
142
+ script.path,
143
+ arguments: [
144
+ parentPidFile.path,
145
+ termFile.path,
146
+ readinessFIFO.path,
147
+ childScript.path,
148
+ childPidFile.path,
149
+ sideEffectFile.path,
150
+ ],
151
+ executionTimeout: 0.2,
152
+ terminationGracePeriod: 0.5,
153
+ forceKillGracePeriod: 0.5,
154
+ pipeDrainTimeout: 0.2,
155
+ beforeExecutionDeadline: {
156
+ guard let handle = FileHandle(forReadingAtPath: readinessFIFO.path) else { return }
157
+ defer { try? handle.close() }
158
+ observedReadiness = handle.readDataToEndOfFile() == Data("ready".utf8)
159
+ guard let parentPid = Self.readPID(from: parentPidFile),
160
+ let childPid = Self.readPID(from: childPidFile) else {
161
+ Issue.record("ready process group did not publish both process identifiers")
162
+ return
163
+ }
164
+ let processGroup = Darwin.getpgid(parentPid)
165
+ #expect(processGroup == parentPid)
166
+ #expect(Darwin.getpgid(childPid) == parentPid)
167
+ #expect(processGroup != Darwin.getpgrp())
168
+ executionDeadlineStartedAt = ContinuousClock.now
169
+ },
170
+ lifecycleObserver: { lifecycleEvents.append($0) }
171
+ )
172
+ Issue.record("command unexpectedly completed before its deadline")
173
+ } catch let error as CLIRunner.ExecutionError {
174
+ guard case let .timedOut(executable, seconds) = error else {
175
+ Issue.record("command returned the wrong execution error: \(error)")
176
+ return
177
+ }
178
+ #expect(executable == script.path)
179
+ #expect(seconds == 0.2)
180
+ #expect(error.localizedDescription.contains("timed out"))
181
+ }
182
+
183
+ let startedAt = try #require(executionDeadlineStartedAt)
184
+ #expect(ContinuousClock.now - startedAt < .seconds(2.5))
185
+ #expect(observedReadiness)
186
+ #expect(try String(contentsOf: termFile, encoding: .utf8) == "term")
187
+ #expect(lifecycleEvents == [
188
+ .processGroupSignaled(SIGTERM),
189
+ .processGroupSignaled(SIGKILL),
190
+ .leaderExitObserved,
191
+ .leaderReaped(128 + SIGKILL),
192
+ ])
193
+ let parentPid = try #require(Self.readPID(from: parentPidFile))
194
+ let childPid = try #require(Self.readPID(from: childPidFile))
195
+ let processGroup = parentPid
196
+ expectProcessIsGone(parentPid)
197
+ expectProcessIsGone(childPid)
198
+ expectProcessGroupIsGone(processGroup)
199
+
200
+ let sideEffectSize = try Data(contentsOf: sideEffectFile).count
201
+ Thread.sleep(forTimeInterval: 0.15)
202
+ #expect(try Data(contentsOf: sideEffectFile).count == sideEffectSize)
203
+ }
204
+
205
+ private static func readPID(from file: URL) -> pid_t? {
206
+ guard let value = try? String(contentsOf: file, encoding: .utf8) else { return nil }
207
+ return pid_t(value.trimmingCharacters(in: .whitespacesAndNewlines))
208
+ }
209
+
210
+ private func expectProcessIsGone(_ pid: pid_t, sourceLocation: SourceLocation = #_sourceLocation) {
211
+ let (result, error) = pollForMissingProcess(pid)
212
+ #expect(result == -1, sourceLocation: sourceLocation)
213
+ #expect(error == ESRCH, sourceLocation: sourceLocation)
214
+ }
215
+
216
+ private func expectProcessGroupIsGone(
217
+ _ processGroup: pid_t,
218
+ sourceLocation: SourceLocation = #_sourceLocation
219
+ ) {
220
+ let (result, error) = pollForMissingProcess(-processGroup)
221
+ #expect(result == -1, sourceLocation: sourceLocation)
222
+ #expect(error == ESRCH, sourceLocation: sourceLocation)
223
+ }
224
+
225
+ private func pollForMissingProcess(_ processOrNegativeGroup: pid_t) -> (Int32, Int32) {
226
+ let deadline = ContinuousClock.now + .milliseconds(500)
227
+ var result: Int32 = 0
228
+ var error: Int32 = 0
229
+ repeat {
230
+ errno = 0
231
+ result = Darwin.kill(processOrNegativeGroup, 0)
232
+ error = errno
233
+ if result == -1, error == ESRCH { return (result, error) }
234
+ Thread.sleep(forTimeInterval: 0.01)
235
+ } while ContinuousClock.now < deadline
236
+ errno = 0
237
+ result = Darwin.kill(processOrNegativeGroup, 0)
238
+ error = errno
239
+ return (result, error)
240
+ }
241
+
242
+ @Test("process runner cleans up a background descendant after its leader exits")
243
+ func cleansUpInheritedPipeDescendant() throws {
244
+ let root = FileManager.default.temporaryDirectory
245
+ .appendingPathComponent("recordings-cli-background-\(UUID().uuidString)")
246
+ let childPidFile = root.appendingPathComponent("child-pid")
247
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
248
+ defer {
249
+ if let childPid = Self.readPID(from: childPidFile) {
250
+ _ = Darwin.kill(childPid, SIGKILL)
251
+ }
252
+ try? FileManager.default.removeItem(at: root)
253
+ }
254
+ let command = """
255
+ /bin/sh -c 'printf "%s" "$$" > "$1"; trap "" TERM HUP; while :; do /bin/sleep 1; done' child '\(childPidFile.path)' &
256
+ while [ ! -s '\(childPidFile.path)' ]; do /bin/sleep 0.01; done
257
+ printf 'complete'
258
+ printf 'warning' >&2
259
+ exit 23
260
+ """
261
+ let startedAt = ContinuousClock.now
262
+ var lifecycleEvents: [CLIRunner.ProcessLifecycleEvent] = []
263
+
264
+ let result = try CLIRunner.runExecutable(
265
+ "/bin/sh",
266
+ arguments: ["-c", command],
267
+ executionTimeout: 1,
268
+ terminationGracePeriod: 0.2,
269
+ forceKillGracePeriod: 0.2,
270
+ pipeDrainTimeout: 0.3,
271
+ lifecycleObserver: { lifecycleEvents.append($0) }
272
+ )
273
+
274
+ #expect(ContinuousClock.now - startedAt < .seconds(1.5))
275
+ #expect(result.terminationStatus == 23)
276
+ #expect(result.stdout == "complete")
277
+ #expect(result.stderr == "warning")
278
+ #expect(lifecycleEvents == [
279
+ .leaderExitObserved,
280
+ .processGroupSignaled(SIGTERM),
281
+ .processGroupSignaled(SIGKILL),
282
+ .leaderReaped(23),
283
+ ])
284
+ expectProcessIsGone(try #require(Self.readPID(from: childPidFile)))
285
+ }
286
+
287
+ @Test("a total wall-clock budget bounds execution, termination grace, kill grace, and pipe drain together")
288
+ func totalWallClockBudgetBoundsCleanup() throws {
289
+ let root = FileManager.default.temporaryDirectory
290
+ .appendingPathComponent("recordings-cli-wall-budget-\(UUID().uuidString)")
291
+ let script = root.appendingPathComponent("ignore-term.sh")
292
+ let pidFile = root.appendingPathComponent("pid")
293
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
294
+ defer {
295
+ if let pid = Self.readPID(from: pidFile) {
296
+ _ = Darwin.kill(pid, SIGKILL)
297
+ }
298
+ try? FileManager.default.removeItem(at: root)
299
+ }
300
+ try """
301
+ #!/bin/sh
302
+ trap '' TERM
303
+ printf '%s' "$$" > "$1"
304
+ while :; do /bin/sleep 0.05; done
305
+ """.write(to: script, atomically: true, encoding: .utf8)
306
+ try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: script.path)
307
+
308
+ let startedAt = ContinuousClock.now
309
+ var lifecycleEvents: [CLIRunner.ProcessLifecycleEvent] = []
310
+ do {
311
+ _ = try CLIRunner.runExecutable(
312
+ script.path,
313
+ arguments: [pidFile.path],
314
+ executionTimeout: 60,
315
+ totalWallClockBudget: 2,
316
+ lifecycleObserver: { lifecycleEvents.append($0) }
317
+ )
318
+ Issue.record("command unexpectedly completed inside its wall budget")
319
+ } catch let error as CLIRunner.ExecutionError {
320
+ guard case let .timedOut(_, seconds) = error else {
321
+ Issue.record("command returned the wrong execution error: \(error)")
322
+ return
323
+ }
324
+ // The execution window is the budget minus the cleanup reserve, never the
325
+ // caller's larger execution timeout.
326
+ #expect(seconds == 2 - CLIRunner.wallClockCleanupReserve)
327
+ }
328
+ #expect(ContinuousClock.now - startedAt < .seconds(2))
329
+ #expect(lifecycleEvents.contains(.processGroupSignaled(SIGTERM)))
330
+ #expect(lifecycleEvents.contains(.processGroupSignaled(SIGKILL)))
331
+ if let pid = Self.readPID(from: pidFile) {
332
+ expectProcessIsGone(pid)
333
+ }
334
+ }
335
+
336
+ @Test("a hung rewrite helper returns within the 10 s interactive budget, cleanup included")
337
+ @MainActor
338
+ func hungRewriteHelperReturnsWithinInteractiveBudget() async throws {
339
+ let home = FileManager.default.temporaryDirectory
340
+ .appendingPathComponent("recordings-rewrite-budget-\(UUID().uuidString)")
341
+ let bin = home.appendingPathComponent(".bun/bin")
342
+ let pidFile = home.appendingPathComponent("pid")
343
+ try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
344
+ let executable = bin.appendingPathComponent("recordings")
345
+ try """
346
+ #!/bin/sh
347
+ trap '' TERM
348
+ printf '%s' "$$" > '\(pidFile.path)'
349
+ while :; do /bin/sleep 0.05; done
350
+ """.write(to: executable, atomically: true, encoding: .utf8)
351
+ try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
352
+ defer {
353
+ if let pid = Self.readPID(from: pidFile) {
354
+ _ = Darwin.kill(pid, SIGKILL)
355
+ }
356
+ try? FileManager.default.removeItem(at: home)
357
+ }
358
+
359
+ // The production seam exactly as runCommandMode uses it: the engine's default
360
+ // commandCLI closure on a detached task, budgeted by commandRewriteTimeout — the
361
+ // observable wall time must stay inside the budget even though the helper never
362
+ // exits on its own and ignores SIGTERM.
363
+ let runCLI = RecordingEngine().commandCLI
364
+ let homePath = home.path
365
+ let startedAt = ContinuousClock.now
366
+ let output = await Task.detached {
367
+ runCLI(["rewrite-selection"], homePath, RecordingEngine.commandRewriteTimeout)
368
+ }.value
369
+ let elapsed = ContinuousClock.now - startedAt
370
+
371
+ #expect(elapsed < .seconds(RecordingEngine.commandRewriteTimeout))
372
+ #expect(output.hasPrefix("ERROR:"))
373
+ #expect(output.contains("timed out"))
374
+ }
375
+
376
+ @Test("an exhausted rewrite deadline with pipes held by an escaped descendant returns under the public ceiling")
377
+ @MainActor
378
+ func exhaustedRewriteDeadlineWithHeldPipesReturnsUnderCeiling() async throws {
379
+ let home = FileManager.default.temporaryDirectory
380
+ .appendingPathComponent("recordings-rewrite-exhaustion-\(UUID().uuidString)")
381
+ let bin = home.appendingPathComponent(".bun/bin")
382
+ let leaderPidFile = home.appendingPathComponent("leader-pid")
383
+ let holderPidFile = home.appendingPathComponent("holder-pid")
384
+ let markerFile = home.appendingPathComponent("holder-epipe")
385
+ let holderSource = home.appendingPathComponent("pipe-holder.c")
386
+ let holderBinary = home.appendingPathComponent("pipe-holder")
387
+ try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
388
+ defer {
389
+ if let holderPid = Self.readPID(from: holderPidFile) {
390
+ _ = Darwin.kill(holderPid, SIGKILL)
391
+ }
392
+ if let leaderPid = Self.readPID(from: leaderPidFile) {
393
+ _ = Darwin.kill(-leaderPid, SIGKILL)
394
+ _ = Darwin.kill(leaderPid, SIGKILL)
395
+ }
396
+ try? FileManager.default.removeItem(at: home)
397
+ }
398
+
399
+ // The holder escapes the CLI process group via setsid before any group-directed
400
+ // signal can reach it, inherits both capture pipes, and keeps their write ends open
401
+ // until the runner closes its read ends — at which point every write fails with
402
+ // EPIPE and the marker file records that both descriptor readers were shut down.
403
+ try """
404
+ #include <errno.h>
405
+ #include <signal.h>
406
+ #include <stdio.h>
407
+ #include <time.h>
408
+ #include <unistd.h>
409
+
410
+ int main(int argc, char **argv) {
411
+ if (argc < 3) return 64;
412
+ if (setsid() == -1) return 65;
413
+ signal(SIGPIPE, SIG_IGN);
414
+ FILE *pid = fopen(argv[1], "w");
415
+ if (!pid) return 66;
416
+ fprintf(pid, "%d", (int)getpid());
417
+ fclose(pid);
418
+ int stdoutBroken = 0;
419
+ int stderrBroken = 0;
420
+ while (!stdoutBroken || !stderrBroken) {
421
+ if (!stdoutBroken && write(STDOUT_FILENO, "x", 1) == -1 && errno == EPIPE) stdoutBroken = 1;
422
+ if (!stderrBroken && write(STDERR_FILENO, "y", 1) == -1 && errno == EPIPE) stderrBroken = 1;
423
+ struct timespec delay = {0, 50000000};
424
+ nanosleep(&delay, 0);
425
+ }
426
+ FILE *marker = fopen(argv[2], "w");
427
+ if (!marker) return 67;
428
+ fputs("both-epipe", marker);
429
+ fclose(marker);
430
+ return 0;
431
+ }
432
+ """.write(to: holderSource, atomically: true, encoding: .utf8)
433
+ let compile = Process()
434
+ compile.executableURL = URL(fileURLWithPath: "/usr/bin/cc")
435
+ compile.arguments = ["-o", holderBinary.path, holderSource.path]
436
+ try compile.run()
437
+ compile.waitUntilExit()
438
+ try #require(compile.terminationStatus == 0)
439
+
440
+ let executable = bin.appendingPathComponent("recordings")
441
+ try """
442
+ #!/bin/sh
443
+ trap '' TERM
444
+ printf '%s' "$$" > '\(leaderPidFile.path)'
445
+ '\(holderBinary.path)' '\(holderPidFile.path)' '\(markerFile.path)' &
446
+ while [ ! -s '\(holderPidFile.path)' ]; do /bin/sleep 0.01; done
447
+ while :; do /bin/sleep 0.05; done
448
+ """.write(to: executable, atomically: true, encoding: .utf8)
449
+ try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
450
+
451
+ // The production seam exactly as runCommandMode uses it. The leader ignores SIGTERM
452
+ // (exhausting the execution window and the termination grace), only dies at the
453
+ // group SIGKILL, and the escaped holder keeps both pipes open so the drain wait
454
+ // also runs to exhaustion. The observable wall time must still land under the
455
+ // public ceiling — the return margin absorbs spawn, poll overshoot, capture
456
+ // shutdown, and the task hops.
457
+ let runCLI = RecordingEngine().commandCLI
458
+ let homePath = home.path
459
+ let startedAt = ContinuousClock.now
460
+ let output = await Task.detached {
461
+ runCLI(["rewrite-selection"], homePath, RecordingEngine.commandRewriteTimeout)
462
+ }.value
463
+ let elapsed = ContinuousClock.now - startedAt
464
+
465
+ // Upper bound is the public promise, with the ~1 s return margin left as CI
466
+ // tolerance above the internal deadline; the lower bound proves the deadline chain
467
+ // really ran to exhaustion instead of the helper exiting early.
468
+ #expect(elapsed < .seconds(RecordingEngine.commandRewriteTimeout))
469
+ #expect(elapsed > .seconds(8.4))
470
+ #expect(output.hasPrefix("ERROR:"))
471
+ #expect(output.contains("timed out"))
472
+
473
+ let leaderPid = try #require(Self.readPID(from: leaderPidFile))
474
+ expectProcessIsGone(leaderPid)
475
+ expectProcessGroupIsGone(leaderPid)
476
+
477
+ // The runner returned while the holder still owned both write ends; its EPIPE
478
+ // marker proves the capture read descriptors were closed rather than left behind
479
+ // with live readers, and the holder exits on its own once both pipes are broken.
480
+ let holderPid = try #require(Self.readPID(from: holderPidFile))
481
+ var marker: String?
482
+ let markerDeadline = ContinuousClock.now + .seconds(3)
483
+ while ContinuousClock.now < markerDeadline {
484
+ if let contents = try? String(contentsOf: markerFile, encoding: .utf8), !contents.isEmpty {
485
+ marker = contents
486
+ break
487
+ }
488
+ try await Task.sleep(for: .milliseconds(50))
489
+ }
490
+ #expect(marker == "both-epipe")
491
+ expectProcessIsGone(holderPid)
492
+ }
493
+
494
+ @Test("the replaced shutdown returned without joining its readers and could snapshot truncated output")
495
+ func oldShutdownReturnedBeforeReadersExited() throws {
496
+ // Faithful reconstruction of the shutdown this runner used before
497
+ // PipeCaptureReader: a blocking reader thread tracked only by a DispatchGroup,
498
+ // and a finishCaptures that waited with a timeout, revoked the descriptor slot
499
+ // with dup2(/dev/null), scheduled the close on an async notify, and returned
500
+ // with no happens-before edge to the reader's exit.
501
+ //
502
+ // (Measured on macOS 26.0.1: dup2 over a blocked read(2) DOES wake the reader
503
+ // and widow the pipe — an undocumented Darwin drain that POSIX does not promise
504
+ // and Linux does not perform. The suspected silent-holder hang therefore only
505
+ // appears if the kernel ever stops extending that courtesy; the defect provable
506
+ // on every schedule is this one: nothing stopped the runner from snapshotting
507
+ // output while a reader sat between its read(2) and its append.)
508
+ final class CapturedBytes: @unchecked Sendable {
509
+ private let lock = NSLock()
510
+ private var storage = Data()
511
+ func append(_ data: Data) {
512
+ lock.lock()
513
+ storage.append(data)
514
+ lock.unlock()
515
+ }
516
+ var snapshot: Data {
517
+ lock.lock()
518
+ defer { lock.unlock() }
519
+ return storage
520
+ }
521
+ }
522
+
523
+ var ends: [Int32] = [0, 0]
524
+ try #require(Darwin.pipe(&ends) == 0)
525
+ let readDescriptor = ends[0]
526
+ let writeDescriptor = ends[1]
527
+ let capture = CapturedBytes()
528
+ let readers = DispatchGroup()
529
+ // A legal scheduler preemption point between the read returning and the append
530
+ // landing; the old shutdown had nothing that excluded this interleaving.
531
+ let preemptedBetweenReadAndAppend = DispatchSemaphore(value: 0)
532
+ readers.enter()
533
+ Thread.detachNewThread {
534
+ var buffer = [UInt8](repeating: 0, count: 65_536)
535
+ var firstRead = true
536
+ while true {
537
+ let count = buffer.withUnsafeMutableBytes {
538
+ Darwin.read(readDescriptor, $0.baseAddress, $0.count)
539
+ }
540
+ if count > 0 {
541
+ if firstRead {
542
+ firstRead = false
543
+ _ = preemptedBetweenReadAndAppend.wait(timeout: .now() + .seconds(10))
544
+ }
545
+ capture.append(Data(bytes: buffer, count: count))
546
+ continue
547
+ }
548
+ if count == -1 && errno == EINTR { continue }
549
+ break
550
+ }
551
+ readers.leave()
552
+ }
553
+
554
+ // The child's entire output arrives and the child exits cleanly — the ordinary
555
+ // nil-budget success shape, no escaped holder anywhere.
556
+ var payload = Array("tail-data".utf8)
557
+ try #require(
558
+ payload.withUnsafeMutableBytes {
559
+ Darwin.write(writeDescriptor, $0.baseAddress, $0.count)
560
+ } == payload.count
561
+ )
562
+ Darwin.close(writeDescriptor)
563
+
564
+ // Old finishCaptures, verbatim in structure: bounded group wait, dup2
565
+ // revocation, async close, return.
566
+ _ = readers.wait(timeout: .now() + .milliseconds(500))
567
+ let devnull = Darwin.open("/dev/null", O_RDONLY)
568
+ try #require(devnull != -1)
569
+ try #require(Darwin.dup2(devnull, readDescriptor) != -1)
570
+ Darwin.close(devnull)
571
+ readers.notify(queue: .global(qos: .utility)) {
572
+ Darwin.close(readDescriptor)
573
+ }
574
+
575
+ // At the exact point the old runner built its ProcessOutput, the reader is still
576
+ // alive and the delivered bytes are missing from the snapshot: truncation.
577
+ #expect(readers.wait(timeout: .now()) == .timedOut)
578
+ #expect(capture.snapshot.isEmpty)
579
+
580
+ // Let the "preempted" reader resume: the very same bytes land after the fact,
581
+ // proving only the missing join — not the child, not the pipe — lost them.
582
+ preemptedBetweenReadAndAppend.signal()
583
+ #expect(readers.wait(timeout: .now() + .seconds(5)) == .success)
584
+ #expect(String(decoding: capture.snapshot, as: UTF8.self) == "tail-data")
585
+ }
586
+
587
+ @Test("capture reader wakes from a silent held-open pipe on cancellation and joins with its data intact")
588
+ func captureReaderCancellationWithSilentWriter() throws {
589
+ let reader = try CLIRunner.PipeCaptureReader()
590
+ defer { reader.closeWriteDescriptor() }
591
+ // Deliver data through the pipe, then go silent while keeping the write end
592
+ // open — the escaped-descendant shape a blocking read could never be woken from.
593
+ var payload = Array("captured-before-silence".utf8)
594
+ try #require(
595
+ payload.withUnsafeMutableBytes {
596
+ Darwin.write(reader.writeDescriptor, $0.baseAddress, $0.count)
597
+ } == payload.count
598
+ )
599
+ let dataDeadline = ContinuousClock.now + .seconds(5)
600
+ while reader.data.isEmpty && ContinuousClock.now < dataDeadline {
601
+ Thread.sleep(forTimeInterval: 0.005)
602
+ }
603
+ #expect(String(decoding: reader.data, as: UTF8.self) == "captured-before-silence")
604
+
605
+ // No end-of-file is coming; a bounded natural-drain wait must report that.
606
+ #expect(!reader.waitUntilExited(deadline: .now() + .milliseconds(200)))
607
+
608
+ let startedAt = ContinuousClock.now
609
+ reader.cancel()
610
+ reader.join()
611
+ #expect(ContinuousClock.now - startedAt < .seconds(1))
612
+
613
+ // Joining proves the reader thread exited and closed the read end: the write end
614
+ // this test still holds must observe a widowed pipe on its very next write,
615
+ // exactly what a silent escaped holder sees the moment the runner returns.
616
+ _ = Darwin.fcntl(reader.writeDescriptor, F_SETNOSIGPIPE, 1)
617
+ var probeByte: UInt8 = 0
618
+ errno = 0
619
+ #expect(Darwin.write(reader.writeDescriptor, &probeByte, 1) == -1)
620
+ #expect(errno == EPIPE)
621
+ #expect(String(decoding: reader.data, as: UTF8.self) == "captured-before-silence")
622
+ }
623
+
624
+ @Test("capture reader cancellation still drains data already buffered in the pipe")
625
+ func captureReaderCancellationDrainsBufferedTail() throws {
626
+ let reader = try CLIRunner.PipeCaptureReader()
627
+ defer { reader.closeWriteDescriptor() }
628
+ var payload = [UInt8](repeating: UInt8(ascii: "t"), count: 8_192)
629
+ try #require(
630
+ payload.withUnsafeMutableBytes {
631
+ Darwin.write(reader.writeDescriptor, $0.baseAddress, $0.count)
632
+ } == payload.count
633
+ )
634
+ reader.cancel()
635
+ reader.join()
636
+ #expect(reader.data.count == 8_192)
637
+ }
638
+
639
+ @Test("capture reader retries interrupted setup and closes every descriptor when setup fails")
640
+ func captureReaderDescriptorSetupIsFailClosed() throws {
641
+ // Four descriptors each get F_GETFD/F_SETFD, followed by F_GETFL/F_SETFL on
642
+ // the capture read end. Force every individual setup operation to fail in turn.
643
+ for failingCall in 1...10 {
644
+ var createdDescriptors: [Int32] = []
645
+ var closeCalls: [Int32: Int] = [:]
646
+ var configurationCall = 0
647
+ let systemCalls = CLIRunner.PipeCaptureReader.SystemCalls(
648
+ makePipe: { _ in
649
+ var ends: [Int32] = [0, 0]
650
+ guard Darwin.pipe(&ends) == 0 else {
651
+ throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
652
+ }
653
+ createdDescriptors.append(contentsOf: ends)
654
+ return (ends[0], ends[1])
655
+ },
656
+ fcntl: { descriptor, command, value in
657
+ configurationCall += 1
658
+ if configurationCall == failingCall {
659
+ errno = EIO
660
+ return -1
661
+ }
662
+ if let value { return Darwin.fcntl(descriptor, command, value) }
663
+ return Darwin.fcntl(descriptor, command)
664
+ },
665
+ close: { descriptor in
666
+ closeCalls[descriptor, default: 0] += 1
667
+ return Darwin.close(descriptor)
668
+ }
669
+ )
670
+
671
+ #expect(throws: (any Error).self) {
672
+ _ = try CLIRunner.PipeCaptureReader(systemCalls: systemCalls)
673
+ }
674
+ #expect(createdDescriptors.count == 4)
675
+ #expect(Set(createdDescriptors).count == 4)
676
+ #expect(closeCalls.count == 4)
677
+ for descriptor in createdDescriptors {
678
+ #expect(closeCalls[descriptor] == 1)
679
+ }
680
+ }
681
+
682
+ // EINTR is retried at every setup call; successful construction still proves
683
+ // both inheritance protection and nonblocking capture configuration landed.
684
+ var interruptedCalls = Set<Int>()
685
+ var configurationCall = 0
686
+ var retryDescriptors: [Int32] = []
687
+ let retryingSystemCalls = CLIRunner.PipeCaptureReader.SystemCalls(
688
+ makePipe: { _ in
689
+ var ends: [Int32] = [0, 0]
690
+ guard Darwin.pipe(&ends) == 0 else {
691
+ throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
692
+ }
693
+ retryDescriptors.append(contentsOf: ends)
694
+ return (ends[0], ends[1])
695
+ },
696
+ fcntl: { descriptor, command, value in
697
+ configurationCall += 1
698
+ let logicalCall = (configurationCall + 1) / 2
699
+ if configurationCall % 2 == 1 {
700
+ interruptedCalls.insert(logicalCall)
701
+ errno = EINTR
702
+ return -1
703
+ }
704
+ if let value { return Darwin.fcntl(descriptor, command, value) }
705
+ return Darwin.fcntl(descriptor, command)
706
+ }
707
+ )
708
+ let reader = try CLIRunner.PipeCaptureReader(systemCalls: retryingSystemCalls)
709
+ defer { reader.closeWriteDescriptor() }
710
+ #expect(interruptedCalls == Set(1...10))
711
+ for descriptor in retryDescriptors {
712
+ #expect(Darwin.fcntl(descriptor, F_GETFD) & FD_CLOEXEC != 0)
713
+ }
714
+ #expect(Darwin.fcntl(reader.readDescriptor, F_GETFL) & O_NONBLOCK != 0)
715
+ reader.cancel()
716
+ reader.join()
717
+ }
718
+
719
+ @Test("capture reader closes every owned descriptor exactly once on EOF and cancellation")
720
+ func captureReaderDescriptorOwnershipIsExact() throws {
721
+ for cancelBeforeEOF in [false, true] {
722
+ let ledgerLock = NSLock()
723
+ var createdDescriptors: [Int32] = []
724
+ var closeCalls: [Int32: Int] = [:]
725
+ let systemCalls = CLIRunner.PipeCaptureReader.SystemCalls(
726
+ makePipe: { _ in
727
+ var ends: [Int32] = [0, 0]
728
+ guard Darwin.pipe(&ends) == 0 else {
729
+ throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
730
+ }
731
+ ledgerLock.lock()
732
+ createdDescriptors.append(contentsOf: ends)
733
+ ledgerLock.unlock()
734
+ return (ends[0], ends[1])
735
+ },
736
+ close: { descriptor in
737
+ ledgerLock.lock()
738
+ closeCalls[descriptor, default: 0] += 1
739
+ ledgerLock.unlock()
740
+ return Darwin.close(descriptor)
741
+ }
742
+ )
743
+
744
+ var reader: CLIRunner.PipeCaptureReader? = try .init(systemCalls: systemCalls)
745
+ if cancelBeforeEOF { reader?.cancel() }
746
+ reader?.closeWriteDescriptor()
747
+ if !cancelBeforeEOF {
748
+ #expect(reader?.waitUntilExited(deadline: .now() + .seconds(1)) == true)
749
+ }
750
+ reader?.join()
751
+ reader = nil
752
+
753
+ ledgerLock.lock()
754
+ let createdSnapshot = createdDescriptors
755
+ let closeSnapshot = closeCalls
756
+ ledgerLock.unlock()
757
+ #expect(createdSnapshot.count == 4)
758
+ #expect(Set(createdSnapshot).count == 4)
759
+ #expect(closeSnapshot.count == 4)
760
+ for descriptor in createdSnapshot {
761
+ #expect(closeSnapshot[descriptor] == 1)
762
+ }
763
+ }
764
+ }
765
+
766
+ @Test("terminal capture read and poll failures are retained after the reader joins")
767
+ func captureReaderRetainsTerminalIOFailures() throws {
768
+ let readFailure = try CLIRunner.PipeCaptureReader(
769
+ systemCalls: .init(read: { _, _, _ in
770
+ errno = EIO
771
+ return -1
772
+ })
773
+ )
774
+ readFailure.closeWriteDescriptor()
775
+ readFailure.join()
776
+ #expect(
777
+ readFailure.terminalError
778
+ == .captureFailed(operation: .read, code: EIO)
779
+ )
780
+
781
+ let pollFailure = try CLIRunner.PipeCaptureReader(
782
+ systemCalls: .init(poll: { _, _, _ in
783
+ errno = EIO
784
+ return -1
785
+ })
786
+ )
787
+ defer { pollFailure.closeWriteDescriptor() }
788
+ pollFailure.join()
789
+ #expect(
790
+ pollFailure.terminalError
791
+ == .captureFailed(operation: .poll, code: EIO)
792
+ )
793
+
794
+ let wakeupPollFailure = try CLIRunner.PipeCaptureReader(
795
+ systemCalls: .init(poll: { descriptors, _, _ in
796
+ guard let descriptors else {
797
+ errno = EFAULT
798
+ return -1
799
+ }
800
+ descriptors[1].revents = Int16(POLLNVAL)
801
+ return 1
802
+ })
803
+ )
804
+ defer { wakeupPollFailure.closeWriteDescriptor() }
805
+ wakeupPollFailure.join()
806
+ #expect(
807
+ wakeupPollFailure.terminalError
808
+ == .captureFailed(operation: .poll, code: EBADF)
809
+ )
810
+ }
811
+
812
+ @Test("an otherwise successful command surfaces a terminal capture failure")
813
+ func runExecutableSurfacesTerminalCaptureFailure() throws {
814
+ let failureGate = NSLock()
815
+ var shouldFailCapture = false
816
+ #expect(
817
+ throws: CLIRunner.ExecutionError.captureFailed(operation: .read, code: EIO)
818
+ ) {
819
+ _ = try CLIRunner.runExecutable(
820
+ "/bin/sleep",
821
+ arguments: ["0.05"],
822
+ beforeExecutionDeadline: {
823
+ failureGate.lock()
824
+ shouldFailCapture = true
825
+ failureGate.unlock()
826
+ },
827
+ captureSystemCalls: .init(read: { descriptor, buffer, count in
828
+ failureGate.lock()
829
+ let shouldFail = shouldFailCapture
830
+ failureGate.unlock()
831
+ if shouldFail {
832
+ errno = EIO
833
+ return -1
834
+ }
835
+ errno = EAGAIN
836
+ return -1
837
+ })
838
+ )
839
+ }
840
+ }
841
+
842
+ @Test("a command timeout remains primary over a competing capture failure")
843
+ func commandTimeoutRemainsPrimaryOverCaptureFailure() throws {
844
+ let failureGate = NSLock()
845
+ var shouldFailCapture = false
846
+ var injectedFailures = 0
847
+ #expect(
848
+ throws: CLIRunner.ExecutionError.timedOut(executable: "/bin/sleep", seconds: 0.05)
849
+ ) {
850
+ _ = try CLIRunner.runExecutable(
851
+ "/bin/sleep",
852
+ arguments: ["1"],
853
+ executionTimeout: 0.05,
854
+ terminationGracePeriod: 0,
855
+ forceKillGracePeriod: 0.1,
856
+ pipeDrainTimeout: 0.1,
857
+ beforeExecutionDeadline: {
858
+ failureGate.lock()
859
+ shouldFailCapture = true
860
+ failureGate.unlock()
861
+ },
862
+ captureSystemCalls: .init(read: { descriptor, buffer, count in
863
+ failureGate.lock()
864
+ let shouldFail = shouldFailCapture
865
+ if shouldFail { injectedFailures += 1 }
866
+ failureGate.unlock()
867
+ if shouldFail {
868
+ errno = EIO
869
+ return -1
870
+ }
871
+ return Darwin.read(descriptor, buffer, count)
872
+ })
873
+ )
874
+ }
875
+ failureGate.lock()
876
+ let injectedFailureCount = injectedFailures
877
+ failureGate.unlock()
878
+ #expect(injectedFailureCount > 0)
879
+ }
880
+
881
+ @Test("a reaper failure still joins capture readers and closes silent escaped pipes")
882
+ func reapFailureStillFinishesCaptures() throws {
883
+ struct InjectedReapFailure: Error {}
884
+ let failureGate = NSLock()
885
+ var shouldFailCapture = false
886
+ var injectedFailures = 0
887
+
888
+ let root = FileManager.default.temporaryDirectory
889
+ .appendingPathComponent("recordings-reap-failure-\(UUID().uuidString)")
890
+ let source = root.appendingPathComponent("silent-holder.c")
891
+ let executable = root.appendingPathComponent("silent-holder")
892
+ let holderPidFile = root.appendingPathComponent("holder-pid")
893
+ let markerFile = root.appendingPathComponent("holder-probe")
894
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
895
+ defer {
896
+ if let holderPid = Self.readPID(from: holderPidFile) {
897
+ _ = Darwin.kill(holderPid, SIGKILL)
898
+ }
899
+ try? FileManager.default.removeItem(at: root)
900
+ }
901
+
902
+ try """
903
+ #include <errno.h>
904
+ #include <signal.h>
905
+ #include <stdio.h>
906
+ #include <stdlib.h>
907
+ #include <sys/wait.h>
908
+ #include <time.h>
909
+ #include <unistd.h>
910
+
911
+ static volatile sig_atomic_t probeRequested = 0;
912
+ static void requestProbe(int signo) { (void)signo; probeRequested = 1; }
913
+
914
+ int main(int argc, char **argv) {
915
+ if (argc < 3) return 64;
916
+ pid_t child = fork();
917
+ if (child == -1) return 65;
918
+ if (child != 0) {
919
+ while (access(argv[1], F_OK) == -1) {
920
+ struct timespec delay = {0, 10000000};
921
+ nanosleep(&delay, 0);
922
+ }
923
+ return 0;
924
+ }
925
+ if (setsid() == -1) _exit(66);
926
+ signal(SIGPIPE, SIG_IGN);
927
+ signal(SIGUSR1, requestProbe);
928
+ FILE *pid = fopen(argv[1], "w");
929
+ if (!pid) _exit(67);
930
+ fprintf(pid, "%d", (int)getpid());
931
+ fclose(pid);
932
+ while (!probeRequested) {
933
+ struct timespec delay = {0, 20000000};
934
+ nanosleep(&delay, 0);
935
+ }
936
+ int outBroken = (write(STDOUT_FILENO, "x", 1) == -1 && errno == EPIPE);
937
+ int errBroken = (write(STDERR_FILENO, "y", 1) == -1 && errno == EPIPE);
938
+ FILE *marker = fopen(argv[2], "w");
939
+ if (!marker) _exit(68);
940
+ fputs(outBroken && errBroken ? "both-epipe" : "still-connected", marker);
941
+ fclose(marker);
942
+ _exit(0);
943
+ }
944
+ """.write(to: source, atomically: true, encoding: .utf8)
945
+ let compile = Process()
946
+ compile.executableURL = URL(fileURLWithPath: "/usr/bin/cc")
947
+ compile.arguments = ["-o", executable.path, source.path]
948
+ try compile.run()
949
+ compile.waitUntilExit()
950
+ try #require(compile.terminationStatus == 0)
951
+
952
+ let startedAt = ContinuousClock.now
953
+ #expect(throws: InjectedReapFailure.self) {
954
+ _ = try CLIRunner.runExecutable(
955
+ executable.path,
956
+ arguments: [holderPidFile.path, markerFile.path],
957
+ pipeDrainTimeout: 0.1,
958
+ leaderReaper: { processIdentifier, _ in
959
+ failureGate.lock()
960
+ shouldFailCapture = true
961
+ failureGate.unlock()
962
+ var status: Int32 = 0
963
+ var result: pid_t
964
+ repeat {
965
+ result = Darwin.waitpid(processIdentifier, &status, 0)
966
+ } while result == -1 && errno == EINTR
967
+ guard result == processIdentifier else {
968
+ throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
969
+ }
970
+ throw InjectedReapFailure()
971
+ },
972
+ captureSystemCalls: .init(read: { descriptor, buffer, count in
973
+ failureGate.lock()
974
+ let shouldFail = shouldFailCapture
975
+ if shouldFail { injectedFailures += 1 }
976
+ failureGate.unlock()
977
+ if shouldFail {
978
+ errno = EIO
979
+ return -1
980
+ }
981
+ return Darwin.read(descriptor, buffer, count)
982
+ })
983
+ )
984
+ }
985
+ #expect(ContinuousClock.now - startedAt < .seconds(2))
986
+ failureGate.lock()
987
+ let injectedFailureCount = injectedFailures
988
+ failureGate.unlock()
989
+ #expect(injectedFailureCount > 0)
990
+
991
+ let holderPid = try #require(Self.readPID(from: holderPidFile))
992
+ #expect(Darwin.kill(holderPid, 0) == 0)
993
+ try #require(Darwin.kill(holderPid, SIGUSR1) == 0)
994
+ var marker: String?
995
+ let markerDeadline = ContinuousClock.now + .seconds(3)
996
+ while ContinuousClock.now < markerDeadline {
997
+ if let contents = try? String(contentsOf: markerFile, encoding: .utf8), !contents.isEmpty {
998
+ marker = contents
999
+ break
1000
+ }
1001
+ Thread.sleep(forTimeInterval: 0.05)
1002
+ }
1003
+ #expect(marker == "both-epipe")
1004
+ expectProcessIsGone(holderPid)
1005
+ }
1006
+
1007
+ @Test("output at the exit/drain boundary is captured completely for nil-budget and budgeted runs")
1008
+ func outputCompleteAtExitDrainBoundary() throws {
1009
+ // 256 KiB per stream overflows the kernel pipe buffer many times over, so data is
1010
+ // still in flight as the runner observes the exit, and the final sentinel lands
1011
+ // in each pipe immediately before end-of-file.
1012
+ let command = """
1013
+ dd if=/dev/zero bs=262144 count=1 2>/dev/null | tr '\\0' o
1014
+ dd if=/dev/zero bs=262144 count=1 2>/dev/null | tr '\\0' e >&2
1015
+ printf 'OUT-END'
1016
+ printf 'ERR-END' >&2
1017
+ """
1018
+ for budget in [nil, 5.0] as [TimeInterval?] {
1019
+ let result = try CLIRunner.runExecutable(
1020
+ "/bin/sh",
1021
+ arguments: ["-c", command],
1022
+ totalWallClockBudget: budget
1023
+ )
1024
+ #expect(result.terminationStatus == 0)
1025
+ #expect(result.stdout.utf8.count == 262_144 + "OUT-END".utf8.count)
1026
+ #expect(result.stderr.utf8.count == 262_144 + "ERR-END".utf8.count)
1027
+ #expect(result.stdout.hasSuffix("OUT-END"))
1028
+ #expect(result.stderr.hasSuffix("ERR-END"))
1029
+ }
1030
+ }
1031
+
1032
+ @Test("a completely silent escaped descendant cannot pin the capture readers past the ceiling")
1033
+ @MainActor
1034
+ func silentEscapedDescendantCannotPinCaptureReaders() async throws {
1035
+ let home = FileManager.default.temporaryDirectory
1036
+ .appendingPathComponent("recordings-rewrite-silent-holder-\(UUID().uuidString)")
1037
+ let bin = home.appendingPathComponent(".bun/bin")
1038
+ let leaderPidFile = home.appendingPathComponent("leader-pid")
1039
+ let holderPidFile = home.appendingPathComponent("holder-pid")
1040
+ let markerFile = home.appendingPathComponent("holder-probe")
1041
+ let holderSource = home.appendingPathComponent("silent-holder.c")
1042
+ let holderBinary = home.appendingPathComponent("silent-holder")
1043
+ try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
1044
+ defer {
1045
+ if let holderPid = Self.readPID(from: holderPidFile) {
1046
+ _ = Darwin.kill(holderPid, SIGKILL)
1047
+ }
1048
+ if let leaderPid = Self.readPID(from: leaderPidFile) {
1049
+ _ = Darwin.kill(-leaderPid, SIGKILL)
1050
+ _ = Darwin.kill(leaderPid, SIGKILL)
1051
+ }
1052
+ try? FileManager.default.removeItem(at: home)
1053
+ }
1054
+
1055
+ // Unlike the EPIPE holder above, this descendant never writes a byte while the
1056
+ // runner is alive: a reader blocked in read(2) would never be handed the data it
1057
+ // needs to run onto a revoked descriptor, which is exactly the shape that pinned
1058
+ // the old blocking readers forever. Only after the runner has returned does the
1059
+ // test send SIGUSR1, and the holder's very first write on each stream must then
1060
+ // fail with EPIPE — possible only if the reader threads already exited and closed
1061
+ // both read ends before the runner returned, not asynchronously afterwards.
1062
+ try """
1063
+ #include <errno.h>
1064
+ #include <signal.h>
1065
+ #include <stdio.h>
1066
+ #include <time.h>
1067
+ #include <unistd.h>
1068
+
1069
+ static volatile sig_atomic_t probeRequested = 0;
1070
+ static void requestProbe(int signo) { (void)signo; probeRequested = 1; }
1071
+
1072
+ int main(int argc, char **argv) {
1073
+ if (argc < 3) return 64;
1074
+ if (setsid() == -1) return 65;
1075
+ signal(SIGPIPE, SIG_IGN);
1076
+ signal(SIGUSR1, requestProbe);
1077
+ FILE *pid = fopen(argv[1], "w");
1078
+ if (!pid) return 66;
1079
+ fprintf(pid, "%d", (int)getpid());
1080
+ fclose(pid);
1081
+ while (!probeRequested) {
1082
+ struct timespec delay = {0, 20000000};
1083
+ nanosleep(&delay, 0);
1084
+ }
1085
+ int stdoutBroken = (write(STDOUT_FILENO, "x", 1) == -1 && errno == EPIPE);
1086
+ int stderrBroken = (write(STDERR_FILENO, "y", 1) == -1 && errno == EPIPE);
1087
+ FILE *marker = fopen(argv[2], "w");
1088
+ if (!marker) return 67;
1089
+ fputs(stdoutBroken && stderrBroken ? "both-epipe" : "still-connected", marker);
1090
+ fclose(marker);
1091
+ return 0;
1092
+ }
1093
+ """.write(to: holderSource, atomically: true, encoding: .utf8)
1094
+ let compile = Process()
1095
+ compile.executableURL = URL(fileURLWithPath: "/usr/bin/cc")
1096
+ compile.arguments = ["-o", holderBinary.path, holderSource.path]
1097
+ try compile.run()
1098
+ compile.waitUntilExit()
1099
+ try #require(compile.terminationStatus == 0)
1100
+
1101
+ let executable = bin.appendingPathComponent("recordings")
1102
+ try """
1103
+ #!/bin/sh
1104
+ trap '' TERM
1105
+ printf '%s' "$$" > '\(leaderPidFile.path)'
1106
+ '\(holderBinary.path)' '\(holderPidFile.path)' '\(markerFile.path)' &
1107
+ while [ ! -s '\(holderPidFile.path)' ]; do /bin/sleep 0.01; done
1108
+ while :; do /bin/sleep 0.05; done
1109
+ """.write(to: executable, atomically: true, encoding: .utf8)
1110
+ try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
1111
+
1112
+ // The production seam exactly as runCommandMode uses it. The leader ignores
1113
+ // SIGTERM and the silent holder keeps both pipes open, so every deadline in the
1114
+ // chain — execution window, termination grace, drain wait — runs to exhaustion
1115
+ // with zero bytes ever arriving to wake a reader.
1116
+ let runCLI = RecordingEngine().commandCLI
1117
+ let homePath = home.path
1118
+ let startedAt = ContinuousClock.now
1119
+ let output = await Task.detached {
1120
+ runCLI(["rewrite-selection"], homePath, RecordingEngine.commandRewriteTimeout)
1121
+ }.value
1122
+ let elapsed = ContinuousClock.now - startedAt
1123
+
1124
+ #expect(elapsed < .seconds(RecordingEngine.commandRewriteTimeout))
1125
+ #expect(elapsed > .seconds(8.4))
1126
+ #expect(output.hasPrefix("ERROR:"))
1127
+ #expect(output.contains("timed out"))
1128
+
1129
+ let leaderPid = try #require(Self.readPID(from: leaderPidFile))
1130
+ expectProcessIsGone(leaderPid)
1131
+ expectProcessGroupIsGone(leaderPid)
1132
+
1133
+ // The holder escaped the group kill and stayed completely silent: it must still
1134
+ // be alive, and must not have probed yet.
1135
+ let holderPid = try #require(Self.readPID(from: holderPidFile))
1136
+ #expect(Darwin.kill(holderPid, 0) == 0)
1137
+ #expect(!FileManager.default.fileExists(atPath: markerFile.path))
1138
+
1139
+ // Ask for the probe only now, after the runner has returned. First-write EPIPE on
1140
+ // both streams proves the capture read ends were closed — and the reader threads
1141
+ // joined — before the return, with no writer traffic ever helping them along.
1142
+ try #require(Darwin.kill(holderPid, SIGUSR1) == 0)
1143
+ var marker: String?
1144
+ let markerDeadline = ContinuousClock.now + .seconds(3)
1145
+ while ContinuousClock.now < markerDeadline {
1146
+ if let contents = try? String(contentsOf: markerFile, encoding: .utf8), !contents.isEmpty {
1147
+ marker = contents
1148
+ break
1149
+ }
1150
+ try await Task.sleep(for: .milliseconds(50))
1151
+ }
1152
+ #expect(marker == "both-epipe")
1153
+ expectProcessIsGone(holderPid)
1154
+ }
1155
+
1156
+ @Test("transcribeCLIArgs passes project, cleanup mode, and transcriber prompt")
1157
+ func transcribeCLIArgsWithPrompt() {
1158
+ let args = RecordingEngine.transcribeCLIArgs(
1159
+ audioPath: "/tmp/audio.wav",
1160
+ activeProjectId: "project-1",
1161
+ transcriberPrompt: "Format as notes",
1162
+ postProcessingMode: "always"
1163
+ )
1164
+ #expect(args == [
1165
+ "--json",
1166
+ "--project", "project-1",
1167
+ "transcribe", "/tmp/audio.wav",
1168
+ "--post-processing", "always",
1169
+ "--transcriber-prompt", "Format as notes",
1170
+ ])
1171
+ #expect(!args.contains("--no-enhance"))
1172
+ }
1173
+
1174
+ @Test("transcribeCLIArgs defaults to auto and omits blank prompt")
1175
+ func transcribeCLIArgsDefaultAuto() {
1176
+ let args = RecordingEngine.transcribeCLIArgs(
1177
+ audioPath: "/tmp/audio.wav",
1178
+ activeProjectId: nil,
1179
+ transcriberPrompt: " ",
1180
+ postProcessingMode: ""
1181
+ )
1182
+ #expect(args == [
1183
+ "--json",
1184
+ "transcribe", "/tmp/audio.wav",
1185
+ "--post-processing", "auto",
1186
+ ])
1187
+ }
1188
+
1189
+ @Test("transcribeCLIArgs falls back to auto for invalid mode")
1190
+ func transcribeCLIArgsInvalidModeFallback() {
1191
+ let args = RecordingEngine.transcribeCLIArgs(
1192
+ audioPath: "/tmp/audio.wav",
1193
+ activeProjectId: "",
1194
+ transcriberPrompt: "",
1195
+ postProcessingMode: "sometimes"
1196
+ )
1197
+ #expect(args == [
1198
+ "--json",
1199
+ "transcribe", "/tmp/audio.wav",
1200
+ "--post-processing", "auto",
1201
+ ])
1202
+ }
1203
+
1204
+ @Test("saveTextCLIArgs persists realtime fast-path transcript")
1205
+ func saveTextCLIArgsRealtimeFastPath() {
1206
+ let args = RecordingEngine.saveTextCLIArgs(
1207
+ textFile: "/tmp/transcript.txt",
1208
+ audioPath: "/tmp/audio.wav",
1209
+ activeProjectId: "project-1",
1210
+ transcriberPrompt: "Format as notes",
1211
+ postProcessingMode: "auto",
1212
+ language: "en",
1213
+ durationMs: 1200,
1214
+ source: "realtime_fast_path",
1215
+ modelUsed: "gpt-realtime-whisper"
1216
+ )
1217
+ #expect(args == [
1218
+ "--json",
1219
+ "--project", "project-1",
1220
+ "save-text",
1221
+ "--text-file", "/tmp/transcript.txt",
1222
+ "--source", "realtime_fast_path",
1223
+ "--model-used", "gpt-realtime-whisper",
1224
+ "--post-processing", "auto",
1225
+ "--audio-path", "/tmp/audio.wav",
1226
+ "--duration-ms", "1200",
1227
+ "--language", "en",
1228
+ "--transcriber-prompt", "Format as notes",
1229
+ ])
1230
+ }
1231
+
1232
+ @Test("saveTextCLIArgs omits an unsafe local project id")
1233
+ func saveTextCLIArgsWithoutCanonicalProject() {
1234
+ let args = RecordingEngine.saveTextCLIArgs(
1235
+ textFile: "/tmp/transcript.txt",
1236
+ audioPath: nil,
1237
+ activeProjectId: nil,
1238
+ transcriberPrompt: "Keep local prompt context",
1239
+ postProcessingMode: "auto",
1240
+ language: "en",
1241
+ durationMs: 0,
1242
+ source: "realtime_fast_path",
1243
+ modelUsed: "gpt-realtime-whisper"
1244
+ )
1245
+
1246
+ #expect(!args.contains("--project"))
1247
+ #expect(args.contains("--transcriber-prompt"))
1248
+ #expect(args.contains("Keep local prompt context"))
1249
+ }
1250
+
5
1251
  @Test("parseError detects ERROR prefix")
6
1252
  func parseError() {
7
1253
  #expect(CLIRunner.parseError("ERROR: OpenAI API key not configured on this Mac") == "OpenAI API key not configured on this Mac")
@@ -37,6 +1283,103 @@ struct CLIRunnerTests {
37
1283
  #expect(result.count <= 120)
38
1284
  }
39
1285
 
1286
+ @Test("parseError sanitizes generic credential-bearing failures")
1287
+ func genericErrorsAreSanitized() {
1288
+ let key = "sk-" + "synthetic-cli-secret-123456"
1289
+ let bearer = "synthetic-bearer-secret-123456"
1290
+ let token = "synthetic-query-secret-123456"
1291
+ let result = CLIRunner.parseError(
1292
+ "ERROR: upstream failed key=\(key) Authorization: Bearer \(bearer) https://example.test?access_token=\(token)"
1293
+ )
1294
+
1295
+ #expect(result?.contains(key) == false)
1296
+ #expect(result?.contains(bearer) == false)
1297
+ #expect(result?.contains(token) == false)
1298
+ #expect(result?.contains("[REDACTED]") == true)
1299
+
1300
+ let structured = CLIRunner.parseError(
1301
+ "ERROR: OPENAI_API_KEY=plain-synthetic-secret CLIENT_SECRET=client-synthetic-secret "
1302
+ + "PASSWORD=password-synthetic-secret payload={\"api_key\":\"json-synthetic-secret\","
1303
+ + "\"private_key\":\"private-synthetic-secret\"}"
1304
+ )
1305
+ #expect(structured?.contains("plain-synthetic-secret") == false)
1306
+ #expect(structured?.contains("client-synthetic-secret") == false)
1307
+ #expect(structured?.contains("password-synthetic-secret") == false)
1308
+ #expect(structured?.contains("json-synthetic-secret") == false)
1309
+ #expect(structured?.contains("private-synthetic-secret") == false)
1310
+
1311
+ let quoted = CLIRunner.parseError(
1312
+ "ERROR: payload={\"password\":\"correct horse battery staple\","
1313
+ + "\"secret\":\"bare secret phrase\",\"private\":\"private phrase\"}"
1314
+ )
1315
+ #expect(quoted?.contains("correct horse battery staple") == false)
1316
+ #expect(quoted?.contains("bare secret phrase") == false)
1317
+ #expect(quoted?.contains("private phrase") == false)
1318
+ #expect(quoted?.contains("horse battery staple") == false)
1319
+
1320
+ let escapedAndUnquoted = CLIRunner.parseError(
1321
+ "ERROR: payload={\"secret\":\"alpha \\\"quoted\\\" beta gamma\"}; "
1322
+ + "private: delta echo foxtrot"
1323
+ )
1324
+ #expect(escapedAndUnquoted?.contains("quoted") == false)
1325
+ #expect(escapedAndUnquoted?.contains("beta gamma") == false)
1326
+ #expect(escapedAndUnquoted?.contains("echo foxtrot") == false)
1327
+
1328
+ let commonCredentials = CLIRunner.parseError(
1329
+ "ERROR: Authorization: Basic synthetic-basic-value; "
1330
+ + "AWS_SECRET_ACCESS_KEY=delta echo foxtrot"
1331
+ )
1332
+ #expect(commonCredentials?.contains("synthetic-basic-value") == false)
1333
+ #expect(commonCredentials?.contains("echo foxtrot") == false)
1334
+ }
1335
+
1336
+ @Test("parseError preserves ordinary generic failures")
1337
+ func ordinaryErrorsRemainUseful() {
1338
+ #expect(CLIRunner.parseError("ERROR: microphone disconnected") == "microphone disconnected")
1339
+ #expect(CLIRunner.parseError("ERROR: Unexpected token: EOF") == "Unexpected token: EOF")
1340
+ #expect(CLIRunner.parseError("ERROR: resource is private: access denied") == "resource is private: access denied")
1341
+ }
1342
+
1343
+ @Test("run sanitizes failed process stderr before returning it")
1344
+ func failedProcessStderrIsSanitized() throws {
1345
+ let key = "sk-" + "synthetic-process-secret-123456"
1346
+ let home = FileManager.default.temporaryDirectory
1347
+ .appendingPathComponent("recordings-cli-sanitize-\(UUID().uuidString)")
1348
+ let bin = home.appendingPathComponent(".bun/bin")
1349
+ try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
1350
+ let executable = bin.appendingPathComponent("recordings")
1351
+ try "#!/bin/sh\nprintf 'request failed: %s' '$KEY' >&2\nexit 1\n"
1352
+ .replacingOccurrences(of: "$KEY", with: key)
1353
+ .write(to: executable, atomically: true, encoding: .utf8)
1354
+ try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
1355
+ defer { try? FileManager.default.removeItem(at: home) }
1356
+
1357
+ let output = CLIRunner.run([], home: home.path)
1358
+
1359
+ #expect(!output.contains(key))
1360
+ #expect(output.contains("[REDACTED]"))
1361
+ }
1362
+
1363
+ @Test("run sanitizes stderr-only output from a successful process")
1364
+ func successfulProcessStderrIsSanitized() throws {
1365
+ let password = "synthetic-process-password-123456"
1366
+ let home = FileManager.default.temporaryDirectory
1367
+ .appendingPathComponent("recordings-cli-success-sanitize-\(UUID().uuidString)")
1368
+ let bin = home.appendingPathComponent(".bun/bin")
1369
+ try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
1370
+ let executable = bin.appendingPathComponent("recordings")
1371
+ try "#!/bin/sh\nprintf 'PASSWORD=%s' '$PASSWORD' >&2\n"
1372
+ .replacingOccurrences(of: "$PASSWORD", with: password)
1373
+ .write(to: executable, atomically: true, encoding: .utf8)
1374
+ try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
1375
+ defer { try? FileManager.default.removeItem(at: home) }
1376
+
1377
+ let output = CLIRunner.run([], home: home.path)
1378
+
1379
+ #expect(!output.contains(password))
1380
+ #expect(output.contains("[REDACTED]"))
1381
+ }
1382
+
40
1383
  @Test("parseJSON extracts raw_text from JSON")
41
1384
  func parseJSONRawText() {
42
1385
  let output = """