@hasna/recordings 0.3.13 → 0.4.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.
- package/README.md +223 -37
- package/bun.lock +37 -38
- package/dist/cli/index.js +1884 -615
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +6 -0
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/http/client.d.ts +183 -22
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2858 -2288
- package/dist/lib/audio-artifact-storage.d.ts +98 -0
- package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
- package/dist/lib/capture-probe.d.ts +3 -3
- package/dist/lib/capture-probe.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +132 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/persistence-probe.d.ts +56 -17
- package/dist/lib/persistence-probe.d.ts.map +1 -1
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1689 -456
- package/dist/sdk/index.d.ts +13 -5
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +904 -1
- package/dist/sdk/resolve.d.ts +53 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +6 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/client-ip.d.ts +39 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/cloud-config.d.ts +12 -0
- 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 +20 -2
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +566 -284
- package/dist/server/migrate-command.d.ts +1 -1
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/openapi.d.ts +21 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.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 +1334 -871
- package/dist/store.d.ts +12 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +9 -8
- package/packaging/macos/build_release_pkg.sh +3 -3
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -2
- package/scripts/build_companion_cli.sh +7 -1
- package/scripts/install_macos_app.sh +27 -24
- package/scripts/macos_artifact.ts +20 -16
- package/scripts/migrate.ts +4 -20
- package/scripts/smoke_macos_app.sh +21 -21
- package/src/native/Recordings/App/ContentView.swift +53 -68
- package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
- package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
- package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
- package/src/native/Recordings/App/RecordingsApp.swift +185 -9
- package/src/native/Recordings/App/RecordingsListView.swift +60 -112
- package/src/native/Recordings/App/RecordingsStore.swift +124 -109
- package/src/native/Recordings/App/Theme.swift +73 -61
- package/src/native/Recordings/Package.swift +1 -0
- package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
- package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +316 -34
- package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
- package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +102 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
- package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
- package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
- package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +13 -4
- package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
- package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
- package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
- package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +195 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +30 -0
- package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
- package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
- package/src/native/Recordings/build.sh +6 -6
- package/src/native/Recordings/App/SidebarView.swift +0 -212
|
@@ -8,7 +8,7 @@ struct CLIRunnerTests {
|
|
|
8
8
|
func bundledCLIIsPreferred() throws {
|
|
9
9
|
let root = FileManager.default.temporaryDirectory
|
|
10
10
|
.appendingPathComponent("recordings-bundled-cli-\(UUID().uuidString)")
|
|
11
|
-
let app = root.appendingPathComponent("
|
|
11
|
+
let app = root.appendingPathComponent("Hasna Recordings.app")
|
|
12
12
|
let helper = app.appendingPathComponent("Contents/Helpers/recordings")
|
|
13
13
|
let home = root.appendingPathComponent("home")
|
|
14
14
|
let external = home.appendingPathComponent(".bun/bin/recordings")
|
|
@@ -34,7 +34,7 @@ struct CLIRunnerTests {
|
|
|
34
34
|
func packagedAppDoesNotUseGlobalFallback() throws {
|
|
35
35
|
let root = FileManager.default.temporaryDirectory
|
|
36
36
|
.appendingPathComponent("recordings-missing-companion-\(UUID().uuidString)")
|
|
37
|
-
let app = root.appendingPathComponent("
|
|
37
|
+
let app = root.appendingPathComponent("Hasna Recordings.app")
|
|
38
38
|
let home = root.appendingPathComponent("home")
|
|
39
39
|
let external = home.appendingPathComponent(".bun/bin/recordings")
|
|
40
40
|
try FileManager.default.createDirectory(at: app, withIntermediateDirectories: true)
|
|
@@ -1275,6 +1275,12 @@ struct CLIRunnerTests {
|
|
|
1275
1275
|
#expect(CLIRunner.parseError(input) == "OpenAI quota exceeded — check the OpenAI account billing")
|
|
1276
1276
|
}
|
|
1277
1277
|
|
|
1278
|
+
@Test("Service authentication and quota errors identify the Recordings API")
|
|
1279
|
+
func serviceErrors() {
|
|
1280
|
+
#expect(CLIRunner.parseError("ERROR: 401 Unauthorized", serviceAPI: true) == "Recordings API authentication failed — check the API connection in Settings")
|
|
1281
|
+
#expect(CLIRunner.parseError("ERROR: 429 Too Many Requests", serviceAPI: true) == "Recordings API request limit reached — try again later")
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1278
1284
|
@Test("parseError truncates long messages to 120 chars")
|
|
1279
1285
|
func truncation() {
|
|
1280
1286
|
let longMsg = String(repeating: "a", count: 200)
|
|
@@ -1297,9 +1303,12 @@ struct CLIRunnerTests {
|
|
|
1297
1303
|
#expect(result?.contains(token) == false)
|
|
1298
1304
|
#expect(result?.contains("[REDACTED]") == true)
|
|
1299
1305
|
|
|
1306
|
+
let plainFixture = "plain-synthetic-secret"
|
|
1307
|
+
let clientFixture = "client-synthetic-secret"
|
|
1308
|
+
let passwordFixture = "password-synthetic-secret"
|
|
1300
1309
|
let structured = CLIRunner.parseError(
|
|
1301
|
-
"ERROR: OPENAI_API_KEY
|
|
1302
|
-
+ "PASSWORD
|
|
1310
|
+
"ERROR: OPENAI_API_KEY=\(plainFixture) CLIENT_SECRET=\(clientFixture) "
|
|
1311
|
+
+ "PASSWORD=\(passwordFixture) payload={\"api_key\":\"json-synthetic-secret\","
|
|
1303
1312
|
+ "\"private_key\":\"private-synthetic-secret\"}"
|
|
1304
1313
|
)
|
|
1305
1314
|
#expect(structured?.contains("plain-synthetic-secret") == false)
|
|
@@ -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
|
-
|
|
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("
|
|
104
|
-
func
|
|
104
|
+
@Test("Keychain key precedes legacy preferences and config")
|
|
105
|
+
func keychainPrecedence() throws {
|
|
105
106
|
let home = try makeHome()
|
|
106
|
-
|
|
107
|
-
|
|
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("
|
|
118
|
-
func
|
|
112
|
+
@Test("Save writes only Keychain and removes legacy plaintext while preserving settings")
|
|
113
|
+
func secureSave() throws {
|
|
119
114
|
let home = try makeHome()
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
let data = try Data(contentsOf:
|
|
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"]
|
|
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("
|
|
138
|
-
func
|
|
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": "
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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,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
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import Foundation
|
|
3
|
+
import Testing
|
|
4
|
+
@testable import RecordingsLib
|
|
5
|
+
|
|
6
|
+
private final class ProviderTestRecorder: PCMRecordingSource, @unchecked Sendable {
|
|
7
|
+
private let lock = NSLock()
|
|
8
|
+
private var callback: (@Sendable (Data) -> Void)?
|
|
9
|
+
private(set) var stopped = false
|
|
10
|
+
func attach(_ callback: @escaping @Sendable (Data) -> Void) { lock.withLock { self.callback = callback } }
|
|
11
|
+
func emit(_ data: Data) { lock.withLock { callback }?(data) }
|
|
12
|
+
func start() throws {}
|
|
13
|
+
func stop() { lock.withLock { stopped = true } }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private final class ProviderTestSession: RecordingTranscriptionSession, @unchecked Sendable {
|
|
17
|
+
private let lock = NSLock()
|
|
18
|
+
private var audio = Data()
|
|
19
|
+
private var request: RecordingTranscriptionRequest?
|
|
20
|
+
private var partial: (@Sendable (String) -> Void)?
|
|
21
|
+
private var cancelled = false
|
|
22
|
+
private var gate: CheckedContinuation<RecordingProviderResult, Never>?
|
|
23
|
+
let delayFinish: Bool
|
|
24
|
+
|
|
25
|
+
init(delayFinish: Bool = false) { self.delayFinish = delayFinish }
|
|
26
|
+
var snapshot: (Data, RecordingTranscriptionRequest?, Bool) { lock.withLock { (audio, request, cancelled) } }
|
|
27
|
+
func configure(_ partial: @escaping @Sendable (String) -> Void) { lock.withLock { self.partial = partial } }
|
|
28
|
+
func emitPartial(_ text: String) { lock.withLock { partial }?(text) }
|
|
29
|
+
func appendPCM(_ data: Data) { lock.withLock { if !cancelled { audio.append(data) } } }
|
|
30
|
+
func finish(_ request: RecordingTranscriptionRequest) async throws -> RecordingProviderResult {
|
|
31
|
+
if delayFinish {
|
|
32
|
+
return await withCheckedContinuation { continuation in
|
|
33
|
+
lock.withLock { self.request = request; gate = continuation }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
lock.withLock { self.request = request }
|
|
37
|
+
return RecordingProviderResult(rawText: "spoken words", processedText: "Spoken words.")
|
|
38
|
+
}
|
|
39
|
+
func cancel() { lock.withLock { cancelled = true } }
|
|
40
|
+
func resolve() {
|
|
41
|
+
let continuation = lock.withLock { let value = gate; gate = nil; return value }
|
|
42
|
+
continuation?.resume(returning: RecordingProviderResult(rawText: "late result"))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private struct ProviderTestAdapter: RecordingTranscriptionProvider {
|
|
47
|
+
let session: ProviderTestSession
|
|
48
|
+
func makeSession(configuration: RecordingProviderSessionConfiguration, onPartialTranscript: @escaping @Sendable (String) -> Void) throws -> any RecordingTranscriptionSession {
|
|
49
|
+
#expect(configuration.sampleRate == 24_000)
|
|
50
|
+
#expect(configuration.channels == 1)
|
|
51
|
+
#expect(configuration.bitsPerSample == 16)
|
|
52
|
+
session.configure(onPartialTranscript)
|
|
53
|
+
return session
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@MainActor
|
|
58
|
+
struct RecordingProviderTests {
|
|
59
|
+
private func engine(_ session: ProviderTestSession, recorder: ProviderTestRecorder, suite: String = "recordings-provider-tests-\(UUID().uuidString)") throws -> RecordingEngine {
|
|
60
|
+
let configuration = try RecordingEngineConfiguration(isolatedHomePath: makeIsolatedTestHome("provider"), preferencesSuiteName: suite)
|
|
61
|
+
let engine = RecordingEngine(configuration: configuration, transcriptionProvider: ProviderTestAdapter(session: session))
|
|
62
|
+
engine.autoPasteEnabled = false
|
|
63
|
+
engine.microphoneAuthorization = { .authorized }
|
|
64
|
+
engine.accessibilityTrustCheck = { false }
|
|
65
|
+
engine.frontmostAppSnapshot = { nil }
|
|
66
|
+
engine.focusedWindowTitleLookup = { _ in nil }
|
|
67
|
+
engine.openAIAPIKeyProvider = { Issue.record("Isolated provider consulted legacy credentials"); return "" }
|
|
68
|
+
engine.commandCLI = { _, _, _ in Issue.record("Isolated provider launched legacy CLI"); return "ERROR: forbidden" }
|
|
69
|
+
engine.recorderFactory = { callback in recorder.attach(callback); return recorder }
|
|
70
|
+
return engine
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private func eventually(_ condition: @MainActor () -> Bool) async -> Bool {
|
|
74
|
+
for _ in 0..<200 {
|
|
75
|
+
if condition() { return true }
|
|
76
|
+
try? await Task.sleep(for: .milliseconds(10))
|
|
77
|
+
}
|
|
78
|
+
return condition()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Test("PCM streams before stop, pause excludes samples, and the final WAV matches the stream")
|
|
82
|
+
func liveStreamingAndPause() async throws {
|
|
83
|
+
let session = ProviderTestSession()
|
|
84
|
+
let recorder = ProviderTestRecorder()
|
|
85
|
+
let engine = try engine(session, recorder: recorder)
|
|
86
|
+
engine.startRecording()
|
|
87
|
+
let first = Data(repeating: 1, count: 4_800)
|
|
88
|
+
recorder.emit(first)
|
|
89
|
+
#expect(await eventually { engine.isRecording && session.snapshot.0 == first })
|
|
90
|
+
#expect(session.snapshot.1 == nil, "Streaming must precede finish")
|
|
91
|
+
session.emitPartial("spoken")
|
|
92
|
+
#expect(await eventually { engine.liveTranscriptionText == "spoken" })
|
|
93
|
+
engine.togglePause()
|
|
94
|
+
recorder.emit(Data(repeating: 9, count: 4_800))
|
|
95
|
+
engine.togglePause()
|
|
96
|
+
let tail = Data(repeating: 2, count: 1_600)
|
|
97
|
+
recorder.emit(tail)
|
|
98
|
+
engine.stopAndTranscribe()
|
|
99
|
+
#expect(await eventually { !engine.isTranscribing })
|
|
100
|
+
let request = try #require(session.snapshot.1)
|
|
101
|
+
#expect(session.snapshot.0 == first + tail)
|
|
102
|
+
#expect(request.duration == Double(6_400) / 48_000)
|
|
103
|
+
let wav = try Data(contentsOf: request.audioURL)
|
|
104
|
+
#expect(String(data: wav.prefix(4), encoding: .utf8) == "RIFF")
|
|
105
|
+
#expect(Data(wav.dropFirst(44)) == first + tail)
|
|
106
|
+
let transcript = try #require(engine.recentTranscriptions.first)
|
|
107
|
+
#expect(transcript.captureID == request.captureID)
|
|
108
|
+
#expect(transcript.audioURL == request.audioURL)
|
|
109
|
+
#expect(transcript.rawText == "spoken words")
|
|
110
|
+
#expect(transcript.displayText == "Spoken words.")
|
|
111
|
+
#expect(engine.flowPhase == .ready("Transcript ready — auto-paste is off"))
|
|
112
|
+
#expect(engine.persistedRecordingRevision == 0, "An adapter result is not a persistence receipt")
|
|
113
|
+
#expect(recorder.stopped)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Test("cancel during provider finalization drops late text and allows another capture")
|
|
117
|
+
func cancellationDropsStaleCompletions() async throws {
|
|
118
|
+
let session = ProviderTestSession(delayFinish: true)
|
|
119
|
+
let recorder = ProviderTestRecorder()
|
|
120
|
+
let engine = try engine(session, recorder: recorder)
|
|
121
|
+
engine.startRecording()
|
|
122
|
+
recorder.emit(Data(repeating: 1, count: 4_800))
|
|
123
|
+
#expect(await eventually { engine.isRecording })
|
|
124
|
+
engine.stopAndTranscribe()
|
|
125
|
+
#expect(await eventually { session.snapshot.1 != nil })
|
|
126
|
+
engine.cancelRecording()
|
|
127
|
+
#expect(session.snapshot.2)
|
|
128
|
+
#expect(engine.canStartRecording)
|
|
129
|
+
session.emitPartial("late partial")
|
|
130
|
+
session.resolve()
|
|
131
|
+
try await Task.sleep(for: .milliseconds(50))
|
|
132
|
+
#expect(engine.liveTranscriptionText.isEmpty)
|
|
133
|
+
#expect(engine.recentTranscriptions.isEmpty)
|
|
134
|
+
#expect(engine.flowPhase == .idle)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Test("discard during warm-up cancels the provider without a final request")
|
|
138
|
+
func warmupCancellation() throws {
|
|
139
|
+
let session = ProviderTestSession()
|
|
140
|
+
let recorder = ProviderTestRecorder()
|
|
141
|
+
let engine = try engine(session, recorder: recorder)
|
|
142
|
+
engine.startRecording()
|
|
143
|
+
engine.cancelRecording()
|
|
144
|
+
#expect(session.snapshot.2)
|
|
145
|
+
#expect(session.snapshot.1 == nil)
|
|
146
|
+
#expect(engine.canStartRecording)
|
|
147
|
+
#expect(recorder.stopped)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@Test("preferences are persistent per candidate suite and never write the legacy defaults")
|
|
151
|
+
func preferencesIsolation() throws {
|
|
152
|
+
let keys = ["useFnKey", "intentDetectionEnabled", "recordingsAutoPaste", "recordingsLanguage"]
|
|
153
|
+
let before = keys.map { UserDefaults.standard.object(forKey: $0) as? NSObject }
|
|
154
|
+
let firstSuite = "recordings-provider-tests-\(UUID().uuidString)"
|
|
155
|
+
let secondSuite = "recordings-provider-tests-\(UUID().uuidString)"
|
|
156
|
+
defer {
|
|
157
|
+
UserDefaults(suiteName: firstSuite)?.removePersistentDomain(forName: firstSuite)
|
|
158
|
+
UserDefaults(suiteName: secondSuite)?.removePersistentDomain(forName: secondSuite)
|
|
159
|
+
}
|
|
160
|
+
let first = try engine(ProviderTestSession(), recorder: ProviderTestRecorder(), suite: firstSuite)
|
|
161
|
+
first.useFnKey = true // Must not install any monitor even when the preference changes.
|
|
162
|
+
first.intentDetectionEnabled = true
|
|
163
|
+
first.transcriptionLanguage = "ro"
|
|
164
|
+
first.autoPasteEnabled = true
|
|
165
|
+
let second = try engine(ProviderTestSession(), recorder: ProviderTestRecorder(), suite: secondSuite)
|
|
166
|
+
#expect(!second.useFnKey)
|
|
167
|
+
#expect(!second.intentDetectionEnabled)
|
|
168
|
+
#expect(second.transcriptionLanguage == "en")
|
|
169
|
+
let reloaded = try engine(ProviderTestSession(), recorder: ProviderTestRecorder(), suite: firstSuite)
|
|
170
|
+
#expect(reloaded.useFnKey)
|
|
171
|
+
#expect(reloaded.intentDetectionEnabled)
|
|
172
|
+
#expect(reloaded.transcriptionLanguage == "ro")
|
|
173
|
+
#expect(keys.map { UserDefaults.standard.object(forKey: $0) as? NSObject } == before)
|
|
174
|
+
#expect(!FileManager.default.fileExists(atPath: "\(first.home)/.hasna/recordings/config.json"))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@Test("the isolated initializer rejects the installed app's state and defaults")
|
|
178
|
+
func rejectsLiveRoots() throws {
|
|
179
|
+
#expect(throws: RecordingProviderError.self) {
|
|
180
|
+
try RecordingEngineConfiguration(isolatedHomePath: FileManager.default.homeDirectoryForCurrentUser.path, preferencesSuiteName: "candidate")
|
|
181
|
+
}
|
|
182
|
+
#expect(throws: RecordingProviderError.self) {
|
|
183
|
+
try RecordingEngineConfiguration(isolatedHomePath: makeIsolatedTestHome("provider"), preferencesSuiteName: "com.hasna.recordings")
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@Test("public paste receipts distinguish an unverified post from evidence of failure")
|
|
188
|
+
func typedPasteOutcomes() {
|
|
189
|
+
#expect(RecordingEngine.recentPasteDeliveryStatus(for: .pasted) == .confirmed)
|
|
190
|
+
#expect(RecordingEngine.recentPasteDeliveryStatus(for: .deliveredUnverified(.readBackNotAttempted)) == .unconfirmed)
|
|
191
|
+
#expect(RecordingEngine.recentPasteDeliveryStatus(for: .deliveryNotObserved) == .notDelivered)
|
|
192
|
+
#expect(RecordingEngine.recentPasteDeliveryStatus(for: .clipboardWriteFailed) == .notDelivered)
|
|
193
|
+
#expect(RecordingEngine.recentPasteDeliveryStatus(for: .targetUnavailable) == .notDelivered)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -6,7 +6,7 @@ struct RecordingStartGateTests {
|
|
|
6
6
|
@Test("permission-only launch skips global handlers and terminates after handling")
|
|
7
7
|
func permissionHelperLaunchPlan() {
|
|
8
8
|
let plan = PermissionRequestLaunchPlan(arguments: [
|
|
9
|
-
"/Applications/
|
|
9
|
+
"/Applications/Hasna Recordings.app/Contents/MacOS/Recordings",
|
|
10
10
|
"--request-permissions",
|
|
11
11
|
"--open-permission-settings",
|
|
12
12
|
])
|
|
@@ -11,8 +11,14 @@ private final class FakePCMRecorder: PCMRecordingSource, @unchecked Sendable {
|
|
|
11
11
|
private let lock = NSLock()
|
|
12
12
|
private var startedFlag = false
|
|
13
13
|
private var stoppedFlag = false
|
|
14
|
+
private var stoppingFlag = false
|
|
15
|
+
private let stopGate: DispatchSemaphore?
|
|
14
16
|
private var onPCM: (@Sendable (Data) -> Void)?
|
|
15
17
|
|
|
18
|
+
init(stopGate: DispatchSemaphore? = nil) { self.stopGate = stopGate }
|
|
19
|
+
|
|
20
|
+
var isStopping: Bool { lock.withLock { stoppingFlag } }
|
|
21
|
+
|
|
16
22
|
var started: Bool {
|
|
17
23
|
lock.withLock { startedFlag }
|
|
18
24
|
}
|
|
@@ -46,6 +52,8 @@ private final class FakePCMRecorder: PCMRecordingSource, @unchecked Sendable {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
func stop() {
|
|
55
|
+
lock.withLock { stoppingFlag = true }
|
|
56
|
+
_ = stopGate?.wait(timeout: .now() + 2)
|
|
49
57
|
lock.withLock { stoppedFlag = true }
|
|
50
58
|
}
|
|
51
59
|
}
|
|
@@ -188,6 +196,28 @@ struct RecordingStartTimingTests {
|
|
|
188
196
|
#expect(engine.canStartRecording)
|
|
189
197
|
}
|
|
190
198
|
|
|
199
|
+
@Test("slow microphone shutdown keeps the UI responsive and finishes draining before transcription")
|
|
200
|
+
func stopDoesNotBlockMainActor() async {
|
|
201
|
+
let stopGate = DispatchSemaphore(value: 0)
|
|
202
|
+
let recorder = FakePCMRecorder(stopGate: stopGate)
|
|
203
|
+
let engine = makeStartableEngine(recorder: recorder)
|
|
204
|
+
engine.startRecording(trigger: .manual)
|
|
205
|
+
#expect(await confirmCapture(engine, recorder))
|
|
206
|
+
|
|
207
|
+
let started = ContinuousClock.now
|
|
208
|
+
engine.stopAndTranscribe()
|
|
209
|
+
#expect(ContinuousClock.now - started < .milliseconds(300))
|
|
210
|
+
#expect(await waitUntil { recorder.isStopping })
|
|
211
|
+
#expect(engine.isTranscribing)
|
|
212
|
+
#expect(!engine.canStartRecording)
|
|
213
|
+
#expect(!recorder.isStopped)
|
|
214
|
+
|
|
215
|
+
stopGate.signal()
|
|
216
|
+
#expect(await waitUntil { !engine.isTranscribing })
|
|
217
|
+
#expect(recorder.isStopped)
|
|
218
|
+
#expect(engine.statusMessage == "No audio captured")
|
|
219
|
+
}
|
|
220
|
+
|
|
191
221
|
@Test("a released key before the capture resolves still cancels cleanly")
|
|
192
222
|
func cancelDuringPendingCapture() async {
|
|
193
223
|
let captureGate = DispatchSemaphore(value: 0)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Testing
|
|
3
|
+
@testable import RecordingsLib
|
|
4
|
+
|
|
5
|
+
struct ServiceAPIConfigurationTests {
|
|
6
|
+
@Test("a configurable service prefix keeps exactly one v1 segment", arguments: [
|
|
7
|
+
"https://api.example.com/recordings", "https://api.example.com/recordings/v1/",
|
|
8
|
+
])
|
|
9
|
+
func normalizesServicePrefix(_ input: String) throws {
|
|
10
|
+
#expect(try ServiceAPIConfiguration.normalizedURL(input) == "https://api.example.com/recordings/v1")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Test("invalid or credential-bearing URLs are rejected", arguments: [
|
|
14
|
+
"", "https://user:password@example.com/v1", "http://example.com/v1",
|
|
15
|
+
"https://example.com/v1?key=value", "https://example.com/v1#fragment",
|
|
16
|
+
])
|
|
17
|
+
func rejectsUnsafeURL(_ input: String) {
|
|
18
|
+
#expect(throws: (any Error).self) { try ServiceAPIConfiguration.normalizedURL(input) }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Test("Finder launches receive saved routing and an endpoint-bound Keychain credential")
|
|
22
|
+
func savedConnectionEnvironment() throws {
|
|
23
|
+
let suite = "recordings-api-test-\(UUID().uuidString)"
|
|
24
|
+
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
25
|
+
defer { defaults.removePersistentDomain(forName: suite) }
|
|
26
|
+
defaults.set("https://service.example.test/recordings/v1/", forKey: ServiceAPIConfiguration.defaultsKey)
|
|
27
|
+
var lookedUp: [String] = []
|
|
28
|
+
let environment = try ServiceAPIConfiguration.childEnvironment(base: ["PATH": "/bin"], defaults: defaults) { endpoint in
|
|
29
|
+
lookedUp.append(endpoint)
|
|
30
|
+
return "fixture-service-credential"
|
|
31
|
+
}
|
|
32
|
+
#expect(lookedUp == ["https://service.example.test/recordings/v1"])
|
|
33
|
+
#expect(environment["HASNA_RECORDINGS_API_URL"] == lookedUp.first)
|
|
34
|
+
#expect(environment["HASNA_RECORDINGS_CLIENT_STORE"] == nil)
|
|
35
|
+
#expect(environment["HASNA_RECORDINGS_API_KEY_OVERRIDE"] != nil)
|
|
36
|
+
#expect(environment["OPENAI_API_KEY"] == nil)
|
|
37
|
+
#expect(environment["PATH"] == "/bin")
|
|
38
|
+
#expect(defaults.string(forKey: "HASNA_RECORDINGS_API_KEY_OVERRIDE") == nil)
|
|
39
|
+
for base in [["HASNA_RECORDINGS_API_URL": "https://another.example.test/v1"], ["HASNA_RECORDINGS_LOCAL": "1"], ["RECORDINGS_LOCAL": "1"]] {
|
|
40
|
+
let overridden = try ServiceAPIConfiguration.childEnvironment(base: base, defaults: defaults) { _ in
|
|
41
|
+
Issue.record("Environment routing must not read the saved endpoint credential")
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
#expect(overridden == base)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test("an unconfigured native client invents no endpoint or local fallback")
|
|
49
|
+
func noDefaultEndpoint() throws {
|
|
50
|
+
let suite = "recordings-empty-api-test-\(UUID().uuidString)"
|
|
51
|
+
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
52
|
+
defer { defaults.removePersistentDomain(forName: suite) }
|
|
53
|
+
#expect(try ServiceAPIConfiguration.childEnvironment(base: [:], defaults: defaults) { _ in
|
|
54
|
+
Issue.record("No endpoint is configured")
|
|
55
|
+
return nil
|
|
56
|
+
}.isEmpty)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@MainActor
|
|
60
|
+
@Test("global recording preferences ignore legacy project selections and prompts")
|
|
61
|
+
func ignoresLegacyProjects() {
|
|
62
|
+
let home = makeIsolatedTestHome("global-recording-preferences")
|
|
63
|
+
let preferences = ProjectStore(filePath: "\(home)/projects.json")
|
|
64
|
+
preferences.settings.globalSystemPrompt = "Global cleanup"
|
|
65
|
+
preferences.settings.postProcessingMode = "always"
|
|
66
|
+
preferences.settings.projects = [RecProject(id: "legacy", name: "Legacy", systemPrompt: "Project cleanup")]
|
|
67
|
+
preferences.settings.activeProjectId = "legacy"
|
|
68
|
+
let engine = RecordingEngine(homePath: home, installsGlobalHandlers: false)
|
|
69
|
+
engine.globalRecordingPreferences = preferences
|
|
70
|
+
#expect(engine.projectStore == nil)
|
|
71
|
+
#expect(engine.recordingCleanupPreferences.prompt == "Global cleanup")
|
|
72
|
+
#expect(engine.recordingCleanupPreferences.mode == "always")
|
|
73
|
+
#expect(preferences.settings.activeProjectId == "legacy")
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import Testing
|
|
3
|
+
@testable import RecordingsLib
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
struct SettingsWindowControllerTests {
|
|
7
|
+
@Test("Settings creates one window and reopens it after closing")
|
|
8
|
+
func reusesSettingsWindow() {
|
|
9
|
+
let policy = NSApplication.shared.activationPolicy()
|
|
10
|
+
var creations = 0
|
|
11
|
+
let controller = SettingsWindowController {
|
|
12
|
+
creations += 1
|
|
13
|
+
return NSView(frame: NSRect(x: 0, y: 0, width: 520, height: 500))
|
|
14
|
+
}
|
|
15
|
+
#expect(creations == 0)
|
|
16
|
+
let first = controller.show()
|
|
17
|
+
defer { first.close() }
|
|
18
|
+
#expect(first.isVisible)
|
|
19
|
+
#expect(first.title == "Hasna Recordings Settings")
|
|
20
|
+
first.close()
|
|
21
|
+
#expect(!first.isVisible)
|
|
22
|
+
let reopened = controller.show()
|
|
23
|
+
#expect(reopened === first)
|
|
24
|
+
#expect(reopened.isVisible)
|
|
25
|
+
#expect(creations == 1)
|
|
26
|
+
#expect(NSApplication.shared.activationPolicy() == policy)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -51,7 +51,7 @@ enum RecordingsBootstrapPreflight {
|
|
|
51
51
|
let manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestData)
|
|
52
52
|
try validateManifest(manifest, payload: payload)
|
|
53
53
|
|
|
54
|
-
let app = arguments.payloadRoot + "/Applications/
|
|
54
|
+
let app = arguments.payloadRoot + "/Applications/Hasna Recordings.app"
|
|
55
55
|
let client = app + "/" + RecordingsUpdateConstants.updateClientRelativePath
|
|
56
56
|
let broker = arguments.payloadRoot + "/Library/PrivilegedHelperTools/com.hasna.recordings.updater"
|
|
57
57
|
let artifactVerifier = arguments.payloadRoot
|
|
@@ -7,7 +7,7 @@ import RecordingsUpdateProtocol
|
|
|
7
7
|
/// operation; first installs use exclusive rename semantics and can never replace a
|
|
8
8
|
/// leaf created concurrently in `/Applications`.
|
|
9
9
|
final class ApplicationNamespace {
|
|
10
|
-
private static let liveLeaf = "
|
|
10
|
+
private static let liveLeaf = "Hasna Recordings.app"
|
|
11
11
|
private static let previousLeaf = "previous.app"
|
|
12
12
|
private static let failedCandidateLeaf = "failed-candidate.app"
|
|
13
13
|
|
|
@@ -22,7 +22,7 @@ final class ApplicationNamespace {
|
|
|
22
22
|
.deletingLastPathComponent
|
|
23
23
|
guard journal.applicationPath == RecordingsUpdateConstants.applicationPath,
|
|
24
24
|
journal.candidateApplicationPath == expectedTransactionDirectory
|
|
25
|
-
+ "/candidate/
|
|
25
|
+
+ "/candidate/Hasna Recordings.app",
|
|
26
26
|
journal.previousApplicationPath == nil ||
|
|
27
27
|
journal.previousApplicationPath == expectedTransactionDirectory + "/previous.app"
|
|
28
28
|
else {
|
|
@@ -276,7 +276,7 @@ final class UpdateBrokerSession: NSObject, RecordingsUpdateXPCProtocol {
|
|
|
276
276
|
guard mkdir(candidateRoot, 0o700) == 0 else {
|
|
277
277
|
throw BrokerOperationError.candidateRejected
|
|
278
278
|
}
|
|
279
|
-
let candidateApplication = candidateRoot + "/
|
|
279
|
+
let candidateApplication = candidateRoot + "/Hasna Recordings.app"
|
|
280
280
|
let verifierIDs = try verifierAccountIDs()
|
|
281
281
|
try CanonicalTreeCopier.copyApplication(
|
|
282
282
|
from: verifierOutput,
|
|
@@ -22,7 +22,7 @@ enum CanonicalTreeCopier {
|
|
|
22
22
|
}
|
|
23
23
|
defer { Darwin.close(source) }
|
|
24
24
|
let destinationRoot = URL(fileURLWithPath: rootCandidatePath).deletingLastPathComponent().path
|
|
25
|
-
guard URL(fileURLWithPath: rootCandidatePath).lastPathComponent == "
|
|
25
|
+
guard URL(fileURLWithPath: rootCandidatePath).lastPathComponent == "Hasna Recordings.app" else {
|
|
26
26
|
throw CanonicalCopyError.nonCanonicalRoot
|
|
27
27
|
}
|
|
28
28
|
let destination = Darwin.open(
|