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.
@@ -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 without claiming an independent copy', async () => {
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, paths: [pending] })
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 => startup recovers only managed blobs', async () => {
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
- 'the visible hardlink still keeps the managed inode live')
771
- assert.strictEqual(fresh.registry.links.size, 0,
772
- 'startup recovery never walks visible source trees')
773
- assert.strictEqual(fresh.registry.links.has(a), false)
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('deduplicate batches expose queued and exact completed file progress', async () => {
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
- vault.sizeThreshold = 1
1141
- const files = []
1142
- for (let index = 0; index < 3; index += 1) {
1143
- const content = crypto.randomBytes(4096)
1144
- files.push({
1145
- content,
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 releaseQueue
1171
- const queueGate = new Promise((resolve) => { releaseQueue = resolve })
1172
- const blocker = vault.runExclusive(() => queueGate)
1173
- const samples = []
1174
- const realConvert = vault.convert.bind(vault)
1175
- vault.convert = async (...args) => {
1176
- const action = vault.progressStatus(appB.id).file_actions
1177
- .find((item) => item.kind === 'deduplicate')
1178
- samples.push({
1179
- phase: action.phase,
1180
- completed: action.files_completed,
1181
- total: action.files_total
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
- scope_id: appB.id,
1188
- paths: files.map((file) => file.pending)
1189
- })
1190
- await new Promise((resolve) => setImmediate(resolve))
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.strictEqual(result.converted, 3)
1202
- assert.deepStrictEqual(samples, [
1203
- { phase: 'deduplicating', completed: 0, total: 3 },
1204
- { phase: 'deduplicating', completed: 1, total: 3 },
1205
- { phase: 'deduplicating', completed: 2, total: 3 }
1206
- ])
1207
- assert.deepStrictEqual(vault.progressStatus(appB.id).file_actions, [])
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('Deduplicate all reports aggregate progress and keeps one batch per location', async () => {
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 batches = []
1214
- const samples = []
1215
- vault.deduplicateScope = async (scopeId, options) => {
1216
- batches.push({ scopeId, batchId: options.batch_id, paths: options.paths })
1217
- for (const filePath of options.paths) {
1218
- options.onFileComplete()
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
- const result = await vault.perform('deduplicate_all', {
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.strictEqual(result.converted, 3)
1239
- assert.strictEqual(result.bytes_saved, 300)
1240
- assert.deepStrictEqual(samples, [
1241
- { path: '/a/one', completed: 1, total: 3, sourceId: null },
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('legacy copy entries migrate to unavailable pending suggestions', async () => {
1167
+ test('manual repair preserves verified copy-mode content groups', async () => {
1252
1168
  const h = await home()
1253
- const root = path.resolve(h, 'vault')
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
- const st = await fs.promises.stat(file)
1259
- const registry = new Registry(root)
1260
- await fs.promises.writeFile(registry.snapshotPath, JSON.stringify({
1261
- blobs: { [hash]: { size: content.length } },
1262
- links: {
1263
- [file]: {
1264
- hash, app: 'appA', source_id: 'app:appA',
1265
- dev: st.dev, ino: st.ino, mode: 'copy'
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 registry.load()
1182
+ await vault.rebuild()
1277
1183
 
1278
- assert.strictEqual(registry.links.has(file), false)
1279
- assert.strictEqual(registry.duplicates.get(file).unavailable_reason, 'unsupported_disk')
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 is unsupported (macOS loopback; skipped elsewhere)', { skip: process.platform !== 'darwin' }, async (t) => {
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, 'unsupported')
1516
+ assert.strictEqual(mode, 'copy')
1606
1517
  } finally {
1607
1518
  if (mountPoint) execSync(`hdiutil detach "${mountPoint}" -force`, { stdio: 'pipe' })
1608
1519
  }