@hasna/recordings 0.2.11 → 0.2.13
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 +349 -69
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1919 -98
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1973 -236
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
|
@@ -1,7 +1,392 @@
|
|
|
1
|
+
import Foundation
|
|
1
2
|
import Testing
|
|
2
3
|
@testable import RecordingsLib
|
|
3
4
|
|
|
4
5
|
struct ProjectStoreTests {
|
|
6
|
+
private struct SyntheticReadError: LocalizedError {
|
|
7
|
+
var errorDescription: String? { "synthetic read failure" }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@Test("legacy project color survives settings decode and encode")
|
|
11
|
+
func legacyProjectColorRoundTrip() throws {
|
|
12
|
+
let input = Data(##"{"globalSystemPrompt":"Global","postProcessingMode":"always","activeProjectId":"legacy-id","projects":[{"id":"legacy-id","name":"Legacy","path":"/tmp/legacy","systemPrompt":"Keep bullets","appBundleIds":["com.example.app"],"canonicalPath":"recordings-app://projects/legacy-id","color":"#12AB34"}]}"##.utf8)
|
|
13
|
+
|
|
14
|
+
let settings = try JSONDecoder().decode(ProjectSettings.self, from: input)
|
|
15
|
+
let encoded = try JSONEncoder().encode(settings)
|
|
16
|
+
let object = try #require(JSONSerialization.jsonObject(with: encoded) as? [String: Any])
|
|
17
|
+
let projects = try #require(object["projects"] as? [[String: Any]])
|
|
18
|
+
let project = try #require(projects.first)
|
|
19
|
+
|
|
20
|
+
#expect(project["color"] as? String == "#12AB34")
|
|
21
|
+
#expect(project["path"] as? String == "/tmp/legacy")
|
|
22
|
+
#expect(project["systemPrompt"] as? String == "Keep bullets")
|
|
23
|
+
#expect((project["appBundleIds"] as? [String]) == ["com.example.app"])
|
|
24
|
+
#expect(object["postProcessingMode"] as? String == "always")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test("legacy app projects migrate to canonical Store ids without losing metadata")
|
|
28
|
+
@MainActor
|
|
29
|
+
func migratesProjectsToCanonicalStore() async throws {
|
|
30
|
+
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
31
|
+
defer { try? FileManager.default.removeItem(at: root) }
|
|
32
|
+
let bin = root.appendingPathComponent(".bun/bin")
|
|
33
|
+
try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
|
|
34
|
+
let executable = bin.appendingPathComponent("recordings")
|
|
35
|
+
let script = """
|
|
36
|
+
#!/bin/sh
|
|
37
|
+
printf '%s' '{"id":"canonical-id","name":"Legacy","path":"recordings-app://projects/legacy-id"}'
|
|
38
|
+
"""
|
|
39
|
+
try script.write(to: executable, atomically: true, encoding: .utf8)
|
|
40
|
+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
|
|
41
|
+
|
|
42
|
+
let store = ProjectStore(filePath: root.appendingPathComponent("projects.json").path)
|
|
43
|
+
store.settings.projects = [
|
|
44
|
+
RecProject(
|
|
45
|
+
id: "legacy-id",
|
|
46
|
+
name: "Legacy",
|
|
47
|
+
path: nil,
|
|
48
|
+
systemPrompt: "Keep bullets",
|
|
49
|
+
appBundleIds: ["com.example.app"],
|
|
50
|
+
color: "#12AB34"
|
|
51
|
+
)
|
|
52
|
+
]
|
|
53
|
+
store.settings.activeProjectId = "legacy-id"
|
|
54
|
+
try store.save()
|
|
55
|
+
|
|
56
|
+
try await store.reconcileWithCanonicalStore(home: root.path)
|
|
57
|
+
|
|
58
|
+
let migrated = try #require(store.settings.projects.first)
|
|
59
|
+
#expect(migrated.id == "canonical-id")
|
|
60
|
+
#expect(migrated.name == "Legacy")
|
|
61
|
+
#expect(migrated.path == nil)
|
|
62
|
+
#expect(migrated.systemPrompt == "Keep bullets")
|
|
63
|
+
#expect(migrated.appBundleIds == ["com.example.app"])
|
|
64
|
+
#expect(migrated.canonicalPath == "recordings-app://projects/legacy-id")
|
|
65
|
+
#expect(migrated.color == "#12AB34")
|
|
66
|
+
#expect(store.settings.activeProjectId == "canonical-id")
|
|
67
|
+
#expect(store.isReadyForRecording)
|
|
68
|
+
|
|
69
|
+
var edited = migrated
|
|
70
|
+
edited.name = "Edited Legacy"
|
|
71
|
+
try store.updateProject(edited)
|
|
72
|
+
let persisted = try JSONDecoder().decode(
|
|
73
|
+
ProjectSettings.self,
|
|
74
|
+
from: Data(contentsOf: root.appendingPathComponent("projects.json"))
|
|
75
|
+
)
|
|
76
|
+
#expect(persisted.projects.first?.name == "Edited Legacy")
|
|
77
|
+
#expect(persisted.projects.first?.color == "#12AB34")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Test("duplicate legacy project ids reconcile without crashing or duplicating canonical rows")
|
|
81
|
+
@MainActor
|
|
82
|
+
func duplicateProjectIDsAreDeduplicated() async throws {
|
|
83
|
+
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
84
|
+
defer { try? FileManager.default.removeItem(at: root) }
|
|
85
|
+
let bin = root.appendingPathComponent(".bun/bin")
|
|
86
|
+
try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
|
|
87
|
+
let executable = bin.appendingPathComponent("recordings")
|
|
88
|
+
let script = """
|
|
89
|
+
#!/bin/sh
|
|
90
|
+
printf '%s' '{"id":"canonical-id","name":"Legacy","path":"recordings-app://projects/duplicate-id"}'
|
|
91
|
+
"""
|
|
92
|
+
try script.write(to: executable, atomically: true, encoding: .utf8)
|
|
93
|
+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
|
|
94
|
+
|
|
95
|
+
let store = ProjectStore(filePath: root.appendingPathComponent("projects.json").path)
|
|
96
|
+
store.settings.projects = [
|
|
97
|
+
RecProject(id: "duplicate-id", name: "First"),
|
|
98
|
+
RecProject(id: "duplicate-id", name: "Second"),
|
|
99
|
+
]
|
|
100
|
+
store.settings.activeProjectId = "duplicate-id"
|
|
101
|
+
try store.save()
|
|
102
|
+
|
|
103
|
+
try await store.reconcileWithCanonicalStore(home: root.path)
|
|
104
|
+
|
|
105
|
+
#expect(store.settings.projects.count == 1)
|
|
106
|
+
#expect(store.settings.projects.first?.id == "canonical-id")
|
|
107
|
+
#expect(store.settings.activeProjectId == "canonical-id")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@Test("failed canonical registration preserves recording metadata and can be retried")
|
|
111
|
+
@MainActor
|
|
112
|
+
func failedRegistrationDoesNotDisableCaptureForAppLifetime() async throws {
|
|
113
|
+
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
114
|
+
defer { try? FileManager.default.removeItem(at: root) }
|
|
115
|
+
let bin = root.appendingPathComponent(".bun/bin")
|
|
116
|
+
try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
|
|
117
|
+
let executable = bin.appendingPathComponent("recordings")
|
|
118
|
+
let failureScript = """
|
|
119
|
+
#!/bin/sh
|
|
120
|
+
echo 'ERROR: synthetic registration failure' >&2
|
|
121
|
+
exit 1
|
|
122
|
+
"""
|
|
123
|
+
try failureScript.write(to: executable, atomically: true, encoding: .utf8)
|
|
124
|
+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
|
|
125
|
+
|
|
126
|
+
let store = ProjectStore(filePath: root.appendingPathComponent("projects.json").path)
|
|
127
|
+
store.settings.projects = [
|
|
128
|
+
RecProject(id: "local-id", name: "Active Local", systemPrompt: "Keep this context")
|
|
129
|
+
]
|
|
130
|
+
store.settings.activeProjectId = "local-id"
|
|
131
|
+
try store.save()
|
|
132
|
+
|
|
133
|
+
await #expect(throws: (any Error).self) {
|
|
134
|
+
try await store.reconcileWithCanonicalStore(home: root.path)
|
|
135
|
+
}
|
|
136
|
+
#expect(!store.isReadyForRecording)
|
|
137
|
+
#expect(store.synchronizationError?.contains("synthetic registration failure") == true)
|
|
138
|
+
#expect(store.persistenceError?.contains("synthetic registration failure") == true)
|
|
139
|
+
#expect(store.activeProject?.id == "local-id")
|
|
140
|
+
#expect(store.activeCanonicalProjectIdForRecording == nil)
|
|
141
|
+
#expect(store.effectiveSystemPrompt == "Keep this context")
|
|
142
|
+
#expect(RecordingEngine.canBeginRecording(isRecording: false, isTranscribing: false))
|
|
143
|
+
|
|
144
|
+
let successScript = """
|
|
145
|
+
#!/bin/sh
|
|
146
|
+
printf '%s' '{"id":"canonical-id","name":"Active Local","path":"recordings-app://projects/local-id"}'
|
|
147
|
+
"""
|
|
148
|
+
try successScript.write(to: executable, atomically: true, encoding: .utf8)
|
|
149
|
+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
|
|
150
|
+
|
|
151
|
+
try await store.reconcileWithCanonicalStore(home: root.path)
|
|
152
|
+
#expect(store.isReadyForRecording)
|
|
153
|
+
#expect(store.synchronizationError == nil)
|
|
154
|
+
#expect(store.persistenceError == nil)
|
|
155
|
+
#expect(store.activeProject?.id == "canonical-id")
|
|
156
|
+
#expect(store.activeCanonicalProjectIdForRecording == "canonical-id")
|
|
157
|
+
#expect(store.effectiveSystemPrompt == "Keep this context")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@Test("persisted canonical project remains safe during startup reconciliation")
|
|
161
|
+
@MainActor
|
|
162
|
+
func canonicalProjectIdSurvivesStartupDegradedState() throws {
|
|
163
|
+
var settings = ProjectSettings()
|
|
164
|
+
settings.projects = [
|
|
165
|
+
RecProject(
|
|
166
|
+
id: "canonical-id",
|
|
167
|
+
name: "Canonical",
|
|
168
|
+
canonicalPath: "recordings-app://projects/local-id",
|
|
169
|
+
canonicalStoreId: "canonical-id"
|
|
170
|
+
)
|
|
171
|
+
]
|
|
172
|
+
settings.activeProjectId = "canonical-id"
|
|
173
|
+
let data = try JSONEncoder().encode(settings)
|
|
174
|
+
let store = ProjectStore(filePath: "/tmp/projects.json") { _ in data }
|
|
175
|
+
|
|
176
|
+
#expect(!store.isReadyForRecording)
|
|
177
|
+
#expect(store.activeCanonicalProjectIdForRecording == "canonical-id")
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Test("adding a project preserves color in canonical local metadata")
|
|
181
|
+
@MainActor
|
|
182
|
+
func addProjectPreservesColor() async throws {
|
|
183
|
+
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
184
|
+
defer { try? FileManager.default.removeItem(at: root) }
|
|
185
|
+
let bin = root.appendingPathComponent(".bun/bin")
|
|
186
|
+
try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
|
|
187
|
+
let executable = bin.appendingPathComponent("recordings")
|
|
188
|
+
let script = """
|
|
189
|
+
#!/bin/sh
|
|
190
|
+
printf '%s' '{"id":"canonical-added","name":"Added","path":"recordings-app://projects/added"}'
|
|
191
|
+
"""
|
|
192
|
+
try script.write(to: executable, atomically: true, encoding: .utf8)
|
|
193
|
+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
|
|
194
|
+
|
|
195
|
+
let file = root.appendingPathComponent("projects.json")
|
|
196
|
+
let store = ProjectStore(filePath: file.path)
|
|
197
|
+
try await store.addProject(name: "Added", color: "#AABBCC", home: root.path)
|
|
198
|
+
|
|
199
|
+
#expect(store.settings.projects.first?.id == "canonical-added")
|
|
200
|
+
#expect(store.settings.projects.first?.color == "#AABBCC")
|
|
201
|
+
let persisted = try JSONDecoder().decode(ProjectSettings.self, from: Data(contentsOf: file))
|
|
202
|
+
#expect(persisted.projects.first?.color == "#AABBCC")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@Test("project persistence failures remain visible to the UI")
|
|
206
|
+
@MainActor
|
|
207
|
+
func reportsPersistenceFailure() throws {
|
|
208
|
+
// An unwritable path fails at load, so the surfaced message is the load failure —
|
|
209
|
+
// what matters to the UI contract is that a failed save leaves a visible error.
|
|
210
|
+
let store = ProjectStore(filePath: "/dev/null/projects.json")
|
|
211
|
+
store.settings.projects = [RecProject(name: "Cannot Save")]
|
|
212
|
+
|
|
213
|
+
#expect(throws: (any Error).self) {
|
|
214
|
+
try store.save()
|
|
215
|
+
}
|
|
216
|
+
let persistenceError = try #require(store.persistenceError)
|
|
217
|
+
#expect(persistenceError.contains("projects"))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@Test("project mutations are rejected while canonical reconciliation is in flight")
|
|
221
|
+
@MainActor
|
|
222
|
+
func serializesReconciliationAndMutations() async throws {
|
|
223
|
+
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
224
|
+
defer { try? FileManager.default.removeItem(at: root) }
|
|
225
|
+
let bin = root.appendingPathComponent(".bun/bin")
|
|
226
|
+
try FileManager.default.createDirectory(at: bin, withIntermediateDirectories: true)
|
|
227
|
+
let executable = bin.appendingPathComponent("recordings")
|
|
228
|
+
let script = """
|
|
229
|
+
#!/bin/sh
|
|
230
|
+
root="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
231
|
+
touch "$root/started"
|
|
232
|
+
while [ ! -f "$root/release" ]; do sleep 0.01; done
|
|
233
|
+
printf '%s' '{"id":"canonical-id","name":"Legacy","path":"recordings-app://projects/legacy-id"}'
|
|
234
|
+
"""
|
|
235
|
+
try script.write(to: executable, atomically: true, encoding: .utf8)
|
|
236
|
+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
|
|
237
|
+
|
|
238
|
+
let store = ProjectStore(filePath: root.appendingPathComponent("projects.json").path)
|
|
239
|
+
store.settings.projects = [RecProject(id: "legacy-id", name: "Legacy")]
|
|
240
|
+
try store.save()
|
|
241
|
+
let reconciliation = Task { try await store.reconcileWithCanonicalStore(home: root.path) }
|
|
242
|
+
for _ in 0..<200 where !FileManager.default.fileExists(atPath: root.appendingPathComponent("started").path) {
|
|
243
|
+
try await Task.sleep(for: .milliseconds(10))
|
|
244
|
+
}
|
|
245
|
+
#expect(store.isSynchronizingProjects)
|
|
246
|
+
var edited = try #require(store.settings.projects.first)
|
|
247
|
+
edited.name = "Must Not Be Lost"
|
|
248
|
+
#expect(throws: ProjectStoreError.self) {
|
|
249
|
+
try store.updateProject(edited)
|
|
250
|
+
}
|
|
251
|
+
try Data().write(to: root.appendingPathComponent("release"))
|
|
252
|
+
try await reconciliation.value
|
|
253
|
+
#expect(store.settings.projects.first?.name == "Legacy")
|
|
254
|
+
#expect(!store.isSynchronizingProjects)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@Test("project decode failures are visible and prevent readiness")
|
|
258
|
+
@MainActor
|
|
259
|
+
func reportsLoadFailure() async throws {
|
|
260
|
+
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
261
|
+
defer { try? FileManager.default.removeItem(at: root) }
|
|
262
|
+
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
263
|
+
let file = root.appendingPathComponent("projects.json")
|
|
264
|
+
try Data("not-json".utf8).write(to: file)
|
|
265
|
+
|
|
266
|
+
let store = ProjectStore(filePath: file.path)
|
|
267
|
+
|
|
268
|
+
#expect(store.persistenceError?.contains("Failed to load projects") == true)
|
|
269
|
+
#expect(!store.isReadyForRecording)
|
|
270
|
+
#expect(!store.canMutateProjects)
|
|
271
|
+
#expect(throws: ProjectStoreError.self) {
|
|
272
|
+
try store.save()
|
|
273
|
+
}
|
|
274
|
+
await #expect(throws: ProjectStoreError.self) {
|
|
275
|
+
try await store.reconcileWithCanonicalStore(home: root.path)
|
|
276
|
+
}
|
|
277
|
+
#expect(try Data(contentsOf: file) == Data("not-json".utf8))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@Test("unreadable project data blocks every mutation without overwriting the file")
|
|
281
|
+
@MainActor
|
|
282
|
+
func unreadableDataBlocksMutations() async throws {
|
|
283
|
+
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
284
|
+
defer { try? FileManager.default.removeItem(at: root) }
|
|
285
|
+
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
286
|
+
let file = root.appendingPathComponent("projects.json")
|
|
287
|
+
let sentinel = Data("existing-project-data".utf8)
|
|
288
|
+
try sentinel.write(to: file)
|
|
289
|
+
|
|
290
|
+
let store = ProjectStore(filePath: file.path) { _ in
|
|
291
|
+
throw SyntheticReadError()
|
|
292
|
+
}
|
|
293
|
+
store.settings.projects = [RecProject(id: "existing", name: "Existing")]
|
|
294
|
+
|
|
295
|
+
#expect(store.persistenceError?.contains("synthetic read failure") == true)
|
|
296
|
+
#expect(!store.isReadyForRecording)
|
|
297
|
+
#expect(!store.canMutateProjects)
|
|
298
|
+
#expect(throws: ProjectStoreError.self) { try store.save() }
|
|
299
|
+
#expect(throws: ProjectStoreError.self) {
|
|
300
|
+
try store.updateProject(RecProject(id: "existing", name: "Changed"))
|
|
301
|
+
}
|
|
302
|
+
#expect(throws: ProjectStoreError.self) { try store.removeProject(id: "existing") }
|
|
303
|
+
#expect(throws: ProjectStoreError.self) { try store.setActive("existing") }
|
|
304
|
+
await #expect(throws: ProjectStoreError.self) {
|
|
305
|
+
try await store.addProject(name: "Added", home: root.path)
|
|
306
|
+
}
|
|
307
|
+
#expect(try Data(contentsOf: file) == sentinel)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@Test("a project file that becomes unreadable after launch is never overwritten")
|
|
311
|
+
@MainActor
|
|
312
|
+
func postLaunchReadFailureBlocksMutations() throws {
|
|
313
|
+
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
314
|
+
defer { try? FileManager.default.removeItem(at: root) }
|
|
315
|
+
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
316
|
+
let file = root.appendingPathComponent("projects.json")
|
|
317
|
+
let initial = try JSONEncoder().encode(ProjectSettings())
|
|
318
|
+
try initial.write(to: file)
|
|
319
|
+
var readError: SyntheticReadError?
|
|
320
|
+
let store = ProjectStore(filePath: file.path) { path in
|
|
321
|
+
if let readError { throw readError }
|
|
322
|
+
return try Data(contentsOf: URL(fileURLWithPath: path))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
readError = SyntheticReadError()
|
|
326
|
+
store.settings.globalSystemPrompt = "Must not be persisted"
|
|
327
|
+
|
|
328
|
+
#expect(throws: ProjectStoreError.self) { try store.save() }
|
|
329
|
+
#expect(!store.canMutateProjects)
|
|
330
|
+
#expect(store.persistenceError?.contains("synthetic read failure") == true)
|
|
331
|
+
#expect(try Data(contentsOf: file) == initial)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
@Test("external project file changes are never replaced by stale in-memory settings")
|
|
335
|
+
@MainActor
|
|
336
|
+
func externalChangeBlocksMutations() throws {
|
|
337
|
+
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
338
|
+
defer { try? FileManager.default.removeItem(at: root) }
|
|
339
|
+
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
340
|
+
let file = root.appendingPathComponent("projects.json")
|
|
341
|
+
try JSONEncoder().encode(ProjectSettings()).write(to: file)
|
|
342
|
+
let store = ProjectStore(filePath: file.path)
|
|
343
|
+
let external = Data("externally-replaced-data".utf8)
|
|
344
|
+
try external.write(to: file)
|
|
345
|
+
|
|
346
|
+
store.settings.globalSystemPrompt = "Must not replace external data"
|
|
347
|
+
|
|
348
|
+
#expect(throws: ProjectStoreError.self) { try store.save() }
|
|
349
|
+
#expect(!store.canMutateProjects)
|
|
350
|
+
#expect(store.persistenceError?.contains("changed on disk") == true)
|
|
351
|
+
#expect(try Data(contentsOf: file) == external)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
@Test("a stale app instance cannot overwrite a newer project save")
|
|
355
|
+
@MainActor
|
|
356
|
+
func staleStoreCannotOverwriteNewerSave() throws {
|
|
357
|
+
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
358
|
+
defer { try? FileManager.default.removeItem(at: root) }
|
|
359
|
+
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
360
|
+
let file = root.appendingPathComponent("projects.json")
|
|
361
|
+
try JSONEncoder().encode(ProjectSettings()).write(to: file)
|
|
362
|
+
let first = ProjectStore(filePath: file.path)
|
|
363
|
+
let stale = ProjectStore(filePath: file.path)
|
|
364
|
+
|
|
365
|
+
first.settings.globalSystemPrompt = "newer value"
|
|
366
|
+
try first.save()
|
|
367
|
+
let newerData = try Data(contentsOf: file)
|
|
368
|
+
stale.settings.globalSystemPrompt = "stale value"
|
|
369
|
+
|
|
370
|
+
#expect(throws: ProjectStoreError.self) { try stale.save() }
|
|
371
|
+
#expect(!stale.canMutateProjects)
|
|
372
|
+
#expect(try Data(contentsOf: file) == newerData)
|
|
373
|
+
let persisted = try JSONDecoder().decode(ProjectSettings.self, from: newerData)
|
|
374
|
+
#expect(persisted.globalSystemPrompt == "newer value")
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@Test("absent project data remains a writable empty store")
|
|
378
|
+
@MainActor
|
|
379
|
+
func absentDataIsWritable() {
|
|
380
|
+
let file = FileManager.default.temporaryDirectory
|
|
381
|
+
.appendingPathComponent(UUID().uuidString)
|
|
382
|
+
.appendingPathComponent("projects.json")
|
|
383
|
+
let store = ProjectStore(filePath: file.path) { _ in nil }
|
|
384
|
+
|
|
385
|
+
#expect(store.persistenceError == nil)
|
|
386
|
+
#expect(store.isReadyForRecording)
|
|
387
|
+
#expect(store.canMutateProjects)
|
|
388
|
+
}
|
|
389
|
+
|
|
5
390
|
@Test("matchProject by bundle ID")
|
|
6
391
|
func matchByBundleId() {
|
|
7
392
|
let projects = [
|