pinokiod 8.0.41 → 8.0.44
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 +1 -5
- package/kernel/vault/hash_worker.js +1 -7
- package/kernel/vault/index.js +515 -873
- package/kernel/vault/registry.js +35 -157
- package/kernel/vault/sweeper.js +106 -181
- package/kernel/vault/walker.js +1 -8
- package/package.json +1 -1
- package/server/index.js +3 -20
- package/server/public/storage-size.js +3 -1
- package/server/public/style.css +0 -13
- package/server/public/vault.css +60 -173
- package/server/public/vault.js +397 -626
- package/server/views/app.ejs +6 -85
- package/server/views/partials/main_sidebar.ejs +1 -4
- package/server/views/partials/vault_workspace.ejs +2 -5
- package/server/views/vault.ejs +1 -1
- package/server/views/vault_app.ejs +1 -1
- package/test/vault-engine.test.js +123 -212
- package/test/vault-sweep.test.js +163 -292
- package/test/vault-ui.test.js +718 -979
- package/server/public/vault-nav.js +0 -96
|
@@ -93,73 +93,10 @@ describe('vault engine (phase 1)', () => {
|
|
|
93
93
|
|
|
94
94
|
assert.deepStrictEqual(startup, { enabled: true, fresh: true })
|
|
95
95
|
assert.strictEqual(fs.existsSync(path.resolve(h, 'vault')), false)
|
|
96
|
-
assert.deepStrictEqual(vault.navigationStatus(), {
|
|
97
|
-
any_scan: false,
|
|
98
|
-
scanned: false,
|
|
99
|
-
scanning: false,
|
|
100
|
-
pending_bytes: 0
|
|
101
|
-
})
|
|
102
|
-
assert.strictEqual(fs.existsSync(path.resolve(h, 'vault')), false, 'navigation status is read-only')
|
|
103
96
|
await vault.ensureInitialized()
|
|
104
97
|
assert.strictEqual(fs.existsSync(path.resolve(h, 'vault', 'registry.json')), true)
|
|
105
98
|
})
|
|
106
99
|
|
|
107
|
-
test('navigation status distinguishes global and app scans without filesystem work', async () => {
|
|
108
|
-
const h = await home()
|
|
109
|
-
const pending = await writeFile(path.resolve(h, 'api', 'appA', 'pending.bin'), 'pending')
|
|
110
|
-
const vault = await makeVault(h)
|
|
111
|
-
const hash = sha256(Buffer.from('pending'))
|
|
112
|
-
const sourceId = 'app:appA'
|
|
113
|
-
vault.registry.addBlob(hash, { size: 7000 })
|
|
114
|
-
vault.registry.setDuplicate(pending, {
|
|
115
|
-
hash,
|
|
116
|
-
size: 7000,
|
|
117
|
-
app: 'appA',
|
|
118
|
-
source_id: sourceId
|
|
119
|
-
})
|
|
120
|
-
vault.registry.setLastScan({ ts: 1, files: 1 }, sourceId)
|
|
121
|
-
|
|
122
|
-
assert.deepStrictEqual(vault.navigationStatus(), {
|
|
123
|
-
any_scan: true,
|
|
124
|
-
scanned: false,
|
|
125
|
-
scanning: false,
|
|
126
|
-
pending_bytes: 7000,
|
|
127
|
-
incomplete: false
|
|
128
|
-
})
|
|
129
|
-
assert.deepStrictEqual(vault.navigationStatusForApp('appA'), {
|
|
130
|
-
any_scan: true,
|
|
131
|
-
scanned: true,
|
|
132
|
-
scanning: false,
|
|
133
|
-
pending_bytes: 7000,
|
|
134
|
-
incomplete: false
|
|
135
|
-
})
|
|
136
|
-
assert.deepStrictEqual(vault.navigationStatusForApp('appB'), {
|
|
137
|
-
any_scan: true,
|
|
138
|
-
scanned: false,
|
|
139
|
-
scanning: false,
|
|
140
|
-
pending_bytes: 0,
|
|
141
|
-
incomplete: false
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
vault.registry.exclude(pending, {
|
|
145
|
-
ts: Date.now(),
|
|
146
|
-
size: 7000,
|
|
147
|
-
source_id: sourceId,
|
|
148
|
-
unverified: true
|
|
149
|
-
})
|
|
150
|
-
assert.strictEqual(vault.navigationStatus().incomplete, true)
|
|
151
|
-
assert.strictEqual(vault.navigationStatusForApp('appA').incomplete, true)
|
|
152
|
-
|
|
153
|
-
vault.sweeper.state = Object.assign(vault.sweeper.idleState(), {
|
|
154
|
-
active: true,
|
|
155
|
-
phase: 'discovering',
|
|
156
|
-
scope_id: sourceId
|
|
157
|
-
})
|
|
158
|
-
assert.strictEqual(vault.navigationStatus().scanning, true)
|
|
159
|
-
assert.strictEqual(vault.navigationStatusForApp('appA').scanning, true)
|
|
160
|
-
assert.strictEqual(vault.navigationStatusForApp('appB').scanning, false)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
100
|
test('adopt: metadata-only, store name shares the inode', async () => {
|
|
164
101
|
const h = await home()
|
|
165
102
|
const vault = await makeVault(h)
|
|
@@ -222,6 +159,7 @@ describe('vault engine (phase 1)', () => {
|
|
|
222
159
|
assert.strictEqual(stA.ino, stB.ino)
|
|
223
160
|
assert.strictEqual(stB.nlink, 3)
|
|
224
161
|
assert.deepStrictEqual(await fs.promises.readFile(b), content)
|
|
162
|
+
assert.strictEqual(vault.registry.totals.lifetime_bytes_saved, content.length)
|
|
225
163
|
const events = await vault.registry.readEvents()
|
|
226
164
|
assert.ok(events.some((e) => e.kind === 'convert' && e.batch_id === 'batch1'))
|
|
227
165
|
})
|
|
@@ -266,6 +204,7 @@ describe('vault engine (phase 1)', () => {
|
|
|
266
204
|
const duplicateStat = await fs.promises.stat(duplicate)
|
|
267
205
|
assert.strictEqual(duplicateStat.ino, storeStat.ino)
|
|
268
206
|
assert.strictEqual(vault.registry.links.get(duplicate).batch_id, 'committed')
|
|
207
|
+
assert.strictEqual(vault.registry.totals.lifetime_bytes_saved, content.length)
|
|
269
208
|
})
|
|
270
209
|
|
|
271
210
|
test('a failed activity append does not misreport a completed conversion', async () => {
|
|
@@ -326,6 +265,7 @@ describe('vault engine (phase 1)', () => {
|
|
|
326
265
|
assert.strictEqual(result.status, 'stale')
|
|
327
266
|
assert.deepStrictEqual(await fs.promises.readFile(duplicate), replacement)
|
|
328
267
|
assert.strictEqual(vault.registry.links.has(duplicate), false)
|
|
268
|
+
assert.strictEqual(vault.registry.totals.lifetime_bytes_saved, 0)
|
|
329
269
|
})
|
|
330
270
|
|
|
331
271
|
test('convert refuses a final store metadata change', {
|
|
@@ -378,11 +318,6 @@ describe('vault engine (phase 1)', () => {
|
|
|
378
318
|
dev: duplicateStat.dev, ino: duplicateStat.ino,
|
|
379
319
|
mtime: duplicateStat.mtimeMs, ctime: duplicateStat.ctimeMs
|
|
380
320
|
})
|
|
381
|
-
vault.registry.setScanEntry(duplicate, {
|
|
382
|
-
hash, size: content.length, source_id: source.id,
|
|
383
|
-
dev: duplicateStat.dev, ino: duplicateStat.ino,
|
|
384
|
-
mtime: duplicateStat.mtimeMs, ctime: duplicateStat.ctimeMs
|
|
385
|
-
})
|
|
386
321
|
// This is the actual crash state: the target is still pending and the
|
|
387
322
|
// temporary name already shares the stored file's identity.
|
|
388
323
|
await fs.promises.link(vault.storePathFor(hash), duplicate + Vault.TMP_SUFFIX)
|
|
@@ -413,6 +348,29 @@ describe('vault engine (phase 1)', () => {
|
|
|
413
348
|
assert.strictEqual(await fs.promises.readFile(registeredTmp, 'utf8'), 'other user data')
|
|
414
349
|
})
|
|
415
350
|
|
|
351
|
+
test('verification preserves a copy-mode hardlink using the reserved suffix', async () => {
|
|
352
|
+
const h = await home()
|
|
353
|
+
const vault = await makeVault(h)
|
|
354
|
+
const content = crypto.randomBytes(4096)
|
|
355
|
+
const hash = sha256(content)
|
|
356
|
+
const file = await writeFile(path.resolve(h, 'api', 'appA', 'm.bin'), content)
|
|
357
|
+
await vault.refreshSources()
|
|
358
|
+
const source = vault.sources().find((item) => item.kind === 'app' && item.app === 'appA')
|
|
359
|
+
const st = await fs.promises.lstat(file)
|
|
360
|
+
vault.registry.addBlob(hash, { size: st.size })
|
|
361
|
+
vault.registry.addLink(file, {
|
|
362
|
+
hash, app: 'appA', source_id: source.id,
|
|
363
|
+
dev: st.dev, ino: st.ino, mode: 'copy'
|
|
364
|
+
})
|
|
365
|
+
const suffix = file + Vault.TMP_SUFFIX
|
|
366
|
+
await fs.promises.link(file, suffix)
|
|
367
|
+
|
|
368
|
+
await vault.verify()
|
|
369
|
+
|
|
370
|
+
assert.strictEqual(fs.existsSync(suffix), true)
|
|
371
|
+
assert.strictEqual(vault.registry.links.has(file), true)
|
|
372
|
+
})
|
|
373
|
+
|
|
416
374
|
test('verify: orphan detection via nlink, dead link pruning, store re-adoption', async () => {
|
|
417
375
|
const h = await home()
|
|
418
376
|
const vault = await makeVault(h)
|
|
@@ -457,25 +415,28 @@ describe('vault engine (phase 1)', () => {
|
|
|
457
415
|
return realRealpath(target, options)
|
|
458
416
|
}
|
|
459
417
|
try {
|
|
460
|
-
await vault.verify()
|
|
418
|
+
await assert.rejects(vault.verify(), (error) => error && error.code === 'EIO')
|
|
461
419
|
} finally {
|
|
462
420
|
fs.promises.realpath = realRealpath
|
|
463
421
|
}
|
|
464
422
|
|
|
465
423
|
assert.strictEqual(vault.registry.links.get(file).batch_id, 'keep-batch')
|
|
466
|
-
assert.strictEqual(vault.registry.links.get(file).unverified, true)
|
|
467
424
|
assert.ok(vault.registry.blobs.has(hash))
|
|
468
425
|
assert.deepStrictEqual(await fs.promises.readFile(file), content)
|
|
469
426
|
})
|
|
470
427
|
|
|
471
|
-
test('verify re-adopts a trusted linked name
|
|
428
|
+
test('verify re-adopts a trusted linked name when a copy-mode name also exists', async () => {
|
|
472
429
|
const h = await home()
|
|
473
430
|
const vault = await makeVault(h)
|
|
474
431
|
const content = crypto.randomBytes(4096)
|
|
475
432
|
const hash = sha256(content)
|
|
476
433
|
const linked = await writeFile(path.resolve(h, 'api', 'appA', 'm.bin'), content)
|
|
477
|
-
await writeFile(path.resolve(h, 'api', 'appB', 'm.bin'), content)
|
|
434
|
+
const copied = await writeFile(path.resolve(h, 'api', 'appB', 'm.bin'), content)
|
|
478
435
|
await vault.adopt(linked, hash, { app: 'appA' })
|
|
436
|
+
const copiedStat = await fs.promises.stat(copied)
|
|
437
|
+
vault.registry.addLink(copied, {
|
|
438
|
+
hash, app: 'appB', dev: copiedStat.dev, ino: copiedStat.ino, mode: 'copy'
|
|
439
|
+
})
|
|
479
440
|
await fs.promises.unlink(vault.storePathFor(hash))
|
|
480
441
|
|
|
481
442
|
await vault.verify()
|
|
@@ -658,8 +619,7 @@ describe('vault engine (phase 1)', () => {
|
|
|
658
619
|
await fs.promises.utimes(pendingPath, future, future)
|
|
659
620
|
|
|
660
621
|
const result = await vault.perform('deduplicate', {
|
|
661
|
-
scope_id: vault.registry.duplicates.get(pendingPath).source_id
|
|
662
|
-
paths: [pendingPath]
|
|
622
|
+
scope_id: vault.registry.duplicates.get(pendingPath).source_id
|
|
663
623
|
})
|
|
664
624
|
assert.strictEqual(result.stale, 1)
|
|
665
625
|
assert.strictEqual(result.converted, 0)
|
|
@@ -679,10 +639,7 @@ describe('vault engine (phase 1)', () => {
|
|
|
679
639
|
await fs.promises.writeFile(original, changedContent)
|
|
680
640
|
|
|
681
641
|
const pendingEntry = vault.registry.duplicates.get(pending)
|
|
682
|
-
const result = await vault.perform('deduplicate', {
|
|
683
|
-
scope_id: pendingEntry.source_id,
|
|
684
|
-
paths: [pending]
|
|
685
|
-
})
|
|
642
|
+
const result = await vault.perform('deduplicate', { scope_id: pendingEntry.source_id })
|
|
686
643
|
|
|
687
644
|
assert.strictEqual(result.converted, 0)
|
|
688
645
|
assert.strictEqual(result.stale, 1)
|
|
@@ -704,10 +661,7 @@ describe('vault engine (phase 1)', () => {
|
|
|
704
661
|
await vault.sweeper.scan()
|
|
705
662
|
const before = await fs.promises.stat(pending)
|
|
706
663
|
const pendingEntry = vault.registry.duplicates.get(pending)
|
|
707
|
-
const result = await vault.perform('deduplicate', {
|
|
708
|
-
scope_id: pendingEntry.source_id,
|
|
709
|
-
paths: [pending]
|
|
710
|
-
})
|
|
664
|
+
const result = await vault.perform('deduplicate', { scope_id: pendingEntry.source_id })
|
|
711
665
|
const after = await fs.promises.stat(pending)
|
|
712
666
|
|
|
713
667
|
if (process.platform === 'win32') assert.ok(result.converted >= 0)
|
|
@@ -737,7 +691,7 @@ describe('vault engine (phase 1)', () => {
|
|
|
737
691
|
dev: pendingStat.dev, ino: pendingStat.ino, mtime: pendingStat.mtimeMs
|
|
738
692
|
})
|
|
739
693
|
|
|
740
|
-
const result = await vault.perform('deduplicate', { scope_id: appB.id
|
|
694
|
+
const result = await vault.perform('deduplicate', { scope_id: appB.id })
|
|
741
695
|
|
|
742
696
|
assert.strictEqual(result.stale, 1)
|
|
743
697
|
assert.strictEqual(result.converted, 0)
|
|
@@ -745,9 +699,11 @@ describe('vault engine (phase 1)', () => {
|
|
|
745
699
|
assert.ok(vault.registry.duplicates.has(pending))
|
|
746
700
|
})
|
|
747
701
|
|
|
748
|
-
test('item 9: registry deleted =>
|
|
702
|
+
test('item 9: registry deleted => rebuild reproduces blobs, links, orphans from disk', async () => {
|
|
749
703
|
const h = await home()
|
|
750
704
|
const vault = await makeVault(h)
|
|
705
|
+
// Rebuild's ino-match walk only considers files >= SIZE_THRESHOLD, so
|
|
706
|
+
// lower the threshold surrogate by using large-enough sparse-ish files.
|
|
751
707
|
const big = Buffer.alloc(Vault.SIZE_THRESHOLD, 7)
|
|
752
708
|
const hash = sha256(big)
|
|
753
709
|
const a = await writeFile(path.resolve(h, 'api', 'appA', 'big.bin'), big)
|
|
@@ -766,11 +722,14 @@ describe('vault engine (phase 1)', () => {
|
|
|
766
722
|
assert.ok(fresh.registry.blobs.has(hash))
|
|
767
723
|
assert.ok(fresh.registry.blobs.has(orphanHash))
|
|
768
724
|
assert.strictEqual(fresh.registry.blobs.get(orphanHash).orphan, true)
|
|
769
|
-
assert.strictEqual(fresh.registry.blobs.get(hash).orphan, false
|
|
770
|
-
|
|
771
|
-
assert.strictEqual(fresh.registry.links.
|
|
772
|
-
|
|
773
|
-
|
|
725
|
+
assert.strictEqual(fresh.registry.blobs.get(hash).orphan, false)
|
|
726
|
+
assert.strictEqual(fresh.registry.links.get(a).hash, hash)
|
|
727
|
+
assert.strictEqual(fresh.registry.links.get(a).app, 'appA')
|
|
728
|
+
assert.strictEqual(
|
|
729
|
+
fresh.registry.links.has(fresh.storePathFor(hash)),
|
|
730
|
+
false,
|
|
731
|
+
'the internal vault name is never exposed as a tracked location'
|
|
732
|
+
)
|
|
774
733
|
})
|
|
775
734
|
|
|
776
735
|
test('item 9: torn events line tolerated; corrupt registry.json triggers rebuild, not failure', async () => {
|
|
@@ -1134,149 +1093,99 @@ describe('vault engine (phase 1)', () => {
|
|
|
1134
1093
|
assert.ok(batches.every((batch) => /^batch-[0-9a-f-]{36}$/.test(batch)))
|
|
1135
1094
|
})
|
|
1136
1095
|
|
|
1137
|
-
test('
|
|
1096
|
+
test('deduplication reports transient file progress and clears it when complete', async () => {
|
|
1138
1097
|
const h = await home()
|
|
1139
1098
|
const vault = await makeVault(h)
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
hash: sha256(content),
|
|
1147
|
-
original: await writeFile(path.resolve(h, 'api', 'appA', `${index}.bin`), content),
|
|
1148
|
-
pending: await writeFile(path.resolve(h, 'api', 'appB', `${index}.bin`), content)
|
|
1149
|
-
})
|
|
1150
|
-
}
|
|
1151
|
-
await vault.refreshSources()
|
|
1152
|
-
const appA = vault.sources().find((source) => source.kind === 'app' && source.app === 'appA')
|
|
1153
|
-
const appB = vault.sources().find((source) => source.kind === 'app' && source.app === 'appB')
|
|
1154
|
-
for (const file of files) {
|
|
1155
|
-
await vault.adopt(file.original, file.hash, { app: 'appA', source_id: appA.id })
|
|
1156
|
-
const st = await fs.promises.stat(file.pending)
|
|
1157
|
-
const entry = {
|
|
1158
|
-
hash: file.hash,
|
|
1159
|
-
size: st.size,
|
|
1160
|
-
app: 'appB',
|
|
1161
|
-
source_id: appB.id,
|
|
1162
|
-
dev: st.dev,
|
|
1163
|
-
ino: st.ino,
|
|
1164
|
-
mtime: st.mtimeMs,
|
|
1165
|
-
ctime: st.ctimeMs
|
|
1166
|
-
}
|
|
1167
|
-
vault.registry.setDuplicate(file.pending, entry, entry)
|
|
1099
|
+
const source = {
|
|
1100
|
+
id: 'app:appA',
|
|
1101
|
+
kind: 'app',
|
|
1102
|
+
app: 'appA',
|
|
1103
|
+
root: path.resolve(h, 'api', 'appA'),
|
|
1104
|
+
shareable: true
|
|
1168
1105
|
}
|
|
1106
|
+
vault.refreshSources = async () => {}
|
|
1107
|
+
vault._sources = [source]
|
|
1108
|
+
vault.sourceForPath = () => source
|
|
1109
|
+
vault.sourceAppIsRunning = () => false
|
|
1110
|
+
vault.canonicalPathIsWithinSource = async () => true
|
|
1111
|
+
vault.registry.duplicates = new Map([
|
|
1112
|
+
['/models/one.bin', { hash: 'a'.repeat(64), dev: 1, ino: 1, mtime: 1, ctime: 1 }],
|
|
1113
|
+
['/models/two.bin', { hash: 'b'.repeat(64), dev: 1, ino: 2, mtime: 1, ctime: 1 }]
|
|
1114
|
+
])
|
|
1169
1115
|
|
|
1170
|
-
let
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1174
|
-
const
|
|
1175
|
-
vault.convert = async (
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
})
|
|
1183
|
-
return realConvert(...args)
|
|
1116
|
+
let calls = 0
|
|
1117
|
+
let releaseSecond
|
|
1118
|
+
let reportSecond
|
|
1119
|
+
const secondStarted = new Promise((resolve) => { reportSecond = resolve })
|
|
1120
|
+
const secondGate = new Promise((resolve) => { releaseSecond = resolve })
|
|
1121
|
+
vault.convert = async () => {
|
|
1122
|
+
calls += 1
|
|
1123
|
+
if (calls === 2) {
|
|
1124
|
+
reportSecond()
|
|
1125
|
+
await secondGate
|
|
1126
|
+
}
|
|
1127
|
+
return { status: 'converted', bytes_saved: 1 }
|
|
1184
1128
|
}
|
|
1185
1129
|
|
|
1186
|
-
const pending = vault.perform('deduplicate', {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
const queued = vault.progressStatus(appB.id).file_actions
|
|
1192
|
-
.find((item) => item.kind === 'deduplicate')
|
|
1193
|
-
assert.strictEqual(queued.phase, 'queued')
|
|
1194
|
-
assert.strictEqual(queued.files_completed, 0)
|
|
1195
|
-
assert.strictEqual(queued.files_total, 3)
|
|
1196
|
-
|
|
1197
|
-
releaseQueue()
|
|
1198
|
-
await blocker
|
|
1130
|
+
const pending = vault.perform('deduplicate', { scope_id: source.id })
|
|
1131
|
+
await secondStarted
|
|
1132
|
+
const active = vault.progressStatus().file_action
|
|
1133
|
+
const dashboard = await vault.status()
|
|
1134
|
+
releaseSecond()
|
|
1199
1135
|
const result = await pending
|
|
1200
1136
|
|
|
1201
|
-
assert.
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1137
|
+
assert.deepStrictEqual(active, {
|
|
1138
|
+
kind: 'deduplicate',
|
|
1139
|
+
scope_id: source.id,
|
|
1140
|
+
selection: 'duplicates',
|
|
1141
|
+
files_total: 2,
|
|
1142
|
+
files_completed: 1
|
|
1143
|
+
})
|
|
1144
|
+
assert.deepStrictEqual(dashboard.file_action, active)
|
|
1145
|
+
assert.strictEqual(result.converted, 2)
|
|
1146
|
+
assert.strictEqual(vault.progressStatus().file_action, null)
|
|
1208
1147
|
})
|
|
1209
1148
|
|
|
1210
|
-
test('
|
|
1149
|
+
test('verification keeps copy-mode content groups without a managed file', async () => {
|
|
1211
1150
|
const h = await home()
|
|
1212
1151
|
const vault = await makeVault(h)
|
|
1213
|
-
const
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
const action = vault.progressStatus().file_actions
|
|
1220
|
-
.find((item) => item.kind === 'deduplicate_all')
|
|
1221
|
-
samples.push({
|
|
1222
|
-
path: filePath,
|
|
1223
|
-
completed: action.files_completed,
|
|
1224
|
-
total: action.files_total,
|
|
1225
|
-
sourceId: action.source_id
|
|
1226
|
-
})
|
|
1227
|
-
}
|
|
1228
|
-
return { converted: options.paths.length, bytes_saved: options.paths.length * 100 }
|
|
1229
|
-
}
|
|
1152
|
+
const content = crypto.randomBytes(4096)
|
|
1153
|
+
const hash = sha256(content)
|
|
1154
|
+
const file = await writeFile(path.resolve(h, 'api', 'appA', 'model.bin'), content)
|
|
1155
|
+
const st = await fs.promises.stat(file)
|
|
1156
|
+
vault.registry.addBlob(hash, { size: content.length })
|
|
1157
|
+
vault.registry.addLink(file, { hash, app: 'appA', dev: st.dev, ino: st.ino, mode: 'copy' })
|
|
1230
1158
|
|
|
1231
|
-
|
|
1232
|
-
groups: [
|
|
1233
|
-
{ scope_id: 'app:a', paths: ['/a/one', '/a/two'] },
|
|
1234
|
-
{ scope_id: 'app:b', paths: ['/b/one'] }
|
|
1235
|
-
]
|
|
1236
|
-
})
|
|
1159
|
+
await vault.verify()
|
|
1237
1160
|
|
|
1238
|
-
assert.
|
|
1239
|
-
assert.
|
|
1240
|
-
assert.
|
|
1241
|
-
|
|
1242
|
-
{ path: '/a/two', completed: 2, total: 3, sourceId: null },
|
|
1243
|
-
{ path: '/b/one', completed: 3, total: 3, sourceId: null }
|
|
1244
|
-
])
|
|
1245
|
-
assert.deepStrictEqual(batches.map((batch) => batch.scopeId), ['app:a', 'app:b'])
|
|
1246
|
-
assert.strictEqual(new Set(batches.map((batch) => batch.batchId)).size, 2)
|
|
1247
|
-
assert.ok(batches.every((batch) => /^batch-[0-9a-f-]{36}$/.test(batch.batchId)))
|
|
1248
|
-
assert.deepStrictEqual(vault.progressStatus().file_actions, [])
|
|
1161
|
+
assert.ok(vault.registry.blobs.has(hash))
|
|
1162
|
+
assert.ok(vault.registry.links.has(file))
|
|
1163
|
+
assert.strictEqual(vault.registry.blobs.get(hash).orphan, false)
|
|
1164
|
+
assert.strictEqual((await vault.reclaim(hash)).status, 'unavailable')
|
|
1249
1165
|
})
|
|
1250
1166
|
|
|
1251
|
-
test('
|
|
1167
|
+
test('manual repair preserves verified copy-mode content groups', async () => {
|
|
1252
1168
|
const h = await home()
|
|
1253
|
-
const
|
|
1254
|
-
await fs.promises.mkdir(root, { recursive: true })
|
|
1169
|
+
const vault = await makeVault(h)
|
|
1255
1170
|
const content = crypto.randomBytes(4096)
|
|
1256
1171
|
const hash = sha256(content)
|
|
1257
1172
|
const file = await writeFile(path.resolve(h, 'api', 'appA', 'model.bin'), content)
|
|
1258
|
-
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
}
|
|
1267
|
-
},
|
|
1268
|
-
scan_index: {
|
|
1269
|
-
[file]: {
|
|
1270
|
-
hash, size: st.size, source_id: 'app:appA',
|
|
1271
|
-
dev: st.dev, ino: st.ino, mtime: st.mtimeMs, ctime: st.ctimeMs
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}))
|
|
1173
|
+
await vault.refreshSources()
|
|
1174
|
+
const source = vault.sources().find((item) => item.kind === 'app' && item.app === 'appA')
|
|
1175
|
+
vault.sizeThreshold = 1
|
|
1176
|
+
vault.mode = 'copy'
|
|
1177
|
+
assert.strictEqual((await vault.adopt(file, hash, {
|
|
1178
|
+
app: 'appA', source_id: source.id
|
|
1179
|
+
})).status, 'copy-mode')
|
|
1180
|
+
vault.registry.lastScan = { ts: 123, files: 1 }
|
|
1275
1181
|
|
|
1276
|
-
await
|
|
1182
|
+
await vault.rebuild()
|
|
1277
1183
|
|
|
1278
|
-
assert.
|
|
1279
|
-
assert.strictEqual(registry.
|
|
1184
|
+
assert.ok(vault.registry.blobs.has(hash))
|
|
1185
|
+
assert.strictEqual(vault.registry.links.get(file).mode, 'copy')
|
|
1186
|
+
assert.strictEqual(vault.registry.scanIndex.get(file).hash, hash)
|
|
1187
|
+
assert.deepStrictEqual(vault.registry.lastScan, { ts: 123, files: 1 })
|
|
1188
|
+
assert.deepStrictEqual(await fs.promises.readFile(file), content)
|
|
1280
1189
|
})
|
|
1281
1190
|
|
|
1282
1191
|
test('manual index repair preserves user decisions, history, and pending review state', async () => {
|
|
@@ -1298,12 +1207,14 @@ describe('vault engine (phase 1)', () => {
|
|
|
1298
1207
|
vault.registry.duplicates.set(pending, pendingEntry)
|
|
1299
1208
|
vault.registry.scanIndex.set(pending, pendingEntry)
|
|
1300
1209
|
vault.registry.excluded.set(independent, { ts: Date.now(), size: 2048 })
|
|
1210
|
+
vault.registry.totals.lifetime_bytes_saved = 12345
|
|
1301
1211
|
vault.registry.lastScan = { ts: 111, dirs: 2, files: 3, bytes_total: 999 }
|
|
1302
1212
|
|
|
1303
1213
|
await vault.rebuild()
|
|
1304
1214
|
|
|
1305
1215
|
assert.ok(vault.registry.excluded.has(independent), 'keep-independent decision survives')
|
|
1306
1216
|
assert.strictEqual(vault.registry.excluded.get(independent).size, 2048)
|
|
1217
|
+
assert.strictEqual(vault.registry.totals.lifetime_bytes_saved, 12345)
|
|
1307
1218
|
assert.deepStrictEqual(vault.registry.lastScan, { ts: 111, dirs: 2, files: 3, bytes_total: 999 })
|
|
1308
1219
|
assert.ok(vault.registry.duplicates.has(pending), 'pending review item survives when still valid')
|
|
1309
1220
|
assert.deepStrictEqual(vault.registry.blobs.get(hash).source_urls, ['https://example.test/model'])
|
|
@@ -1586,7 +1497,7 @@ describe('vault engine (phase 1)', () => {
|
|
|
1586
1497
|
assert.deepStrictEqual(await fs.promises.readFile(pending), content)
|
|
1587
1498
|
})
|
|
1588
1499
|
|
|
1589
|
-
test('item 11: exFAT volume
|
|
1500
|
+
test('item 11: exFAT volume enters copy mode (macOS loopback; skipped elsewhere)', { skip: process.platform !== 'darwin' }, async (t) => {
|
|
1590
1501
|
const h = await home()
|
|
1591
1502
|
const vault = await makeVault(h)
|
|
1592
1503
|
const dmg = path.resolve(h, 'exfat-test.dmg')
|
|
@@ -1602,7 +1513,7 @@ describe('vault engine (phase 1)', () => {
|
|
|
1602
1513
|
}
|
|
1603
1514
|
try {
|
|
1604
1515
|
const mode = await vault.probe(mountPoint)
|
|
1605
|
-
assert.strictEqual(mode, '
|
|
1516
|
+
assert.strictEqual(mode, 'copy')
|
|
1606
1517
|
} finally {
|
|
1607
1518
|
if (mountPoint) execSync(`hdiutil detach "${mountPoint}" -force`, { stdio: 'pipe' })
|
|
1608
1519
|
}
|