@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
@@ -86,7 +86,8 @@ struct OpenAIAPIKeyStoreTests {
86
86
  .appendingPathComponent("hasnaxyz")
87
87
  .appendingPathComponent("openai")
88
88
  try FileManager.default.createDirectory(at: secretDir, withIntermediateDirectories: true)
89
- try "export OPENAI_API_KEY='secret-key'\n".write(
89
+ let fixture = "secret-key"
90
+ try "export OPENAI_API_KEY='\(fixture)'\n".write(
90
91
  to: secretDir.appendingPathComponent("live.env"),
91
92
  atomically: true,
92
93
  encoding: .utf8
@@ -100,53 +101,57 @@ struct OpenAIAPIKeyStoreTests {
100
101
  #expect(key == "secret-key")
101
102
  }
102
103
 
103
- @Test("Saving a key writes it to config.json so the CLI uses the same key")
104
- func saveWritesConfig() throws {
104
+ @Test("Keychain key precedes legacy preferences and config")
105
+ func keychainPrecedence() throws {
105
106
  let home = try makeHome()
106
-
107
- try OpenAIAPIKeyStore.save(key: "sk-new-key", homePath: home.path)
108
-
109
- let key = OpenAIAPIKeyStore.load(
110
- homePath: home.path,
111
- environment: [:],
112
- userDefaultKey: nil
113
- )
114
- #expect(key == "sk-new-key")
107
+ try writeConfig(home: home, ["openai_api_key": "old-config"])
108
+ #expect(OpenAIAPIKeyStore.load(homePath: home.path, environment: [:],
109
+ userDefaultKey: "old-default", keychainLoader: { "keychain-fixture" }) == "keychain-fixture")
115
110
  }
116
111
 
117
- @Test("Saving a key preserves unrelated config.json fields")
118
- func savePreservesOtherFields() throws {
112
+ @Test("Save writes only Keychain and removes legacy plaintext while preserving settings")
113
+ func secureSave() throws {
119
114
  let home = try makeHome()
120
- try writeConfig(home: home, [
121
- "openai_api_key": "old-key",
122
- "transcription_model": "gpt-4o-transcribe",
123
- ])
124
-
125
- try OpenAIAPIKeyStore.save(key: "sk-rotated", homePath: home.path)
126
-
127
- let configURL = home
128
- .appendingPathComponent(".hasna")
129
- .appendingPathComponent("recordings")
130
- .appendingPathComponent("config.json")
131
- let data = try Data(contentsOf: configURL)
115
+ let suite = "provider-test-\(UUID().uuidString)"
116
+ let defaults = try #require(UserDefaults(suiteName: suite))
117
+ defer { defaults.removePersistentDomain(forName: suite) }
118
+ defaults.set("old-default", forKey: "openAIAPIKey")
119
+ try writeConfig(home: home, ["openai_api_key": "old-key", "api_key": "older-key",
120
+ "transcription_model": "gpt-4o-transcribe"])
121
+ var stored = ""
122
+ try OpenAIAPIKeyStore.save(key: " keychain-fixture ", homePath: home.path,
123
+ defaults: defaults, keychainWriter: { stored = $0 })
124
+ #expect(stored == "keychain-fixture")
125
+ #expect(defaults.string(forKey: "openAIAPIKey") == nil)
126
+ let data = try Data(contentsOf: home.appendingPathComponent(".hasna/recordings/config.json"))
132
127
  let json = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
133
- #expect(json["openai_api_key"] as? String == "sk-rotated")
128
+ #expect(json["openai_api_key"] == nil)
129
+ #expect(json["api_key"] == nil)
134
130
  #expect(json["transcription_model"] as? String == "gpt-4o-transcribe")
131
+ #expect(String(data: data, encoding: .utf8)?.contains(stored) == false)
135
132
  }
136
133
 
137
- @Test("Saving an empty key removes it from config.json")
138
- func saveEmptyRemovesKey() throws {
134
+ @Test("Keychain failure preserves existing configuration")
135
+ func failedSave() throws {
139
136
  let home = try makeHome()
140
- try writeConfig(home: home, ["openai_api_key": "old-key"])
141
-
142
- try OpenAIAPIKeyStore.save(key: " ", homePath: home.path)
137
+ try writeConfig(home: home, ["openai_api_key": "legacy-fixture"])
138
+ #expect(throws: OpenAIAPIKeyStore.KeychainFailure.self) {
139
+ try OpenAIAPIKeyStore.save(key: "new-fixture", homePath: home.path,
140
+ keychainWriter: { _ in throw OpenAIAPIKeyStore.KeychainFailure(status: -50) })
141
+ }
142
+ #expect(OpenAIAPIKeyStore.load(homePath: home.path, environment: [:], userDefaultKey: nil) == "legacy-fixture")
143
+ }
143
144
 
144
- let key = OpenAIAPIKeyStore.load(
145
- homePath: home.path,
146
- environment: [:],
147
- userDefaultKey: nil
148
- )
149
- #expect(key == "")
145
+ @Test("Provider key reaches the helper without becoming service authentication")
146
+ func separateProviderEnvironment() {
147
+ let result = OpenAIAPIKeyStore.childEnvironment(base: [
148
+ "RECORDINGS_API_KEY": "provider-fixture",
149
+ "HASNA_RECORDINGS_API_KEY_OVERRIDE": "service-fixture",
150
+ ], homePath: "/tmp/test-home", keyLoader: { "provider-fixture" })
151
+ #expect(result["RECORDINGS_API_KEY"] == nil)
152
+ #expect(result["OPENAI_API_KEY"] == "provider-fixture")
153
+ #expect(result["RECORDINGS_OPENAI_API_KEY"] == "provider-fixture")
154
+ #expect(result["HASNA_RECORDINGS_API_KEY_OVERRIDE"] == "service-fixture")
150
155
  }
151
156
 
152
157
  private func makeHome() throws -> URL {
@@ -0,0 +1,215 @@
1
+ import Darwin
2
+ import Foundation
3
+ import Testing
4
+
5
+ /// Test-only observation of the original pipes, independent of transient pre-exec copies.
6
+ enum PipeClosureFixture {
7
+ static func now() -> UInt64 {
8
+ var value = timespec()
9
+ precondition(clock_gettime(CLOCK_MONOTONIC, &value) == 0)
10
+ return UInt64(value.tv_sec) * 1_000_000_000 + UInt64(value.tv_nsec)
11
+ }
12
+
13
+ static func request(pid: pid_t, deadlineFile: URL) throws -> UInt64 {
14
+ let deadline = now() + 3_000_000_000
15
+ try String(deadline).write(to: deadlineFile, atomically: true, encoding: .utf8)
16
+ try #require(Darwin.kill(pid, SIGUSR1) == 0)
17
+ return deadline
18
+ }
19
+
20
+ static func marker(at file: URL) -> String? {
21
+ guard let value = try? String(contentsOf: file, encoding: .utf8), !value.isEmpty else { return nil }
22
+ return value
23
+ }
24
+
25
+ static func waitForMarker(at file: URL, deadline: UInt64) -> String? {
26
+ while now() < deadline {
27
+ if let value = marker(at: file), now() < deadline { return value }
28
+ Thread.sleep(forTimeInterval: 0.01)
29
+ }
30
+ return nil
31
+ }
32
+
33
+ // Shared by both silent-holder fixtures and the deterministic observer controls.
34
+ // Success is published atomically only when both EPIPE observations are before the
35
+ // same parent-supplied deadline. Retry the observation, never the command or test.
36
+ static let observerSource = #"""
37
+ #include <errno.h>
38
+ #include <fcntl.h>
39
+ #include <limits.h>
40
+ #include <stdint.h>
41
+ #include <stdio.h>
42
+ #include <time.h>
43
+ #include <unistd.h>
44
+
45
+ static uint64_t monotonic_ns(void) {
46
+ struct timespec value;
47
+ if (clock_gettime(CLOCK_MONOTONIC, &value)) _exit(70);
48
+ return (uint64_t)value.tv_sec * 1000000000ULL + (uint64_t)value.tv_nsec;
49
+ }
50
+
51
+ static int observePipeClosure(const char *deadlinePath, const char *markerPath,
52
+ const char *detailsPath, const char *connectedPath, void (*owners)(FILE *, int)) {
53
+ unsigned long long deadline;
54
+ FILE *request = fopen(deadlinePath, "r");
55
+ if (!request) return 71;
56
+ int parsed = fscanf(request, "%llu", &deadline);
57
+ fclose(request);
58
+ if (parsed != 1) return 72;
59
+ for (int fd = STDOUT_FILENO; fd <= STDERR_FILENO; fd++) {
60
+ int flags = fcntl(fd, F_GETFL);
61
+ if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) return 73;
62
+ }
63
+ ssize_t results[2] = {0, 0}, first[2] = {0, 0};
64
+ int errors[2] = {0, 0}, firstErrors[2] = {0, 0}, broken[2] = {0, 0};
65
+ unsigned attempts = 0;
66
+ uint64_t observedAt = 0;
67
+ int invalid = 0;
68
+ while (monotonic_ns() < deadline) {
69
+ for (int i = 0; i < 2; i++) {
70
+ if (broken[i]) continue;
71
+ errno = 0;
72
+ results[i] = write(STDOUT_FILENO + i, "x", 1);
73
+ errors[i] = errno;
74
+ broken[i] = results[i] == -1 && errors[i] == EPIPE;
75
+ if (results[i] != 1 && !broken[i] &&
76
+ !(results[i] == -1 && (errors[i] == EINTR || errors[i] == EAGAIN))) invalid = 1;
77
+ }
78
+ observedAt = monotonic_ns();
79
+ if (attempts++ == 0) {
80
+ for (int i = 0; i < 2; i++) { first[i] = results[i]; firstErrors[i] = errors[i]; }
81
+ if (connectedPath && !broken[0] && !broken[1] && !invalid) {
82
+ FILE *connected = fopen(connectedPath, "w");
83
+ if (!connected) return 74;
84
+ fputs("connected", connected);
85
+ fclose(connected);
86
+ }
87
+ }
88
+ if (invalid || (broken[0] && broken[1]) || observedAt >= deadline) break;
89
+ uint64_t remaining = deadline - observedAt;
90
+ struct timespec delay = {0, (long)(remaining < 10000000 ? remaining : 10000000)};
91
+ nanosleep(&delay, NULL);
92
+ }
93
+ int success = !invalid && broken[0] && broken[1] && observedAt < deadline;
94
+ FILE *details = detailsPath ? fopen(detailsPath, "w") : NULL;
95
+ if (details) {
96
+ fprintf(details, "first_stdout=%ld,errno=%d; first_stderr=%ld,errno=%d; stdout=%ld,errno=%d; stderr=%ld,errno=%d; attempts=%u; observed_ns=%llu; deadline_ns=%llu",
97
+ (long)first[0], firstErrors[0], (long)first[1], firstErrors[1],
98
+ (long)results[0], errors[0], (long)results[1], errors[1], attempts,
99
+ (unsigned long long)observedAt, deadline);
100
+ if (!success && owners) { owners(details, STDOUT_FILENO); owners(details, STDERR_FILENO); }
101
+ fclose(details);
102
+ }
103
+ char pending[PATH_MAX];
104
+ int length = snprintf(pending, sizeof(pending), "%s.pending", markerPath);
105
+ if (length < 0 || length >= (int)sizeof(pending)) return 75;
106
+ FILE *marker = fopen(pending, "w");
107
+ if (!marker) return 76;
108
+ fputs(success ? "both-epipe" : invalid ? "probe-error" : "still-connected", marker);
109
+ if (fclose(marker) || rename(pending, markerPath)) return 77;
110
+ return 0;
111
+ }
112
+ """#
113
+ }
114
+
115
+ extension CLIRunnerTests {
116
+ @Test("pipe closure observer distinguishes transient readers from one persistent reader", arguments: [false, true])
117
+ func pipeClosureObserverControls(persistentReader: Bool) throws {
118
+ let root = FileManager.default.temporaryDirectory
119
+ .appendingPathComponent("recordings-pipe-observer-\(UUID().uuidString)")
120
+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
121
+ defer { try? FileManager.default.removeItem(at: root) }
122
+ let source = root.appendingPathComponent("observer.c")
123
+ let executable = root.appendingPathComponent("observer")
124
+ let ready = root.appendingPathComponent("ready")
125
+ let deadlineFile = root.appendingPathComponent("deadline")
126
+ let marker = root.appendingPathComponent("result")
127
+ let details = root.appendingPathComponent("details")
128
+ let connected = root.appendingPathComponent("connected")
129
+ try """
130
+ #include <signal.h>
131
+ \(PipeClosureFixture.observerSource)
132
+ static volatile sig_atomic_t requested = 0;
133
+ static void requestProbe(int signo) { (void)signo; requested = 1; }
134
+ int main(int argc, char **argv) {
135
+ if (argc != 6) return 64;
136
+ signal(SIGPIPE, SIG_IGN);
137
+ signal(SIGUSR1, requestProbe);
138
+ FILE *ready = fopen(argv[1], "w");
139
+ if (!ready) return 65;
140
+ fputs("ready", ready);
141
+ fclose(ready);
142
+ while (!requested) {
143
+ struct timespec delay = {0, 1000000};
144
+ nanosleep(&delay, NULL);
145
+ }
146
+ return observePipeClosure(argv[2], argv[3], argv[4], argv[5], NULL);
147
+ }
148
+ """.write(to: source, atomically: true, encoding: .utf8)
149
+ let compile = Process()
150
+ compile.executableURL = URL(fileURLWithPath: "/usr/bin/cc")
151
+ compile.arguments = ["-Wall", "-Wextra", "-Werror", "-o", executable.path, source.path]
152
+ try compile.run()
153
+ compile.waitUntilExit()
154
+ try #require(compile.terminationStatus == 0)
155
+
156
+ let output = Pipe(), error = Pipe()
157
+ var leases: [Int32] = []
158
+ defer {
159
+ for descriptor in leases where descriptor >= 0 { _ = Darwin.close(descriptor) }
160
+ for handle in [output.fileHandleForReading, output.fileHandleForWriting,
161
+ error.fileHandleForReading, error.fileHandleForWriting] { try? handle.close() }
162
+ }
163
+ for pipe in [output, error] {
164
+ let descriptor = Darwin.fcntl(pipe.fileHandleForReading.fileDescriptor, F_DUPFD_CLOEXEC, 3)
165
+ try #require(descriptor >= 0)
166
+ leases.append(descriptor)
167
+ }
168
+ let child = Process()
169
+ child.executableURL = executable
170
+ child.arguments = [ready.path, deadlineFile.path, marker.path, details.path, connected.path]
171
+ child.standardOutput = output
172
+ child.standardError = error
173
+ try child.run()
174
+ defer {
175
+ if child.isRunning { _ = Darwin.kill(child.processIdentifier, SIGKILL) }
176
+ child.waitUntilExit()
177
+ }
178
+ try output.fileHandleForReading.close()
179
+ try error.fileHandleForReading.close()
180
+ try output.fileHandleForWriting.close()
181
+ try error.fileHandleForWriting.close()
182
+ try #require(PipeClosureFixture.waitForMarker(at: ready, deadline: PipeClosureFixture.now() + 3_000_000_000) == "ready")
183
+ let deadline = try PipeClosureFixture.request(pid: child.processIdentifier, deadlineFile: deadlineFile)
184
+ // The observer must actually see retained readers before this test releases
185
+ // either lease. No delay or scheduling assumption substitutes for that handshake.
186
+ try #require(PipeClosureFixture.waitForMarker(at: connected, deadline: deadline) == "connected")
187
+ try #require(Darwin.close(leases[0]) == 0)
188
+ leases[0] = -1
189
+ if !persistentReader {
190
+ try #require(Darwin.close(leases[1]) == 0)
191
+ leases[1] = -1
192
+ #expect(PipeClosureFixture.waitForMarker(at: marker, deadline: deadline) == "both-epipe")
193
+ }
194
+ // This extra second is only for reaping/reporting the negative control after
195
+ // its original deadline expires. It cannot admit a late successful observation.
196
+ while child.isRunning && PipeClosureFixture.now() < deadline + 1_000_000_000 {
197
+ Thread.sleep(forTimeInterval: 0.01)
198
+ }
199
+ try #require(!child.isRunning)
200
+ child.waitUntilExit()
201
+ #expect(child.terminationStatus == 0)
202
+ let observations = try String(contentsOf: details, encoding: .utf8)
203
+ #expect(observations.contains("first_stdout=1,errno=0; first_stderr=1,errno=0"))
204
+ if persistentReader {
205
+ #expect(PipeClosureFixture.now() >= deadline)
206
+ #expect(PipeClosureFixture.marker(at: marker) == "still-connected")
207
+ #expect(observations.contains("stdout=-1,errno=\(EPIPE); stderr=1,errno=0"))
208
+ try #require(Darwin.close(leases[1]) == 0)
209
+ leases[1] = -1
210
+ #expect(PipeClosureFixture.marker(at: marker) == "still-connected")
211
+ } else {
212
+ #expect(observations.contains("stdout=-1,errno=\(EPIPE); stderr=-1,errno=\(EPIPE)"))
213
+ }
214
+ }
215
+ }
@@ -27,7 +27,7 @@ struct ProjectStoreTests {
27
27
  @Test("legacy app projects migrate to canonical Store ids without losing metadata")
28
28
  @MainActor
29
29
  func migratesProjectsToCanonicalStore() async throws {
30
- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
30
+ let root = URL(fileURLWithPath: makeIsolatedTestHome("project-store"), isDirectory: true)
31
31
  defer { try? FileManager.default.removeItem(at: root) }
32
32
  let bin = root.appendingPathComponent(".bun/bin")
33
33
  try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
@@ -80,7 +80,7 @@ struct ProjectStoreTests {
80
80
  @Test("duplicate legacy project ids reconcile without crashing or duplicating canonical rows")
81
81
  @MainActor
82
82
  func duplicateProjectIDsAreDeduplicated() async throws {
83
- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
83
+ let root = URL(fileURLWithPath: makeIsolatedTestHome("project-store"), isDirectory: true)
84
84
  defer { try? FileManager.default.removeItem(at: root) }
85
85
  let bin = root.appendingPathComponent(".bun/bin")
86
86
  try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
@@ -110,7 +110,7 @@ struct ProjectStoreTests {
110
110
  @Test("failed canonical registration preserves recording metadata and can be retried")
111
111
  @MainActor
112
112
  func failedRegistrationDoesNotDisableCaptureForAppLifetime() async throws {
113
- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
113
+ let root = URL(fileURLWithPath: makeIsolatedTestHome("project-store"), isDirectory: true)
114
114
  defer { try? FileManager.default.removeItem(at: root) }
115
115
  let bin = root.appendingPathComponent(".bun/bin")
116
116
  try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
@@ -180,7 +180,7 @@ struct ProjectStoreTests {
180
180
  @Test("adding a project preserves color in canonical local metadata")
181
181
  @MainActor
182
182
  func addProjectPreservesColor() async throws {
183
- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
183
+ let root = URL(fileURLWithPath: makeIsolatedTestHome("project-store"), isDirectory: true)
184
184
  defer { try? FileManager.default.removeItem(at: root) }
185
185
  let bin = root.appendingPathComponent(".bun/bin")
186
186
  try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
@@ -220,7 +220,7 @@ struct ProjectStoreTests {
220
220
  @Test("project mutations are rejected while canonical reconciliation is in flight")
221
221
  @MainActor
222
222
  func serializesReconciliationAndMutations() async throws {
223
- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
223
+ let root = URL(fileURLWithPath: makeIsolatedTestHome("project-store"), isDirectory: true)
224
224
  defer { try? FileManager.default.removeItem(at: root) }
225
225
  let bin = root.appendingPathComponent(".bun/bin")
226
226
  try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
@@ -257,7 +257,7 @@ struct ProjectStoreTests {
257
257
  @Test("project decode failures are visible and prevent readiness")
258
258
  @MainActor
259
259
  func reportsLoadFailure() async throws {
260
- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
260
+ let root = URL(fileURLWithPath: makeIsolatedTestHome("project-store"), isDirectory: true)
261
261
  defer { try? FileManager.default.removeItem(at: root) }
262
262
  try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
263
263
  let file = root.appendingPathComponent("projects.json")
@@ -280,7 +280,7 @@ struct ProjectStoreTests {
280
280
  @Test("unreadable project data blocks every mutation without overwriting the file")
281
281
  @MainActor
282
282
  func unreadableDataBlocksMutations() async throws {
283
- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
283
+ let root = URL(fileURLWithPath: makeIsolatedTestHome("project-store"), isDirectory: true)
284
284
  defer { try? FileManager.default.removeItem(at: root) }
285
285
  try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
286
286
  let file = root.appendingPathComponent("projects.json")
@@ -310,7 +310,7 @@ struct ProjectStoreTests {
310
310
  @Test("a project file that becomes unreadable after launch is never overwritten")
311
311
  @MainActor
312
312
  func postLaunchReadFailureBlocksMutations() throws {
313
- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
313
+ let root = URL(fileURLWithPath: makeIsolatedTestHome("project-store"), isDirectory: true)
314
314
  defer { try? FileManager.default.removeItem(at: root) }
315
315
  try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
316
316
  let file = root.appendingPathComponent("projects.json")
@@ -334,7 +334,7 @@ struct ProjectStoreTests {
334
334
  @Test("external project file changes are never replaced by stale in-memory settings")
335
335
  @MainActor
336
336
  func externalChangeBlocksMutations() throws {
337
- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
337
+ let root = URL(fileURLWithPath: makeIsolatedTestHome("project-store"), isDirectory: true)
338
338
  defer { try? FileManager.default.removeItem(at: root) }
339
339
  try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
340
340
  let file = root.appendingPathComponent("projects.json")
@@ -354,7 +354,7 @@ struct ProjectStoreTests {
354
354
  @Test("a stale app instance cannot overwrite a newer project save")
355
355
  @MainActor
356
356
  func staleStoreCannotOverwriteNewerSave() throws {
357
- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
357
+ let root = URL(fileURLWithPath: makeIsolatedTestHome("project-store"), isDirectory: true)
358
358
  defer { try? FileManager.default.removeItem(at: root) }
359
359
  try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
360
360
  let file = root.appendingPathComponent("projects.json")
@@ -0,0 +1,60 @@
1
+ import Foundation
2
+ import Testing
3
+ @testable import RecordingsLib
4
+
5
+ struct CaptureMonitorTests {
6
+ @Test func pauseExcludesAudioAndDuration() {
7
+ let monitor = CaptureMonitor()
8
+ let chunk = Data(repeating: 64, count: 4_800)
9
+ #expect(monitor.admit(chunk))
10
+ monitor.setPaused(true)
11
+ #expect(!monitor.admit(chunk))
12
+ #expect(monitor.snapshot().duration == 0.1)
13
+ monitor.setPaused(false)
14
+ #expect(monitor.admit(chunk))
15
+ #expect(monitor.snapshot().duration == 0.2)
16
+ }
17
+
18
+ @Test func meterReflectsActualPCM() {
19
+ let monitor = CaptureMonitor()
20
+ #expect(monitor.admit(Data(repeating: 0, count: 4_800)))
21
+ #expect(monitor.snapshot().level == 0)
22
+ #expect(monitor.admit(Data(repeating: 64, count: 4_800)))
23
+ #expect(monitor.snapshot().level > 0.5)
24
+ }
25
+ }
26
+
27
+ @MainActor
28
+ struct RecorderSurfaceDeliveryTests {
29
+ @Test func discardingPausedCaptureRestoresIdleControls() {
30
+ let engine = RecordingEngine(homePath: makeIsolatedTestHome("discard-paused"), installsGlobalHandlers: false)
31
+ engine.configureVisualPreview()
32
+ engine.togglePause()
33
+ engine.cancelRecording()
34
+ #expect(!engine.isPaused)
35
+ #expect(engine.recordingDuration == 0)
36
+ #expect(engine.audioLevel == 0)
37
+ }
38
+
39
+ @Test func disabledAutoPasteNeverReachesPasteBoundary() async {
40
+ let engine = RecordingEngine(homePath: makeIsolatedTestHome("auto-paste-off"))
41
+ engine.autoPasteEnabled = false
42
+ defer { engine.autoPasteEnabled = true }
43
+ var pasted = false
44
+ engine.pasteInterceptorForTesting = { _, _, _ in pasted = true }
45
+ let generation = engine.beginPipelineForTesting()
46
+ await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
47
+ engine.finishWithText("Blue sky", rawTranscript: "Blue sky", targetAppBundleIdentifier: nil,
48
+ targetAppPid: nil, selectionToken: nil, canonicalProjectId: nil, activeProjectId: nil,
49
+ activeProjectName: nil,
50
+ processingConfiguration: RecordingProcessingConfiguration(transcriptionPrompt: "", transcriberPrompt: "",
51
+ postProcessingMode: "off", transcriptionLanguage: "en", transcriptionModel: "test", transcriberModel: "test",
52
+ enhancementModel: "test", intentModel: "test", intentDetectionEnabled: false,
53
+ enhanceTriggersJSON: "[]", keywordTransformsJSON: "{}"),
54
+ pipelineTrace: nil, pipelineGeneration: generation, deliveryCompleted: { continuation.resume() })
55
+ }
56
+ #expect(!pasted)
57
+ #expect(engine.recentTranscriptions.first?.displayText == "Blue sky")
58
+ #expect(engine.statusMessage == "Transcript ready — auto-paste is off")
59
+ }
60
+ }
@@ -0,0 +1,26 @@
1
+ import Foundation
2
+ import Testing
3
+ @testable import RecordingsLib
4
+
5
+ struct RecordingDateTests {
6
+ @Test("History accepts PostgreSQL, ISO, and SQLite dates", arguments: [
7
+ "2026-09-06 10:52:42.523193+00",
8
+ "2026-09-06 10:52:42+00",
9
+ "2026-09-06 16:22:42.523193+05:30",
10
+ "2026-09-06 16:22:42+0530",
11
+ "2026-09-06 06:52:42-04",
12
+ "2026-09-06T10:52:42.523Z",
13
+ "2026-09-06T10:52:42Z",
14
+ "2026-09-06 10:52:42",
15
+ ])
16
+ func supportedDates(value: String) throws {
17
+ let expected = try #require(ISO8601DateFormatter().date(from: "2026-09-06T10:52:42Z"))
18
+ let actual = try #require(Recording.parseDate(value))
19
+ #expect(abs(actual.timeIntervalSince(expected)) < 1)
20
+ }
21
+
22
+ @Test("Missing and invalid dates remain absent", arguments: ["", "invalid", "2026-09-06 10:52:42+99"])
23
+ func invalidDates(value: String) {
24
+ #expect(Recording.parseDate(value) == nil)
25
+ }
26
+ }
@@ -67,7 +67,7 @@ private func makeEngine(
67
67
  accessibilityTrusted: Bool = true,
68
68
  pasteRecorder: PasteRecorder? = nil
69
69
  ) -> RecordingEngine {
70
- let engine = RecordingEngine(homePath: makeIsolatedTestHome("delivery-tests"))
70
+ let engine = RecordingEngine(homePath: makeIsolatedTestHome("delivery-tests"), installsGlobalHandlers: false)
71
71
  engine.openAIAPIKeyProvider = { "" }
72
72
  engine.microphoneAuthorization = { .denied }
73
73
  engine.accessibilityTrustCheck = { accessibilityTrusted }
@@ -0,0 +1,50 @@
1
+ import AVFoundation
2
+ import Foundation
3
+ import Testing
4
+ @testable import RecordingsLib
5
+
6
+ private struct TargetNoopProvider: RecordingTranscriptionProvider {
7
+ func makeSession(configuration: RecordingProviderSessionConfiguration, onPartialTranscript: @escaping @Sendable (String) -> Void) throws -> any RecordingTranscriptionSession {
8
+ throw RecordingProviderError.noAudio
9
+ }
10
+ }
11
+ @MainActor struct RecordingFrozenPasteTargetTests {
12
+ private let a = PasteApplicationObservation(pid: 90101, bundleIdentifier: "example.editor", launchDate: Date(timeIntervalSince1970: 100), name: "Editor")
13
+ private let b = PasteApplicationObservation(pid: 90102, bundleIdentifier: "example.notes", launchDate: Date(timeIntervalSince1970: 200), name: "Notes")
14
+ private func engine() throws -> RecordingEngine {
15
+ let configuration = try RecordingEngineConfiguration(isolatedHomePath: makeIsolatedTestHome("frozen-target"), preferencesSuiteName: "example.frozen-tests.\(UUID().uuidString)")
16
+ let engine = RecordingEngine(configuration: configuration, transcriptionProvider: TargetNoopProvider())
17
+ engine.microphoneAuthorization = { .denied }
18
+ engine.accessibilityTrustCheck = { false }
19
+ engine.protectedOperationTrust = { AccessibilityTrustResult(trusted: true, didPrompt: false) }
20
+ engine.focusedWindowTitleLookup = { _ in nil }
21
+ engine.selectionCapture = { _ in Issue.record("No Accessibility reads allowed"); return nil }
22
+ engine.frontmostAppSnapshot = { FrontmostAppSnapshot(pid: ProcessInfo.processInfo.processIdentifier, bundleIdentifier: "example.recorder", launchDate: Date()) }
23
+ engine.pasteTargetApplicationLookup = { pid in pid == a.pid ? a : b }
24
+ return engine
25
+ }
26
+ private func candidates(_ apps: [PasteApplicationObservation]) -> [PasteTargetCandidate] {
27
+ apps.map { PasteTargetCandidate(pid: $0.pid, bundleIdentifier: $0.bundleIdentifier, isRegularApp: $0.isRegular, launchDate: $0.launchDate) }
28
+ }
29
+ @Test func explicitObservedTargetSurvivesSelfFocusAndLaterAppFocus() throws {
30
+ let engine = try engine()
31
+ let target = try #require(RecordingPasteTarget(observation: a, currentPID: ProcessInfo.processInfo.processIdentifier))
32
+ engine.startRecording(pasteTarget: .frozen(target))
33
+ let generation = engine.recordingGeneration
34
+ #expect(engine.resolvePasteTarget(candidates: candidates([a, b]), targetBundleIdentifier: a.bundleIdentifier, targetPid: a.pid, frontmostPid: b.pid, pipelineGeneration: generation)?.pid == a.pid)
35
+ var reused = a; reused.launchDate = Date(timeIntervalSince1970: 101)
36
+ #expect(engine.resolvePasteTarget(candidates: candidates([reused, b]), targetBundleIdentifier: a.bundleIdentifier, targetPid: a.pid, frontmostPid: b.pid, pipelineGeneration: generation) == nil)
37
+ #expect(engine.resolvePasteTarget(candidates: candidates([b]), targetBundleIdentifier: a.bundleIdentifier, targetPid: a.pid, frontmostPid: b.pid, pipelineGeneration: generation) == nil)
38
+ var helper = a; helper.isRegular = false
39
+ #expect(engine.resolvePasteTarget(candidates: candidates([helper]), targetBundleIdentifier: a.bundleIdentifier, targetPid: a.pid, frontmostPid: a.pid, pipelineGeneration: generation) == nil)
40
+ }
41
+ @Test func explicitNilAndInvalidAtStartNeverAdoptTheFinishTimeApp() throws {
42
+ for target in [nil, RecordingPasteTarget(observation: a, currentPID: ProcessInfo.processInfo.processIdentifier)] {
43
+ let engine = try engine(); engine.pasteTargetApplicationLookup = { _ in nil }
44
+ engine.startRecording(pasteTarget: .frozen(target))
45
+ #expect(engine.resolvePasteTarget(candidates: candidates([b]), targetBundleIdentifier: nil, targetPid: nil, frontmostPid: b.pid, pipelineGeneration: engine.recordingGeneration) == nil)
46
+ }
47
+ let legacy = try engine(); legacy.startRecording()
48
+ #expect(legacy.resolvePasteTarget(candidates: candidates([b]), targetBundleIdentifier: nil, targetPid: nil, frontmostPid: b.pid, pipelineGeneration: legacy.recordingGeneration)?.pid == b.pid, "Omitting explicit selection preserves the legacy default")
49
+ }
50
+ }
@@ -0,0 +1,48 @@
1
+ import Foundation
2
+ import Testing
3
+ @testable import RecordingsLib
4
+
5
+ @MainActor struct RecordingPasteTargetTrackerTests {
6
+ private let a = PasteApplicationObservation(pid: 801, bundleIdentifier: "example.editor", launchDate: Date(timeIntervalSince1970: 100), name: "Editor")
7
+ private let b = PasteApplicationObservation(pid: 802, bundleIdentifier: "example.notes", launchDate: Date(timeIntervalSince1970: 200), name: "Notes")
8
+ private let own = PasteApplicationObservation(pid: 900, bundleIdentifier: "example.recorder", launchDate: Date(timeIntervalSince1970: 300))
9
+
10
+ @Test func remembersObservedExternalAcrossRecorderActivationAndFreezesValue() throws {
11
+ var frontmost = a
12
+ let tracker = RecordingPasteTargetTracker(currentPID: own.pid, frontmost: { frontmost }, lookup: { pid in pid == a.pid ? a : b })
13
+ frontmost = own; tracker.observe(own)
14
+ let frozen = try #require(tracker.snapshot())
15
+ #expect(frozen.processIdentifier == a.pid)
16
+ #expect(frozen.bundleIdentifier == a.bundleIdentifier)
17
+ #expect(frozen.launchDate == a.launchDate)
18
+ frontmost = b; tracker.observe(b)
19
+ #expect(tracker.snapshot()?.processIdentifier == b.pid)
20
+ #expect(frozen.processIdentifier == a.pid, "Later focus cannot mutate a capture's value")
21
+ }
22
+ @Test func refusesUnobservedSelfNonregularIncompleteAndTerminatedApps() {
23
+ let tracker = RecordingPasteTargetTracker(currentPID: own.pid, frontmost: { own }, lookup: { _ in a })
24
+ #expect(tracker.snapshot() == nil)
25
+ for invalid in [PasteApplicationObservation(pid: 803, bundleIdentifier: "example.helper", launchDate: Date(), isRegular: false),
26
+ PasteApplicationObservation(pid: 804, bundleIdentifier: nil, launchDate: Date()),
27
+ PasteApplicationObservation(pid: 805, bundleIdentifier: "example.unknown", launchDate: nil),
28
+ PasteApplicationObservation(pid: 806, bundleIdentifier: "example.dead", launchDate: Date(), isTerminated: true)] {
29
+ tracker.observe(invalid); #expect(tracker.snapshot() == nil)
30
+ }
31
+ }
32
+ @Test func refusesPIDReuseAndDoesNotRecoverAnOlderApp() throws {
33
+ var live = b
34
+ let tracker = RecordingPasteTargetTracker(currentPID: own.pid, frontmost: { own }, lookup: { _ in live })
35
+ tracker.observe(a); tracker.observe(b)
36
+ #expect(tracker.snapshot()?.processIdentifier == b.pid)
37
+ live.launchDate = Date(timeIntervalSince1970: 201)
38
+ #expect(tracker.snapshot() == nil)
39
+ live = b
40
+ #expect(tracker.snapshot() == nil, "Revalidation failure consumes the remembered destination")
41
+ tracker.observe(b)
42
+ var oldIncarnation = b; oldIncarnation.launchDate = Date(timeIntervalSince1970: 199)
43
+ tracker.terminated(oldIncarnation)
44
+ #expect(tracker.snapshot() != nil)
45
+ tracker.terminated(b)
46
+ #expect(tracker.snapshot() == nil)
47
+ }
48
+ }