pinokiod 8.0.47 → 8.0.49
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/kernel/vault/constants.js +2 -2
- package/kernel/vault/index.js +18 -0
- package/package.json +1 -1
- package/server/public/vault.css +33 -2
- package/server/public/vault.js +194 -68
- package/server/views/partials/vault_workspace.ejs +1 -0
- package/test/vault-engine.test.js +21 -0
- package/test/vault-ui.test.js +484 -14
package/test/vault-ui.test.js
CHANGED
|
@@ -76,12 +76,12 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
const accepted = await vault.perform('scan', {
|
|
79
|
-
candidate_size: CANDIDATE_SIZE_OPTIONS[
|
|
79
|
+
candidate_size: CANDIDATE_SIZE_OPTIONS[0]
|
|
80
80
|
})
|
|
81
81
|
assert.deepStrictEqual(accepted, { started: true })
|
|
82
82
|
assert.deepStrictEqual(startedWith, {
|
|
83
83
|
scopeId: null,
|
|
84
|
-
sizeThreshold: CANDIDATE_SIZE_OPTIONS[
|
|
84
|
+
sizeThreshold: CANDIDATE_SIZE_OPTIONS[0]
|
|
85
85
|
})
|
|
86
86
|
|
|
87
87
|
startedWith = null
|
|
@@ -674,7 +674,8 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
674
674
|
await vault.sweeper.scan()
|
|
675
675
|
const status = await vault.status()
|
|
676
676
|
assert.strictEqual(status.enabled, true)
|
|
677
|
-
assert.strictEqual(status.bytes_on_disk, content.length)
|
|
677
|
+
assert.strictEqual(status.bytes_on_disk, content.length * 2)
|
|
678
|
+
assert.strictEqual(status.bytes_without_sharing, content.length * 2)
|
|
678
679
|
assert.ok(status.last_scan && status.last_scan.files > 0)
|
|
679
680
|
assert.strictEqual(status.excluded.length, 1)
|
|
680
681
|
assert.strictEqual(status.excluded[0].size, content.length)
|
|
@@ -705,6 +706,79 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
705
706
|
assert.strictEqual(status.saved_by_sharing, 0)
|
|
706
707
|
})
|
|
707
708
|
|
|
709
|
+
test('global storage metrics include scanned files below the candidate threshold', async () => {
|
|
710
|
+
const { home, vault } = await makeEnv()
|
|
711
|
+
const { content } = await makeSharedPair(home, vault)
|
|
712
|
+
const small = crypto.randomBytes(512)
|
|
713
|
+
await writeFile(path.resolve(home, 'api', 'appA', 'config.json'), small)
|
|
714
|
+
await vault.sweeper.scan()
|
|
715
|
+
|
|
716
|
+
const status = await vault.status()
|
|
717
|
+
assert.strictEqual(status.last_scan.bytes_total, (content.length * 2) + small.length)
|
|
718
|
+
assert.strictEqual(status.bytes_on_disk, content.length + small.length)
|
|
719
|
+
assert.strictEqual(status.bytes_without_sharing, (content.length * 2) + small.length)
|
|
720
|
+
assert.strictEqual(status.saved_by_sharing, content.length)
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
test('global storage metrics do not disappear when every scanned file is below the candidate threshold', async () => {
|
|
724
|
+
const { home, vault } = await makeEnv()
|
|
725
|
+
const first = crypto.randomBytes(511)
|
|
726
|
+
const second = crypto.randomBytes(257)
|
|
727
|
+
await writeFile(path.resolve(home, 'api', 'appA', 'config.json'), first)
|
|
728
|
+
await writeFile(path.resolve(home, 'api', 'appB', 'notes.txt'), second)
|
|
729
|
+
await vault.sweeper.scan()
|
|
730
|
+
|
|
731
|
+
const status = await vault.status()
|
|
732
|
+
const total = first.length + second.length
|
|
733
|
+
assert.strictEqual(status.last_scan.bytes_total, total)
|
|
734
|
+
assert.strictEqual(status.bytes_on_disk, total)
|
|
735
|
+
assert.strictEqual(status.bytes_without_sharing, total)
|
|
736
|
+
assert.strictEqual(status.saved_by_sharing, 0)
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
test('global storage metrics include below-threshold files from external locations', async (t) => {
|
|
740
|
+
const { home, vault } = await makeEnv()
|
|
741
|
+
const external = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-ui-size-external-'))
|
|
742
|
+
homes.push(external)
|
|
743
|
+
const local = crypto.randomBytes(401)
|
|
744
|
+
const imported = crypto.randomBytes(607)
|
|
745
|
+
await writeFile(path.resolve(home, 'api', 'appA', 'local.json'), local)
|
|
746
|
+
await writeFile(path.resolve(external, 'external.json'), imported)
|
|
747
|
+
try {
|
|
748
|
+
await vault.addExternalSource(external)
|
|
749
|
+
} catch (error) {
|
|
750
|
+
t.skip(`directory links unavailable: ${error.message}`)
|
|
751
|
+
return
|
|
752
|
+
}
|
|
753
|
+
await vault.sweeper.scan()
|
|
754
|
+
|
|
755
|
+
const status = await vault.status()
|
|
756
|
+
const total = local.length + imported.length
|
|
757
|
+
assert.strictEqual(status.last_scan.home_bytes_total, local.length)
|
|
758
|
+
assert.strictEqual(status.last_scan.bytes_total, total)
|
|
759
|
+
assert.strictEqual(status.bytes_on_disk, total)
|
|
760
|
+
assert.strictEqual(status.bytes_without_sharing, total)
|
|
761
|
+
assert.strictEqual(status.saved_by_sharing, 0)
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
test('global storage metrics add unused managed files without double-counting scanned files', async () => {
|
|
765
|
+
const { home, vault } = await makeEnv()
|
|
766
|
+
const managed = crypto.randomBytes(4096)
|
|
767
|
+
const small = crypto.randomBytes(379)
|
|
768
|
+
const managedPath = await writeFile(path.resolve(home, 'api', 'appA', 'unused.bin'), managed)
|
|
769
|
+
await vault.adopt(managedPath, sha256(managed), { app: 'appA' })
|
|
770
|
+
await fs.promises.unlink(managedPath)
|
|
771
|
+
await writeFile(path.resolve(home, 'api', 'appA', 'settings.json'), small)
|
|
772
|
+
await vault.sweeper.scan()
|
|
773
|
+
|
|
774
|
+
const status = await vault.status()
|
|
775
|
+
assert.strictEqual(status.last_scan.bytes_total, small.length)
|
|
776
|
+
assert.strictEqual(status.reclaimable, managed.length)
|
|
777
|
+
assert.strictEqual(status.bytes_on_disk, managed.length + small.length)
|
|
778
|
+
assert.strictEqual(status.bytes_without_sharing, managed.length + small.length)
|
|
779
|
+
assert.strictEqual(status.saved_by_sharing, 0)
|
|
780
|
+
})
|
|
781
|
+
|
|
708
782
|
test('app status exposes only that app while retaining its matching locations', async () => {
|
|
709
783
|
const { home, vault } = await makeEnv()
|
|
710
784
|
const content = crypto.randomBytes(4096)
|
|
@@ -1061,6 +1135,81 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1061
1135
|
}
|
|
1062
1136
|
})
|
|
1063
1137
|
|
|
1138
|
+
test('an older status response cannot replace newer scan progress', async () => {
|
|
1139
|
+
const views = path.resolve(__dirname, '..', 'server', 'views')
|
|
1140
|
+
const publicRoot = path.resolve(__dirname, '..', 'server', 'public')
|
|
1141
|
+
const workspace = await ejs.renderFile(
|
|
1142
|
+
path.resolve(views, 'partials', 'vault_workspace.ejs'),
|
|
1143
|
+
{ appMode: false }
|
|
1144
|
+
)
|
|
1145
|
+
const baseStatus = {
|
|
1146
|
+
enabled: true,
|
|
1147
|
+
mode: 'link',
|
|
1148
|
+
last_scan: null,
|
|
1149
|
+
bytes_on_disk: 0,
|
|
1150
|
+
bytes_without_sharing: 0,
|
|
1151
|
+
saved_by_sharing: 0,
|
|
1152
|
+
lifetime_bytes_saved: 0,
|
|
1153
|
+
pending_bytes: 0,
|
|
1154
|
+
reclaimable: 0,
|
|
1155
|
+
file_action: null,
|
|
1156
|
+
activity_error: null,
|
|
1157
|
+
cloud_sync_warning: null,
|
|
1158
|
+
sources: [{
|
|
1159
|
+
id: 'pinokio', kind: 'pinokio', label: 'Pinokio', root: '/pinokio',
|
|
1160
|
+
display_path: '/pinokio', parent_id: null, available: true, shareable: true
|
|
1161
|
+
}],
|
|
1162
|
+
blobs: [], duplicates: [], excluded: [], events: [], undo_batches: []
|
|
1163
|
+
}
|
|
1164
|
+
const scan = (queued, currentFile) => ({
|
|
1165
|
+
active: true, pending: false, phase: 'analyzing', scope_id: null,
|
|
1166
|
+
dirs: 4, files: 8, total_files: 8, bytes_total: 8000,
|
|
1167
|
+
hash_total: 937, queued, current_file: currentFile,
|
|
1168
|
+
current_file_bytes: 0, current_file_size: 1000,
|
|
1169
|
+
started: 123
|
|
1170
|
+
})
|
|
1171
|
+
const response = (data) => ({ ok: true, json: async () => data })
|
|
1172
|
+
let requestCount = 0
|
|
1173
|
+
let resolveOlder
|
|
1174
|
+
const olderResponse = new Promise((resolve) => { resolveOlder = resolve })
|
|
1175
|
+
const dom = new JSDOM(
|
|
1176
|
+
`<body data-platform="darwin" data-vault-mode="global">${workspace}</body>`,
|
|
1177
|
+
{ url: 'http://localhost/vault', runScripts: 'dangerously' }
|
|
1178
|
+
)
|
|
1179
|
+
dom.window.fetch = async () => {
|
|
1180
|
+
requestCount += 1
|
|
1181
|
+
if (requestCount === 1) {
|
|
1182
|
+
return response(Object.assign({}, baseStatus, {
|
|
1183
|
+
scan: { active: false, pending: false, queued: 0, phase: 'idle', scope_id: null }
|
|
1184
|
+
}))
|
|
1185
|
+
}
|
|
1186
|
+
if (requestCount === 2) return olderResponse
|
|
1187
|
+
return response(Object.assign({}, baseStatus, { scan: scan(115, 'newer.bin') }))
|
|
1188
|
+
}
|
|
1189
|
+
try {
|
|
1190
|
+
const formatter = await fs.promises.readFile(path.resolve(publicRoot, 'storage-size.js'), 'utf8')
|
|
1191
|
+
const source = await fs.promises.readFile(path.resolve(publicRoot, 'vault.js'), 'utf8')
|
|
1192
|
+
dom.window.eval(formatter)
|
|
1193
|
+
dom.window.eval(source.replace(/\nrefresh\(\)\s*$/, '\nwindow.__testVaultRefresh = refresh\nrefresh()'))
|
|
1194
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
1195
|
+
|
|
1196
|
+
const olderRefresh = dom.window.__testVaultRefresh()
|
|
1197
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
1198
|
+
const newerRefresh = dom.window.__testVaultRefresh()
|
|
1199
|
+
await newerRefresh
|
|
1200
|
+
assert.strictEqual(dom.window.document.querySelector('.vault-scan-percent').textContent, '87.7%')
|
|
1201
|
+
assert.match(dom.window.document.querySelector('.vault-scan-detail').textContent, /822 of 937 large files checked/)
|
|
1202
|
+
|
|
1203
|
+
resolveOlder(response(Object.assign({}, baseStatus, { scan: scan(204, 'older.bin') })))
|
|
1204
|
+
await olderRefresh
|
|
1205
|
+
assert.strictEqual(dom.window.document.querySelector('.vault-scan-percent').textContent, '87.7%')
|
|
1206
|
+
assert.match(dom.window.document.querySelector('.vault-scan-detail').textContent, /822 of 937 large files checked/)
|
|
1207
|
+
assert.doesNotMatch(dom.window.document.querySelector('.vault-scan-detail').textContent, /older\.bin/)
|
|
1208
|
+
} finally {
|
|
1209
|
+
dom.window.close()
|
|
1210
|
+
}
|
|
1211
|
+
})
|
|
1212
|
+
|
|
1064
1213
|
test('item 16: vocabulary lint — vault page copy avoids forbidden terms', async () => {
|
|
1065
1214
|
const forbidden = /hard.?link|junction|symlink|inode|\bblob\b|\bstore\b|\bdedupe\b|\bvault\b/i
|
|
1066
1215
|
const vaultPage = await vaultPageSource()
|
|
@@ -1086,24 +1235,33 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1086
1235
|
test('copy-mode tracking never claims physical sharing savings', async () => {
|
|
1087
1236
|
const { home, vault } = await makeEnv()
|
|
1088
1237
|
const content = crypto.randomBytes(4096)
|
|
1238
|
+
const small = crypto.randomBytes(383)
|
|
1089
1239
|
const hash = sha256(content)
|
|
1090
1240
|
const first = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), content)
|
|
1091
1241
|
const second = await writeFile(path.resolve(home, 'api', 'appB', 'm.bin'), content)
|
|
1242
|
+
await writeFile(path.resolve(home, 'api', 'appA', 'config.json'), small)
|
|
1092
1243
|
const firstStat = await fs.promises.stat(first)
|
|
1093
1244
|
const secondStat = await fs.promises.stat(second)
|
|
1094
1245
|
vault.registry.addBlob(hash, { size: content.length })
|
|
1095
1246
|
vault.registry.addLink(first, { hash, app: 'appA', dev: firstStat.dev, ino: firstStat.ino, mode: 'copy' })
|
|
1096
1247
|
vault.registry.addLink(second, { hash, app: 'appB', dev: secondStat.dev, ino: secondStat.ino, mode: 'copy' })
|
|
1248
|
+
vault.registry.setLastScan({
|
|
1249
|
+
ts: Date.now(),
|
|
1250
|
+
files: 3,
|
|
1251
|
+
bytes_total: (content.length * 2) + small.length,
|
|
1252
|
+
home_bytes_total: (content.length * 2) + small.length
|
|
1253
|
+
})
|
|
1097
1254
|
|
|
1098
1255
|
const status = await vault.status()
|
|
1099
|
-
assert.strictEqual(status.bytes_on_disk, content.length * 2)
|
|
1256
|
+
assert.strictEqual(status.bytes_on_disk, (content.length * 2) + small.length)
|
|
1257
|
+
assert.strictEqual(status.bytes_without_sharing, (content.length * 2) + small.length)
|
|
1100
1258
|
assert.strictEqual(status.saved_by_sharing, 0)
|
|
1101
1259
|
})
|
|
1102
1260
|
|
|
1103
1261
|
test('repair is advanced, metrics distinguish detected and explicit savings, and refresh retries', async () => {
|
|
1104
1262
|
const vaultPage = await vaultPageSource()
|
|
1105
1263
|
assert.match(vaultPage, /disk_space_saved:\s*"of disk space saved"/)
|
|
1106
|
-
assert.match(vaultPage, /before_help:\s*"Estimated
|
|
1264
|
+
assert.match(vaultPage, /before_help:\s*"Estimated size of every scanned location if each app stored its own copy\. File Explorer may count deduplicated files differently\."/)
|
|
1107
1265
|
assert.match(vaultPage, /nothing_more_to_save:\s*"Nothing else to save"/)
|
|
1108
1266
|
assert.match(vaultPage, /more_can_be_saved:\s*"\{size\} more can be saved"/)
|
|
1109
1267
|
assert.match(vaultPage, /fmt\(data\.saved_by_sharing\)/)
|
|
@@ -1186,7 +1344,8 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1186
1344
|
assert.match(vaultPage, /pinokio:vault:reviewed-scan/)
|
|
1187
1345
|
assert.match(vaultPage, /completed \|\| incomplete \|\| \(!state\.scanResult && unreviewed\)/)
|
|
1188
1346
|
assert.match(vaultPage, /state\.data\.undo_batches/)
|
|
1189
|
-
assert.match(vaultPage, /data-undo="\$\{attr\(
|
|
1347
|
+
assert.match(vaultPage, /data-undo="\$\{attr\(item\.batch_id\)\}"/)
|
|
1348
|
+
assert.match(vaultPage, /data-undo="\$\{attr\(event\.batch_id\)\}"/)
|
|
1190
1349
|
assert.match(vaultPage, /last_scan && data\.last_scan\.hash_failures/)
|
|
1191
1350
|
assert.match(vaultPage, /scan_not_analyzed:\s*"could not be analyzed"/)
|
|
1192
1351
|
assert.match(vaultPage, /\.vault-progress-bar\.determinate \{[\s\S]*?transform:\s*scaleX/)
|
|
@@ -1323,7 +1482,7 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1323
1482
|
assert.match(dom.window.document.getElementById('btn-scan').textContent, /Scan this app/)
|
|
1324
1483
|
const candidateSize = dom.window.document.getElementById('vault-candidate-size')
|
|
1325
1484
|
assert.deepStrictEqual([...candidateSize.options].map((option) => option.textContent),
|
|
1326
|
-
['10 MB+', '50 MB+', '100 MB+', '500 MB+', '1 GB+'])
|
|
1485
|
+
['1 MB+', '10 MB+', '50 MB+', '100 MB+', '500 MB+', '1 GB+'])
|
|
1327
1486
|
assert.strictEqual(candidateSize.value, '100000000')
|
|
1328
1487
|
candidateSize.value = '50000000'
|
|
1329
1488
|
candidateSize.dispatchEvent(new dom.window.Event('change', { bubbles: true }))
|
|
@@ -1387,11 +1546,14 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1387
1546
|
const formatter = await fs.promises.readFile(
|
|
1388
1547
|
path.resolve(__dirname, '..', 'server', 'public', 'storage-size.js'), 'utf8')
|
|
1389
1548
|
dom.window.eval(formatter)
|
|
1549
|
+
const screenshotFolderBytes = 2_002_022_055_452
|
|
1390
1550
|
assert.strictEqual(dom.window.PinokioFormatStorageSize(7.05e9), '7.05 GB')
|
|
1391
1551
|
assert.strictEqual(dom.window.PinokioFormatStorageSize(5.3e9), '5.3 GB')
|
|
1392
1552
|
assert.strictEqual(dom.window.PinokioFormatStorageSize(1024 ** 3), '1.07 GB')
|
|
1553
|
+
assert.strictEqual(dom.window.PinokioFormatStorageSize(screenshotFolderBytes), '2 TB')
|
|
1393
1554
|
dom.window.document.body.dataset.platform = 'win32'
|
|
1394
1555
|
assert.strictEqual(dom.window.PinokioFormatStorageSize(1024 ** 3), '1 GB')
|
|
1556
|
+
assert.strictEqual(dom.window.PinokioFormatStorageSize(screenshotFolderBytes), '1.82 TB')
|
|
1395
1557
|
|
|
1396
1558
|
const appView = await fs.promises.readFile(
|
|
1397
1559
|
path.resolve(__dirname, '..', 'server', 'views', 'app.ejs'), 'utf8')
|
|
@@ -1443,7 +1605,8 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1443
1605
|
assert.deepStrictEqual([...dom.window.document.querySelectorAll('.vault-compare-label')].map((node) => node.firstChild.textContent.trim()), ['Before', 'After'])
|
|
1444
1606
|
assert.deepStrictEqual([...dom.window.document.querySelectorAll('.vault-compare-value')].map((node) => node.textContent), ['661.75 GB', '404.8 GB'])
|
|
1445
1607
|
assert.match(dom.window.document.querySelector('.vault-compare-fill.after').getAttribute('style'), /--vault-after-ratio:61\.17%/)
|
|
1446
|
-
assert.strictEqual(dom.window.document.getElementById('vault-before-help').textContent,
|
|
1608
|
+
assert.strictEqual(dom.window.document.getElementById('vault-before-help').textContent,
|
|
1609
|
+
'Estimated size of every scanned location if each app stored its own copy. File Explorer may count deduplicated files differently.')
|
|
1447
1610
|
assert.strictEqual(dom.window.document.querySelector('.vault-compare-info').getAttribute('tabindex'), '0')
|
|
1448
1611
|
assert.match(dom.window.document.querySelector('.vault-summary-side').textContent, /13\.31 GB more can be saved/)
|
|
1449
1612
|
assert.strictEqual(dom.window.document.getElementById('btn-review-metric').textContent, 'Review files')
|
|
@@ -1451,12 +1614,21 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1451
1614
|
assert.strictEqual(dom.window.document.getElementById('btn-scan').classList.contains('primary'), false)
|
|
1452
1615
|
assert.match(dom.window.document.getElementById('vault-storage-details').textContent, /Pinokio folder\s*166\.22 GB/)
|
|
1453
1616
|
assert.match(dom.window.document.getElementById('vault-storage-details').textContent, /Saved by your actions\s*258\.77 GB/)
|
|
1617
|
+
const cleanupNotice = dom.window.document.getElementById('vault-cleanup-notice')
|
|
1618
|
+
assert.strictEqual(cleanupNotice.classList.contains('show'), true)
|
|
1619
|
+
assert.match(cleanupNotice.textContent, /ready to clean up/)
|
|
1620
|
+
assert.match(cleanupNotice.textContent, /1 private link left/)
|
|
1621
|
+
dom.window.document.getElementById('btn-review-cleanup').click()
|
|
1622
|
+
assert.strictEqual(dom.window.document.querySelector('[data-view="reclaimable"]').classList.contains('selected'), true)
|
|
1623
|
+
assert.strictEqual(cleanupNotice.classList.contains('show'), false)
|
|
1624
|
+
dom.window.document.querySelector('[data-view="all"]').click()
|
|
1454
1625
|
const viewDescriptions = {
|
|
1455
1626
|
all: 'Every scanned file and its current deduplication status.',
|
|
1456
1627
|
duplicates: 'Identical files waiting to be deduplicated or kept separate.',
|
|
1457
1628
|
shared: 'Files that share disk storage across multiple locations.',
|
|
1629
|
+
tracked: 'Files with no duplicate action required.',
|
|
1458
1630
|
independent: 'Duplicate files that remain as separate copies.',
|
|
1459
|
-
reclaimable: '
|
|
1631
|
+
reclaimable: 'Private links no longer used by any linked file.',
|
|
1460
1632
|
activity: 'A history of scans and changes made by Save space.'
|
|
1461
1633
|
}
|
|
1462
1634
|
for (const [view, description] of Object.entries(viewDescriptions)) {
|
|
@@ -1472,9 +1644,10 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1472
1644
|
const unusedView = dom.window.document.querySelector('[data-view="reclaimable"]')
|
|
1473
1645
|
assert.strictEqual(unusedView.querySelector('.vault-nav-name').textContent, 'Unused files')
|
|
1474
1646
|
unusedView.click()
|
|
1475
|
-
assert.strictEqual(dom.window.document.getElementById('btn-reclaim-all').textContent, '
|
|
1476
|
-
assert.strictEqual(dom.window.document.querySelector('[data-reclaim]').textContent, '
|
|
1477
|
-
assert.match(dom.window.document.getElementById('vault-pane-footer').textContent,
|
|
1647
|
+
assert.strictEqual(dom.window.document.getElementById('btn-reclaim-all').textContent, 'Clean up all')
|
|
1648
|
+
assert.strictEqual(dom.window.document.querySelector('[data-reclaim]').textContent, 'Clean up')
|
|
1649
|
+
assert.match(dom.window.document.getElementById('vault-pane-footer').textContent,
|
|
1650
|
+
/Cleaning them up frees disk space/)
|
|
1478
1651
|
dom.window.close()
|
|
1479
1652
|
})
|
|
1480
1653
|
|
|
@@ -1517,6 +1690,7 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1517
1690
|
['10 GB', '7 GB'])
|
|
1518
1691
|
assert.match(dom.window.document.querySelector('.vault-summary-side').textContent,
|
|
1519
1692
|
/1 GB more can be saved.*Not scanned yet/)
|
|
1693
|
+
assert.strictEqual(dom.window.document.getElementById('vault-cleanup-notice').classList.contains('show'), false)
|
|
1520
1694
|
dom.window.close()
|
|
1521
1695
|
})
|
|
1522
1696
|
|
|
@@ -1549,8 +1723,8 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1549
1723
|
assert.match(source, /headers = \[COPY\.name, COPY\.size, COPY\.can_free, ""\]/)
|
|
1550
1724
|
assert.match(source, /headers = \[COPY\.name, COPY\.size, COPY\.last_scanned, ""\]/)
|
|
1551
1725
|
assert.match(source, /reclaimable:\s*"Unused files"/)
|
|
1552
|
-
assert.match(source, /reclaim:\s*"
|
|
1553
|
-
assert.match(source, /reclaim_all:\s*"
|
|
1726
|
+
assert.match(source, /reclaim:\s*"Clean up"/)
|
|
1727
|
+
assert.match(source, /reclaim_all:\s*"Clean up all"/)
|
|
1554
1728
|
assert.match(source, /data-sort-size/)
|
|
1555
1729
|
assert.match(source, /data-display-mode="folders"/)
|
|
1556
1730
|
assert.match(source, /data-display-mode="files"/)
|
|
@@ -1853,6 +2027,244 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1853
2027
|
}
|
|
1854
2028
|
})
|
|
1855
2029
|
|
|
2030
|
+
test('No action needed is a counted, filtered, paginated file view', async () => {
|
|
2031
|
+
const views = path.resolve(__dirname, '..', 'server', 'views')
|
|
2032
|
+
const html = await ejs.renderFile(path.resolve(views, 'vault_app.ejs'), {
|
|
2033
|
+
theme: 'light', platform: 'darwin', agent: 'electron', scope_id: 'app:appB'
|
|
2034
|
+
})
|
|
2035
|
+
const dom = new JSDOM(html, {
|
|
2036
|
+
url: 'http://localhost/vault/app/appB',
|
|
2037
|
+
runScripts: 'dangerously'
|
|
2038
|
+
})
|
|
2039
|
+
const blobs = Array.from({ length: 501 }, (_, index) => {
|
|
2040
|
+
const name = `tracked-${String(index).padStart(4, '0')}.bin`
|
|
2041
|
+
return {
|
|
2042
|
+
hash: index.toString(16).padStart(64, '0'),
|
|
2043
|
+
size: 1024 + index,
|
|
2044
|
+
orphan: false,
|
|
2045
|
+
nlink: 2,
|
|
2046
|
+
names: [{
|
|
2047
|
+
path: `/pinokio/api/appB/${name}`,
|
|
2048
|
+
relative_path: name,
|
|
2049
|
+
source_id: 'app:appB',
|
|
2050
|
+
source_label: 'appB',
|
|
2051
|
+
mode: 'link'
|
|
2052
|
+
}]
|
|
2053
|
+
}
|
|
2054
|
+
})
|
|
2055
|
+
const status = {
|
|
2056
|
+
enabled: true,
|
|
2057
|
+
mode: 'link',
|
|
2058
|
+
scan: { active: false, pending: false, queued: 0, phase: 'idle', scope_id: null },
|
|
2059
|
+
last_scan: { ts: Date.now(), bytes_total: 1024 * 502 },
|
|
2060
|
+
tracked_bytes: 1024 * 502,
|
|
2061
|
+
effective_bytes: 1024 * 502,
|
|
2062
|
+
shared_bytes: 0,
|
|
2063
|
+
pending_bytes: 1024,
|
|
2064
|
+
activity_error: null,
|
|
2065
|
+
cloud_sync_warning: null,
|
|
2066
|
+
sources: [{
|
|
2067
|
+
id: 'app:appB', kind: 'app', label: 'appB', root: '/pinokio/api/appB',
|
|
2068
|
+
display_path: '/pinokio/api/appB', parent_id: null, available: true, shareable: true
|
|
2069
|
+
}],
|
|
2070
|
+
blobs,
|
|
2071
|
+
duplicates: [{
|
|
2072
|
+
path: '/pinokio/api/appB/duplicate.bin',
|
|
2073
|
+
relative_path: 'duplicate.bin',
|
|
2074
|
+
source_id: 'app:appB',
|
|
2075
|
+
source_label: 'appB',
|
|
2076
|
+
size: 1024,
|
|
2077
|
+
shareable: true,
|
|
2078
|
+
match: { path: '/pinokio/api/appA/duplicate.bin' }
|
|
2079
|
+
}],
|
|
2080
|
+
excluded: [],
|
|
2081
|
+
events: [],
|
|
2082
|
+
undo_batches: []
|
|
2083
|
+
}
|
|
2084
|
+
dom.window.fetch = async () => ({ ok: true, json: async () => status })
|
|
2085
|
+
await runVaultScript(dom)
|
|
2086
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
2087
|
+
|
|
2088
|
+
const trackedView = dom.window.document.querySelector('[data-view="tracked"]')
|
|
2089
|
+
assert.strictEqual(trackedView.querySelector('.vault-nav-name').textContent, 'No action needed')
|
|
2090
|
+
assert.strictEqual(trackedView.querySelector('.vault-nav-count').textContent, '501')
|
|
2091
|
+
trackedView.click()
|
|
2092
|
+
|
|
2093
|
+
const rows = () => [...dom.window.document.querySelectorAll('.vault-table .vault-file-row')]
|
|
2094
|
+
assert.strictEqual(rows().length, 500)
|
|
2095
|
+
assert.strictEqual(dom.window.document.getElementById('vault-search').placeholder,
|
|
2096
|
+
'Search files with no action needed')
|
|
2097
|
+
assert.strictEqual(dom.window.document.querySelector('.vault-page-range').textContent, '1–500 of 501')
|
|
2098
|
+
assert.doesNotMatch(dom.window.document.getElementById('vault-table-wrap').textContent, /duplicate\.bin/)
|
|
2099
|
+
|
|
2100
|
+
dom.window.document.querySelector('[data-page="next"]').click()
|
|
2101
|
+
assert.strictEqual(rows().length, 1)
|
|
2102
|
+
assert.strictEqual(rows()[0].querySelector('.vault-file-name').textContent, 'tracked-0500.bin')
|
|
2103
|
+
assert.strictEqual(dom.window.document.querySelector('.vault-page-range').textContent, '501–501 of 501')
|
|
2104
|
+
dom.window.close()
|
|
2105
|
+
})
|
|
2106
|
+
|
|
2107
|
+
test('Activity is paginated without repeating a batch undo action', async () => {
|
|
2108
|
+
const views = path.resolve(__dirname, '..', 'server', 'views')
|
|
2109
|
+
const html = await ejs.renderFile(path.resolve(views, 'vault_app.ejs'), {
|
|
2110
|
+
theme: 'light', platform: 'darwin', agent: 'electron', scope_id: 'app:appB'
|
|
2111
|
+
})
|
|
2112
|
+
const dom = new JSDOM(html, {
|
|
2113
|
+
url: 'http://localhost/vault/app/appB',
|
|
2114
|
+
runScripts: 'dangerously'
|
|
2115
|
+
})
|
|
2116
|
+
const events = Array.from({ length: 501 }, (_, index) => ({
|
|
2117
|
+
kind: 'convert',
|
|
2118
|
+
path: `/pinokio/api/appB/event-${String(index).padStart(4, '0')}.bin`,
|
|
2119
|
+
relative_path: `event-${String(index).padStart(4, '0')}.bin`,
|
|
2120
|
+
source_id: 'app:appB',
|
|
2121
|
+
source_label: 'appB',
|
|
2122
|
+
batch_id: 'batch-large',
|
|
2123
|
+
undoable: index !== 0,
|
|
2124
|
+
bytes_saved: 1024 + index,
|
|
2125
|
+
ts: Date.now() - index
|
|
2126
|
+
}))
|
|
2127
|
+
const status = {
|
|
2128
|
+
enabled: true,
|
|
2129
|
+
mode: 'link',
|
|
2130
|
+
scan: { active: false, pending: false, queued: 0, phase: 'idle', scope_id: null },
|
|
2131
|
+
last_scan: { ts: Date.now(), bytes_total: 1024 * 501 },
|
|
2132
|
+
tracked_bytes: 0,
|
|
2133
|
+
effective_bytes: 0,
|
|
2134
|
+
shared_bytes: 0,
|
|
2135
|
+
pending_bytes: 0,
|
|
2136
|
+
activity_error: null,
|
|
2137
|
+
cloud_sync_warning: null,
|
|
2138
|
+
sources: [{
|
|
2139
|
+
id: 'app:appB', kind: 'app', label: 'appB', root: '/pinokio/api/appB',
|
|
2140
|
+
display_path: '/pinokio/api/appB', parent_id: null, available: true, shareable: true
|
|
2141
|
+
}],
|
|
2142
|
+
blobs: [],
|
|
2143
|
+
duplicates: [],
|
|
2144
|
+
excluded: [],
|
|
2145
|
+
events,
|
|
2146
|
+
undo_batches: [{
|
|
2147
|
+
batch_id: 'batch-large',
|
|
2148
|
+
files: events.length,
|
|
2149
|
+
bytes: events.reduce((sum, event) => sum + event.bytes_saved, 0),
|
|
2150
|
+
ts: events[0].ts
|
|
2151
|
+
}, {
|
|
2152
|
+
batch_id: 'batch-compacted',
|
|
2153
|
+
files: 2,
|
|
2154
|
+
bytes: 4096,
|
|
2155
|
+
ts: events[events.length - 1].ts - 1
|
|
2156
|
+
}]
|
|
2157
|
+
}
|
|
2158
|
+
dom.window.fetch = async () => ({ ok: true, json: async () => status })
|
|
2159
|
+
await runVaultScript(dom)
|
|
2160
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
2161
|
+
|
|
2162
|
+
const activityView = dom.window.document.querySelector('[data-view="activity"]')
|
|
2163
|
+
assert.strictEqual(activityView.querySelector('.vault-nav-count').textContent, '502')
|
|
2164
|
+
activityView.click()
|
|
2165
|
+
|
|
2166
|
+
const rows = () => [...dom.window.document.querySelectorAll('.vault-table.activity .vault-file-row')]
|
|
2167
|
+
assert.strictEqual(rows().length, 500)
|
|
2168
|
+
assert.strictEqual(dom.window.document.querySelectorAll('[data-undo="batch-large"]').length, 1)
|
|
2169
|
+
assert.strictEqual(dom.window.document.querySelectorAll('[data-undo="batch-compacted"]').length, 1)
|
|
2170
|
+
assert.strictEqual(dom.window.document.querySelector('.vault-page-range').textContent, '1–500 of 502')
|
|
2171
|
+
|
|
2172
|
+
dom.window.document.querySelector('[data-page="next"]').click()
|
|
2173
|
+
assert.strictEqual(rows().length, 2)
|
|
2174
|
+
assert.strictEqual(dom.window.document.querySelectorAll('[data-undo="batch-large"]').length, 0)
|
|
2175
|
+
assert.strictEqual(dom.window.document.querySelectorAll('[data-undo="batch-compacted"]').length, 0)
|
|
2176
|
+
assert.strictEqual(dom.window.document.querySelector('.vault-page-range').textContent, '501–502 of 502')
|
|
2177
|
+
dom.window.close()
|
|
2178
|
+
})
|
|
2179
|
+
|
|
2180
|
+
test('large file lists render bounded pages without narrowing Deduplicate all', async () => {
|
|
2181
|
+
const views = path.resolve(__dirname, '..', 'server', 'views')
|
|
2182
|
+
const html = await ejs.renderFile(path.resolve(views, 'vault_app.ejs'), {
|
|
2183
|
+
theme: 'light', platform: 'darwin', agent: 'electron', scope_id: 'app:appB'
|
|
2184
|
+
})
|
|
2185
|
+
const dom = new JSDOM(html, {
|
|
2186
|
+
url: 'http://localhost/vault/app/appB',
|
|
2187
|
+
runScripts: 'dangerously'
|
|
2188
|
+
})
|
|
2189
|
+
const actions = []
|
|
2190
|
+
const duplicates = Array.from({ length: 501 }, (_, index) => {
|
|
2191
|
+
const name = `file-${String(index).padStart(4, '0')}.bin`
|
|
2192
|
+
return {
|
|
2193
|
+
path: `/pinokio/api/appB/${name}`,
|
|
2194
|
+
relative_path: name,
|
|
2195
|
+
source_id: 'app:appB',
|
|
2196
|
+
source_label: 'appB',
|
|
2197
|
+
size: 1024 + index,
|
|
2198
|
+
shareable: true,
|
|
2199
|
+
match: { path: `/pinokio/api/appA/${name}` }
|
|
2200
|
+
}
|
|
2201
|
+
})
|
|
2202
|
+
const status = {
|
|
2203
|
+
enabled: true,
|
|
2204
|
+
mode: 'link',
|
|
2205
|
+
scan: { active: false, pending: false, queued: 0, phase: 'idle', scope_id: null },
|
|
2206
|
+
last_scan: { ts: Date.now(), bytes_total: 1024 * 501 },
|
|
2207
|
+
tracked_bytes: 1024 * 501,
|
|
2208
|
+
effective_bytes: 1024 * 501,
|
|
2209
|
+
shared_bytes: 0,
|
|
2210
|
+
pending_bytes: duplicates.reduce((sum, item) => sum + item.size, 0),
|
|
2211
|
+
activity_error: null,
|
|
2212
|
+
cloud_sync_warning: null,
|
|
2213
|
+
sources: [{
|
|
2214
|
+
id: 'app:appB', kind: 'app', label: 'appB', root: '/pinokio/api/appB',
|
|
2215
|
+
display_path: '/pinokio/api/appB', parent_id: null, available: true, shareable: true
|
|
2216
|
+
}],
|
|
2217
|
+
blobs: [],
|
|
2218
|
+
duplicates,
|
|
2219
|
+
excluded: [],
|
|
2220
|
+
events: [],
|
|
2221
|
+
undo_batches: []
|
|
2222
|
+
}
|
|
2223
|
+
dom.window.fetch = async (url, options = {}) => {
|
|
2224
|
+
if (options.method === 'POST') {
|
|
2225
|
+
actions.push(JSON.parse(options.body))
|
|
2226
|
+
return { ok: true, json: async () => ({ converted: 501, bytes_saved: status.pending_bytes }) }
|
|
2227
|
+
}
|
|
2228
|
+
return { ok: true, json: async () => status }
|
|
2229
|
+
}
|
|
2230
|
+
await runVaultScript(dom)
|
|
2231
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
2232
|
+
|
|
2233
|
+
dom.window.document.querySelector('[data-view="duplicates"]').click()
|
|
2234
|
+
const rows = () => [...dom.window.document.querySelectorAll('.vault-table.matches .vault-file-row')]
|
|
2235
|
+
let bulk = dom.window.document.querySelector('[data-deduplicate-all="duplicates"]')
|
|
2236
|
+
assert.strictEqual(rows().length, 500)
|
|
2237
|
+
assert.strictEqual(bulk.textContent, 'Deduplicate 501 files')
|
|
2238
|
+
assert.strictEqual(dom.window.document.querySelector('.vault-page-range').textContent, '1–500 of 501')
|
|
2239
|
+
assert.strictEqual(dom.window.document.querySelector('[data-page="previous"]').disabled, true)
|
|
2240
|
+
assert.strictEqual(dom.window.document.querySelector('[data-page="next"]').disabled, false)
|
|
2241
|
+
|
|
2242
|
+
dom.window.document.querySelector('[data-page="next"]').click()
|
|
2243
|
+
assert.strictEqual(rows().length, 1)
|
|
2244
|
+
assert.strictEqual(rows()[0].querySelector('.vault-file-name').textContent, 'file-0500.bin')
|
|
2245
|
+
assert.strictEqual(dom.window.document.querySelector('.vault-page-range').textContent, '501–501 of 501')
|
|
2246
|
+
assert.strictEqual(dom.window.document.querySelector('[data-page="previous"]').disabled, false)
|
|
2247
|
+
assert.strictEqual(dom.window.document.querySelector('[data-page="next"]').disabled, true)
|
|
2248
|
+
|
|
2249
|
+
bulk = dom.window.document.querySelector('[data-deduplicate-all="duplicates"]')
|
|
2250
|
+
assert.strictEqual(bulk.textContent, 'Deduplicate 501 files')
|
|
2251
|
+
bulk.click()
|
|
2252
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
2253
|
+
assert.deepStrictEqual(actions[0], {
|
|
2254
|
+
action: 'deduplicate',
|
|
2255
|
+
selection: 'duplicates',
|
|
2256
|
+
scope_id: 'app:appB'
|
|
2257
|
+
})
|
|
2258
|
+
|
|
2259
|
+
const search = dom.window.document.getElementById('vault-search')
|
|
2260
|
+
search.value = 'file-0001.bin'
|
|
2261
|
+
search.dispatchEvent(new dom.window.Event('input', { bubbles: true }))
|
|
2262
|
+
assert.strictEqual(rows().length, 1)
|
|
2263
|
+
assert.strictEqual(rows()[0].querySelector('.vault-file-name').textContent, 'file-0001.bin')
|
|
2264
|
+
assert.strictEqual(dom.window.document.querySelector('.vault-pagination'), null)
|
|
2265
|
+
dom.window.close()
|
|
2266
|
+
})
|
|
2267
|
+
|
|
1856
2268
|
test('app inventory uses explicit sharing actions without ambiguous switches', async () => {
|
|
1857
2269
|
const views = path.resolve(__dirname, '..', 'server', 'views')
|
|
1858
2270
|
const html = await ejs.renderFile(path.resolve(views, 'vault_app.ejs'), {
|
|
@@ -2060,6 +2472,64 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
2060
2472
|
dom.window.close()
|
|
2061
2473
|
})
|
|
2062
2474
|
|
|
2475
|
+
test('external file rows and expanded matches show the full target path', async () => {
|
|
2476
|
+
const views = path.resolve(__dirname, '..', 'server', 'views')
|
|
2477
|
+
const html = await ejs.renderFile(path.resolve(views, 'vault_app.ejs'), {
|
|
2478
|
+
theme: 'light', platform: 'darwin', agent: 'electron', scope_id: 'external:api'
|
|
2479
|
+
})
|
|
2480
|
+
const dom = new JSDOM(html, { url: 'http://localhost/vault/app/external%3Aapi', runScripts: 'dangerously' })
|
|
2481
|
+
const targetRoot = '/Volumes/Models/api'
|
|
2482
|
+
const relativePath = 'ideogram/app/comfy_models/model.safetensors'
|
|
2483
|
+
const externalPath = `${targetRoot}/${relativePath}`
|
|
2484
|
+
const status = {
|
|
2485
|
+
enabled: true,
|
|
2486
|
+
mode: 'link',
|
|
2487
|
+
scan: { active: false, pending: false, queued: 0, phase: 'idle', scope_id: null },
|
|
2488
|
+
last_scan: { ts: Date.now(), bytes_total: 4096 },
|
|
2489
|
+
tracked_bytes: 4096,
|
|
2490
|
+
effective_bytes: 2048,
|
|
2491
|
+
shared_bytes: 4096,
|
|
2492
|
+
pending_bytes: 0,
|
|
2493
|
+
activity_error: null,
|
|
2494
|
+
cloud_sync_warning: null,
|
|
2495
|
+
sources: [{
|
|
2496
|
+
id: 'external:api', kind: 'external', label: 'api', root: targetRoot,
|
|
2497
|
+
display_path: '/pinokio/vault/sources/api', target_path: targetRoot,
|
|
2498
|
+
parent_id: null, available: true, shareable: true
|
|
2499
|
+
}],
|
|
2500
|
+
blobs: [{
|
|
2501
|
+
hash: 'a'.repeat(64), size: 4096, orphan: false, nlink: 3,
|
|
2502
|
+
names: [
|
|
2503
|
+
{
|
|
2504
|
+
path: externalPath, relative_path: relativePath,
|
|
2505
|
+
source_id: 'external:api', source_label: 'api', mode: 'link'
|
|
2506
|
+
},
|
|
2507
|
+
{
|
|
2508
|
+
path: `/pinokio/api/appA/${relativePath}`, relative_path: relativePath,
|
|
2509
|
+
source_id: 'app:appA', source_label: 'appA', mode: 'link'
|
|
2510
|
+
}
|
|
2511
|
+
]
|
|
2512
|
+
}],
|
|
2513
|
+
duplicates: [], excluded: [], events: [], undo_batches: []
|
|
2514
|
+
}
|
|
2515
|
+
dom.window.fetch = async () => ({ ok: true, json: async () => status })
|
|
2516
|
+
await runVaultScript(dom)
|
|
2517
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
2518
|
+
|
|
2519
|
+
dom.window.document.querySelector('[data-view="shared"]').click()
|
|
2520
|
+
assert.strictEqual(
|
|
2521
|
+
dom.window.document.querySelector('.vault-flat-location').textContent,
|
|
2522
|
+
externalPath
|
|
2523
|
+
)
|
|
2524
|
+
dom.window.document.querySelector('[data-expand-file]').click()
|
|
2525
|
+
assert.deepStrictEqual(
|
|
2526
|
+
[...dom.window.document.querySelectorAll('.vault-location-detail span')]
|
|
2527
|
+
.map((node) => node.textContent),
|
|
2528
|
+
[externalPath, `appA / ${relativePath}`]
|
|
2529
|
+
)
|
|
2530
|
+
dom.window.close()
|
|
2531
|
+
})
|
|
2532
|
+
|
|
2063
2533
|
test('vault route provisions the dev requirements needed by its folder picker', async () => {
|
|
2064
2534
|
const serverSource = await fs.promises.readFile(path.resolve(__dirname, '..', 'server', 'index.js'), 'utf8')
|
|
2065
2535
|
const routeStart = serverSource.indexOf('this.app.get("/vault"')
|