-
Notifications
You must be signed in to change notification settings - Fork 948
Expand file tree
/
Copy pathKeepDownloadedRecursiveTests.swift
More file actions
544 lines (481 loc) Β· 24.2 KB
/
KeepDownloadedRecursiveTests.swift
File metadata and controls
544 lines (481 loc) Β· 24.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: LGPL-3.0-or-later
@preconcurrency import FileProvider
import Foundation
@testable import NextcloudFileProviderKit
import NextcloudFileProviderKitMocks
import RealmSwift
import TestInterface
import XCTest
///
/// Tests for the recursive "Always keep downloaded" behaviour covering the
/// database-level propagation that `Item.set(keepDownloaded:domain:)` depends
/// on. The `NSFileProviderManager` side (download requests, last-used-date
/// bumps) is intentionally out of scope here β it cannot be exercised without
/// a registered File Provider extension. See issue #9057.
///
final class KeepDownloadedRecursiveTests: NextcloudFileProviderKitTestCase {
static let account = Account(
user: "testUser", id: "testUserId", serverUrl: "https://mock.nc.com", password: "abcd"
)
static let dbManager = FilesDatabaseManager(
account: account,
databaseDirectory: makeDatabaseDirectory(),
fileProviderDomainIdentifier: NSFileProviderDomainIdentifier("test"),
log: FileProviderLogMock()
)
override func setUp() {
super.setUp()
Realm.Configuration.defaultConfiguration.inMemoryIdentifier = name
}
///
/// Seed a small tree β folder β subfolder β file, folder β file β into
/// the database and return the top-level folder's metadata.
///
private func seedTree() throws -> SendableItemMetadata {
let folder = RealmItemMetadata()
folder.ocId = "folder-1"
folder.account = "TestAccount"
folder.serverUrl = "https://cloud.example.com/files"
folder.fileName = "documents"
folder.directory = true
let directChildFile = RealmItemMetadata()
directChildFile.ocId = "direct-child-file"
directChildFile.account = "TestAccount"
directChildFile.serverUrl = "https://cloud.example.com/files/documents"
directChildFile.fileName = "report.pdf"
let subfolder = RealmItemMetadata()
subfolder.ocId = "subfolder-1"
subfolder.account = "TestAccount"
subfolder.serverUrl = "https://cloud.example.com/files/documents"
subfolder.fileName = "nested"
subfolder.directory = true
let deepFile = RealmItemMetadata()
deepFile.ocId = "deep-file"
deepFile.account = "TestAccount"
deepFile.serverUrl = "https://cloud.example.com/files/documents/nested"
deepFile.fileName = "note.txt"
let realm = Self.dbManager.ncDatabase()
try realm.write {
realm.add(folder)
realm.add(directChildFile)
realm.add(subfolder)
realm.add(deepFile)
}
return SendableItemMetadata(value: folder)
}
///
/// `childItems(directoryMetadata:)` must return the full subtree, not just
/// direct children. This is the primitive the recursive keep-downloaded
/// flow relies on β if it ever regressed to depth-1, every pinned folder
/// would silently leak deep descendants.
///
func testChildItemsReturnsFullSubtree() throws {
let folderMetadata = try seedTree()
let descendants = Self.dbManager.childItems(directoryMetadata: folderMetadata)
let descendantOcIds = Set(descendants.map(\.ocId))
XCTAssertEqual(
descendantOcIds,
["direct-child-file", "subfolder-1", "deep-file"],
"childItems must walk the whole subtree, including grandchildren."
)
}
///
/// Apply the keep-downloaded flag to every descendant returned by
/// `childItems`, mirroring what `Item.set(keepDownloaded:domain:)` does
/// after its recursive enumeration. Every descendant must end up pinned.
///
func testRecursivePropagationEnablesKeepDownloadedOnAllDescendants() throws {
let folderMetadata = try seedTree()
// Flip the root.
_ = try Self.dbManager.set(keepDownloaded: true, for: folderMetadata)
// Flip every descendant β this is the loop Item.set performs.
for child in Self.dbManager.childItems(directoryMetadata: folderMetadata) {
_ = try Self.dbManager.set(keepDownloaded: true, for: child)
}
for ocId in ["folder-1", "direct-child-file", "subfolder-1", "deep-file"] {
let stored = try XCTUnwrap(
Self.dbManager.itemMetadata(ocId: ocId),
"Seeded metadata \(ocId) must still be present."
)
XCTAssertTrue(
stored.keepDownloaded,
"\(ocId) must be flagged keepDownloaded after recursive enable."
)
}
}
///
/// The inverse: disabling on the folder must clear the flag on every
/// descendant, so the Finder UI and the eviction policy stop treating
/// them as pinned.
///
func testRecursivePropagationClearsKeepDownloadedOnAllDescendants() throws {
let folderMetadata = try seedTree()
// Seed: everything pinned first.
_ = try Self.dbManager.set(keepDownloaded: true, for: folderMetadata)
for child in Self.dbManager.childItems(directoryMetadata: folderMetadata) {
_ = try Self.dbManager.set(keepDownloaded: true, for: child)
}
// Act: clear.
_ = try Self.dbManager.set(keepDownloaded: false, for: folderMetadata)
for child in Self.dbManager.childItems(directoryMetadata: folderMetadata) {
_ = try Self.dbManager.set(keepDownloaded: false, for: child)
}
for ocId in ["folder-1", "direct-child-file", "subfolder-1", "deep-file"] {
let stored = try XCTUnwrap(
Self.dbManager.itemMetadata(ocId: ocId),
"Seeded metadata \(ocId) must still be present."
)
XCTAssertFalse(
stored.keepDownloaded,
"\(ocId) must no longer be flagged keepDownloaded after recursive disable."
)
}
}
///
/// Siblings that live outside the pinned subtree must not be touched.
///
/// This guards against a `serverUrl.starts(with:)` prefix-match hazard β
/// `"β¦/documents"` is a prefix of `"β¦/documents-archive"`, but the latter
/// is a sibling folder and must keep its original flag state.
///
///
/// Items flagged as "Always keep downloaded" must expose a Finder decoration
/// Without this, the user has no visual cue that an item is pinned β which was the whole point of the feature.
///
func testItemDecorationsReflectKeepDownloadedFlag() throws {
var pinnedMetadata = SendableItemMetadata(ocId: "pinned-id", fileName: "pinned.txt", account: Self.account)
pinnedMetadata.keepDownloaded = true
let pinnedItem = Item(
metadata: pinnedMetadata,
parentItemIdentifier: .rootContainer,
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager
)
let decoration = try XCTUnwrap(pinnedItem.decorations?.first)
XCTAssertTrue(decoration.rawValue.hasSuffix(".keep-downloaded"), "Pinned items must expose the keepDownloaded badge identifier.")
let unpinnedMetadata = SendableItemMetadata(ocId: "unpinned-id", fileName: "unpinned.txt", account: Self.account)
let unpinnedItem = Item(
metadata: unpinnedMetadata,
parentItemIdentifier: .rootContainer,
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager
)
XCTAssertNil(unpinnedItem.decorations, "Unpinned items must not declare any badge, so Finder shows no overlay.")
}
///
/// Seed `seedTree`'s structure plus an additional sibling at each level
/// along the path to the deep file. After recursive pin and a fragmenting
/// unpin of the deep file, the path ancestors must lose `keepDownloaded`
/// and every off-path sibling must keep it (#9891).
///
/// Tree shape:
/// documents/ (path ancestor β pin root)
/// report.pdf (off-path sibling at level 0)
/// nested/ (path ancestor)
/// note.txt (deep file β the unpin target)
/// level-1-sibling.txt (off-path sibling at level 1)
/// documents-archive.zip lives outside `documents/`; only
/// here as a control to ensure the walk
/// stops at the first non-pinned ancestor
/// even when names share a prefix.
///
func testFragmentDeepUnpinUnderRecursivePin() throws {
_ = try seedTree()
let levelOneSibling = RealmItemMetadata()
levelOneSibling.ocId = "level-1-sibling"
levelOneSibling.account = "TestAccount"
levelOneSibling.serverUrl = "https://cloud.example.com/files/documents/nested"
levelOneSibling.fileName = "level-1-sibling.txt"
let realm = Self.dbManager.ncDatabase()
try realm.write { realm.add(levelOneSibling) }
// Seed: pin the whole subtree (mirrors the recursive enable path).
let folderMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "folder-1"))
_ = try Self.dbManager.set(keepDownloaded: true, for: folderMetadata)
for child in Self.dbManager.childItems(directoryMetadata: folderMetadata) {
_ = try Self.dbManager.set(keepDownloaded: true, for: child)
}
// Act: fragment the path from deep file up to the pin root.
let deepFileMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "deep-file"))
let deepFileItem = Item(
metadata: deepFileMetadata,
parentItemIdentifier: NSFileProviderItemIdentifier("subfolder-1"),
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager
)
let outcome = deepFileItem.fragmentPathToRootInDatabase()
// The walk must touch both intermediate dir and root, immediate parent first.
XCTAssertEqual(outcome.unpinnedAncestors.map(\.ocId), ["subfolder-1", "folder-1"])
// Every cousin was already pinned by the recursive enable, so nothing
// newly transitioned from `.inherited` to strict.
XCTAssertTrue(outcome.newlyPinnedCousins.isEmpty)
// Path ancestors flipped to false.
for ocId in ["folder-1", "subfolder-1"] {
let stored = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: ocId))
XCTAssertFalse(
stored.keepDownloaded,
"Path ancestor \(ocId) must be unpinned after fragmentation."
)
}
// Off-path siblings retain their pin (the recursive enable already set them).
for ocId in ["direct-child-file", "level-1-sibling"] {
let stored = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: ocId))
XCTAssertTrue(
stored.keepDownloaded,
"Off-path sibling \(ocId) must remain pinned after fragmentation."
)
}
// The deep file itself was not touched by fragmentation (the caller β
// `Item.set(keepDownloaded:domain:)` β flips it before invoking the
// fragmentation walk). At this point it is still flagged as pinned in
// the DB, which we assert to pin down the contract: fragmentation
// operates only on ancestors.
let deepStored = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "deep-file"))
XCTAssertTrue(
deepStored.keepDownloaded,
"fragmentPathToRootInDatabase must not touch the target item."
)
}
///
/// Cousins enumerated *after* the original recursive enable arrive in the
/// database with `keepDownloaded == false` β the recursive walk has long
/// since finished and there is nobody to back-fill the flag for new
/// siblings. Without explicit cousin pinning, fragmenting an unpin path
/// would silently drop them out of the strict-pin chain when the path
/// ancestors flip to `.inherited` (#9891).
///
/// The fragmentation walk must therefore set the flag on every off-path
/// immediate child it finds without it, signal those cousins back via
/// `newlyPinnedCousins`, and only then flip the path ancestor.
///
func testFragmentPinsCousinsThatLackKeepDownloadedFlag() throws {
_ = try seedTree()
// An off-path sibling at the deepest level that was added to the DB
// *after* the original recursive pin and so missed it.
let lateCousin = RealmItemMetadata()
lateCousin.ocId = "late-cousin"
lateCousin.account = "TestAccount"
lateCousin.serverUrl = "https://cloud.example.com/files/documents/nested"
lateCousin.fileName = "late-cousin.txt"
// No keepDownloaded set β defaults to false.
let realm = Self.dbManager.ncDatabase()
try realm.write { realm.add(lateCousin) }
// Original recursive enable: every then-known descendant gets flagged.
let folderMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "folder-1"))
_ = try Self.dbManager.set(keepDownloaded: true, for: folderMetadata)
for child in Self.dbManager.childItems(directoryMetadata: folderMetadata) where child.ocId != "late-cousin" {
_ = try Self.dbManager.set(keepDownloaded: true, for: child)
}
// Sanity: the late cousin still has no flag.
XCTAssertFalse(try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "late-cousin")).keepDownloaded)
// Act: fragment from the deep file.
let deepFileMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "deep-file"))
let deepFileItem = Item(
metadata: deepFileMetadata,
parentItemIdentifier: NSFileProviderItemIdentifier("subfolder-1"),
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager
)
let outcome = deepFileItem.fragmentPathToRootInDatabase()
// The late cousin must be reported as newly pinned and persisted with
// the flag set, so once the path ancestors flip to `.inherited` the
// OS still sees an explicit `.downloadEagerlyAndKeepDownloaded` here.
XCTAssertEqual(outcome.newlyPinnedCousins.map(\.ocId), ["late-cousin"])
XCTAssertTrue(try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "late-cousin")).keepDownloaded)
// The off-path sibling that was already pinned must not be re-reported.
XCTAssertFalse(
outcome.newlyPinnedCousins.map(\.ocId).contains("direct-child-file"),
"Cousins already flagged at fragmentation time must not be reported as newly pinned."
)
XCTAssertTrue(try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "direct-child-file")).keepDownloaded)
}
///
/// Re-pinning the original pin root after fragmentation must collapse the
/// hole. The recursive enable in `Item.set(keepDownloaded:domain:)` does
/// this naturally β every descendant flag is overwritten to `true`.
///
func testRePinAfterFragmentationCollapses() throws {
_ = try seedTree()
let folderMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "folder-1"))
_ = try Self.dbManager.set(keepDownloaded: true, for: folderMetadata)
for child in Self.dbManager.childItems(directoryMetadata: folderMetadata) {
_ = try Self.dbManager.set(keepDownloaded: true, for: child)
}
// Fragment from the deep file.
let deepFileMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "deep-file"))
let deepFileItem = Item(
metadata: deepFileMetadata,
parentItemIdentifier: NSFileProviderItemIdentifier("subfolder-1"),
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager
)
_ = deepFileItem.fragmentPathToRootInDatabase()
// Plus the deep file itself, as Item.set(keepDownloaded:domain:) would.
_ = try Self.dbManager.set(keepDownloaded: false, for: deepFileMetadata)
// Sanity: pin root and intermediate dir are now unpinned.
XCTAssertFalse(try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "folder-1")).keepDownloaded)
XCTAssertFalse(try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "subfolder-1")).keepDownloaded)
// Re-pin the root using the same pattern Item.set follows.
let refreshedFolder = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "folder-1"))
_ = try Self.dbManager.set(keepDownloaded: true, for: refreshedFolder)
for child in Self.dbManager.childItems(directoryMetadata: refreshedFolder) {
_ = try Self.dbManager.set(keepDownloaded: true, for: child)
}
for ocId in ["folder-1", "direct-child-file", "subfolder-1", "deep-file"] {
let stored = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: ocId))
XCTAssertTrue(
stored.keepDownloaded,
"\(ocId) must be pinned again after re-pinning the root."
)
}
}
///
/// Unpinning the pin root directly must not produce any fragmentation β
/// there is no strict ancestor above it to cut. The walk returns an empty
/// list and no DB writes happen on parents.
///
func testShallowUnpinIsNoFragmentation() throws {
_ = try seedTree()
let folderMetadata = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "folder-1"))
_ = try Self.dbManager.set(keepDownloaded: true, for: folderMetadata)
for child in Self.dbManager.childItems(directoryMetadata: folderMetadata) {
_ = try Self.dbManager.set(keepDownloaded: true, for: child)
}
// Act: fragment from the pin root itself.
let pinnedFolder = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "folder-1"))
let folderItem = Item(
metadata: pinnedFolder,
parentItemIdentifier: .rootContainer,
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager
)
let outcome = folderItem.fragmentPathToRootInDatabase()
XCTAssertTrue(
outcome.unpinnedAncestors.isEmpty,
"Fragmenting from the pin root must touch no ancestors β there is no strict ancestor."
)
XCTAssertTrue(
outcome.newlyPinnedCousins.isEmpty,
"Fragmenting from the pin root must not touch any cousins."
)
// Sanity: the root and every descendant are still pinned. The fragmentation
// walk does not touch the target item, and the recursive disable that
// `Item.set(keepDownloaded:domain:)` performs after fragmentation is what
// would clear them β and we have not invoked it here.
for ocId in ["folder-1", "direct-child-file", "subfolder-1", "deep-file"] {
let stored = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: ocId))
XCTAssertTrue(stored.keepDownloaded)
}
}
func testRecursivePropagationDoesNotLeakToSiblingsWithSimilarNames() throws {
let folderMetadata = try seedTree()
let sibling = RealmItemMetadata()
sibling.ocId = "sibling-file"
sibling.account = "TestAccount"
sibling.serverUrl = "https://cloud.example.com/files"
sibling.fileName = "documents-archive.zip"
// Same parent ("/files") as the target folder, but NOT inside the
// target folder. A naive prefix match against "/files/documents"
// would still reject this β but if anyone ever changed the match to
// use just the parent path, this guards against that regression.
let realm = Self.dbManager.ncDatabase()
try realm.write { realm.add(sibling) }
_ = try Self.dbManager.set(keepDownloaded: true, for: folderMetadata)
for child in Self.dbManager.childItems(directoryMetadata: folderMetadata) {
_ = try Self.dbManager.set(keepDownloaded: true, for: child)
}
let storedSibling = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: "sibling-file"))
XCTAssertFalse(
storedSibling.keepDownloaded,
"A sibling outside the pinned folder must not inherit keepDownloaded."
)
}
///
/// End-to-end regression for #9923: a paginated read that lands on
/// already-pinned descendants must not silently clear their flag. Before
/// the fix, the bulk write at the end of
/// ``Enumerator.handlePagedReadResults`` used ``addItemMetadata`` which
/// replaces rows wholesale via Realm's `update: .all`, dropping every
/// local-only field β including ``keepDownloaded``. The user's pin walk
/// runs without pagination, but the OS-driven `enumerateItems` that
/// follows uses pagination, so a recently-pinned subtree was reliably
/// undone.
///
func testPaginatedReadDoesNotClobberRecursivelyPinnedDescendants() throws {
// Pin the seeded tree the way `Item.set(keepDownloaded:domain:)` does.
let folderMetadata = try seedTree()
_ = try Self.dbManager.set(keepDownloaded: true, for: folderMetadata)
for child in Self.dbManager.childItems(directoryMetadata: folderMetadata) {
_ = try Self.dbManager.set(keepDownloaded: true, for: child)
}
// Build NKFiles with ocIds matching the seeded rows so the
// preservation lookup hits. The `MockRemoteItem` parent chain
// provides the right `serverUrl` on each `NKFile`.
let parent = MockRemoteItem(
identifier: "files-parent",
name: "files",
remotePath: "https://cloud.example.com/files",
directory: true,
account: Self.account.ncKitAccount,
username: Self.account.username,
userId: Self.account.id,
serverUrl: Self.account.serverUrl
)
let folderMock = MockRemoteItem(
identifier: "folder-1",
name: "documents",
remotePath: "https://cloud.example.com/files/documents",
directory: true,
account: Self.account.ncKitAccount,
username: Self.account.username,
userId: Self.account.id,
serverUrl: Self.account.serverUrl
)
folderMock.parent = parent
let directChildMock = MockRemoteItem(
identifier: "direct-child-file",
name: "report.pdf",
remotePath: "https://cloud.example.com/files/documents/report.pdf",
account: Self.account.ncKitAccount,
username: Self.account.username,
userId: Self.account.id,
serverUrl: Self.account.serverUrl
)
directChildMock.parent = folderMock
let subfolderMock = MockRemoteItem(
identifier: "subfolder-1",
name: "nested",
remotePath: "https://cloud.example.com/files/documents/nested",
directory: true,
account: Self.account.ncKitAccount,
username: Self.account.username,
userId: Self.account.id,
serverUrl: Self.account.serverUrl
)
subfolderMock.parent = folderMock
// Simulate the page-0 paginated PROPFIND response Finder would issue
// for the pinned folder.
let (_, error) = Enumerator.handlePagedReadResults(
files: [folderMock.toNKFile(), directChildMock.toNKFile(), subfolderMock.toNKFile()],
pageIndex: 0,
dbManager: Self.dbManager
)
XCTAssertNil(error)
// Both directly re-enumerated descendants and the deeper file
// (untouched by this PROPFIND but pinned in step 1) must remain
// pinned after the paginated overwrite.
for ocId in ["folder-1", "direct-child-file", "subfolder-1", "deep-file"] {
let stored = try XCTUnwrap(Self.dbManager.itemMetadata(ocId: ocId))
XCTAssertTrue(
stored.keepDownloaded,
"\(ocId) must remain pinned after a paginated read overwrites the row (#9923)."
)
}
}
}