@hasna/recordings 0.2.14 → 0.3.2

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 (123) hide show
  1. package/Dockerfile.package +4 -5
  2. package/README.md +113 -8
  3. package/bun.lock +2 -2
  4. package/dist/__tests__/helpers/installer-guard-execution.d.ts +22 -0
  5. package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -0
  6. package/dist/__tests__/helpers/installer-preflight.d.ts +22 -0
  7. package/dist/__tests__/helpers/installer-preflight.d.ts.map +1 -0
  8. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -1
  9. package/dist/__tests__/helpers/source-assertions.d.ts +169 -0
  10. package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -0
  11. package/dist/__tests__/preload.d.ts +1 -4
  12. package/dist/__tests__/preload.d.ts.map +1 -1
  13. package/dist/cli/desktop-snapshot.d.ts +20 -0
  14. package/dist/cli/desktop-snapshot.d.ts.map +1 -0
  15. package/dist/cli/index.js +2094 -280
  16. package/dist/cli/macos-permissions.d.ts +72 -0
  17. package/dist/cli/macos-permissions.d.ts.map +1 -1
  18. package/dist/cli/macos-shortcut.d.ts +139 -0
  19. package/dist/cli/macos-shortcut.d.ts.map +1 -0
  20. package/dist/cli/trigger-probe.d.ts +172 -0
  21. package/dist/cli/trigger-probe.d.ts.map +1 -0
  22. package/dist/db/database.d.ts +9 -0
  23. package/dist/db/database.d.ts.map +1 -1
  24. package/dist/http/client.d.ts +19 -7
  25. package/dist/http/client.d.ts.map +1 -1
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +635 -50
  29. package/dist/lib/capture-probe.d.ts +216 -0
  30. package/dist/lib/capture-probe.d.ts.map +1 -0
  31. package/dist/lib/config.d.ts.map +1 -1
  32. package/dist/lib/macos-bundle.d.ts +50 -0
  33. package/dist/lib/macos-bundle.d.ts.map +1 -0
  34. package/dist/lib/persistence-probe.d.ts +208 -0
  35. package/dist/lib/persistence-probe.d.ts.map +1 -0
  36. package/dist/lib/recorder.d.ts.map +1 -1
  37. package/dist/lib/retired-deployment-modes.d.ts +24 -0
  38. package/dist/lib/retired-deployment-modes.d.ts.map +1 -0
  39. package/dist/lib/transcriber.d.ts +60 -0
  40. package/dist/lib/transcriber.d.ts.map +1 -1
  41. package/dist/mcp/index.js +141 -57
  42. package/dist/server/cloud-config.d.ts +25 -3
  43. package/dist/server/cloud-config.d.ts.map +1 -1
  44. package/dist/server/cloud-readiness.d.ts.map +1 -1
  45. package/dist/server/cloud.d.ts +8 -7
  46. package/dist/server/cloud.d.ts.map +1 -1
  47. package/dist/server/index.js +188 -80
  48. package/dist/server/migrate-command.d.ts.map +1 -1
  49. package/dist/server/serve.d.ts.map +1 -1
  50. package/dist/storage.d.ts +2 -2
  51. package/dist/storage.d.ts.map +1 -1
  52. package/dist/storage.js +109 -50
  53. package/dist/store.d.ts +1 -1
  54. package/dist/store.d.ts.map +1 -1
  55. package/dist/version.d.ts +1 -1
  56. package/dist/version.d.ts.map +1 -1
  57. package/package.json +25 -6
  58. package/scripts/ci-linux-suite.ts +403 -0
  59. package/scripts/ci-native-build.ts +402 -0
  60. package/scripts/enforce_identity_migration.sh +110 -0
  61. package/scripts/generate-sdk.ts +1 -1
  62. package/scripts/install_macos_app.sh +192 -31
  63. package/scripts/macos_artifact.ts +323 -43
  64. package/scripts/native/realtime-settle-probe/README.md +39 -0
  65. package/scripts/native/realtime-settle-probe/main.swift +135 -0
  66. package/scripts/policy/local-only-approved-targets.txt +21 -0
  67. package/scripts/read_local_only_targets.sh +203 -0
  68. package/scripts/scan-artifact.ts +40 -0
  69. package/scripts/set-version.ts +145 -0
  70. package/scripts/smoke_macos_app.sh +62 -16
  71. package/scripts/vacuity-manifest-gen.ts +96 -0
  72. package/scripts/vacuity-manifests/enhancement-screen-mirror.tsv +44 -0
  73. package/scripts/vacuity-manifests/install-chain.tsv +13 -0
  74. package/scripts/vacuity-manifests/must-survive-controls.tsv +10 -0
  75. package/scripts/vacuity-manifests/reorder.tsv +4 -0
  76. package/scripts/vacuity-manifests/source-side.tsv +15 -0
  77. package/scripts/vacuity-manifests/variable-operands.tsv +16 -0
  78. package/scripts/vacuity-manifests/version-sites.tsv +61 -0
  79. package/scripts/vacuity-mutation-battery.ts +266 -0
  80. package/src/native/Recordings/App/MenuBarStatusView.swift +23 -11
  81. package/src/native/Recordings/App/RecordingsApp.swift +37 -1
  82. package/src/native/Recordings/App/RuntimeSmoke.swift +5 -1
  83. package/src/native/Recordings/App/SidebarView.swift +1 -1
  84. package/src/native/Recordings/Package.swift +24 -5
  85. package/src/native/Recordings/RecordingsLib/EnhancementScreen.swift +91 -0
  86. package/src/native/Recordings/RecordingsLib/FnKeyMonitor.swift +27 -4
  87. package/src/native/Recordings/RecordingsLib/Info.plist +2 -2
  88. package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +56 -3
  89. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +13 -0
  90. package/src/native/Recordings/RecordingsLib/PasteDeliveryVerification.swift +460 -0
  91. package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +21 -1
  92. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +41 -7
  93. package/src/native/Recordings/RecordingsLib/RecordingAttemptAlert.swift +36 -0
  94. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +1071 -75
  95. package/src/native/Recordings/RecordingsLib/SettingsView.swift +47 -3
  96. package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +11 -6
  97. package/src/native/Recordings/RecordingsTests/BarOnlyLaunchPlanTests.swift +99 -0
  98. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +3 -3
  99. package/src/native/Recordings/RecordingsTests/EnhancementScreenTests.swift +191 -0
  100. package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +144 -4
  101. package/src/native/Recordings/RecordingsTests/PasteDeliveryVerificationTests.swift +463 -0
  102. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +33 -20
  103. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +1 -1
  104. package/src/native/Recordings/RecordingsTests/RealtimeSettleBenchmark.swift +200 -0
  105. package/src/native/Recordings/RecordingsTests/RealtimeSettleBudgetTests.swift +80 -0
  106. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +22 -4
  107. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +12 -6
  108. package/src/native/Recordings/RecordingsTests/RecordingEngineHomeIsolationTests.swift +67 -0
  109. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +12 -3
  110. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +207 -11
  111. package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +17 -6
  112. package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +23 -0
  113. package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +7 -1
  114. package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +10 -10
  115. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +5 -1
  116. package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +1 -0
  117. package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +59 -3
  118. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +21 -2
  119. package/src/native/Recordings/Updater/ProtocolTests/ExpectationIntegrityTests.swift +60 -0
  120. package/src/native/Recordings/Updater/ProtocolTests/UpdateXPCInterfaceTests.swift +113 -0
  121. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +106 -5
  122. package/src/native/Recordings/build.sh +226 -20
  123. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
@@ -0,0 +1,463 @@
1
+ import AppKit
2
+ import Testing
3
+ @testable import RecordingsLib
4
+
5
+ /// The defect these tests exist for: `CGEvent.post` returns `Void`, so the paste path used to
6
+ /// report success as soon as two key events had been *constructed*. Every log line and status
7
+ /// message that said "pasted" was therefore unfalsifiable — it said the same thing whether the
8
+ /// text landed in the focused field, went to another app, or went nowhere at all.
9
+ @MainActor
10
+ struct PasteDeliveryHonestyTests {
11
+ private func makeCoordinator(
12
+ scheduled: @escaping @MainActor @Sendable (@escaping @MainActor @Sendable () -> Void) -> Void,
13
+ postPaste: @escaping PasteTransactionCoordinator.PastePoster
14
+ ) -> PasteTransactionCoordinator {
15
+ PasteTransactionCoordinator(
16
+ schedule: { _, operation in scheduled(operation) },
17
+ writeAndVerify: { _ in PasteboardWriteResult(verified: true, ownershipChangeCount: 1) },
18
+ postPaste: postPaste
19
+ )
20
+ }
21
+
22
+ @Test("a posted keystroke with no read-back never completes as pasted")
23
+ func postedWithoutEvidenceIsNotPasted() {
24
+ var scheduled: [@MainActor @Sendable () -> Void] = []
25
+ var posts = 0
26
+ var outcomes: [PasteDeliveryOutcome] = []
27
+ let coordinator = makeCoordinator(
28
+ scheduled: { scheduled.append($0) },
29
+ postPaste: {
30
+ posts += 1
31
+ return .posted
32
+ }
33
+ )
34
+
35
+ // No `verify:` — exactly the situation the old code was in, where posting *was* the
36
+ // success condition.
37
+ #expect(coordinator.submit(text: "hello", generation: 1, delay: 0) { _, outcome in
38
+ outcomes.append(outcome)
39
+ })
40
+ scheduled.removeFirst()()
41
+
42
+ #expect(posts == 1)
43
+ #expect(outcomes == [.deliveredUnverified(.readBackNotAttempted)])
44
+ #expect(outcomes.first != .pasted, "constructing and posting a CGEvent is not delivery")
45
+ #expect(!coordinator.hasPendingTransaction)
46
+ }
47
+
48
+ @Test("only confirming read-back evidence can produce the pasted outcome")
49
+ func onlyConfirmedEvidenceMapsToPasted() {
50
+ #expect(PasteDeliveryOutcome.forDeliveryEvidence(.confirmedByFocusedValue) == .pasted)
51
+ #expect(PasteDeliveryOutcome.forDeliveryEvidence(.confirmedBySelectedText) == .pasted)
52
+ #expect(
53
+ PasteDeliveryOutcome.forDeliveryEvidence(.notObservedFocusedValueUnchanged)
54
+ == .deliveryNotObserved
55
+ )
56
+ for reason: PasteDeliveryUnverifiedReason in [
57
+ .readBackNotAttempted,
58
+ .emptyPayload,
59
+ .baselineUnreadable(.elementUnavailable),
60
+ .baselineUnreadable(.valueUnreadable),
61
+ .baselineUnreadable(.valueTooLarge),
62
+ .readBackUnreadable(.elementChanged),
63
+ .changedWithoutMatch,
64
+ ] {
65
+ let outcome = PasteDeliveryOutcome.forDeliveryEvidence(.unverified(reason))
66
+ #expect(outcome == .deliveredUnverified(reason))
67
+ #expect(outcome != .pasted)
68
+ }
69
+ }
70
+
71
+ @Test("an unverified delivery is never reported to the UI as a success")
72
+ func unverifiedDeliveryIsItsOwnStatusKind() {
73
+ #expect(RecordingEngine.deliveryStatusKind(for: .pasted) == .success)
74
+ #expect(
75
+ RecordingEngine.deliveryStatusKind(for: .deliveredUnverified(.changedWithoutMatch))
76
+ == .unverified
77
+ )
78
+ #expect(RecordingEngine.deliveryStatusKind(for: .deliveryNotObserved) == .failure)
79
+ #expect(
80
+ RecordingEngine.deliveryStatusKind(
81
+ for: .secureInputActive(SecureInputHolder(pid: 501, bundleIdentifier: nil))
82
+ ) == .failure
83
+ )
84
+ #expect(RecordingEngine.deliveryStatusKind(for: .eventPostFailed) == .failure)
85
+ #expect(RecordingEngine.pasteTraceStage(for: .pasted) == "paste_delivery_confirmed")
86
+ #expect(
87
+ RecordingEngine.pasteTraceStage(for: .deliveredUnverified(.readBackNotAttempted))
88
+ == "paste_delivery_unverified"
89
+ )
90
+ #expect(
91
+ RecordingEngine.pasteTraceStage(for: .deliveryNotObserved)
92
+ == "paste_delivery_not_observed"
93
+ )
94
+ }
95
+
96
+ @Test("secure input refuses the keystroke instead of reporting a paste")
97
+ func secureInputRefusesAndReportsItself() {
98
+ var scheduled: [@MainActor @Sendable () -> Void] = []
99
+ var verifyCalls = 0
100
+ var outcomes: [PasteDeliveryOutcome] = []
101
+ var settlements: [PasteDeliveryOutcome] = []
102
+ let holder = SecureInputHolder(pid: 902, bundleIdentifier: "com.example.locked")
103
+ let coordinator = makeCoordinator(
104
+ scheduled: { scheduled.append($0) },
105
+ postPaste: { .refusedSecureInput(holder) }
106
+ )
107
+
108
+ #expect(coordinator.submit(
109
+ text: "hello",
110
+ generation: 1,
111
+ delay: 0,
112
+ verify: {
113
+ verifyCalls += 1
114
+ return .confirmedByFocusedValue
115
+ }
116
+ ) { _, outcome in
117
+ outcomes.append(outcome)
118
+ } settlement: { _, outcome in
119
+ settlements.append(outcome)
120
+ })
121
+ scheduled.removeFirst()()
122
+
123
+ #expect(outcomes == [.secureInputActive(holder)])
124
+ #expect(settlements == [.secureInputActive(holder)])
125
+ #expect(verifyCalls == 0, "nothing was posted, so there is nothing to verify")
126
+ #expect(!coordinator.hasPendingTransaction)
127
+ }
128
+
129
+ @Test("only the unchanged-field verdict is retried, and a later confirmation wins")
130
+ func readBackRetriesOnlyTheNegativeVerdict() {
131
+ var scheduled: [@MainActor @Sendable () -> Void] = []
132
+ var verdicts: [PasteDeliveryEvidence] = [
133
+ .notObservedFocusedValueUnchanged,
134
+ .confirmedByFocusedValue,
135
+ ]
136
+ var outcomes: [PasteDeliveryOutcome] = []
137
+ let coordinator = makeCoordinator(
138
+ scheduled: { scheduled.append($0) },
139
+ postPaste: { .posted }
140
+ )
141
+
142
+ #expect(coordinator.submit(
143
+ text: "hello",
144
+ generation: 1,
145
+ delay: 0,
146
+ verify: { verdicts.isEmpty ? .notObservedFocusedValueUnchanged : verdicts.removeFirst() },
147
+ verificationDelay: 0.15,
148
+ verificationAttempts: 3
149
+ ) { _, outcome in
150
+ outcomes.append(outcome)
151
+ })
152
+
153
+ scheduled.removeFirst()() // paste hop: posts, then schedules the first read-back
154
+ #expect(outcomes.isEmpty, "the transaction stays open until a read-back answers")
155
+ #expect(coordinator.hasPendingTransaction)
156
+ scheduled.removeFirst()() // read-back 1: field unchanged -> retry
157
+ #expect(outcomes.isEmpty)
158
+ scheduled.removeFirst()() // read-back 2: confirmed
159
+ #expect(outcomes == [.pasted])
160
+ #expect(scheduled.isEmpty, "a confirmed read-back must not schedule another")
161
+ #expect(!coordinator.hasPendingTransaction)
162
+ }
163
+
164
+ @Test("an exhausted read-back budget reports the paste as not delivered")
165
+ func exhaustedReadBackReportsNotObserved() {
166
+ var scheduled: [@MainActor @Sendable () -> Void] = []
167
+ var verifyCalls = 0
168
+ var outcomes: [PasteDeliveryOutcome] = []
169
+ let coordinator = makeCoordinator(
170
+ scheduled: { scheduled.append($0) },
171
+ postPaste: { .posted }
172
+ )
173
+
174
+ #expect(coordinator.submit(
175
+ text: "hello",
176
+ generation: 1,
177
+ delay: 0,
178
+ verify: {
179
+ verifyCalls += 1
180
+ return .notObservedFocusedValueUnchanged
181
+ },
182
+ verificationDelay: 0.15,
183
+ verificationAttempts: 2
184
+ ) { _, outcome in
185
+ outcomes.append(outcome)
186
+ })
187
+
188
+ scheduled.removeFirst()()
189
+ scheduled.removeFirst()()
190
+ #expect(outcomes.isEmpty)
191
+ scheduled.removeFirst()()
192
+ #expect(verifyCalls == 2)
193
+ #expect(outcomes == [.deliveryNotObserved])
194
+ #expect(!coordinator.hasPendingTransaction)
195
+ }
196
+
197
+ @Test("a failed event construction is still reported as a post failure")
198
+ func constructionFailureIsNotADeliveryVerdict() {
199
+ var scheduled: [@MainActor @Sendable () -> Void] = []
200
+ var outcomes: [PasteDeliveryOutcome] = []
201
+ let coordinator = makeCoordinator(
202
+ scheduled: { scheduled.append($0) },
203
+ postPaste: { .constructionFailed }
204
+ )
205
+
206
+ #expect(coordinator.submit(text: "hello", generation: 1, delay: 0) { _, outcome in
207
+ outcomes.append(outcome)
208
+ })
209
+ scheduled.removeFirst()()
210
+
211
+ #expect(outcomes == [.eventPostFailed])
212
+ }
213
+ }
214
+
215
+ struct FocusedFieldEvidenceTests {
216
+ private func read(_ value: String, selected: String? = nil) -> FocusedTextRead {
217
+ .read(FocusedTextSnapshot(value: value, selectedText: selected))
218
+ }
219
+
220
+ @Test("a field that gained the pasted text confirms delivery")
221
+ func gainedTextConfirms() {
222
+ #expect(PasteDeliveryVerifier.classify(
223
+ pastedText: "hello world",
224
+ baseline: read("draft: "),
225
+ readBack: read("draft: hello world")
226
+ ) == .confirmedByFocusedValue)
227
+ }
228
+
229
+ @Test("a field that did not change contradicts delivery")
230
+ func unchangedFieldContradicts() {
231
+ #expect(PasteDeliveryVerifier.classify(
232
+ pastedText: "hello world",
233
+ baseline: read("draft: "),
234
+ readBack: read("draft: ")
235
+ ) == .notObservedFocusedValueUnchanged)
236
+ }
237
+
238
+ @Test("text that was already in the field cannot be counted as this paste")
239
+ func preexistingTextIsNotEvidence() {
240
+ // The transcript is already present (a previous dictation) and the field changed for
241
+ // an unrelated reason. Counting occurrences instead of testing containment is what
242
+ // keeps this from reading as a confirmation.
243
+ #expect(PasteDeliveryVerifier.classify(
244
+ pastedText: "hello world",
245
+ baseline: read("hello world"),
246
+ readBack: read("hello world!")
247
+ ) == .unverified(.changedWithoutMatch))
248
+ }
249
+
250
+ @Test("a second copy of the same text does confirm")
251
+ func repeatedPasteConfirms() {
252
+ #expect(PasteDeliveryVerifier.classify(
253
+ pastedText: "hello",
254
+ baseline: read("hello"),
255
+ readBack: read("hellohello")
256
+ ) == .confirmedByFocusedValue)
257
+ }
258
+
259
+ @Test("line-ending rewrites still confirm; other transformations do not")
260
+ func lineEndingsAreNormalized() {
261
+ #expect(PasteDeliveryVerifier.classify(
262
+ pastedText: "first\r\nsecond",
263
+ baseline: read(""),
264
+ readBack: read("first\nsecond")
265
+ ) == .confirmedByFocusedValue)
266
+ #expect(PasteDeliveryVerifier.classify(
267
+ pastedText: "first second",
268
+ baseline: read(""),
269
+ readBack: read("FIRST SECOND")
270
+ ) == .unverified(.changedWithoutMatch))
271
+ }
272
+
273
+ @Test("a selection that reads back as the pasted text confirms only when it changed")
274
+ func selectionEvidenceRequiresAChange() {
275
+ #expect(PasteDeliveryVerifier.classify(
276
+ pastedText: "rewritten",
277
+ baseline: read("rewritten", selected: "rewritten"),
278
+ readBack: read("rewritten", selected: "rewritten")
279
+ ) == .notObservedFocusedValueUnchanged)
280
+ #expect(PasteDeliveryVerifier.classify(
281
+ pastedText: "rewritten",
282
+ baseline: read("original", selected: "original"),
283
+ readBack: read("original", selected: "rewritten")
284
+ ) == .confirmedBySelectedText)
285
+ }
286
+
287
+ @Test("an unreadable surface degrades to unverified, naming which read failed")
288
+ func unreadableSurfacesAreDisclosed() {
289
+ // Chrome/Electron web inputs, terminals, and apps with Accessibility disabled land
290
+ // here. Unverified is the honest answer; success would be a fabrication and failure
291
+ // would be a fabrication too.
292
+ #expect(PasteDeliveryVerifier.classify(
293
+ pastedText: "hello",
294
+ baseline: .unreadable(.valueUnreadable),
295
+ readBack: read("hello")
296
+ ) == .unverified(.baselineUnreadable(.valueUnreadable)))
297
+ #expect(PasteDeliveryVerifier.classify(
298
+ pastedText: "hello",
299
+ baseline: read(""),
300
+ readBack: .unreadable(.elementChanged)
301
+ ) == .unverified(.readBackUnreadable(.elementChanged)))
302
+ #expect(PasteDeliveryVerifier.classify(
303
+ pastedText: "hello",
304
+ baseline: .unreadable(.elementUnavailable),
305
+ readBack: .unreadable(.elementUnavailable)
306
+ ) == .unverified(.baselineUnreadable(.elementUnavailable)))
307
+ }
308
+
309
+ @Test("an empty payload is never confirmed")
310
+ func emptyPayloadIsUnverifiable() {
311
+ #expect(PasteDeliveryVerifier.classify(
312
+ pastedText: "",
313
+ baseline: read("a"),
314
+ readBack: read("ab")
315
+ ) == .unverified(.emptyPayload))
316
+ }
317
+ }
318
+
319
+ struct SecureInputProbeTests {
320
+ @Test("secure input is reported active, inactive, or unknown — never assumed off")
321
+ func decisionTable() {
322
+ #expect(SecureInputProbe.state(
323
+ sessionAvailable: true,
324
+ sessionMarkerPresent: true,
325
+ secureInputPID: 733,
326
+ resolveBundleIdentifier: { _ in "com.example.locked" }
327
+ ) == .active(SecureInputHolder(pid: 733, bundleIdentifier: "com.example.locked")))
328
+ #expect(SecureInputProbe.state(
329
+ sessionAvailable: true,
330
+ sessionMarkerPresent: true,
331
+ secureInputPID: nil
332
+ ) == .inactive)
333
+ #expect(SecureInputProbe.state(
334
+ sessionAvailable: true,
335
+ sessionMarkerPresent: true,
336
+ secureInputPID: 0
337
+ ) == .inactive)
338
+ // No window session (headless, ssh) and an unrecognisable session dictionary both mean
339
+ // we cannot tell, which must not read as "safe to post".
340
+ #expect(SecureInputProbe.state(
341
+ sessionAvailable: false,
342
+ sessionMarkerPresent: false,
343
+ secureInputPID: nil
344
+ ) == .unknown)
345
+ #expect(SecureInputProbe.state(
346
+ sessionAvailable: true,
347
+ sessionMarkerPresent: false,
348
+ secureInputPID: nil
349
+ ) == .unknown)
350
+ #expect(!SecureInputState.unknown.isActive)
351
+ #expect(!SecureInputState.inactive.isActive)
352
+ }
353
+ }
354
+
355
+ struct PasteDeliveryReportTests {
356
+ private func report(
357
+ attempt: PasteAttempt,
358
+ secureInput: SecureInputState?,
359
+ evidence: PasteDeliveryEvidence,
360
+ readBackAttempts: Int
361
+ ) -> PasteDeliveryReport {
362
+ PasteDeliveryReport(
363
+ targetBundleIdentifier: "com.example.editor",
364
+ characterCount: 11,
365
+ clipboardWriteVerified: true,
366
+ clipboardChangeCountAdvanced: true,
367
+ attempt: attempt,
368
+ secureInput: secureInput,
369
+ evidence: evidence,
370
+ readBackAttempts: readBackAttempts
371
+ )
372
+ }
373
+
374
+ @Test("the log line separates clipboard, keystroke, secure input, and delivery")
375
+ func logLineKeepsTheStepsSeparate() {
376
+ let confirmed = report(
377
+ attempt: .posted,
378
+ secureInput: .inactive,
379
+ evidence: .confirmedByFocusedValue,
380
+ readBackAttempts: 1
381
+ ).logLine
382
+ #expect(confirmed.hasPrefix("paste_delivery target=com.example.editor chars=11"))
383
+ #expect(confirmed.contains("clipboard=verified"))
384
+ #expect(confirmed.contains("clipboard_change_count=advanced"))
385
+ #expect(confirmed.contains("events=constructed_and_posted"))
386
+ #expect(confirmed.contains("secure_input=inactive"))
387
+ #expect(confirmed.contains("delivery=confirmed_focused_value"))
388
+ #expect(confirmed.contains("read_back_attempts=1"))
389
+ }
390
+
391
+ @Test("a posted-but-unproven paste never prints a confirmed delivery")
392
+ func unprovenPasteReadsAsUnverified() {
393
+ let unverified = report(
394
+ attempt: .posted,
395
+ secureInput: .unknown,
396
+ evidence: .unverified(.baselineUnreadable(.valueUnreadable)),
397
+ readBackAttempts: 1
398
+ ).logLine
399
+ #expect(unverified.contains("events=constructed_and_posted"))
400
+ #expect(unverified.contains("delivery=unverified:baseline_unreadable:focused_value_unreadable"))
401
+ #expect(!unverified.contains("delivery=confirmed"))
402
+ #expect(unverified.contains("secure_input=unknown"))
403
+
404
+ let refused = report(
405
+ attempt: .refusedSecureInput(SecureInputHolder(pid: 61, bundleIdentifier: "com.example.vault")),
406
+ secureInput: .active(SecureInputHolder(pid: 61, bundleIdentifier: "com.example.vault")),
407
+ evidence: .unverified(.readBackNotAttempted),
408
+ readBackAttempts: 0
409
+ ).logLine
410
+ #expect(refused.contains("events=not_posted_secure_input"))
411
+ #expect(refused.contains("secure_input=active(pid=61,app=com.example.vault)"))
412
+ #expect(refused.contains("delivery=unverified:read_back_not_attempted"))
413
+ #expect(!refused.contains("delivery=confirmed"))
414
+ }
415
+
416
+ @Test("a paste that never reached the keystroke says so instead of implying one")
417
+ func preKeystrokeFailuresReportNoAttempt() {
418
+ #expect(PasteAttempt.forOutcome(.targetUnavailable) == .notAttempted)
419
+ #expect(PasteAttempt.forOutcome(.clipboardWriteFailed) == .notAttempted)
420
+ #expect(PasteAttempt.forOutcome(.clipboardOwnershipLost) == .notAttempted)
421
+ #expect(PasteAttempt.forOutcome(.eventPostFailed) == .constructionFailed)
422
+ #expect(PasteAttempt.forOutcome(.pasted) == .posted)
423
+ #expect(PasteAttempt.forOutcome(.deliveryNotObserved) == .posted)
424
+ #expect(PasteAttempt.forOutcome(.deliveredUnverified(.emptyPayload)) == .posted)
425
+ let holder = SecureInputHolder(pid: 5, bundleIdentifier: nil)
426
+ #expect(PasteAttempt.forOutcome(.secureInputActive(holder)) == .refusedSecureInput(holder))
427
+
428
+ let notAttempted = PasteDeliveryReport(
429
+ targetBundleIdentifier: nil,
430
+ characterCount: 4,
431
+ clipboardWriteVerified: false,
432
+ clipboardChangeCountAdvanced: false,
433
+ attempt: .notAttempted,
434
+ secureInput: nil,
435
+ evidence: .unverified(.readBackNotAttempted),
436
+ readBackAttempts: 0
437
+ ).logLine
438
+ #expect(notAttempted.contains("target=?"))
439
+ #expect(notAttempted.contains("clipboard=unverified"))
440
+ #expect(notAttempted.contains("clipboard_change_count=unchanged"))
441
+ #expect(notAttempted.contains("events=not_attempted"))
442
+ #expect(notAttempted.contains("secure_input=not_probed"))
443
+ #expect(notAttempted.contains("read_back_attempts=0"))
444
+ }
445
+ }
446
+
447
+ struct PasteboardWriteEvidenceTests {
448
+ @Test("a clipboard write reports both that the pasteboard moved and that it holds our text")
449
+ func clipboardWriteReportsChangeCountMovement() {
450
+ let pasteboard = NSPasteboard(name: NSPasteboard.Name("recordings-write-evidence-\(UUID().uuidString)"))
451
+ defer { pasteboard.releaseGlobally() }
452
+ pasteboard.clearContents()
453
+ pasteboard.setString("previous", forType: .string)
454
+ let changeCountBefore = pasteboard.changeCount
455
+
456
+ let result = RecordingEngine.writeClipboardAttempt("dictated text", to: pasteboard)
457
+
458
+ #expect(result.verified)
459
+ #expect(result.changeCountAdvanced)
460
+ #expect(result.ownershipChangeCount > changeCountBefore)
461
+ #expect(pasteboard.string(forType: .string) == "dictated text")
462
+ }
463
+ }
@@ -46,7 +46,7 @@ struct PasteTargetTests {
46
46
  },
47
47
  postPaste: {
48
48
  postedPayloads.append(pasteboardText)
49
- return true
49
+ return .posted
50
50
  }
51
51
  )
52
52
 
@@ -55,7 +55,11 @@ struct PasteTargetTests {
55
55
  generation: 41,
56
56
  delay: 0.5,
57
57
  settlementDelay: 0.6,
58
- prepare: { preparations += 1 }
58
+ prepare: { preparations += 1 },
59
+ // A posted keystroke is not a delivery, so `.pasted` is only reachable when the
60
+ // read-back confirms the text landed. Coordinator tests that mean "the paste
61
+ // worked" have to say so here; omitting this yields `.deliveredUnverified`.
62
+ verify: { .confirmedByFocusedValue }
59
63
  ) { transaction, outcome in
60
64
  completions.append((transaction.generation, outcome))
61
65
  })
@@ -78,7 +82,12 @@ struct PasteTargetTests {
78
82
  scheduled.removeFirst()()
79
83
  #expect(!coordinator.hasPendingTransaction)
80
84
 
81
- #expect(coordinator.submit(text: "recording B", generation: 42, delay: 0) { transaction, outcome in
85
+ #expect(coordinator.submit(
86
+ text: "recording B",
87
+ generation: 42,
88
+ delay: 0,
89
+ verify: { .confirmedByFocusedValue }
90
+ ) { transaction, outcome in
82
91
  completions.append((transaction.generation, outcome))
83
92
  })
84
93
  pasteboardText = "another mutation"
@@ -96,7 +105,7 @@ struct PasteTargetTests {
96
105
  let coordinator = PasteTransactionCoordinator(
97
106
  schedule: { _, operation in scheduled.append(operation) },
98
107
  writeAndVerify: { _ in PasteboardWriteResult(verified: true, ownershipChangeCount: 1) },
99
- postPaste: { true }
108
+ postPaste: { .posted }
100
109
  )
101
110
  coordinator.pendingTransactionWillChange = { announcements += 1 }
102
111
 
@@ -115,7 +124,7 @@ struct PasteTargetTests {
115
124
  let failing = PasteTransactionCoordinator(
116
125
  schedule: { _, operation in scheduled.append(operation) },
117
126
  writeAndVerify: { _ in PasteboardWriteResult(verified: false, ownershipChangeCount: 1) },
118
- postPaste: { false }
127
+ postPaste: { .constructionFailed }
119
128
  )
120
129
  var failureAnnouncements = 0
121
130
  failing.pendingTransactionWillChange = { failureAnnouncements += 1 }
@@ -137,7 +146,7 @@ struct PasteTargetTests {
137
146
  writeAndVerify: { _ in PasteboardWriteResult(verified: false, ownershipChangeCount: 1) },
138
147
  postPaste: {
139
148
  postCalls += 1
140
- return false
149
+ return .constructionFailed
141
150
  }
142
151
  )
143
152
 
@@ -159,7 +168,7 @@ struct PasteTargetTests {
159
168
  writeAndVerify: { _ in PasteboardWriteResult(verified: true, ownershipChangeCount: 1) },
160
169
  postPaste: {
161
170
  postCalls += 1
162
- return false
171
+ return .constructionFailed
163
172
  }
164
173
  )
165
174
  #expect(postFailureCoordinator.submit(text: "B", generation: 2, delay: 0) { _, outcome in
@@ -192,7 +201,7 @@ struct PasteTargetTests {
192
201
  },
193
202
  postPaste: {
194
203
  postCalls += 1
195
- return true
204
+ return .posted
196
205
  }
197
206
  )
198
207
 
@@ -230,7 +239,7 @@ struct PasteTargetTests {
230
239
  writeCalls += 1
231
240
  return PasteboardWriteResult(verified: true, ownershipChangeCount: 1)
232
241
  },
233
- postPaste: { true }
242
+ postPaste: { .posted }
234
243
  )
235
244
 
236
245
  #expect(coordinator.submit(
@@ -265,7 +274,7 @@ struct PasteTargetTests {
265
274
  },
266
275
  postPaste: {
267
276
  postCalls += 1
268
- return true
277
+ return .posted
269
278
  }
270
279
  )
271
280
 
@@ -302,7 +311,7 @@ struct PasteTargetTests {
302
311
  },
303
312
  postPaste: {
304
313
  postCalls += 1
305
- return true
314
+ return .posted
306
315
  }
307
316
  )
308
317
 
@@ -587,7 +596,7 @@ struct PasteTargetTests {
587
596
  writeAndVerify: { _ in
588
597
  PasteboardWriteResult(verified: true, ownershipChangeCount: 1)
589
598
  },
590
- postPaste: { true }
599
+ postPaste: { .posted }
591
600
  )
592
601
 
593
602
  #expect(coordinator.submit(
@@ -632,7 +641,7 @@ struct PasteTargetTests {
632
641
  writeAndVerify: { _ in
633
642
  PasteboardWriteResult(verified: true, ownershipChangeCount: 1)
634
643
  },
635
- postPaste: { true }
644
+ postPaste: { .posted }
636
645
  )
637
646
 
638
647
  #expect(coordinator.submit(
@@ -686,9 +695,11 @@ struct PasteTargetTests {
686
695
  schedule: { _, operation in scheduled.append(operation) },
687
696
  writeAndVerify: { text in RecordingEngine.writeClipboardAttempt(text, to: pasteboard) },
688
697
  postPaste: {
689
- guard let payload = pasteboard.string(forType: .string) else { return false }
698
+ guard let payload = pasteboard.string(forType: .string) else {
699
+ return .constructionFailed
700
+ }
690
701
  postedPayloads.append(payload)
691
- return true
702
+ return .posted
692
703
  }
693
704
  )
694
705
 
@@ -717,7 +728,7 @@ struct PasteTargetTests {
717
728
  let coordinator = PasteTransactionCoordinator(
718
729
  schedule: { _, operation in scheduled.append(operation) },
719
730
  writeAndVerify: { text in RecordingEngine.writeClipboardAttempt(text, to: pasteboard) },
720
- postPaste: { true }
731
+ postPaste: { .posted }
721
732
  )
722
733
 
723
734
  #expect(coordinator.submit(
@@ -725,7 +736,8 @@ struct PasteTargetTests {
725
736
  generation: 1,
726
737
  delay: 0,
727
738
  settlementDelay: 0.6,
728
- writeAttempted: { ownedChangeCount = $0.ownershipChangeCount }
739
+ writeAttempted: { ownedChangeCount = $0.ownershipChangeCount },
740
+ verify: { .confirmedByFocusedValue }
729
741
  ) { _, _ in
730
742
  } settlement: { transaction, outcome in
731
743
  guard outcome == .pasted,
@@ -767,7 +779,7 @@ struct PasteTargetTests {
767
779
  ownershipChangeCount: attempted.ownershipChangeCount
768
780
  )
769
781
  },
770
- postPaste: { true }
782
+ postPaste: { .posted }
771
783
  )
772
784
 
773
785
  #expect(coordinator.submit(
@@ -815,7 +827,7 @@ struct PasteTargetTests {
815
827
  let coordinator = PasteTransactionCoordinator(
816
828
  schedule: { _, operation in scheduled.append(operation) },
817
829
  writeAndVerify: { text in RecordingEngine.writeClipboardAttempt(text, to: pasteboard) },
818
- postPaste: { true }
830
+ postPaste: { .posted }
819
831
  )
820
832
 
821
833
  #expect(coordinator.submit(
@@ -824,7 +836,8 @@ struct PasteTargetTests {
824
836
  delay: 0,
825
837
  settlementDelay: 0.6,
826
838
  prepare: { prePasteValue = pasteboard.string(forType: .string) },
827
- writeAttempted: { ownedChangeCount = $0.ownershipChangeCount }
839
+ writeAttempted: { ownedChangeCount = $0.ownershipChangeCount },
840
+ verify: { .confirmedByFocusedValue }
828
841
  ) { _, _ in
829
842
  } settlement: { transaction, outcome in
830
843
  guard outcome == .pasted,
@@ -139,7 +139,7 @@ struct ProjectStoreTests {
139
139
  #expect(store.activeProject?.id == "local-id")
140
140
  #expect(store.activeCanonicalProjectIdForRecording == nil)
141
141
  #expect(store.effectiveSystemPrompt == "Keep this context")
142
- #expect(RecordingEngine.canBeginRecording(isRecording: false, isTranscribing: false))
142
+ #expect(RecordingEngine.canBeginRecording(isRecording: false, isTranscribing: false, isWarmingUpCapture: false))
143
143
 
144
144
  let successScript = """
145
145
  #!/bin/sh