@stacksjs/defaults 0.74.33 → 0.74.34
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/ai/skills/stacks-auto-imports/SKILL.md +1 -1
- package/ai/skills/stacks-dashboard/SKILL.md +60 -0
- package/ai/skills/stacks-orm/SKILL.md +1 -1
- package/ai/skills/stacks-storage/SKILL.md +58 -4
- package/app/Actions/Dashboard/Content/FileFavoriteAction.ts +25 -0
- package/app/Actions/Dashboard/Content/FileReprocessAction.ts +31 -0
- package/app/Actions/Dashboard/Content/FileTagsAction.ts +27 -0
- package/app/Actions/Dashboard/Content/file-manager.test.ts +47 -27
- package/app/Actions/Dashboard/Content/file-manager.ts +338 -12
- package/app/Actions/Dashboard/Content/file-metadata-store.ts +432 -0
- package/app/Actions/Dashboard/Content/file-metadata.test.ts +357 -0
- package/app/Actions/Dashboard/Content/file-metadata.ts +550 -0
- package/app/Actions/Dashboard/Content/file-pipeline.test.ts +344 -0
- package/app/Jobs/OptimizeStorageImageJob.ts +74 -0
- package/app/Jobs/TagStorageMediaJob.ts +122 -0
- package/app/Jobs/TranscodeStorageVideoJob.ts +88 -0
- package/app/Models/StorageItem.ts +123 -0
- package/app/Models/StorageItemTask.ts +134 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/routes/dashboard-api.ts +10 -0
- package/vcs/github/workflows/buddy-bot.yml +109 -0
- package/vcs/github/workflows/ci.yml +3 -3
- package/vcs/github/workflows/release.yml +1 -1
- package/vcs/github/renovate.json +0 -5
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// The file manager's metadata layer (stacksjs/stacks#2577).
|
|
2
|
+
//
|
|
3
|
+
// Two halves, tested separately because they fail differently. The pure rules -
|
|
4
|
+
// prefix matching, repathing, tag normalization - are asserted directly, since
|
|
5
|
+
// a bug there is silent: renaming `reports` would also rewrite
|
|
6
|
+
// `reports-archive`, and nothing would say so. The reconciliation is asserted
|
|
7
|
+
// through the real file-manager operations against a real temp disk, because
|
|
8
|
+
// what matters is not that `followRename` works but that `renameDashboardFile`
|
|
9
|
+
// calls it, after the move, with the right prefix.
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
12
|
+
import { mkdir, mkdtemp, rm } from 'node:fs/promises'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { StorageManager } from '@stacksjs/storage'
|
|
16
|
+
import type { DashboardFileNode } from './file-manager'
|
|
17
|
+
import {
|
|
18
|
+
deleteDashboardFile,
|
|
19
|
+
duplicateDashboardFile,
|
|
20
|
+
getDashboardFileSnapshot,
|
|
21
|
+
renameDashboardFile,
|
|
22
|
+
setDashboardFileFavorite,
|
|
23
|
+
setDashboardFileTags,
|
|
24
|
+
} from './file-manager'
|
|
25
|
+
import {
|
|
26
|
+
createMemoryMetadataStore,
|
|
27
|
+
isEmptyMetadata,
|
|
28
|
+
isUnderPrefix,
|
|
29
|
+
normalizeTags,
|
|
30
|
+
repathUnderPrefix,
|
|
31
|
+
sweepMetadata,
|
|
32
|
+
} from './file-metadata'
|
|
33
|
+
|
|
34
|
+
let root = ''
|
|
35
|
+
let manager: StorageManager
|
|
36
|
+
let store = createMemoryMetadataStore()
|
|
37
|
+
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
store = createMemoryMetadataStore()
|
|
40
|
+
root = await mkdtemp(join(tmpdir(), 'stacks-file-metadata-'))
|
|
41
|
+
await mkdir(join(root, 'public'), { recursive: true })
|
|
42
|
+
manager = new StorageManager().init({
|
|
43
|
+
default: 'public',
|
|
44
|
+
disks: {
|
|
45
|
+
public: { driver: 'local', root: join(root, 'public'), url: '/storage', visibility: 'public' },
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
afterEach(async () => {
|
|
51
|
+
manager.reset()
|
|
52
|
+
await rm(root, { force: true, recursive: true })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/** Find a node by path anywhere in the snapshot tree. */
|
|
56
|
+
function nodeAt(node: DashboardFileNode, path: string): DashboardFileNode | undefined {
|
|
57
|
+
if (node.path === path)
|
|
58
|
+
return node
|
|
59
|
+
for (const child of node.items ?? []) {
|
|
60
|
+
const found = nodeAt(child, path)
|
|
61
|
+
if (found)
|
|
62
|
+
return found
|
|
63
|
+
}
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('isUnderPrefix', () => {
|
|
68
|
+
test('matches the prefix itself and what is beneath it', () => {
|
|
69
|
+
expect(isUnderPrefix('reports', 'reports')).toBeTrue()
|
|
70
|
+
expect(isUnderPrefix('reports/q1.pdf', 'reports')).toBeTrue()
|
|
71
|
+
expect(isUnderPrefix('reports/2024/q1.pdf', 'reports')).toBeTrue()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('does not match a sibling that shares a spelling', () => {
|
|
75
|
+
// The separator is what makes this correct, and its absence is the bug that
|
|
76
|
+
// only appears once somebody has both folders.
|
|
77
|
+
expect(isUnderPrefix('reports-archive/q1.pdf', 'reports')).toBeFalse()
|
|
78
|
+
expect(isUnderPrefix('reportsarchive', 'reports')).toBeFalse()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('the empty prefix is the whole disk', () => {
|
|
82
|
+
expect(isUnderPrefix('anything/at/all.txt', '')).toBeTrue()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('repathUnderPrefix', () => {
|
|
87
|
+
test('rewrites the prefix and keeps the rest', () => {
|
|
88
|
+
expect(repathUnderPrefix('reports', 'reports', 'archive')).toBe('archive')
|
|
89
|
+
expect(repathUnderPrefix('reports/q1.pdf', 'reports', 'archive')).toBe('archive/q1.pdf')
|
|
90
|
+
expect(repathUnderPrefix('reports/2024/q1.pdf', 'reports', 'archive')).toBe('archive/2024/q1.pdf')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('leaves a path that is not under the prefix alone', () => {
|
|
94
|
+
expect(repathUnderPrefix('reports-archive/q1.pdf', 'reports', 'archive')).toBe('reports-archive/q1.pdf')
|
|
95
|
+
expect(repathUnderPrefix('other.txt', 'reports', 'archive')).toBe('other.txt')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('normalizeTags', () => {
|
|
100
|
+
test('trims, drops blanks, and deduplicates case-insensitively', () => {
|
|
101
|
+
expect(normalizeTags([' Invoice ', 'invoice', '', ' ', 'Draft'])).toEqual(['Draft', 'Invoice'])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('keeps the first spelling of a duplicate', () => {
|
|
105
|
+
expect(normalizeTags(['Invoice', 'INVOICE'])).toEqual(['Invoice'])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('sorts, so the same set sent in a different order is the same record', () => {
|
|
109
|
+
expect(normalizeTags(['b', 'a'])).toEqual(normalizeTags(['a', 'b']))
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('ignores anything that is not a string', () => {
|
|
113
|
+
expect(normalizeTags([1, null, undefined, {}, 'kept'])).toEqual(['kept'])
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('isEmptyMetadata', () => {
|
|
118
|
+
test('a record saying nothing is empty, and is why unstarring deletes the row', () => {
|
|
119
|
+
expect(isEmptyMetadata({ favorite: false, tags: [] })).toBeTrue()
|
|
120
|
+
expect(isEmptyMetadata({ favorite: true, tags: [] })).toBeFalse()
|
|
121
|
+
expect(isEmptyMetadata({ favorite: false, tags: ['x'] })).toBeFalse()
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('setDashboardFileFavorite', () => {
|
|
126
|
+
test('stars a file, and the snapshot reports it', async () => {
|
|
127
|
+
await manager.disk('public').write('reports/q1.pdf', 'x')
|
|
128
|
+
|
|
129
|
+
const result = await setDashboardFileFavorite({ path: 'reports/q1.pdf', favorite: true }, manager, store)
|
|
130
|
+
expect(result).toEqual({ path: 'reports/q1.pdf', favorite: true, tags: [] })
|
|
131
|
+
|
|
132
|
+
const snapshot = await getDashboardFileSnapshot({}, manager, store)
|
|
133
|
+
expect(nodeAt(snapshot.root, 'reports/q1.pdf')?.starred).toBeTrue()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('unstarring removes the record rather than storing false', async () => {
|
|
137
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
138
|
+
|
|
139
|
+
await setDashboardFileFavorite({ path: 'a.txt', favorite: true }, manager, store)
|
|
140
|
+
await setDashboardFileFavorite({ path: 'a.txt', favorite: false }, manager, store)
|
|
141
|
+
|
|
142
|
+
expect(await store.under('public', '')).toEqual(new Map())
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('leaves tags alone, because the two arrive from different controls', async () => {
|
|
146
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
147
|
+
await setDashboardFileTags({ path: 'a.txt', tags: ['invoice'] }, manager, store)
|
|
148
|
+
|
|
149
|
+
const result = await setDashboardFileFavorite({ path: 'a.txt', favorite: true }, manager, store)
|
|
150
|
+
expect(result.tags).toEqual(['invoice'])
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('404s for a path the disk does not have', async () => {
|
|
154
|
+
// Otherwise the row is an orphan the moment it is written, and the caller
|
|
155
|
+
// gets a 200 for it.
|
|
156
|
+
expect(setDashboardFileFavorite({ path: 'missing.txt', favorite: true }, manager, store))
|
|
157
|
+
.rejects.toThrow(/was not found/)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('rejects a non-boolean', async () => {
|
|
161
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
162
|
+
expect(setDashboardFileFavorite({ path: 'a.txt', favorite: 'yes' }, manager, store))
|
|
163
|
+
.rejects.toThrow(/true or false/)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('stars a folder too', async () => {
|
|
167
|
+
await manager.disk('public').write('reports/q1.pdf', 'x')
|
|
168
|
+
|
|
169
|
+
await setDashboardFileFavorite({ path: 'reports', favorite: true }, manager, store)
|
|
170
|
+
const snapshot = await getDashboardFileSnapshot({}, manager, store)
|
|
171
|
+
|
|
172
|
+
expect(nodeAt(snapshot.root, 'reports')?.starred).toBeTrue()
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('setDashboardFileTags', () => {
|
|
177
|
+
test('replaces the whole set, because there is no signal for a removal', async () => {
|
|
178
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
179
|
+
|
|
180
|
+
await setDashboardFileTags({ path: 'a.txt', tags: ['invoice', 'draft'] }, manager, store)
|
|
181
|
+
const result = await setDashboardFileTags({ path: 'a.txt', tags: ['invoice'] }, manager, store)
|
|
182
|
+
|
|
183
|
+
expect(result.tags).toEqual(['invoice'])
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('normalizes what it stores, and the snapshot reports it', async () => {
|
|
187
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
188
|
+
await setDashboardFileTags({ path: 'a.txt', tags: [' Invoice ', 'invoice', 'Draft'] }, manager, store)
|
|
189
|
+
|
|
190
|
+
const snapshot = await getDashboardFileSnapshot({}, manager, store)
|
|
191
|
+
expect(nodeAt(snapshot.root, 'a.txt')?.tags).toEqual(['Draft', 'Invoice'])
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('clearing the tags of an unstarred file removes the record', async () => {
|
|
195
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
196
|
+
|
|
197
|
+
await setDashboardFileTags({ path: 'a.txt', tags: ['invoice'] }, manager, store)
|
|
198
|
+
await setDashboardFileTags({ path: 'a.txt', tags: [] }, manager, store)
|
|
199
|
+
|
|
200
|
+
expect(await store.under('public', '')).toEqual(new Map())
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('keeps the record while the file is still starred', async () => {
|
|
204
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
205
|
+
|
|
206
|
+
await setDashboardFileFavorite({ path: 'a.txt', favorite: true }, manager, store)
|
|
207
|
+
await setDashboardFileTags({ path: 'a.txt', tags: [] }, manager, store)
|
|
208
|
+
|
|
209
|
+
expect((await store.under('public', '')).get('a.txt')).toEqual({ favorite: true, tags: [] })
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('bounds what one file may carry', async () => {
|
|
213
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
214
|
+
|
|
215
|
+
expect(setDashboardFileTags({ path: 'a.txt', tags: Array.from({ length: 33 }, (_, i) => `t${i}`) }, manager, store))
|
|
216
|
+
.rejects.toThrow(/at most 32/)
|
|
217
|
+
expect(setDashboardFileTags({ path: 'a.txt', tags: ['x'.repeat(61)] }, manager, store))
|
|
218
|
+
.rejects.toThrow(/60 characters/)
|
|
219
|
+
expect(setDashboardFileTags({ path: 'a.txt', tags: 'invoice' }, manager, store))
|
|
220
|
+
.rejects.toThrow(/must be an array/)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
describe('reconciliation on rename', () => {
|
|
225
|
+
test('a renamed file keeps its star and tags', async () => {
|
|
226
|
+
await manager.disk('public').write('reports/q1.pdf', 'x')
|
|
227
|
+
await setDashboardFileFavorite({ path: 'reports/q1.pdf', favorite: true }, manager, store)
|
|
228
|
+
await setDashboardFileTags({ path: 'reports/q1.pdf', tags: ['invoice'] }, manager, store)
|
|
229
|
+
|
|
230
|
+
await renameDashboardFile({ path: 'reports/q1.pdf', name: 'q1-final.pdf' }, manager, store)
|
|
231
|
+
|
|
232
|
+
const records = await store.under('public', '')
|
|
233
|
+
expect(records.get('reports/q1.pdf')).toBeUndefined()
|
|
234
|
+
expect(records.get('reports/q1-final.pdf')).toEqual({ favorite: true, tags: ['invoice'] })
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('a renamed folder carries every record beneath it', async () => {
|
|
238
|
+
// The part #2577 flagged as the one that would bite: a folder rename moves
|
|
239
|
+
// every file under it, so this is a prefix update rather than one row.
|
|
240
|
+
const disk = manager.disk('public')
|
|
241
|
+
await disk.write('reports/q1.pdf', 'x')
|
|
242
|
+
await disk.write('reports/2024/q2.pdf', 'x')
|
|
243
|
+
await setDashboardFileFavorite({ path: 'reports/q1.pdf', favorite: true }, manager, store)
|
|
244
|
+
await setDashboardFileFavorite({ path: 'reports/2024/q2.pdf', favorite: true }, manager, store)
|
|
245
|
+
|
|
246
|
+
await renameDashboardFile({ path: 'reports', name: 'archive' }, manager, store)
|
|
247
|
+
|
|
248
|
+
const records = await store.under('public', '')
|
|
249
|
+
expect([...records.keys()].sort()).toEqual(['archive/2024/q2.pdf', 'archive/q1.pdf'])
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('does not touch a sibling folder that shares a spelling', async () => {
|
|
253
|
+
const disk = manager.disk('public')
|
|
254
|
+
await disk.write('reports/q1.pdf', 'x')
|
|
255
|
+
await disk.write('reports-archive/old.pdf', 'x')
|
|
256
|
+
await setDashboardFileFavorite({ path: 'reports/q1.pdf', favorite: true }, manager, store)
|
|
257
|
+
await setDashboardFileFavorite({ path: 'reports-archive/old.pdf', favorite: true }, manager, store)
|
|
258
|
+
|
|
259
|
+
await renameDashboardFile({ path: 'reports', name: 'archive' }, manager, store)
|
|
260
|
+
|
|
261
|
+
const records = await store.under('public', '')
|
|
262
|
+
expect(records.get('reports-archive/old.pdf')).toEqual({ favorite: true, tags: [] })
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
describe('reconciliation on delete', () => {
|
|
267
|
+
test('a deleted file forgets its record', async () => {
|
|
268
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
269
|
+
await setDashboardFileFavorite({ path: 'a.txt', favorite: true }, manager, store)
|
|
270
|
+
|
|
271
|
+
await deleteDashboardFile({ path: 'a.txt' }, manager, store)
|
|
272
|
+
|
|
273
|
+
expect(await store.under('public', '')).toEqual(new Map())
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('a deleted folder forgets the whole subtree', async () => {
|
|
277
|
+
const disk = manager.disk('public')
|
|
278
|
+
await disk.write('reports/q1.pdf', 'x')
|
|
279
|
+
await disk.write('reports/2024/q2.pdf', 'x')
|
|
280
|
+
await setDashboardFileFavorite({ path: 'reports/q1.pdf', favorite: true }, manager, store)
|
|
281
|
+
await setDashboardFileFavorite({ path: 'reports/2024/q2.pdf', favorite: true }, manager, store)
|
|
282
|
+
|
|
283
|
+
await deleteDashboardFile({ path: 'reports' }, manager, store)
|
|
284
|
+
|
|
285
|
+
expect(await store.under('public', '')).toEqual(new Map())
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
describe('reconciliation on copy', () => {
|
|
290
|
+
test('a copy carries the star and tags, and the source keeps its own', async () => {
|
|
291
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
292
|
+
await setDashboardFileFavorite({ path: 'a.txt', favorite: true }, manager, store)
|
|
293
|
+
await setDashboardFileTags({ path: 'a.txt', tags: ['invoice'] }, manager, store)
|
|
294
|
+
|
|
295
|
+
const { to } = await duplicateDashboardFile({ path: 'a.txt' }, manager, store)
|
|
296
|
+
|
|
297
|
+
const records = await store.under('public', '')
|
|
298
|
+
expect(records.get('a.txt')).toEqual({ favorite: true, tags: ['invoice'] })
|
|
299
|
+
expect(records.get(to)).toEqual({ favorite: true, tags: ['invoice'] })
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('writes no record for a file that had none', async () => {
|
|
303
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
304
|
+
|
|
305
|
+
await duplicateDashboardFile({ path: 'a.txt' }, manager, store)
|
|
306
|
+
|
|
307
|
+
expect(await store.under('public', '')).toEqual(new Map())
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
describe('orphan sweeping', () => {
|
|
312
|
+
test('a listing removes records for paths it did not see', async () => {
|
|
313
|
+
// The answer to "what about files changed outside the dashboard": the disk
|
|
314
|
+
// is authoritative, and the listing pass already knows every path, so
|
|
315
|
+
// sweeping is free rather than a second pass.
|
|
316
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
317
|
+
await setDashboardFileFavorite({ path: 'a.txt', favorite: true }, manager, store)
|
|
318
|
+
await store.write('public', 'gone.txt', { favorite: true, tags: [] })
|
|
319
|
+
|
|
320
|
+
await getDashboardFileSnapshot({}, manager, store)
|
|
321
|
+
|
|
322
|
+
const records = await store.under('public', '')
|
|
323
|
+
expect([...records.keys()]).toEqual(['a.txt'])
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('a truncated listing sweeps nothing', async () => {
|
|
327
|
+
// A truncated walk has not shown a path is absent, only that it stopped
|
|
328
|
+
// before reaching it. Sweeping on it would delete the metadata of every
|
|
329
|
+
// file past the limit.
|
|
330
|
+
const disk = manager.disk('public')
|
|
331
|
+
await disk.write('a.txt', 'x')
|
|
332
|
+
await disk.write('b.txt', 'x')
|
|
333
|
+
await disk.write('c.txt', 'x')
|
|
334
|
+
await store.write('public', 'starred-but-past-the-limit.txt', { favorite: true, tags: [] })
|
|
335
|
+
|
|
336
|
+
const snapshot = await getDashboardFileSnapshot({ maxEntries: 1 }, manager, store)
|
|
337
|
+
expect(snapshot.truncated).toBeTrue()
|
|
338
|
+
|
|
339
|
+
expect((await store.under('public', '')).has('starred-but-past-the-limit.txt')).toBeTrue()
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test('sweepMetadata refuses directly too, not only through the snapshot', async () => {
|
|
343
|
+
await store.write('public', 'x.txt', { favorite: true, tags: [] })
|
|
344
|
+
|
|
345
|
+
expect(await sweepMetadata(store, 'public', '', new Set(), { truncated: true })).toBe(0)
|
|
346
|
+
expect(await sweepMetadata(store, 'public', '', new Set(), { truncated: false })).toBe(1)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('leaves another disk\'s records alone', async () => {
|
|
350
|
+
await manager.disk('public').write('a.txt', 'x')
|
|
351
|
+
await store.write('other-disk', 'kept.txt', { favorite: true, tags: [] })
|
|
352
|
+
|
|
353
|
+
await getDashboardFileSnapshot({}, manager, store)
|
|
354
|
+
|
|
355
|
+
expect((await store.under('other-disk', '')).has('kept.txt')).toBeTrue()
|
|
356
|
+
})
|
|
357
|
+
})
|