native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #18 from onevcat/startup/repository-snapshot-cache

Add repository snapshot startup cache

authored by

Wei Wang and committed by
GitHub
b5d57ee0 8a700c80

+533 -2
+79
doc-onevcat/plans/2026-03-20-repository-snapshot-cache-design.md
··· 1 + # Repository Snapshot Cache Design 2 + 3 + ## Goal 4 + 5 + Add a small startup cache that restores repository UI immediately on app launch, while keeping live repository discovery as the only source of truth. 6 + 7 + ## Principles 8 + 9 + - Keep the cache disposable. 10 + - Keep the payload small and structural. 11 + - Do not let bad cache data affect settings loading. 12 + - Always run a normal live refresh after cache restore. 13 + - Only overwrite the cache after a complete successful live load. 14 + 15 + ## Storage 16 + 17 + Use a standalone JSON file at `~/.prowl/repository-snapshot.json`. 18 + 19 + Reasoning: 20 + 21 + - Cache decode failures stay isolated from `settings.json`. 22 + - The file can be deleted safely with no migration burden. 23 + - The payload can evolve with an explicit schema version. 24 + 25 + ## Payload 26 + 27 + Persist only data needed for first paint: 28 + 29 + - repositories in UI order 30 + - repository root path 31 + - repository display name 32 + - worktree name 33 + - worktree detail string 34 + - worktree working-directory path 35 + - worktree `createdAt` 36 + 37 + Do not cache: 38 + 39 + - PR state 40 + - line changes 41 + - watcher state 42 + - notifications 43 + - `lastFocusedRepositoryID` 44 + 45 + Selection restoration continues to use existing `lastFocusedWorktreeID` persistence. 46 + 47 + ## Invalidation 48 + 49 + Treat the cache as a miss when any of the following happens: 50 + 51 + - file is missing or empty 52 + - schema version mismatch 53 + - JSON decode failure 54 + - any cached repository root path no longer exists 55 + - any cached worktree path no longer exists 56 + 57 + When invalid, discard the cache file and continue with a normal live load. 58 + 59 + ## Startup Flow 60 + 61 + 1. Load pinned/archive/order/last-focused persisted state. 62 + 2. Load repository snapshot cache. 63 + 3. If snapshot exists, restore repositories into state immediately. 64 + 4. Mark initial load complete so the main UI renders. 65 + 5. Start the usual live repository loading flow. 66 + 6. Apply live results to the UI. 67 + 7. If the live load succeeds with no repository failures, overwrite the snapshot file. 68 + 69 + ## Refresh Rules 70 + 71 + Overwrite the snapshot only after complete successful repository loads: 72 + 73 + - initial startup refresh 74 + - manual refresh 75 + - other flows that end in a full successful repository snapshot 76 + 77 + Do not overwrite the snapshot on partial or failed loads. 78 + 79 + No TTL is needed because the cache is only a startup accelerator. Freshness comes from the unconditional live refresh that always runs after startup.
+193 -1
supacode/Clients/Repositories/RepositoryPersistenceClient.swift
··· 15 15 var saveWorktreeOrderByRepository: @Sendable ([Repository.ID: [Worktree.ID]]) async -> Void 16 16 var loadLastFocusedWorktreeID: @Sendable () async -> Worktree.ID? 17 17 var saveLastFocusedWorktreeID: @Sendable (Worktree.ID?) async -> Void 18 + var loadRepositorySnapshot: @Sendable () async -> [Repository]? 19 + var saveRepositorySnapshot: @Sendable ([Repository]) async -> Void 18 20 } 19 21 20 22 extension RepositoryPersistenceClient: DependencyKey { ··· 82 84 $sharedLastFocused.withLock { 83 85 $0 = id 84 86 } 87 + }, 88 + loadRepositorySnapshot: { 89 + let snapshotURL = SupacodePaths.repositorySnapshotURL 90 + guard let data = try? Data(contentsOf: snapshotURL) else { 91 + return nil 92 + } 93 + guard !data.isEmpty else { 94 + discardRepositorySnapshot(at: snapshotURL) 95 + return nil 96 + } 97 + let decoder = JSONDecoder() 98 + do { 99 + let payload = try await MainActor.run { 100 + try decoder.decode(RepositorySnapshotCachePayload.self, from: data) 101 + } 102 + guard let repositories = await MainActor.run( 103 + resultType: [Repository]?.self, 104 + body: { 105 + payload.restoreRepositories( 106 + pathExists: { FileManager.default.fileExists(atPath: $0) } 107 + ) 108 + } 109 + ) else { 110 + discardRepositorySnapshot(at: snapshotURL) 111 + return nil 112 + } 113 + return repositories 114 + } catch { 115 + repositoryPersistenceLogger.warning( 116 + "Unable to decode repository snapshot cache: \(error.localizedDescription)" 117 + ) 118 + discardRepositorySnapshot(at: snapshotURL) 119 + return nil 120 + } 121 + }, 122 + saveRepositorySnapshot: { repositories in 123 + let snapshotURL = SupacodePaths.repositorySnapshotURL 124 + guard !repositories.isEmpty else { 125 + discardRepositorySnapshot(at: snapshotURL) 126 + return 127 + } 128 + do { 129 + try FileManager.default.createDirectory( 130 + at: SupacodePaths.baseDirectory, 131 + withIntermediateDirectories: true 132 + ) 133 + let encoder = JSONEncoder() 134 + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 135 + let payload = await MainActor.run { 136 + RepositorySnapshotCachePayload(repositories: repositories) 137 + } 138 + let data = try await MainActor.run { 139 + try encoder.encode(payload) 140 + } 141 + try data.write(to: snapshotURL, options: .atomic) 142 + } catch { 143 + repositoryPersistenceLogger.warning( 144 + "Unable to write repository snapshot cache: \(error.localizedDescription)" 145 + ) 146 + } 85 147 } 86 148 ) 87 149 }() ··· 97 159 loadWorktreeOrderByRepository: { [:] }, 98 160 saveWorktreeOrderByRepository: { _ in }, 99 161 loadLastFocusedWorktreeID: { nil }, 100 - saveLastFocusedWorktreeID: { _ in } 162 + saveLastFocusedWorktreeID: { _ in }, 163 + loadRepositorySnapshot: { nil }, 164 + saveRepositorySnapshot: { _ in } 101 165 ) 102 166 } 103 167 ··· 106 170 get { self[RepositoryPersistenceClient.self] } 107 171 set { self[RepositoryPersistenceClient.self] = newValue } 108 172 } 173 + } 174 + 175 + private nonisolated let repositoryPersistenceLogger = SupaLogger("Repositories") 176 + 177 + private nonisolated func discardRepositorySnapshot(at url: URL) { 178 + guard FileManager.default.fileExists(atPath: url.path(percentEncoded: false)) else { 179 + return 180 + } 181 + do { 182 + try FileManager.default.removeItem(at: url) 183 + } catch { 184 + repositoryPersistenceLogger.warning( 185 + "Unable to remove repository snapshot cache: \(error.localizedDescription)" 186 + ) 187 + } 188 + } 189 + 190 + struct RepositorySnapshotCachePayload: Codable, Equatable, Sendable { 191 + static let currentVersion = 1 192 + 193 + let version: Int 194 + let repositories: [SnapshotRepository] 195 + 196 + init(repositories: [Repository]) { 197 + version = Self.currentVersion 198 + self.repositories = repositories.map { SnapshotRepository(repository: $0) } 199 + } 200 + 201 + func restoreRepositories( 202 + pathExists: @Sendable (String) -> Bool 203 + ) -> [Repository]? { 204 + guard version == Self.currentVersion, !repositories.isEmpty else { 205 + return nil 206 + } 207 + 208 + var restored: [Repository] = [] 209 + restored.reserveCapacity(repositories.count) 210 + 211 + for repository in repositories { 212 + guard let restoredRepository = repository.restore(pathExists: pathExists) else { 213 + return nil 214 + } 215 + restored.append(restoredRepository) 216 + } 217 + 218 + return restored 219 + } 220 + } 221 + 222 + extension RepositorySnapshotCachePayload { 223 + struct SnapshotRepository: Codable, Equatable, Sendable { 224 + let rootPath: String 225 + let name: String 226 + let worktrees: [SnapshotWorktree] 227 + 228 + init(repository: Repository) { 229 + rootPath = repository.rootURL.path(percentEncoded: false) 230 + name = repository.name 231 + worktrees = repository.worktrees.map { SnapshotWorktree(worktree: $0) } 232 + } 233 + 234 + func restore( 235 + pathExists: @Sendable (String) -> Bool 236 + ) -> Repository? { 237 + guard let normalizedRootPath = normalizePath(rootPath), pathExists(normalizedRootPath) else { 238 + return nil 239 + } 240 + 241 + let rootURL = URL(fileURLWithPath: normalizedRootPath).standardizedFileURL 242 + var restoredWorktrees: [Worktree] = [] 243 + restoredWorktrees.reserveCapacity(worktrees.count) 244 + 245 + for worktree in worktrees { 246 + guard let restoredWorktree = worktree.restore( 247 + repositoryRootURL: rootURL, 248 + pathExists: pathExists 249 + ) else { 250 + return nil 251 + } 252 + restoredWorktrees.append(restoredWorktree) 253 + } 254 + 255 + let repositoryName = name.trimmingCharacters(in: .whitespacesAndNewlines) 256 + return Repository( 257 + id: normalizedRootPath, 258 + rootURL: rootURL, 259 + name: repositoryName.isEmpty ? Repository.name(for: rootURL) : repositoryName, 260 + worktrees: IdentifiedArray(uniqueElements: restoredWorktrees) 261 + ) 262 + } 263 + } 264 + 265 + struct SnapshotWorktree: Codable, Equatable, Sendable { 266 + let name: String 267 + let detail: String 268 + let workingDirectoryPath: String 269 + let createdAt: Date? 270 + 271 + init(worktree: Worktree) { 272 + name = worktree.name 273 + detail = worktree.detail 274 + workingDirectoryPath = worktree.workingDirectory.path(percentEncoded: false) 275 + createdAt = worktree.createdAt 276 + } 277 + 278 + func restore( 279 + repositoryRootURL: URL, 280 + pathExists: @Sendable (String) -> Bool 281 + ) -> Worktree? { 282 + guard let normalizedPath = normalizePath(workingDirectoryPath), pathExists(normalizedPath) else { 283 + return nil 284 + } 285 + 286 + let worktreeURL = URL(fileURLWithPath: normalizedPath).standardizedFileURL 287 + return Worktree( 288 + id: normalizedPath, 289 + name: name, 290 + detail: detail, 291 + workingDirectory: worktreeURL, 292 + repositoryRootURL: repositoryRootURL, 293 + createdAt: createdAt 294 + ) 295 + } 296 + } 297 + } 298 + 299 + private func normalizePath(_ path: String) -> String? { 300 + RepositoryPathNormalizer.normalize([path]).first 109 301 } 110 302 111 303 nonisolated enum RepositoryOrderNormalizer {
+55
supacode/Features/Repositories/Reducer/RepositoriesFeature.swift
··· 123 123 124 124 enum Action { 125 125 case task 126 + case repositorySnapshotLoaded([Repository]?) 126 127 case setOpenPanelPresented(Bool) 127 128 case loadPersistedRepositories 128 129 case pinnedWorktreeIDsLoaded([Worktree.ID]) ··· 314 315 let repositoryOrderIDs = await repositoryPersistence.loadRepositoryOrderIDs() 315 316 let worktreeOrderByRepository = 316 317 await repositoryPersistence.loadWorktreeOrderByRepository() 318 + let repositorySnapshot = await repositoryPersistence.loadRepositorySnapshot() 317 319 await send(.pinnedWorktreeIDsLoaded(pinned)) 318 320 await send(.archivedWorktreeIDsLoaded(archived)) 319 321 await send(.repositoryOrderIDsLoaded(repositoryOrderIDs)) 320 322 await send(.worktreeOrderByRepositoryLoaded(worktreeOrderByRepository)) 321 323 await send(.lastFocusedWorktreeIDLoaded(lastFocused)) 324 + await send(.repositorySnapshotLoaded(repositorySnapshot)) 322 325 await send(.loadPersistedRepositories) 323 326 } 324 327 328 + case .repositorySnapshotLoaded(let repositories): 329 + guard let repositories, !repositories.isEmpty else { 330 + return .none 331 + } 332 + state.isRefreshingWorktrees = false 333 + let roots = repositories.map(\.rootURL) 334 + let previousSelection = state.selectedWorktreeID 335 + let previousSelectedWorktree = state.worktree(for: previousSelection) 336 + let incomingRepositories = IdentifiedArray(uniqueElements: repositories) 337 + let repositoriesChanged = incomingRepositories != state.repositories 338 + _ = applyRepositories( 339 + repositories, 340 + roots: roots, 341 + shouldPruneArchivedWorktreeIDs: true, 342 + state: &state, 343 + animated: false 344 + ) 345 + state.repositoryRoots = roots 346 + state.isInitialLoadComplete = true 347 + state.loadFailuresByID = [:] 348 + let selectedWorktree = state.worktree(for: state.selectedWorktreeID) 349 + let selectionChanged = selectionDidChange( 350 + previousSelectionID: previousSelection, 351 + previousSelectedWorktree: previousSelectedWorktree, 352 + selectedWorktreeID: state.selectedWorktreeID, 353 + selectedWorktree: selectedWorktree 354 + ) 355 + var allEffects: [Effect<Action>] = [] 356 + if repositoriesChanged { 357 + allEffects.append(.send(.delegate(.repositoriesChanged(state.repositories)))) 358 + } 359 + if selectionChanged { 360 + allEffects.append(.send(.delegate(.selectedWorktreeChanged(selectedWorktree)))) 361 + } 362 + return .merge(allEffects) 363 + 325 364 case .pinnedWorktreeIDsLoaded(let pinnedWorktreeIDs): 326 365 state.pinnedWorktreeIDs = pinnedWorktreeIDs 327 366 return .none ··· 440 479 } 441 480 ) 442 481 } 482 + if failures.isEmpty { 483 + let repositories = Array(state.repositories) 484 + allEffects.append( 485 + .run { _ in 486 + await repositoryPersistence.saveRepositorySnapshot(repositories) 487 + } 488 + ) 489 + } 443 490 return .merge(allEffects) 444 491 445 492 case .openRepositories(let urls): ··· 538 585 allEffects.append( 539 586 .run { _ in 540 587 await repositoryPersistence.saveArchivedWorktreeIDs(archivedWorktreeIDs) 588 + } 589 + ) 590 + } 591 + if failures.isEmpty { 592 + let repositories = Array(state.repositories) 593 + allEffects.append( 594 + .run { _ in 595 + await repositoryPersistence.saveRepositorySnapshot(repositories) 541 596 } 542 597 ) 543 598 }
+4
supacode/Support/SupacodePaths.swift
··· 93 93 baseDirectory.appending(path: "settings.json", directoryHint: .notDirectory) 94 94 } 95 95 96 + static var repositorySnapshotURL: URL { 97 + baseDirectory.appending(path: "repository-snapshot.json", directoryHint: .notDirectory) 98 + } 99 + 96 100 static func repositorySettingsURL(for rootURL: URL) -> URL { 97 101 repositorySettingsDirectory(for: rootURL) 98 102 .appending(path: "prowl.json", directoryHint: .notDirectory)
+7 -1
supacodeTests/RepositoriesFeaturePersistenceTests.swift
··· 45 45 calls.withValue { $0.append("loadLastFocusedWorktreeID") } 46 46 return nil 47 47 }, 48 - saveLastFocusedWorktreeID: { _ in } 48 + saveLastFocusedWorktreeID: { _ in }, 49 + loadRepositorySnapshot: { 50 + calls.withValue { $0.append("loadRepositorySnapshot") } 51 + return nil 52 + }, 53 + saveRepositorySnapshot: { _ in } 49 54 ) 50 55 } 51 56 ··· 59 64 "loadLastFocusedWorktreeID", 60 65 "loadRepositoryOrderIDs", 61 66 "loadWorktreeOrderByRepository", 67 + "loadRepositorySnapshot", 62 68 "loadRoots", 63 69 ]) 64 70 }
+145
supacodeTests/RepositoriesFeatureTests.swift
··· 65 65 } 66 66 } 67 67 68 + @Test func taskRestoresRepositorySnapshotBeforeLiveRefreshCompletes() async { 69 + let repoRoot = "/tmp/repo" 70 + let worktree = makeWorktree(id: "\(repoRoot)/main", name: "main", repoRoot: repoRoot) 71 + let repository = makeRepository(id: repoRoot, worktrees: [worktree]) 72 + let worktreeID = worktree.id 73 + let liveRefreshGate = AsyncGate() 74 + 75 + let store = TestStore(initialState: RepositoriesFeature.State()) { 76 + RepositoriesFeature() 77 + } withDependencies: { 78 + $0.repositoryPersistence.loadLastFocusedWorktreeID = { worktreeID } 79 + $0.repositoryPersistence.loadRepositorySnapshot = { [repository] } 80 + $0.repositoryPersistence.loadRoots = { [repoRoot] } 81 + $0.repositoryPersistence.saveRepositorySnapshot = { _ in } 82 + $0.gitClient.worktrees = { _ in 83 + await liveRefreshGate.wait() 84 + return [worktree] 85 + } 86 + } 87 + 88 + await store.send(.task) 89 + await store.receive(\.pinnedWorktreeIDsLoaded) 90 + await store.receive(\.archivedWorktreeIDsLoaded) 91 + await store.receive(\.repositoryOrderIDsLoaded) 92 + await store.receive(\.worktreeOrderByRepositoryLoaded) 93 + await store.receive(\.lastFocusedWorktreeIDLoaded) { 94 + $0.lastFocusedWorktreeID = worktreeID 95 + $0.shouldRestoreLastFocusedWorktree = true 96 + } 97 + await store.receive(\.repositorySnapshotLoaded) { 98 + $0.repositories = [repository] 99 + $0.repositoryRoots = [URL(fileURLWithPath: repoRoot)] 100 + $0.selection = .worktree(worktreeID) 101 + $0.shouldRestoreLastFocusedWorktree = false 102 + $0.isInitialLoadComplete = true 103 + } 104 + await store.receive(\.delegate.repositoriesChanged) 105 + await store.receive(\.delegate.selectedWorktreeChanged) 106 + await store.receive(\.loadPersistedRepositories) 107 + 108 + await liveRefreshGate.resume() 109 + 110 + await store.receive(\.repositoriesLoaded) 111 + await store.finish() 112 + } 113 + 114 + @Test func taskFallsBackToLiveLoadWhenRepositorySnapshotIsMissing() async { 115 + let repoRoot = "/tmp/repo" 116 + let worktree = makeWorktree(id: "\(repoRoot)/main", name: "main", repoRoot: repoRoot) 117 + let repository = makeRepository(id: repoRoot, worktrees: [worktree]) 118 + 119 + let store = TestStore(initialState: RepositoriesFeature.State()) { 120 + RepositoriesFeature() 121 + } withDependencies: { 122 + $0.repositoryPersistence.loadRoots = { [repoRoot] } 123 + $0.repositoryPersistence.loadRepositorySnapshot = { nil } 124 + $0.repositoryPersistence.saveRepositorySnapshot = { _ in } 125 + $0.gitClient.worktrees = { _ in [worktree] } 126 + } 127 + 128 + await store.send(.task) 129 + await store.receive(\.pinnedWorktreeIDsLoaded) 130 + await store.receive(\.archivedWorktreeIDsLoaded) 131 + await store.receive(\.repositoryOrderIDsLoaded) 132 + await store.receive(\.worktreeOrderByRepositoryLoaded) 133 + await store.receive(\.lastFocusedWorktreeIDLoaded) { 134 + $0.shouldRestoreLastFocusedWorktree = true 135 + } 136 + await store.receive(\.repositorySnapshotLoaded) 137 + await store.receive(\.loadPersistedRepositories) 138 + await store.receive(\.repositoriesLoaded) { 139 + $0.repositories = [repository] 140 + $0.repositoryRoots = [URL(fileURLWithPath: repoRoot)] 141 + $0.shouldRestoreLastFocusedWorktree = false 142 + $0.isInitialLoadComplete = true 143 + } 144 + await store.receive(\.delegate.repositoriesChanged) 145 + await store.finish() 146 + } 147 + 148 + @Test func repositoriesLoadedPersistsRepositorySnapshotOnSuccess() async { 149 + let repoRoot = "/tmp/repo" 150 + let worktree = makeWorktree(id: "\(repoRoot)/main", name: "main", repoRoot: repoRoot) 151 + let repository = makeRepository(id: repoRoot, worktrees: [worktree]) 152 + let savedSnapshots = LockIsolated<[[Repository]]>([]) 153 + 154 + let store = TestStore(initialState: RepositoriesFeature.State()) { 155 + RepositoriesFeature() 156 + } withDependencies: { 157 + $0.repositoryPersistence.saveRepositorySnapshot = { repositories in 158 + savedSnapshots.withValue { $0.append(repositories) } 159 + } 160 + } 161 + 162 + await store.send( 163 + .repositoriesLoaded( 164 + [repository], 165 + failures: [], 166 + roots: [URL(fileURLWithPath: repoRoot)], 167 + animated: false 168 + ) 169 + ) { 170 + $0.repositories = [repository] 171 + $0.repositoryRoots = [URL(fileURLWithPath: repoRoot)] 172 + $0.isInitialLoadComplete = true 173 + } 174 + await store.receive(\.delegate.repositoriesChanged) 175 + await store.finish() 176 + 177 + #expect(savedSnapshots.value == [[repository]]) 178 + } 179 + 180 + @Test func repositoriesLoadedSkipsRepositorySnapshotPersistenceWhenLoadFails() async { 181 + let repoRoot = "/tmp/repo" 182 + let worktree = makeWorktree(id: "\(repoRoot)/main", name: "main", repoRoot: repoRoot) 183 + let repository = makeRepository(id: repoRoot, worktrees: [worktree]) 184 + let savedSnapshots = LockIsolated<[[Repository]]>([]) 185 + 186 + let store = TestStore(initialState: RepositoriesFeature.State()) { 187 + RepositoriesFeature() 188 + } withDependencies: { 189 + $0.repositoryPersistence.saveRepositorySnapshot = { repositories in 190 + savedSnapshots.withValue { $0.append(repositories) } 191 + } 192 + } 193 + 194 + await store.send( 195 + .repositoriesLoaded( 196 + [repository], 197 + failures: [.init(rootID: repoRoot, message: "wt failed")], 198 + roots: [URL(fileURLWithPath: repoRoot)], 199 + animated: false 200 + ) 201 + ) { 202 + $0.repositories = [repository] 203 + $0.repositoryRoots = [URL(fileURLWithPath: repoRoot)] 204 + $0.isInitialLoadComplete = true 205 + $0.loadFailuresByID = [repoRoot: "wt failed"] 206 + } 207 + await store.receive(\.delegate.repositoriesChanged) 208 + await store.finish() 209 + 210 + #expect(savedSnapshots.value.isEmpty) 211 + } 212 + 68 213 @Test func selectWorktreeSendsDelegate() async { 69 214 let worktree = makeWorktree(id: "/tmp/wt", name: "fox") 70 215 let repository = makeRepository(id: "/tmp/repo", worktrees: [worktree])
+50
supacodeTests/RepositoryPersistenceClientTests.swift
··· 1 1 import Dependencies 2 2 import DependenciesTestSupport 3 3 import Foundation 4 + import IdentifiedCollections 4 5 import Sharing 5 6 import Testing 6 7 ··· 48 49 } 49 50 50 51 #expect(finalSettings.global.appearanceMode == .dark) 52 + } 53 + 54 + @Test func repositorySnapshotPayloadRoundTripsRepositories() { 55 + let repoRoot = "/tmp/repo" 56 + let worktree = Worktree( 57 + id: "\(repoRoot)/main", 58 + name: "main", 59 + detail: ".", 60 + workingDirectory: URL(fileURLWithPath: "\(repoRoot)/main"), 61 + repositoryRootURL: URL(fileURLWithPath: repoRoot), 62 + createdAt: Date(timeIntervalSince1970: 123) 63 + ) 64 + let repository = Repository( 65 + id: repoRoot, 66 + rootURL: URL(fileURLWithPath: repoRoot), 67 + name: "repo", 68 + worktrees: IdentifiedArray(uniqueElements: [worktree]) 69 + ) 70 + 71 + let payload = RepositorySnapshotCachePayload(repositories: [repository]) 72 + let restored = payload.restoreRepositories { path in 73 + [repoRoot, "\(repoRoot)/main"].contains(path) 74 + } 75 + 76 + #expect(restored == [repository]) 77 + } 78 + 79 + @Test func repositorySnapshotPayloadRejectsMissingWorktreePath() { 80 + let repoRoot = "/tmp/repo" 81 + let worktree = Worktree( 82 + id: "\(repoRoot)/main", 83 + name: "main", 84 + detail: ".", 85 + workingDirectory: URL(fileURLWithPath: "\(repoRoot)/main"), 86 + repositoryRootURL: URL(fileURLWithPath: repoRoot) 87 + ) 88 + let repository = Repository( 89 + id: repoRoot, 90 + rootURL: URL(fileURLWithPath: repoRoot), 91 + name: "repo", 92 + worktrees: IdentifiedArray(uniqueElements: [worktree]) 93 + ) 94 + 95 + let payload = RepositorySnapshotCachePayload(repositories: [repository]) 96 + let restored = payload.restoreRepositories { path in 97 + path == repoRoot 98 + } 99 + 100 + #expect(restored == nil) 51 101 } 52 102 }