@hasna/recordings 0.3.13 → 0.5.0

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 (148) hide show
  1. package/README.md +279 -43
  2. package/bun.lock +37 -38
  3. package/contracts/v1/fixtures.json +1206 -0
  4. package/dist/cli/index.js +1909 -619
  5. package/dist/cli/macos-shortcut.d.ts +2 -2
  6. package/dist/contracts/hosted-v1.d.ts +105 -0
  7. package/dist/contracts/hosted-v1.d.ts.map +1 -0
  8. package/dist/contracts/hosted-v1.js +41 -0
  9. package/dist/contracts/stream-v1.d.ts +117 -0
  10. package/dist/contracts/stream-v1.d.ts.map +1 -0
  11. package/dist/contracts/stream-v1.js +35 -0
  12. package/dist/db/database.d.ts.map +1 -1
  13. package/dist/db/pg-migrations.d.ts.map +1 -1
  14. package/dist/db/recordings.d.ts +6 -0
  15. package/dist/db/recordings.d.ts.map +1 -1
  16. package/dist/hosted/index.d.ts +58 -0
  17. package/dist/hosted/index.d.ts.map +1 -0
  18. package/dist/hosted/index.js +266 -0
  19. package/dist/hosted/transport.d.ts +36 -0
  20. package/dist/hosted/transport.d.ts.map +1 -0
  21. package/dist/hosted-v1-aavn7ktb.js +4114 -0
  22. package/dist/hosted-v1-gdr9extc.js +84 -0
  23. package/dist/http/client.d.ts +183 -22
  24. package/dist/http/client.d.ts.map +1 -1
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2879 -2288
  28. package/dist/lib/audio-artifact-storage.d.ts +98 -0
  29. package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
  30. package/dist/lib/capture-probe.d.ts +3 -3
  31. package/dist/lib/capture-probe.d.ts.map +1 -1
  32. package/dist/lib/config.d.ts.map +1 -1
  33. package/dist/lib/local-opt-in.d.ts +132 -0
  34. package/dist/lib/local-opt-in.d.ts.map +1 -0
  35. package/dist/lib/macos-bundle.d.ts +1 -1
  36. package/dist/lib/persistence-probe.d.ts +56 -17
  37. package/dist/lib/persistence-probe.d.ts.map +1 -1
  38. package/dist/lib/transcriber.d.ts.map +1 -1
  39. package/dist/mcp/index.d.ts.map +1 -1
  40. package/dist/mcp/index.js +1713 -459
  41. package/dist/sdk/index.d.ts +13 -5
  42. package/dist/sdk/index.d.ts.map +1 -1
  43. package/dist/sdk/index.js +904 -1
  44. package/dist/sdk/resolve.d.ts +53 -0
  45. package/dist/sdk/resolve.d.ts.map +1 -0
  46. package/dist/sdk/v1.generated.d.ts +6 -0
  47. package/dist/sdk/v1.generated.d.ts.map +1 -1
  48. package/dist/server/client-ip.d.ts +39 -0
  49. package/dist/server/client-ip.d.ts.map +1 -0
  50. package/dist/server/cloud-config.d.ts +12 -0
  51. package/dist/server/cloud-config.d.ts.map +1 -1
  52. package/dist/server/cloud-readiness.d.ts.map +1 -1
  53. package/dist/server/cloud.d.ts +20 -2
  54. package/dist/server/cloud.d.ts.map +1 -1
  55. package/dist/server/index.js +590 -287
  56. package/dist/server/migrate-command.d.ts +1 -1
  57. package/dist/server/migrate-command.d.ts.map +1 -1
  58. package/dist/server/openapi.d.ts +21 -0
  59. package/dist/server/openapi.d.ts.map +1 -1
  60. package/dist/server/repo.d.ts.map +1 -1
  61. package/dist/server/serve.d.ts.map +1 -1
  62. package/dist/storage.d.ts +2 -2
  63. package/dist/storage.d.ts.map +1 -1
  64. package/dist/storage.js +1358 -874
  65. package/dist/store.d.ts +12 -3
  66. package/dist/store.d.ts.map +1 -1
  67. package/dist/types/index.d.ts +15 -0
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/docs/hosted-sdk.md +71 -0
  70. package/docs/wire-contracts.md +24 -0
  71. package/package.json +33 -11
  72. package/packaging/macos/build_release_pkg.sh +3 -3
  73. package/packaging/macos/managed_bootstrap.sh +4 -4
  74. package/packaging/macos/scripts/postinstall +2 -2
  75. package/packaging/macos/scripts/preinstall +2 -2
  76. package/scripts/build_companion_cli.sh +7 -1
  77. package/scripts/ci-linux-suite.ts +23 -13
  78. package/scripts/install_macos_app.sh +27 -24
  79. package/scripts/macos_artifact.ts +60 -49
  80. package/scripts/migrate.ts +4 -20
  81. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  82. package/scripts/native/recordings_fs_guard.c +36 -4
  83. package/scripts/native-core-receipt.py +171 -0
  84. package/scripts/native_fs_guard.ts +2 -0
  85. package/scripts/release-suite-gate.ts +227 -150
  86. package/scripts/resolve_tailscale_cli.sh +24 -3
  87. package/scripts/smoke_macos_app.sh +21 -21
  88. package/src/native/Recordings/App/ContentView.swift +53 -68
  89. package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
  90. package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
  91. package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
  92. package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
  93. package/src/native/Recordings/App/RecordingsApp.swift +185 -9
  94. package/src/native/Recordings/App/RecordingsListView.swift +60 -112
  95. package/src/native/Recordings/App/RecordingsStore.swift +124 -109
  96. package/src/native/Recordings/App/Theme.swift +73 -61
  97. package/src/native/Recordings/Package.swift +1 -0
  98. package/src/native/Recordings/RecordingsLib/BlockingOperation.swift +29 -0
  99. package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
  101. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
  102. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +4 -4
  103. package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
  104. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +539 -89
  105. package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
  106. package/src/native/Recordings/RecordingsLib/RecordingPasteTarget.swift +114 -0
  107. package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +112 -0
  108. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
  109. package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
  110. package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
  111. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
  112. package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
  113. package/src/native/Recordings/RecordingsTests/BlockingOperationTests.swift +85 -0
  114. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +454 -76
  115. package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
  116. package/src/native/Recordings/RecordingsTests/PipeClosureFixture.swift +215 -0
  117. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +10 -10
  118. package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
  119. package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
  120. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +1 -1
  121. package/src/native/Recordings/RecordingsTests/RecordingFrozenPasteTargetTests.swift +50 -0
  122. package/src/native/Recordings/RecordingsTests/RecordingPasteTargetTrackerTests.swift +48 -0
  123. package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +308 -0
  124. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
  125. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +123 -21
  126. package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
  127. package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
  128. package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +5 -1
  129. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
  130. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
  131. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
  132. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
  133. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
  134. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
  135. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
  136. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
  137. package/src/native/Recordings/build.sh +8 -7
  138. package/dist/__tests__/helpers/installer-guard-execution.d.ts +0 -22
  139. package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +0 -1
  140. package/dist/__tests__/helpers/installer-preflight.d.ts +0 -22
  141. package/dist/__tests__/helpers/installer-preflight.d.ts.map +0 -1
  142. package/dist/__tests__/helpers/native-fs-guard.d.ts +0 -2
  143. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +0 -1
  144. package/dist/__tests__/helpers/source-assertions.d.ts +0 -171
  145. package/dist/__tests__/helpers/source-assertions.d.ts.map +0 -1
  146. package/dist/__tests__/preload.d.ts +0 -2
  147. package/dist/__tests__/preload.d.ts.map +0 -1
  148. package/src/native/Recordings/App/SidebarView.swift +0 -212
@@ -4,11 +4,114 @@ import Testing
4
4
  @testable import RecordingsLib
5
5
 
6
6
  struct CLIRunnerTests {
7
+ private final class DeadlineClock: @unchecked Sendable {
8
+ private let lock = NSLock()
9
+ private var nanoseconds: UInt64 = 1_000_000_000
10
+
11
+ func now() -> UInt64 {
12
+ lock.lock()
13
+ defer { lock.unlock() }
14
+ return nanoseconds
15
+ }
16
+
17
+ func advance(seconds: UInt64) {
18
+ lock.lock()
19
+ defer { lock.unlock() }
20
+ nanoseconds += seconds * 1_000_000_000
21
+ }
22
+ }
23
+
24
+ @Test("a fractional supplied deadline respects its original budget with a nonadvancing clock")
25
+ func fractionalDeadlineDoesNotExceedOriginalBudget() throws {
26
+ let clock = DeadlineClock()
27
+ let budget = 3.0000000001
28
+ let result = try CLIRunner.runExecutable(
29
+ "/usr/bin/true", arguments: [], environment: [:],
30
+ totalWallClockBudget: budget,
31
+ wallClockDeadline: .init(after: budget, now: { clock.now() })
32
+ )
33
+ #expect(result.terminationStatus == 0)
34
+ #expect(result.stdout.isEmpty)
35
+ #expect(result.stderr.isEmpty)
36
+ }
37
+
38
+ @Test("rewrite admission consumes its original budget before the blocking worker starts")
39
+ func rewriteAdmissionConsumesOriginalBudget() async {
40
+ let clock = DeadlineClock()
41
+ let operation = RecordingEngine.makeCommandRewriteOperation(
42
+ args: ["rewrite-selection"], home: "/fictional-test-home",
43
+ runCLI: { args, home, budget in
44
+ #expect(args == ["rewrite-selection"])
45
+ #expect(home == "/fictional-test-home")
46
+ #expect(budget == 7)
47
+ return "fictional rewrite"
48
+ },
49
+ deadline: .init(after: 10, now: { clock.now() })
50
+ )
51
+ clock.advance(seconds: 3)
52
+ #expect(await BlockingOperation.run(operation) == "fictional rewrite")
53
+ }
54
+
55
+ @Test("expired and cleanup-only rewrite admission never calls the command seam", arguments: [8, 10, 11])
56
+ func expiredRewriteAdmissionAvoidsCommand(seconds: UInt64) async {
57
+ let clock = DeadlineClock()
58
+ let operation = RecordingEngine.makeCommandRewriteOperation(
59
+ args: ["rewrite-selection"], home: "/fictional-test-home",
60
+ runCLI: { _, _, _ in
61
+ Issue.record("An exhausted queued rewrite invoked its command seam")
62
+ return "unexpected command"
63
+ },
64
+ deadline: .init(after: 10, now: { clock.now() })
65
+ )
66
+ clock.advance(seconds: seconds)
67
+ let output = await BlockingOperation.run(operation)
68
+ #expect(output.hasPrefix("ERROR:"))
69
+ #expect(output.contains("timed out"))
70
+ }
71
+
72
+ @Test("command preparation consumes the original deadline and cannot spawn after exhaustion", arguments: [false, true], [0, 2, 3])
73
+ func preparationDeadlineAvoidsExpiredSpawn(expiredBeforePreparation: Bool, spentSeconds: UInt64) throws {
74
+ let home = URL(fileURLWithPath: makeIsolatedTestHome("rewrite-preparation-deadline"), isDirectory: true)
75
+ defer { try? FileManager.default.removeItem(at: home) }
76
+ let executable = home.appendingPathComponent(".bun/bin/recordings")
77
+ let marker = home.appendingPathComponent("spawned")
78
+ try FileManager.default.createDirectory(at: executable.deletingLastPathComponent(), withIntermediateDirectories: true)
79
+ try """
80
+ #!/bin/sh
81
+ printf spawned > "$1"
82
+ printf complete
83
+ """.write(to: executable, atomically: true, encoding: .utf8)
84
+ try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
85
+ let clock = DeadlineClock()
86
+ let deadline = CLIRunner.WallClockDeadline(after: 3, now: { clock.now() })
87
+ if expiredBeforePreparation { clock.advance(seconds: spentSeconds) }
88
+ var preparations = 0
89
+ let output = CLIRunner.run(
90
+ [marker.path], home: home.path, timeout: 3, totalWallClockBudget: 3,
91
+ wallClockDeadline: deadline,
92
+ environmentProvider: {
93
+ preparations += 1
94
+ if !expiredBeforePreparation { clock.advance(seconds: spentSeconds) }
95
+ return [:]
96
+ }
97
+ )
98
+ if spentSeconds == 0 {
99
+ #expect(preparations == 1)
100
+ #expect(output == "complete")
101
+ #expect(try String(contentsOf: marker, encoding: .utf8) == "spawned")
102
+ } else {
103
+ #expect(preparations == (expiredBeforePreparation ? 0 : 1))
104
+ #expect(output.hasPrefix("ERROR:"))
105
+ #expect(output.contains("timed out"))
106
+ #expect(!FileManager.default.fileExists(atPath: marker.path))
107
+ }
108
+ }
109
+
7
110
  @Test("bundled CLI takes precedence over a stale user installation")
8
111
  func bundledCLIIsPreferred() throws {
9
112
  let root = FileManager.default.temporaryDirectory
10
113
  .appendingPathComponent("recordings-bundled-cli-\(UUID().uuidString)")
11
- let app = root.appendingPathComponent("HasnaRecordings.app")
114
+ let app = root.appendingPathComponent("Hasna Recordings.app")
12
115
  let helper = app.appendingPathComponent("Contents/Helpers/recordings")
13
116
  let home = root.appendingPathComponent("home")
14
117
  let external = home.appendingPathComponent(".bun/bin/recordings")
@@ -34,7 +137,7 @@ struct CLIRunnerTests {
34
137
  func packagedAppDoesNotUseGlobalFallback() throws {
35
138
  let root = FileManager.default.temporaryDirectory
36
139
  .appendingPathComponent("recordings-missing-companion-\(UUID().uuidString)")
37
- let app = root.appendingPathComponent("HasnaRecordings.app")
140
+ let app = root.appendingPathComponent("Hasna Recordings.app")
38
141
  let home = root.appendingPathComponent("home")
39
142
  let external = home.appendingPathComponent(".bun/bin/recordings")
40
143
  try FileManager.default.createDirectory(at: app, withIntermediateDirectories: true)
@@ -66,6 +169,74 @@ struct CLIRunnerTests {
66
169
  #expect(result.stderr.first == "e")
67
170
  }
68
171
 
172
+ @Test("command spawn inherits only its standard streams, never another capture pipe")
173
+ func commandSpawnDoesNotInheritUnrelatedPipe() throws {
174
+ // Keep an unrelated pipe deliberately not close-on-exec. This is the exact
175
+ // state another reader can expose between pipe() and its FD_CLOEXEC setup.
176
+ var unrelated: [Int32] = [0, 0]
177
+ try #require(Darwin.pipe(&unrelated) == 0)
178
+ defer { for descriptor in unrelated { Darwin.close(descriptor) } }
179
+ // Do not turn a previously closed standard stream into this probe pipe.
180
+ for index in unrelated.indices where unrelated[index] < 3 {
181
+ let moved = Darwin.fcntl(unrelated[index], F_DUPFD, 3)
182
+ try #require(moved >= 3)
183
+ Darwin.close(unrelated[index])
184
+ unrelated[index] = moved
185
+ }
186
+ try #require(Darwin.fcntl(unrelated[0], F_GETFD) & FD_CLOEXEC == 0)
187
+
188
+ let home = URL(fileURLWithPath: makeIsolatedTestHome("spawn-inheritance"), isDirectory: true)
189
+ defer { try? FileManager.default.removeItem(at: home) }
190
+ let source = home.appendingPathComponent("probe.c")
191
+ let executable = home.appendingPathComponent("probe")
192
+ try """
193
+ #include <sys/stat.h>
194
+ #include <unistd.h>
195
+ #include <errno.h>
196
+ #include <fcntl.h>
197
+ #include <stdio.h>
198
+ #include <stdlib.h>
199
+ int main(int argc, char **argv) {
200
+ if (argc != 5) return 64;
201
+ struct stat input;
202
+ int inputResult = fstat(STDIN_FILENO, &input);
203
+ if (atoi(argv[2])) {
204
+ if (inputResult != 0 ||
205
+ (unsigned long long)input.st_dev != strtoull(argv[3], 0, 10) ||
206
+ (unsigned long long)input.st_ino != strtoull(argv[4], 0, 10)) return 66;
207
+ } else if (inputResult == 0) return 67;
208
+ fputs("stdout-preserved", stdout);
209
+ fputs("stderr-preserved", stderr);
210
+ errno = 0;
211
+ if (fcntl(atoi(argv[1]), F_GETFD) != -1 || errno != EBADF) return 65;
212
+ return 0;
213
+ }
214
+ """.write(to: source, atomically: true, encoding: .utf8)
215
+ let compiled = try CLIRunner.runExecutable(
216
+ "/usr/bin/cc", arguments: ["-o", executable.path, source.path],
217
+ environment: ["TMPDIR": home.path]
218
+ )
219
+ try #require(compiled.terminationStatus == 0)
220
+
221
+ // Inspect only stdin metadata; never read or print the caller's input.
222
+ var input = stat()
223
+ let inputFlags = Darwin.fcntl(STDIN_FILENO, F_GETFD)
224
+ let inheritsInput = inputFlags >= 0 && inputFlags & FD_CLOEXEC == 0
225
+ && !unrelated.contains(STDIN_FILENO)
226
+ if inheritsInput { try #require(Darwin.fstat(STDIN_FILENO, &input) == 0) }
227
+ let result = try CLIRunner.runExecutable(
228
+ executable.path,
229
+ arguments: [
230
+ String(unrelated[0]), inheritsInput ? "1" : "0",
231
+ String(UInt64(bitPattern: Int64(input.st_dev))), String(input.st_ino),
232
+ ],
233
+ environment: [:]
234
+ )
235
+ #expect(result.terminationStatus == 0)
236
+ #expect(result.stdout == "stdout-preserved")
237
+ #expect(result.stderr == "stderr-preserved")
238
+ }
239
+
69
240
  @Test("process runner observes immediate exits without false timeouts")
70
241
  func observesImmediateExits() throws {
71
242
  for _ in 0..<25 {
@@ -336,8 +507,7 @@ struct CLIRunnerTests {
336
507
  @Test("a hung rewrite helper returns within the 10 s interactive budget, cleanup included")
337
508
  @MainActor
338
509
  func hungRewriteHelperReturnsWithinInteractiveBudget() async throws {
339
- let home = FileManager.default.temporaryDirectory
340
- .appendingPathComponent("recordings-rewrite-budget-\(UUID().uuidString)")
510
+ let home = URL(fileURLWithPath: makeIsolatedTestHome("rewrite-budget"), isDirectory: true)
341
511
  let bin = home.appendingPathComponent(".bun/bin")
342
512
  let pidFile = home.appendingPathComponent("pid")
343
513
  try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
@@ -357,18 +527,27 @@ struct CLIRunnerTests {
357
527
  }
358
528
 
359
529
  // The production seam exactly as runCommandMode uses it: the engine's default
360
- // commandCLI closure on a detached task, budgeted by commandRewriteTimeout — the
530
+ // commandCLI closure on the blocking queue, budgeted by commandRewriteTimeout — the
361
531
  // observable wall time must stay inside the budget even though the helper never
362
532
  // exits on its own and ignores SIGTERM.
363
- let runCLI = RecordingEngine(homePath: home.path).commandCLI
533
+ let runCLI = RecordingEngine(homePath: home.path, installsGlobalHandlers: false).commandCLI
364
534
  let homePath = home.path
365
535
  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
536
+ let operation = RecordingEngine.makeCommandRewriteOperation(
537
+ args: ["rewrite-selection"], home: homePath, runCLI: runCLI
538
+ )
539
+ let (output, workerStartedAt, workerFinishedAt) = await BlockingOperation.run {
540
+ let workerStartedAt = ContinuousClock.now
541
+ let output = operation()
542
+ return (output, workerStartedAt, ContinuousClock.now)
543
+ }
544
+ let resumedAt = ContinuousClock.now
545
+ let elapsed = resumedAt - startedAt
546
+ // Report only on a failed elapsed assertion. Admission includes the executor
547
+ // and dispatch queue hops; the worker interval includes CLI preparation/cleanup.
548
+ let timing: Comment = "CLI phases: admission=\(workerStartedAt - startedAt), worker=\(workerFinishedAt - workerStartedAt), resume=\(resumedAt - workerFinishedAt)"
370
549
 
371
- #expect(elapsed < .seconds(RecordingEngine.commandRewriteTimeout))
550
+ #expect(elapsed < .seconds(RecordingEngine.commandRewriteTimeout), timing)
372
551
  #expect(output.hasPrefix("ERROR:"))
373
552
  #expect(output.contains("timed out"))
374
553
  }
@@ -376,8 +555,7 @@ struct CLIRunnerTests {
376
555
  @Test("an exhausted rewrite deadline with pipes held by an escaped descendant returns under the public ceiling")
377
556
  @MainActor
378
557
  func exhaustedRewriteDeadlineWithHeldPipesReturnsUnderCeiling() async throws {
379
- let home = FileManager.default.temporaryDirectory
380
- .appendingPathComponent("recordings-rewrite-exhaustion-\(UUID().uuidString)")
558
+ let home = URL(fileURLWithPath: makeIsolatedTestHome("rewrite-exhaustion"), isDirectory: true)
381
559
  let bin = home.appendingPathComponent(".bun/bin")
382
560
  let leaderPidFile = home.appendingPathComponent("leader-pid")
383
561
  let holderPidFile = home.appendingPathComponent("holder-pid")
@@ -454,19 +632,28 @@ struct CLIRunnerTests {
454
632
  // also runs to exhaustion. The observable wall time must still land under the
455
633
  // public ceiling — the return margin absorbs spawn, poll overshoot, capture
456
634
  // shutdown, and the task hops.
457
- let runCLI = RecordingEngine(homePath: home.path).commandCLI
635
+ let runCLI = RecordingEngine(homePath: home.path, installsGlobalHandlers: false).commandCLI
458
636
  let homePath = home.path
459
637
  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
638
+ let operation = RecordingEngine.makeCommandRewriteOperation(
639
+ args: ["rewrite-selection"], home: homePath, runCLI: runCLI
640
+ )
641
+ let (output, workerStartedAt, workerFinishedAt) = await BlockingOperation.run {
642
+ let workerStartedAt = ContinuousClock.now
643
+ let output = operation()
644
+ return (output, workerStartedAt, ContinuousClock.now)
645
+ }
646
+ let resumedAt = ContinuousClock.now
647
+ let elapsed = resumedAt - startedAt
648
+ // Report only on a failed elapsed assertion. Admission includes the executor
649
+ // and dispatch queue hops; the worker interval includes CLI preparation/cleanup.
650
+ let timing: Comment = "CLI phases: admission=\(workerStartedAt - startedAt), worker=\(workerFinishedAt - workerStartedAt), resume=\(resumedAt - workerFinishedAt)"
464
651
 
465
652
  // Upper bound is the public promise, with the ~1 s return margin left as CI
466
653
  // tolerance above the internal deadline; the lower bound proves the deadline chain
467
654
  // really ran to exhaustion instead of the helper exiting early.
468
- #expect(elapsed < .seconds(RecordingEngine.commandRewriteTimeout))
469
- #expect(elapsed > .seconds(8.4))
655
+ #expect(elapsed < .seconds(RecordingEngine.commandRewriteTimeout), timing)
656
+ #expect(elapsed > .seconds(8.4), timing)
470
657
  #expect(output.hasPrefix("ERROR:"))
471
658
  #expect(output.contains("timed out"))
472
659
 
@@ -878,12 +1065,27 @@ struct CLIRunnerTests {
878
1065
  #expect(injectedFailureCount > 0)
879
1066
  }
880
1067
 
881
- @Test("a reaper failure still joins capture readers and closes silent escaped pipes")
1068
+ @Test("a warmed reaper-failure fixture still joins capture readers and closes silent escaped pipes")
882
1069
  func reapFailureStillFinishesCaptures() throws {
883
1070
  struct InjectedReapFailure: Error {}
884
1071
  let failureGate = NSLock()
885
1072
  var shouldFailCapture = false
886
1073
  var injectedFailures = 0
1074
+ var closeObservations: [String] = []
1075
+ let liveCaptureCalls = CLIRunner.PipeCaptureReader.SystemCalls.live
1076
+ var ownedReadDescriptors: Set<Int32> = []
1077
+ var successfulReadCloses: [Int32: UInt64] = [:]
1078
+ var returnedAt: UInt64 = 0
1079
+ var phaseTimes: [String: UInt64] = [:]
1080
+ var pipeTimes: [(read: Int32, write: Int32, at: UInt64)] = []
1081
+ var closeTimes: [(descriptor: Int32, result: Int32, at: UInt64)] = []
1082
+ var firstInjectedReadTimes: [Int32: UInt64] = [:]
1083
+ func recordPhase(_ name: String) {
1084
+ let at = PipeClosureFixture.now()
1085
+ failureGate.lock()
1086
+ phaseTimes[name] = at
1087
+ failureGate.unlock()
1088
+ }
887
1089
 
888
1090
  let root = FileManager.default.temporaryDirectory
889
1091
  .appendingPathComponent("recordings-reap-failure-\(UUID().uuidString)")
@@ -891,6 +1093,10 @@ struct CLIRunnerTests {
891
1093
  let executable = root.appendingPathComponent("silent-holder")
892
1094
  let holderPidFile = root.appendingPathComponent("holder-pid")
893
1095
  let markerFile = root.appendingPathComponent("holder-probe")
1096
+ let detailsFile = root.appendingPathComponent("holder-probe-details")
1097
+ let deadlineFile = root.appendingPathComponent("holder-probe-deadline")
1098
+ let leaderTimingFile = root.appendingPathComponent("leader-timing")
1099
+ let holderTimingFile = root.appendingPathComponent("holder-timing")
894
1100
  try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
895
1101
  defer {
896
1102
  if let holderPid = Self.readPID(from: holderPidFile) {
@@ -899,27 +1105,75 @@ struct CLIRunnerTests {
899
1105
  try? FileManager.default.removeItem(at: root)
900
1106
  }
901
1107
 
1108
+ // On failure, identify only descriptors that share these two fixture pipes.
1109
+ // Bounded numeric diagnostics contain no process names, file paths or contents.
902
1110
  try """
903
1111
  #include <errno.h>
1112
+ #include <libproc.h>
1113
+ #include <sys/proc_info.h>
904
1114
  #include <signal.h>
905
1115
  #include <stdio.h>
906
1116
  #include <stdlib.h>
1117
+ #include <string.h>
907
1118
  #include <sys/wait.h>
908
1119
  #include <time.h>
909
1120
  #include <unistd.h>
910
1121
 
1122
+ \(PipeClosureFixture.observerSource)
1123
+
911
1124
  static volatile sig_atomic_t probeRequested = 0;
912
1125
  static void requestProbe(int signo) { (void)signo; probeRequested = 1; }
913
1126
 
1127
+ static void recordFixtureTiming(const char *path, uint64_t entered, uint64_t forked, uint64_t milestone) {
1128
+ FILE *timing = fopen(path, "w");
1129
+ if (!timing) _exit(69);
1130
+ fprintf(timing, "%llu %llu %llu", (unsigned long long)entered,
1131
+ (unsigned long long)forked, (unsigned long long)milestone);
1132
+ if (fclose(timing)) _exit(69);
1133
+ }
1134
+
1135
+ static void reportPipeOwners(FILE *file, int stream) {
1136
+ struct pipe_fdinfo target = {0};
1137
+ int inspected = proc_pidfdinfo(getpid(), stream, PROC_PIDFDPIPEINFO, &target, sizeof(target));
1138
+ fprintf(file, "; target fd=%d result=%d handle=%llx peer=%llx", stream, inspected,
1139
+ (unsigned long long)target.pipeinfo.pipe_handle, (unsigned long long)target.pipeinfo.pipe_peerhandle);
1140
+ if (inspected != sizeof(target)) return;
1141
+ pid_t pids[4096];
1142
+ int pidBytes = proc_listpids(PROC_UID_ONLY, getuid(), pids, sizeof(pids));
1143
+ if (pidBytes > (int)sizeof(pids)) pidBytes = (int)sizeof(pids);
1144
+ int matches = 0;
1145
+ for (int i = 0; i < pidBytes / (int)sizeof(pid_t) && matches < 64; i++) {
1146
+ struct proc_fdinfo descriptors[2048];
1147
+ int fdBytes = proc_pidinfo(pids[i], PROC_PIDLISTFDS, 0, descriptors, sizeof(descriptors));
1148
+ if (fdBytes > (int)sizeof(descriptors)) fdBytes = (int)sizeof(descriptors);
1149
+ for (int j = 0; j < fdBytes / (int)sizeof(struct proc_fdinfo) && matches < 64; j++) {
1150
+ if (descriptors[j].proc_fdtype != PROX_FDTYPE_PIPE) continue;
1151
+ struct pipe_fdinfo candidate = {0};
1152
+ if (proc_pidfdinfo(pids[i], descriptors[j].proc_fd, PROC_PIDFDPIPEINFO, &candidate, sizeof(candidate)) != sizeof(candidate)) continue;
1153
+ if ((target.pipeinfo.pipe_handle && candidate.pipeinfo.pipe_handle == target.pipeinfo.pipe_handle) ||
1154
+ (target.pipeinfo.pipe_peerhandle && candidate.pipeinfo.pipe_handle == target.pipeinfo.pipe_peerhandle)) {
1155
+ fprintf(file, "; owner pid=%d fd=%d flags=%u status=%u handle=%llx peer=%llx", pids[i], descriptors[j].proc_fd,
1156
+ candidate.pfi.fi_openflags, candidate.pfi.fi_status,
1157
+ (unsigned long long)candidate.pipeinfo.pipe_handle, (unsigned long long)candidate.pipeinfo.pipe_peerhandle);
1158
+ matches++;
1159
+ }
1160
+ }
1161
+ }
1162
+ }
1163
+
914
1164
  int main(int argc, char **argv) {
915
- if (argc < 3) return 64;
1165
+ uint64_t enteredAt = monotonic_ns();
1166
+ if (argc == 2 && strcmp(argv[1], "--fixture-preflight") == 0) return 0;
1167
+ if (argc < 7) return 64;
916
1168
  pid_t child = fork();
1169
+ uint64_t forkedAt = monotonic_ns();
917
1170
  if (child == -1) return 65;
918
1171
  if (child != 0) {
919
1172
  while (access(argv[1], F_OK) == -1) {
920
1173
  struct timespec delay = {0, 10000000};
921
1174
  nanosleep(&delay, 0);
922
1175
  }
1176
+ recordFixtureTiming(argv[5], enteredAt, forkedAt, monotonic_ns());
923
1177
  return 0;
924
1178
  }
925
1179
  if (setsid() == -1) _exit(66);
@@ -929,17 +1183,13 @@ struct CLIRunnerTests {
929
1183
  if (!pid) _exit(67);
930
1184
  fprintf(pid, "%d", (int)getpid());
931
1185
  fclose(pid);
1186
+ uint64_t readyClosedAt = monotonic_ns();
1187
+ recordFixtureTiming(argv[6], enteredAt, forkedAt, readyClosedAt);
932
1188
  while (!probeRequested) {
933
1189
  struct timespec delay = {0, 20000000};
934
1190
  nanosleep(&delay, 0);
935
1191
  }
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);
1192
+ _exit(observePipeClosure(argv[4], argv[2], argv[3], NULL, reportPipeOwners));
943
1193
  }
944
1194
  """.write(to: source, atomically: true, encoding: .utf8)
945
1195
  let compile = Process()
@@ -949,30 +1199,113 @@ struct CLIRunnerTests {
949
1199
  compile.waitUntilExit()
950
1200
  try #require(compile.terminationStatus == 0)
951
1201
 
1202
+ // This nil-total-budget fixture tests warmed reaper-error lifecycle cleanup,
1203
+ // not a native cold-start SLA. A fresh helper has spent >2s before main while
1204
+ // cleanup stayed <0.61s; that cold-start cause remains unresolved. Prepare only
1205
+ // this exact helper once, with a separate finite budget and a retained receipt.
1206
+ // The other full-budget tests still execute their helpers without preflight.
1207
+ let fixtureBytes = try Data(contentsOf: executable)
1208
+ let preflightBudget = RecordingEngine.commandRewriteTimeout
1209
+ let digest = try CLIRunner.runExecutable(
1210
+ "/usr/bin/shasum", arguments: ["-a", "256", executable.path],
1211
+ executionTimeout: preflightBudget, totalWallClockBudget: preflightBudget
1212
+ )
1213
+ try #require(digest.terminationStatus == 0 && digest.stderr.isEmpty)
1214
+ let fixtureSHA256 = try #require(digest.stdout.split(whereSeparator: \.isWhitespace).first).description
1215
+ try #require(fixtureSHA256.count == 64 && fixtureSHA256.allSatisfy(\.isHexDigit))
1216
+ let preflightStartedAt = PipeClosureFixture.now()
1217
+ let preflight: CLIRunner.ProcessOutput
1218
+ do {
1219
+ preflight = try CLIRunner.runExecutable(
1220
+ executable.path, arguments: ["--fixture-preflight"],
1221
+ executionTimeout: preflightBudget, totalWallClockBudget: preflightBudget
1222
+ )
1223
+ } catch {
1224
+ print("fixture_preflight sha256=\(fixtureSHA256) elapsed_ns=\(PipeClosureFixture.now() - preflightStartedAt) budget_s=\(preflightBudget) threw=1")
1225
+ throw error
1226
+ }
1227
+ let preflightElapsed = PipeClosureFixture.now() - preflightStartedAt
1228
+ let unchangedAfterPreflight = try Data(contentsOf: executable) == fixtureBytes
1229
+ print("fixture_preflight sha256=\(fixtureSHA256) elapsed_ns=\(preflightElapsed) budget_s=\(preflightBudget) status=\(preflight.terminationStatus) stdout_bytes=\(preflight.stdout.utf8.count) stderr_bytes=\(preflight.stderr.utf8.count) unchanged=\(unchangedAfterPreflight)")
1230
+ try #require(preflightElapsed < UInt64(preflightBudget * 1_000_000_000))
1231
+ try #require(preflight.terminationStatus == 0 && preflight.stdout.isEmpty && preflight.stderr.isEmpty)
1232
+ try #require(unchangedAfterPreflight)
1233
+ try #require(!FileManager.default.fileExists(atPath: holderPidFile.path))
1234
+
1235
+ let timingOrigin = PipeClosureFixture.now()
952
1236
  let startedAt = ContinuousClock.now
953
1237
  #expect(throws: InjectedReapFailure.self) {
1238
+ recordPhase("runner_enter")
1239
+ defer {
1240
+ returnedAt = PipeClosureFixture.now()
1241
+ failureGate.lock()
1242
+ phaseTimes["runner_return"] = returnedAt
1243
+ failureGate.unlock()
1244
+ }
954
1245
  _ = try CLIRunner.runExecutable(
955
1246
  executable.path,
956
- arguments: [holderPidFile.path, markerFile.path],
1247
+ arguments: [holderPidFile.path, markerFile.path, detailsFile.path, deadlineFile.path,
1248
+ leaderTimingFile.path, holderTimingFile.path],
957
1249
  pipeDrainTimeout: 0.1,
1250
+ beforeExecutionDeadline: { recordPhase("setup_spawn_complete") },
1251
+ lifecycleObserver: { event in
1252
+ switch event {
1253
+ case .leaderExitObserved: recordPhase("leader_exit_observed")
1254
+ case let .processGroupSignaled(signal): recordPhase("signal_\(signal)_requested")
1255
+ case .leaderReaped: recordPhase("leader_reaped")
1256
+ }
1257
+ },
958
1258
  leaderReaper: { processIdentifier, _ in
1259
+ recordPhase("reaper_enter")
959
1260
  failureGate.lock()
960
1261
  shouldFailCapture = true
961
1262
  failureGate.unlock()
962
1263
  var status: Int32 = 0
963
1264
  var result: pid_t
1265
+ recordPhase("waitpid_enter")
964
1266
  repeat {
965
1267
  result = Darwin.waitpid(processIdentifier, &status, 0)
966
1268
  } while result == -1 && errno == EINTR
1269
+ let reapError = errno
1270
+ recordPhase("waitpid_return")
967
1271
  guard result == processIdentifier else {
968
- throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
1272
+ throw NSError(domain: NSPOSIXErrorDomain, code: Int(reapError))
969
1273
  }
1274
+ recordPhase("reaper_throw")
970
1275
  throw InjectedReapFailure()
971
1276
  },
972
- captureSystemCalls: .init(read: { descriptor, buffer, count in
1277
+ captureSystemCalls: .init(makePipe: { operation in
1278
+ let pipe = try liveCaptureCalls.makePipe(operation)
1279
+ let createdAt = PipeClosureFixture.now()
1280
+ failureGate.lock()
1281
+ pipeTimes.append((pipe.read, pipe.write, createdAt))
1282
+ if operation == "create capture pipe" {
1283
+ ownedReadDescriptors.insert(pipe.read)
1284
+ }
1285
+ failureGate.unlock()
1286
+ return pipe
1287
+ }, close: { descriptor in
1288
+ let result = Darwin.close(descriptor)
1289
+ let closeError = errno
1290
+ let closedAt = PipeClosureFixture.now()
1291
+ failureGate.lock()
1292
+ closeTimes.append((descriptor, result, closedAt))
1293
+ closeObservations.append("fd=\(descriptor),result=\(result),errno=\(closeError)")
1294
+ if result == 0 && ownedReadDescriptors.contains(descriptor) {
1295
+ successfulReadCloses[descriptor] = closedAt
1296
+ }
1297
+ failureGate.unlock()
1298
+ errno = closeError
1299
+ return result
1300
+ }, read: { descriptor, buffer, count in
973
1301
  failureGate.lock()
974
1302
  let shouldFail = shouldFailCapture
975
- if shouldFail { injectedFailures += 1 }
1303
+ if shouldFail {
1304
+ injectedFailures += 1
1305
+ if firstInjectedReadTimes[descriptor] == nil {
1306
+ firstInjectedReadTimes[descriptor] = PipeClosureFixture.now()
1307
+ }
1308
+ }
976
1309
  failureGate.unlock()
977
1310
  if shouldFail {
978
1311
  errno = EIO
@@ -982,25 +1315,58 @@ struct CLIRunnerTests {
982
1315
  })
983
1316
  )
984
1317
  }
985
- #expect(ContinuousClock.now - startedAt < .seconds(2))
1318
+ let elapsed = ContinuousClock.now - startedAt
1319
+ recordPhase("assertion_return")
986
1320
  failureGate.lock()
987
1321
  let injectedFailureCount = injectedFailures
1322
+ let ownedReads = ownedReadDescriptors
1323
+ let closedReads = successfulReadCloses
1324
+ let phaseSnapshot = phaseTimes
1325
+ let pipeSnapshot = pipeTimes
1326
+ let closeTimingSnapshot = closeTimes
1327
+ let readTimingSnapshot = firstInjectedReadTimes
988
1328
  failureGate.unlock()
1329
+ // Emit bounded numeric diagnostics only when the unchanged runner ceiling
1330
+ // fails. Formatting is outside the measured interval; there is no per-event I/O.
1331
+ let phases = phaseSnapshot.sorted { $0.value < $1.value }
1332
+ .map { "\($0.key)=\($0.value - timingOrigin)" }.joined(separator: ",")
1333
+ let pipes = pipeSnapshot.map { "\($0.read)/\($0.write)@\($0.at - timingOrigin)" }.joined(separator: ",")
1334
+ let closes = closeTimingSnapshot.map { "\($0.descriptor):\($0.result)@\($0.at - timingOrigin)" }.joined(separator: ",")
1335
+ let reads = readTimingSnapshot.sorted { $0.key < $1.key }
1336
+ .map { "\($0.key)@\($0.value - timingOrigin)" }.joined(separator: ",")
1337
+ func fixtureTimes(_ file: URL) -> String {
1338
+ guard let text = try? String(contentsOf: file, encoding: .utf8) else { return "unavailable" }
1339
+ let values = text.split(whereSeparator: \.isWhitespace).compactMap { UInt64($0) }
1340
+ guard values.count == 3, values.allSatisfy({ $0 >= timingOrigin }) else { return "invalid" }
1341
+ return values.map { String($0 - timingOrigin) }.joined(separator: ",")
1342
+ }
1343
+ // The leader's final tick precedes only its small timing-sidecar write and return;
1344
+ // it is a userspace pre-exit marker, not a timestamp of the kernel's exit event.
1345
+ let fixtureTiming = elapsed >= .seconds(2)
1346
+ ? "leader(main,fork,pre_exit)_ns=\(fixtureTimes(leaderTimingFile)); holder(main,fork,ready_closed)_ns=\(fixtureTimes(holderTimingFile))"
1347
+ : ""
1348
+ let timing: Comment = "runner phases_ns: \(phases); pipes(read/write@ns): \(pipes); closes(fd:result@ns): \(closes); injected_reads(fd@ns): \(reads); \(fixtureTiming)"
1349
+ #expect(elapsed < .seconds(2), timing)
1350
+ let elapsedSeconds = Double(elapsed.components.seconds) + Double(elapsed.components.attoseconds) / 1e18
1351
+ print("fixture_lifecycle sha256=\(fixtureSHA256) elapsed_s=\(elapsedSeconds) budget_s=2")
1352
+ #expect(try Data(contentsOf: executable) == fixtureBytes)
989
1353
  #expect(injectedFailureCount > 0)
1354
+ // Runner-owned closes must already have completed at return. Other concurrent
1355
+ // spawns may retain copied read endpoints until exec despite CLOEXEC_DEFAULT.
1356
+ #expect(ownedReads.count == 2)
1357
+ #expect(ownedReads.allSatisfy { closedReads[$0].map { $0 <= returnedAt } == true })
990
1358
 
991
1359
  let holderPid = try #require(Self.readPID(from: holderPidFile))
992
1360
  #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")
1361
+ let markerDeadline = try PipeClosureFixture.request(pid: holderPid, deadlineFile: deadlineFile)
1362
+ let marker = PipeClosureFixture.waitForMarker(at: markerFile, deadline: markerDeadline)
1363
+ let writeObservations = (try? String(contentsOf: detailsFile, encoding: .utf8)) ?? "unavailable"
1364
+ failureGate.lock()
1365
+ let closeSnapshot = closeObservations
1366
+ failureGate.unlock()
1367
+ // Numeric fixture diagnostics only; close errno is meaningful only on failure.
1368
+ let diagnostics: Comment = "pipe writes: \(writeObservations); closes: \(closeSnapshot)"
1369
+ #expect(marker == "both-epipe", diagnostics)
1004
1370
  expectProcessIsGone(holderPid)
1005
1371
  }
1006
1372
 
@@ -1032,12 +1398,12 @@ struct CLIRunnerTests {
1032
1398
  @Test("a completely silent escaped descendant cannot pin the capture readers past the ceiling")
1033
1399
  @MainActor
1034
1400
  func silentEscapedDescendantCannotPinCaptureReaders() async throws {
1035
- let home = FileManager.default.temporaryDirectory
1036
- .appendingPathComponent("recordings-rewrite-silent-holder-\(UUID().uuidString)")
1401
+ let home = URL(fileURLWithPath: makeIsolatedTestHome("rewrite-silent-holder"), isDirectory: true)
1037
1402
  let bin = home.appendingPathComponent(".bun/bin")
1038
1403
  let leaderPidFile = home.appendingPathComponent("leader-pid")
1039
1404
  let holderPidFile = home.appendingPathComponent("holder-pid")
1040
1405
  let markerFile = home.appendingPathComponent("holder-probe")
1406
+ let deadlineFile = home.appendingPathComponent("holder-probe-deadline")
1041
1407
  let holderSource = home.appendingPathComponent("silent-holder.c")
1042
1408
  let holderBinary = home.appendingPathComponent("silent-holder")
1043
1409
  try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
@@ -1056,9 +1422,9 @@ struct CLIRunnerTests {
1056
1422
  // runner is alive: a reader blocked in read(2) would never be handed the data it
1057
1423
  // needs to run onto a revoked descriptor, which is exactly the shape that pinned
1058
1424
  // 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.
1425
+ // test send SIGUSR1. Both streams must reach EPIPE within one closure deadline;
1426
+ // a concurrent spawn may temporarily retain copied read endpoints before exec.
1427
+ // The injectable reaper test separately checks runner-owned closure at return.
1062
1428
  try """
1063
1429
  #include <errno.h>
1064
1430
  #include <signal.h>
@@ -1066,11 +1432,13 @@ struct CLIRunnerTests {
1066
1432
  #include <time.h>
1067
1433
  #include <unistd.h>
1068
1434
 
1435
+ \(PipeClosureFixture.observerSource)
1436
+
1069
1437
  static volatile sig_atomic_t probeRequested = 0;
1070
1438
  static void requestProbe(int signo) { (void)signo; probeRequested = 1; }
1071
1439
 
1072
1440
  int main(int argc, char **argv) {
1073
- if (argc < 3) return 64;
1441
+ if (argc < 4) return 64;
1074
1442
  if (setsid() == -1) return 65;
1075
1443
  signal(SIGPIPE, SIG_IGN);
1076
1444
  signal(SIGUSR1, requestProbe);
@@ -1082,13 +1450,7 @@ struct CLIRunnerTests {
1082
1450
  struct timespec delay = {0, 20000000};
1083
1451
  nanosleep(&delay, 0);
1084
1452
  }
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;
1453
+ return observePipeClosure(argv[3], argv[2], NULL, NULL, NULL);
1092
1454
  }
1093
1455
  """.write(to: holderSource, atomically: true, encoding: .utf8)
1094
1456
  let compile = Process()
@@ -1103,7 +1465,7 @@ struct CLIRunnerTests {
1103
1465
  #!/bin/sh
1104
1466
  trap '' TERM
1105
1467
  printf '%s' "$$" > '\(leaderPidFile.path)'
1106
- '\(holderBinary.path)' '\(holderPidFile.path)' '\(markerFile.path)' &
1468
+ '\(holderBinary.path)' '\(holderPidFile.path)' '\(markerFile.path)' '\(deadlineFile.path)' &
1107
1469
  while [ ! -s '\(holderPidFile.path)' ]; do /bin/sleep 0.01; done
1108
1470
  while :; do /bin/sleep 0.05; done
1109
1471
  """.write(to: executable, atomically: true, encoding: .utf8)
@@ -1113,16 +1475,25 @@ struct CLIRunnerTests {
1113
1475
  // SIGTERM and the silent holder keeps both pipes open, so every deadline in the
1114
1476
  // chain — execution window, termination grace, drain wait — runs to exhaustion
1115
1477
  // with zero bytes ever arriving to wake a reader.
1116
- let runCLI = RecordingEngine(homePath: home.path).commandCLI
1478
+ let runCLI = RecordingEngine(homePath: home.path, installsGlobalHandlers: false).commandCLI
1117
1479
  let homePath = home.path
1118
1480
  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))
1481
+ let operation = RecordingEngine.makeCommandRewriteOperation(
1482
+ args: ["rewrite-selection"], home: homePath, runCLI: runCLI
1483
+ )
1484
+ let (output, workerStartedAt, workerFinishedAt) = await BlockingOperation.run {
1485
+ let workerStartedAt = ContinuousClock.now
1486
+ let output = operation()
1487
+ return (output, workerStartedAt, ContinuousClock.now)
1488
+ }
1489
+ let resumedAt = ContinuousClock.now
1490
+ let elapsed = resumedAt - startedAt
1491
+ // Report only on a failed elapsed assertion. Admission includes the executor
1492
+ // and dispatch queue hops; the worker interval includes CLI preparation/cleanup.
1493
+ let timing: Comment = "CLI phases: admission=\(workerStartedAt - startedAt), worker=\(workerFinishedAt - workerStartedAt), resume=\(resumedAt - workerFinishedAt)"
1494
+
1495
+ #expect(elapsed < .seconds(RecordingEngine.commandRewriteTimeout), timing)
1496
+ #expect(elapsed > .seconds(8.4), timing)
1126
1497
  #expect(output.hasPrefix("ERROR:"))
1127
1498
  #expect(output.contains("timed out"))
1128
1499
 
@@ -1136,14 +1507,12 @@ struct CLIRunnerTests {
1136
1507
  #expect(Darwin.kill(holderPid, 0) == 0)
1137
1508
  #expect(!FileManager.default.fileExists(atPath: markerFile.path))
1138
1509
 
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)
1510
+ // Ask only after the real command path returns, with no writer traffic having
1511
+ // helped the readers finish. Temporary pre-exec copies share the same 3s bound.
1512
+ let markerDeadline = try PipeClosureFixture.request(pid: holderPid, deadlineFile: deadlineFile)
1143
1513
  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 {
1514
+ while PipeClosureFixture.now() < markerDeadline {
1515
+ if let contents = PipeClosureFixture.marker(at: markerFile), PipeClosureFixture.now() < markerDeadline {
1147
1516
  marker = contents
1148
1517
  break
1149
1518
  }
@@ -1275,6 +1644,12 @@ struct CLIRunnerTests {
1275
1644
  #expect(CLIRunner.parseError(input) == "OpenAI quota exceeded — check the OpenAI account billing")
1276
1645
  }
1277
1646
 
1647
+ @Test("Service authentication and quota errors identify the Recordings API")
1648
+ func serviceErrors() {
1649
+ #expect(CLIRunner.parseError("ERROR: 401 Unauthorized", serviceAPI: true) == "Recordings API authentication failed — check the API connection in Settings")
1650
+ #expect(CLIRunner.parseError("ERROR: 429 Too Many Requests", serviceAPI: true) == "Recordings API request limit reached — try again later")
1651
+ }
1652
+
1278
1653
  @Test("parseError truncates long messages to 120 chars")
1279
1654
  func truncation() {
1280
1655
  let longMsg = String(repeating: "a", count: 200)
@@ -1297,9 +1672,12 @@ struct CLIRunnerTests {
1297
1672
  #expect(result?.contains(token) == false)
1298
1673
  #expect(result?.contains("[REDACTED]") == true)
1299
1674
 
1675
+ let plainFixture = "plain-synthetic-secret"
1676
+ let clientFixture = "client-synthetic-secret"
1677
+ let passwordFixture = "password-synthetic-secret"
1300
1678
  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\","
1679
+ "ERROR: OPENAI_API_KEY=\(plainFixture) CLIENT_SECRET=\(clientFixture) "
1680
+ + "PASSWORD=\(passwordFixture) payload={\"api_key\":\"json-synthetic-secret\","
1303
1681
  + "\"private_key\":\"private-synthetic-secret\"}"
1304
1682
  )
1305
1683
  #expect(structured?.contains("plain-synthetic-secret") == false)