@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.
- package/Dockerfile.package +4 -5
- package/README.md +113 -8
- package/bun.lock +2 -2
- package/dist/__tests__/helpers/installer-guard-execution.d.ts +22 -0
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts +22 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts.map +1 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +169 -0
- package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -0
- package/dist/__tests__/preload.d.ts +1 -4
- package/dist/__tests__/preload.d.ts.map +1 -1
- package/dist/cli/desktop-snapshot.d.ts +20 -0
- package/dist/cli/desktop-snapshot.d.ts.map +1 -0
- package/dist/cli/index.js +2094 -280
- package/dist/cli/macos-permissions.d.ts +72 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -1
- package/dist/cli/macos-shortcut.d.ts +139 -0
- package/dist/cli/macos-shortcut.d.ts.map +1 -0
- package/dist/cli/trigger-probe.d.ts +172 -0
- package/dist/cli/trigger-probe.d.ts.map +1 -0
- package/dist/db/database.d.ts +9 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/http/client.d.ts +19 -7
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +635 -50
- package/dist/lib/capture-probe.d.ts +216 -0
- package/dist/lib/capture-probe.d.ts.map +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/macos-bundle.d.ts +50 -0
- package/dist/lib/macos-bundle.d.ts.map +1 -0
- package/dist/lib/persistence-probe.d.ts +208 -0
- package/dist/lib/persistence-probe.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/retired-deployment-modes.d.ts +24 -0
- package/dist/lib/retired-deployment-modes.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts +60 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.js +141 -57
- package/dist/server/cloud-config.d.ts +25 -3
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud-readiness.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +8 -7
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +188 -80
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +109 -50
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +25 -6
- package/scripts/ci-linux-suite.ts +403 -0
- package/scripts/ci-native-build.ts +402 -0
- package/scripts/enforce_identity_migration.sh +110 -0
- package/scripts/generate-sdk.ts +1 -1
- package/scripts/install_macos_app.sh +192 -31
- package/scripts/macos_artifact.ts +323 -43
- package/scripts/native/realtime-settle-probe/README.md +39 -0
- package/scripts/native/realtime-settle-probe/main.swift +135 -0
- package/scripts/policy/local-only-approved-targets.txt +21 -0
- package/scripts/read_local_only_targets.sh +203 -0
- package/scripts/scan-artifact.ts +40 -0
- package/scripts/set-version.ts +145 -0
- package/scripts/smoke_macos_app.sh +62 -16
- package/scripts/vacuity-manifest-gen.ts +96 -0
- package/scripts/vacuity-manifests/enhancement-screen-mirror.tsv +44 -0
- package/scripts/vacuity-manifests/install-chain.tsv +13 -0
- package/scripts/vacuity-manifests/must-survive-controls.tsv +10 -0
- package/scripts/vacuity-manifests/reorder.tsv +4 -0
- package/scripts/vacuity-manifests/source-side.tsv +15 -0
- package/scripts/vacuity-manifests/variable-operands.tsv +16 -0
- package/scripts/vacuity-manifests/version-sites.tsv +61 -0
- package/scripts/vacuity-mutation-battery.ts +266 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +23 -11
- package/src/native/Recordings/App/RecordingsApp.swift +37 -1
- package/src/native/Recordings/App/RuntimeSmoke.swift +5 -1
- package/src/native/Recordings/App/SidebarView.swift +1 -1
- package/src/native/Recordings/Package.swift +24 -5
- package/src/native/Recordings/RecordingsLib/EnhancementScreen.swift +91 -0
- package/src/native/Recordings/RecordingsLib/FnKeyMonitor.swift +27 -4
- package/src/native/Recordings/RecordingsLib/Info.plist +2 -2
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +56 -3
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +13 -0
- package/src/native/Recordings/RecordingsLib/PasteDeliveryVerification.swift +460 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +21 -1
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +41 -7
- package/src/native/Recordings/RecordingsLib/RecordingAttemptAlert.swift +36 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +1071 -75
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +47 -3
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +11 -6
- package/src/native/Recordings/RecordingsTests/BarOnlyLaunchPlanTests.swift +99 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +3 -3
- package/src/native/Recordings/RecordingsTests/EnhancementScreenTests.swift +191 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +144 -4
- package/src/native/Recordings/RecordingsTests/PasteDeliveryVerificationTests.swift +463 -0
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +33 -20
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBenchmark.swift +200 -0
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBudgetTests.swift +80 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +22 -4
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +12 -6
- package/src/native/Recordings/RecordingsTests/RecordingEngineHomeIsolationTests.swift +67 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +12 -3
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +207 -11
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +17 -6
- package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +23 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +7 -1
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +10 -10
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +5 -1
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +1 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +59 -3
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +21 -2
- package/src/native/Recordings/Updater/ProtocolTests/ExpectationIntegrityTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/UpdateXPCInterfaceTests.swift +113 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +106 -5
- package/src/native/Recordings/build.sh +226 -20
- 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
|
|
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(
|
|
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: {
|
|
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: {
|
|
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
|
|
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
|
|
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
|
|
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: {
|
|
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
|
|
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
|
|
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: {
|
|
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: {
|
|
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 {
|
|
698
|
+
guard let payload = pasteboard.string(forType: .string) else {
|
|
699
|
+
return .constructionFailed
|
|
700
|
+
}
|
|
690
701
|
postedPayloads.append(payload)
|
|
691
|
-
return
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|