pinokiod 8.0.39 → 8.0.40

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.
@@ -1,336 +0,0 @@
1
- const { test, describe, after } = require('node:test')
2
- const assert = require('node:assert')
3
- const fs = require('fs')
4
- const os = require('os')
5
- const path = require('path')
6
- const crypto = require('crypto')
7
- const Vault = require('../kernel/vault')
8
-
9
- const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex')
10
-
11
- const writeFile = async (p, content) => {
12
- await fs.promises.mkdir(path.dirname(p), { recursive: true })
13
- await fs.promises.writeFile(p, content)
14
- return p
15
- }
16
-
17
- describe('vault manual scan (phase 3)', () => {
18
- let homes = []
19
- const makeEnv = async () => {
20
- const home = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-scan-'))
21
- homes.push(home)
22
- await fs.promises.mkdir(path.resolve(home, 'api'), { recursive: true })
23
- const kernel = {
24
- homedir: home,
25
- platform: process.platform,
26
- path: (...args) => path.resolve(home, ...args)
27
- }
28
- const vault = new Vault(kernel)
29
- await vault.init()
30
- vault.sizeThreshold = 1024
31
- kernel.vault = vault
32
- return { home, vault, kernel }
33
- }
34
- after(async () => {
35
- for (const h of homes) {
36
- await fs.promises.rm(h, { recursive: true, force: true }).catch(() => {})
37
- }
38
- })
39
-
40
- test('scan discovers, hashes, adopts — and records folder totals', async () => {
41
- const { home, vault } = await makeEnv()
42
- const content = crypto.randomBytes(4096)
43
- const file = await writeFile(path.resolve(home, 'api', 'appA', 'models', 'm.safetensors'), content)
44
- await writeFile(path.resolve(home, 'api', 'appA', 'small.txt'), 'tiny')
45
- const result = await vault.sweeper.scan()
46
- assert.strictEqual((await fs.promises.stat(file)).nlink, 2)
47
- assert.ok(vault.registry.blobs.has(sha256(content)))
48
- assert.ok(result.bytes_total >= content.length + 4, 'folder total includes small files')
49
- assert.ok(vault.registry.lastScan && vault.registry.lastScan.bytes_total === result.bytes_total)
50
- assert.ok(vault.registry.lastScan.duration_ms >= 0, 'scan duration is measured')
51
- assert.ok(vault.registry.lastScan.walk_duration_ms >= 0, 'walk duration is measured')
52
- assert.ok(vault.registry.lastScan.hash_duration_ms >= 0, 'hash duration is measured')
53
- assert.strictEqual(vault.registry.lastScan.hash_total, 1, 'hash work has a determinate total after walking')
54
- assert.deepStrictEqual(vault.registry.lastScan.source_ids, ['pinokio'], 'scan source set is recorded for later estimates')
55
- })
56
-
57
- test('scans NEVER convert: every byte-identical copy is pending, even in HF caches', async () => {
58
- const { home, vault } = await makeEnv()
59
- const content = crypto.randomBytes(4096)
60
- const hash = sha256(content)
61
- const a = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), content)
62
- const b = await writeFile(path.resolve(home, 'api', 'appB', 'm.bin'), content)
63
- const hfBlob = await writeFile(
64
- path.resolve(home, 'api', 'appC', 'cache', 'HF_HOME', 'hub', 'models--org--m', 'blobs', hash), content)
65
- await vault.sweeper.scan()
66
-
67
- const nlinks = [a, b, hfBlob].map((p) => fs.statSync(p).nlink)
68
- assert.strictEqual(nlinks.filter((n) => n === 2).length, 1, 'exactly one copy adopted')
69
- assert.strictEqual(vault.registry.duplicates.size, 2, 'other two are pending, not converted')
70
- const events = await vault.registry.readEvents()
71
- assert.strictEqual(events.filter((e) => e.kind === 'convert').length, 0, 'no conversion without a click')
72
- assert.ok(events.some((e) => e.kind === 'found'))
73
-
74
- const summary = await vault.sweeper.convertPending(undefined)
75
- assert.strictEqual(summary.converted, 2)
76
- assert.strictEqual((await fs.promises.stat(a)).ino, (await fs.promises.stat(b)).ino)
77
- })
78
-
79
- test('no name heuristics: files inside venv-like folders are found', async () => {
80
- const { home, vault } = await makeEnv()
81
- const content = crypto.randomBytes(4096)
82
- const inVenv = await writeFile(
83
- path.resolve(home, 'api', 'appA', 'coreml_venv', 'lib', 'site-packages', 'lib.dylib'), content)
84
- await vault.sweeper.scan()
85
- assert.strictEqual((await fs.promises.stat(inVenv)).nlink, 2, 'adopted despite venv-ish path')
86
- })
87
-
88
- test('symlinked dirs and files are skipped; cycles cannot hang the walk', async () => {
89
- const { home, vault } = await makeEnv()
90
- const real = await writeFile(path.resolve(home, 'api', 'appA', 'models', 'real.bin'), crypto.randomBytes(4096))
91
- await fs.promises.symlink(path.resolve(home, 'api', 'appA'), path.resolve(home, 'api', 'appA', 'loop'))
92
- await vault.sweeper.scan()
93
- assert.strictEqual((await fs.promises.stat(real)).nlink, 2)
94
- })
95
-
96
- test('top-level linked imports are scanned as external sources while nested links stay skipped', async (t) => {
97
- const { home, vault } = await makeEnv()
98
- const external = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-external-'))
99
- const nested = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-external-nested-'))
100
- homes.push(external, nested)
101
- const content = crypto.randomBytes(4096)
102
- const local = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), content)
103
- const imported = await writeFile(path.resolve(external, 'models', 'm.bin'), content)
104
- const nestedFile = await writeFile(path.resolve(nested, 'should-not-scan.bin'), crypto.randomBytes(4096))
105
- try {
106
- await fs.promises.symlink(external, path.resolve(home, 'api', 'linked-models'), process.platform === 'win32' ? 'junction' : 'dir')
107
- await fs.promises.symlink(nested, path.resolve(external, 'nested-link'), process.platform === 'win32' ? 'junction' : 'dir')
108
- } catch (error) {
109
- t.skip(`directory links unavailable: ${error.message}`)
110
- return
111
- }
112
-
113
- await vault.sweeper.scan()
114
- const source = vault.sources().find((item) => item.kind === 'external' && item.label === 'linked-models')
115
- const canonicalImported = path.resolve(await fs.promises.realpath(external), 'models', 'm.bin')
116
- const canonicalNestedFile = path.resolve(await fs.promises.realpath(nested), 'should-not-scan.bin')
117
- assert.ok(source, 'linked import becomes an external source')
118
- assert.strictEqual(source.parent_id, 'external')
119
- assert.ok(vault.registry.scanIndex.has(canonicalImported), 'external root was scanned')
120
- assert.strictEqual(vault.registry.scanIndex.has(canonicalNestedFile), false, 'nested directory link was not followed')
121
- assert.strictEqual(vault.registry.duplicates.get(canonicalImported).source_id, source.id)
122
- assert.strictEqual((await fs.promises.stat(local)).nlink, 2)
123
-
124
- const status = await vault.status()
125
- assert.ok(status.sources.some((item) => item.id === source.id && item.kind === 'external'))
126
- assert.strictEqual(status.duplicates.find((item) => item.path === canonicalImported).source_id, source.id)
127
- assert.strictEqual(status.last_scan.home_bytes_total, content.length, 'Pinokio metric excludes external files')
128
- assert.strictEqual(status.last_scan.source_bytes[source.id], content.length, 'external totals remain attributable')
129
- })
130
-
131
- test('adding an external source creates one persistent import without starting a scan', async () => {
132
- const { home, vault } = await makeEnv()
133
- const external = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-added-external-'))
134
- homes.push(external)
135
- const content = crypto.randomBytes(4096)
136
- const imported = await writeFile(path.resolve(external, 'models', 'added.bin'), content)
137
-
138
- const added = await vault.addExternalSource(external)
139
- assert.strictEqual(added.created, true)
140
- assert.strictEqual(added.source.kind, 'external')
141
- assert.strictEqual(added.source.parent_id, 'external')
142
- assert.strictEqual(vault.registry.scanIndex.size, 0, 'adding a source never scans it')
143
- assert.strictEqual((await fs.promises.lstat(added.source.mount_path)).isSymbolicLink(), true)
144
-
145
- const repeated = await vault.addExternalSource(external)
146
- assert.strictEqual(repeated.created, false, 'the same physical folder is not imported twice')
147
- assert.strictEqual(vault.sources().filter((source) => source.kind === 'external').length, 1)
148
-
149
- await vault.sweeper.scan()
150
- assert.ok(vault.registry.scanIndex.has(path.resolve(await fs.promises.realpath(imported))))
151
- })
152
-
153
- test('excluded paths are respected: no adoption, no pending, no re-listing', async () => {
154
- const { home, vault } = await makeEnv()
155
- const content = crypto.randomBytes(4096)
156
- const a = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), content)
157
- const b = await writeFile(path.resolve(home, 'api', 'appB', 'm.bin'), content)
158
- vault.registry.excluded.set(b, { ts: Date.now() })
159
- await vault.sweeper.scan()
160
- assert.strictEqual((await fs.promises.stat(a)).nlink, 2)
161
- assert.strictEqual((await fs.promises.stat(b)).nlink, 1)
162
- assert.strictEqual(vault.registry.duplicates.has(b), false)
163
- })
164
-
165
- test('steady-state rescan performs zero content hashes', async () => {
166
- const { home, vault } = await makeEnv()
167
- await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), crypto.randomBytes(4096))
168
- await vault.sweeper.scan()
169
- const after = vault.sweeper.stats.hashes
170
- const priorFiles = vault.registry.lastScan.files
171
- await vault.sweeper.scan()
172
- assert.strictEqual(vault.sweeper.stats.hashes, after)
173
- assert.strictEqual(vault.sweeper.state.estimated_files, priorFiles, 'same-source rescan uses the prior exact file count')
174
- })
175
-
176
- test('file metadata inspection uses bounded concurrency without changing results', async () => {
177
- const { home, vault } = await makeEnv()
178
- const dir = path.resolve(home, 'api', 'bulk')
179
- await Promise.all(Array.from({ length: 48 }, (_, index) =>
180
- writeFile(path.resolve(dir, `dir-${index}`, 'small.txt'), 'x')))
181
- const realStat = fs.promises.stat
182
- let active = 0
183
- let maximum = 0
184
- fs.promises.stat = async (target) => {
185
- if (String(target).startsWith(dir + path.sep)) {
186
- active += 1
187
- maximum = Math.max(maximum, active)
188
- await new Promise((resolve) => setTimeout(resolve, 3))
189
- try {
190
- return await realStat(target)
191
- } finally {
192
- active -= 1
193
- }
194
- }
195
- return realStat(target)
196
- }
197
- try {
198
- await vault.sweeper.scan()
199
- } finally {
200
- fs.promises.stat = realStat
201
- }
202
- assert.ok(maximum > 1, `expected concurrent metadata reads, saw ${maximum}`)
203
- assert.ok(maximum <= vault.statConcurrency, `metadata concurrency exceeded its bound: ${maximum}`)
204
- assert.strictEqual(vault.registry.lastScan.files >= 48, true)
205
- })
206
-
207
- test('multiple names for one inode share one exact hash job', async () => {
208
- const { home, vault } = await makeEnv()
209
- const content = crypto.randomBytes(4096)
210
- const a = await writeFile(path.resolve(home, 'api', 'appA', 'model.bin'), content)
211
- const b = path.resolve(home, 'api', 'appB', 'model.bin')
212
- await fs.promises.mkdir(path.dirname(b), { recursive: true })
213
- await fs.promises.link(a, b)
214
-
215
- await vault.sweeper.scan()
216
-
217
- assert.strictEqual(vault.registry.lastScan.hashed, 1, 'physical content was hashed once')
218
- assert.strictEqual(vault.registry.lastScan.inode_reuses, 1, 'second name reused the inode hash job')
219
- assert.strictEqual(vault.registry.duplicates.size, 0, 'existing hard links are already shared')
220
- assert.strictEqual(vault.registry.links.get(a).hash, sha256(content))
221
- assert.strictEqual(vault.registry.links.get(b).hash, sha256(content))
222
- })
223
-
224
- test('a file changed during hashing never publishes the stale digest', async () => {
225
- const { home, vault } = await makeEnv()
226
- const original = crypto.randomBytes(4096)
227
- const oldHash = sha256(original)
228
- const file = await writeFile(path.resolve(home, 'api', 'appA', 'changing.bin'), original)
229
- const realHashFile = vault.hashFile.bind(vault)
230
- vault.hashFile = async (target) => {
231
- const result = await realHashFile(target)
232
- await fs.promises.writeFile(target, crypto.randomBytes(original.length))
233
- const future = new Date(Date.now() + 2000)
234
- await fs.promises.utimes(target, future, future)
235
- return result
236
- }
237
-
238
- try {
239
- await vault.sweeper.scan()
240
- } finally {
241
- vault.hashFile = realHashFile
242
- }
243
-
244
- assert.strictEqual(vault.registry.lastScan.unstable_hashes, 1)
245
- assert.strictEqual(vault.registry.blobs.has(oldHash), false, 'stale digest was discarded')
246
- assert.strictEqual(vault.registry.scanIndex.has(file), false, 'unstable path remains unclassified')
247
- assert.strictEqual((await fs.promises.stat(file)).nlink, 1, 'unstable file was not adopted')
248
- })
249
-
250
- test('conversion tmp files are never candidates', async () => {
251
- const { home, vault } = await makeEnv()
252
- const stray = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin.pinokio-dedup-tmp'), crypto.randomBytes(4096))
253
- await vault.sweeper.scan()
254
- assert.strictEqual(vault.registry.scanIndex.has(stray), false)
255
- assert.strictEqual((await fs.promises.stat(stray)).nlink, 1)
256
- })
257
-
258
- test('hf --local-dir metadata provides hashes without content reads', async () => {
259
- const { home, vault } = await makeEnv()
260
- const content = crypto.randomBytes(4096)
261
- const hash = sha256(content)
262
- const dir = path.resolve(home, 'api', 'appA', 'ckpt')
263
- const file = await writeFile(path.resolve(dir, 'model.safetensors'), content)
264
- const future = (Date.now() + 60000) / 1000
265
- await writeFile(
266
- path.resolve(dir, '.cache', 'huggingface', 'model.safetensors.metadata'),
267
- `commit123\n"${hash}"\n${future}\n`)
268
- await vault.sweeper.scan()
269
- assert.strictEqual(vault.sweeper.stats.hashes, 0, 'hash harvested, not computed')
270
- assert.strictEqual((await fs.promises.stat(file)).nlink, 2)
271
- assert.ok(vault.registry.blobs.has(hash))
272
- })
273
-
274
- test('in-place mutation propagates to all names; scan detects divergence and evicts', async () => {
275
- const { home, vault } = await makeEnv()
276
- const original = crypto.randomBytes(4096)
277
- const oldHash = sha256(original)
278
- const a = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), original)
279
- await vault.adopt(a, oldHash, { app: 'appA' })
280
- const b = path.resolve(home, 'api', 'appB', 'm.bin')
281
- await writeFile(b, original)
282
- await vault.convert(b, oldHash, { app: 'appB' })
283
- await vault.sweeper.scan()
284
-
285
- const mutated = crypto.randomBytes(4096)
286
- await fs.promises.writeFile(b, mutated)
287
- assert.deepStrictEqual(await fs.promises.readFile(a), mutated, 'in-place writes propagate to every name')
288
-
289
- await vault.sweeper.scan()
290
- assert.strictEqual(vault.registry.blobs.has(oldHash), false, 'stale blob evicted')
291
- assert.strictEqual(fs.existsSync(vault.storePathFor(oldHash)), false)
292
- assert.ok(vault.registry.blobs.has(sha256(mutated)))
293
- const events = await vault.registry.readEvents()
294
- assert.ok(events.some((e) => e.kind === 'diverged' && e.hash === oldHash))
295
- })
296
-
297
- test('scan of a 10k-entry folder at steady state finishes fast with zero reads', async () => {
298
- const { home, vault } = await makeEnv()
299
- const dir = path.resolve(home, 'api', 'bigapp', 'stuff')
300
- await fs.promises.mkdir(dir, { recursive: true })
301
- const tiny = Buffer.from('x')
302
- await Promise.all(Array.from({ length: 10000 }, (_, i) =>
303
- fs.promises.writeFile(path.resolve(dir, `f${i}.txt`), tiny)))
304
- await vault.sweeper.scan()
305
- const started = Date.now()
306
- await vault.sweeper.scan()
307
- const elapsed = Date.now() - started
308
- assert.ok(elapsed < 2000, `steady-state rescan took ${elapsed}ms`)
309
- assert.strictEqual(vault.sweeper.stats.hashes, 0)
310
- })
311
-
312
- test('locked conversions stay pending and can be retried', async () => {
313
- const { home, vault } = await makeEnv()
314
- const content = crypto.randomBytes(4096)
315
- const hash = sha256(content)
316
- const a = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), content)
317
- await vault.adopt(a, hash, { app: 'appA' })
318
- const b = await writeFile(path.resolve(home, 'api', 'appB', 'm.bin'), content)
319
- vault.registry.duplicates.set(b, { hash, size: content.length, app: 'appB' })
320
-
321
- const realConvert = vault.convert.bind(vault)
322
- let calls = 0
323
- vault.convert = async (...args) => {
324
- calls += 1
325
- if (calls === 1) return { status: 'locked' }
326
- return realConvert(...args)
327
- }
328
- const first = await vault.sweeper.convertPending('appB')
329
- assert.strictEqual(first.locked, 1)
330
- assert.ok(vault.registry.duplicates.has(b), 'still pending after lock')
331
- const second = await vault.sweeper.convertPending('appB')
332
- vault.convert = realConvert
333
- assert.strictEqual(second.converted, 1)
334
- assert.strictEqual((await fs.promises.stat(b)).ino, (await fs.promises.stat(a)).ino)
335
- })
336
- })
@@ -1,324 +0,0 @@
1
- const { test, describe, after } = require('node:test')
2
- const assert = require('node:assert')
3
- const fs = require('fs')
4
- const os = require('os')
5
- const path = require('path')
6
- const crypto = require('crypto')
7
- const Vault = require('../kernel/vault')
8
-
9
- const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex')
10
-
11
- const writeFile = async (p, content) => {
12
- await fs.promises.mkdir(path.dirname(p), { recursive: true })
13
- await fs.promises.writeFile(p, content)
14
- return p
15
- }
16
-
17
- describe('vault dashboard backend (phase 4)', () => {
18
- let homes = []
19
- const makeEnv = async () => {
20
- const home = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-ui-'))
21
- homes.push(home)
22
- await fs.promises.mkdir(path.resolve(home, 'api'), { recursive: true })
23
- const kernel = { homedir: home, platform: process.platform, path: (...a) => path.resolve(home, ...a) }
24
- const vault = new Vault(kernel)
25
- await vault.init()
26
- vault.sizeThreshold = 1024
27
- return { home, vault }
28
- }
29
- after(async () => {
30
- for (const h of homes) await fs.promises.rm(h, { recursive: true, force: true }).catch(() => {})
31
- })
32
-
33
- const makeSharedPair = async (home, vault, name = 'm.bin') => {
34
- const content = crypto.randomBytes(4096)
35
- const hash = sha256(content)
36
- const a = await writeFile(path.resolve(home, 'api', 'appA', name), content)
37
- await vault.adopt(a, hash, { app: 'appA' })
38
- const b = await writeFile(path.resolve(home, 'api', 'appB', name), content)
39
- const conv = await vault.convert(b, hash, { app: 'appB', batch_id: `batch-${name}` })
40
- assert.strictEqual(conv.status, 'converted')
41
- return { content, hash, a, b }
42
- }
43
-
44
- test('item 15: undo of a conversion batch restores independent files', async () => {
45
- const { home, vault } = await makeEnv()
46
- const { content, a, b } = await makeSharedPair(home, vault)
47
- const savedBefore = vault.registry.totals.lifetime_bytes_saved
48
-
49
- const result = await vault.undoBatch('batch-m.bin')
50
- assert.strictEqual(result.undone, 1)
51
- assert.strictEqual((await fs.promises.stat(b)).nlink, 1, 'independent file again')
52
- assert.deepStrictEqual(await fs.promises.readFile(b), content)
53
- assert.strictEqual((await fs.promises.stat(a)).nlink, 2, 'other names untouched')
54
- assert.strictEqual(vault.registry.totals.lifetime_bytes_saved, savedBefore - content.length)
55
- assert.ok(vault.registry.duplicates.has(b), 'undone file is pending again')
56
- const events = await vault.registry.readEvents()
57
- assert.ok(events.some((e) => e.kind === 'undo' && e.batch_id === 'batch-m.bin'))
58
- })
59
-
60
- test('detach makes one name independent and pins it against future scans', async () => {
61
- const { home, vault } = await makeEnv()
62
- const { content, a, b } = await makeSharedPair(home, vault)
63
-
64
- const result = await vault.detach(b)
65
- assert.strictEqual(result.status, 'detached')
66
- assert.strictEqual((await fs.promises.stat(b)).nlink, 1)
67
- assert.deepStrictEqual(await fs.promises.readFile(b), content)
68
- assert.strictEqual((await fs.promises.stat(a)).nlink, 2)
69
- assert.ok(vault.registry.excluded.has(b))
70
- assert.strictEqual(vault.registry.excluded.get(b).size, content.length)
71
- assert.strictEqual(fs.existsSync(b + '.pinokio-dedup-tmp'), false)
72
-
73
- // A scan neither re-adopts nor re-lists the pinned path.
74
- await vault.sweeper.scan()
75
- assert.strictEqual((await fs.promises.stat(b)).nlink, 1)
76
- assert.strictEqual(vault.registry.duplicates.has(b), false)
77
-
78
- // Re-share un-pins; the next scan lists it as pending again (no auto-convert).
79
- const reshared = await vault.reshare(b)
80
- assert.strictEqual(reshared.status, 'resharable')
81
- await vault.sweeper.scan()
82
- assert.ok(vault.registry.duplicates.has(b))
83
- assert.strictEqual((await fs.promises.stat(b)).nlink, 1)
84
- })
85
-
86
- test('detach on a pending duplicate just ignores it', async () => {
87
- const { home, vault } = await makeEnv()
88
- const content = crypto.randomBytes(4096)
89
- const hash = sha256(content)
90
- const a = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), content)
91
- await vault.adopt(a, hash, { app: 'appA' })
92
- const b = await writeFile(path.resolve(home, 'api', 'appB', 'm.bin'), content)
93
- vault.registry.duplicates.set(b, { hash, size: content.length, app: 'appB' })
94
-
95
- const result = await vault.detach(b)
96
- assert.strictEqual(result.status, 'ignored')
97
- assert.strictEqual(vault.registry.duplicates.has(b), false)
98
- assert.ok(vault.registry.excluded.has(b))
99
- assert.strictEqual(vault.registry.excluded.get(b).size, content.length)
100
- })
101
-
102
- test('status(): disk figures, scan metadata, excluded list', async () => {
103
- const { home, vault } = await makeEnv()
104
- const { content, hash } = await makeSharedPair(home, vault)
105
- await vault.detach(path.resolve(home, 'api', 'appB', 'm.bin'))
106
- await vault.sweeper.scan()
107
- const status = await vault.status()
108
- assert.strictEqual(status.enabled, true)
109
- assert.strictEqual(status.bytes_on_disk, content.length)
110
- assert.ok(status.last_scan && status.last_scan.files > 0)
111
- assert.strictEqual(status.excluded.length, 1)
112
- assert.strictEqual(status.excluded[0].size, content.length)
113
- assert.strictEqual(status.blobs.length, 1)
114
- assert.deepStrictEqual(status.blobs[0].apps, ['appA'])
115
- assert.ok(status.scan && status.scan.active === false)
116
- })
117
-
118
- test('reclaimAll frees every orphan and nothing else', async () => {
119
- const { home, vault } = await makeEnv()
120
- const keep = crypto.randomBytes(4096)
121
- const drop = crypto.randomBytes(4096)
122
- const keptFile = await writeFile(path.resolve(home, 'api', 'appA', 'keep.bin'), keep)
123
- const droppedFile = await writeFile(path.resolve(home, 'api', 'appA', 'drop.bin'), drop)
124
- await vault.adopt(keptFile, sha256(keep), { app: 'appA' })
125
- await vault.adopt(droppedFile, sha256(drop), { app: 'appA' })
126
- await fs.promises.unlink(droppedFile)
127
- await vault.verify()
128
- const result = await vault.reclaimAll()
129
- assert.strictEqual(result.reclaimed, 1)
130
- assert.strictEqual(result.bytes_freed, drop.length)
131
- assert.ok(vault.registry.blobs.has(sha256(keep)))
132
- assert.strictEqual(vault.registry.blobs.has(sha256(drop)), false)
133
- })
134
-
135
- test('item 16: vocabulary lint — vault page copy avoids forbidden terms', async () => {
136
- const forbidden = /hard.?link|junction|symlink|inode|\bblob\b|\bstore\b|\bdedupe\b/i
137
- const vaultPage = await fs.promises.readFile(path.resolve(__dirname, '..', 'server', 'views', 'vault.ejs'), 'utf8')
138
- const copyBlock = vaultPage.match(/const COPY = \{[\s\S]*?\n\}/)
139
- assert.ok(copyBlock, 'vault.ejs must keep user copy in a COPY object')
140
- for (const m of copyBlock[0].matchAll(/:\s*"([^"]*)"/g)) {
141
- assert.ok(!forbidden.test(m[1]), `forbidden term in vault page copy: "${m[1]}"`)
142
- }
143
- })
144
-
145
- test('duplicate matches show complete filesystem paths without ellipsis', async () => {
146
- const vaultPage = await fs.promises.readFile(path.resolve(__dirname, '..', 'server', 'views', 'vault.ejs'), 'utf8')
147
- assert.match(vaultPage, /class="vault-match-path">\$\{esc\(match\.path\)\}/)
148
- const style = vaultPage.match(/\.vault-match-path \{[\s\S]*?\n\}/)
149
- assert.ok(style, 'match path must have dedicated wrapping styles')
150
- assert.match(style[0], /overflow-wrap:\s*anywhere/)
151
- assert.match(style[0], /white-space:\s*normal/)
152
- assert.doesNotMatch(style[0], /text-overflow:\s*ellipsis/)
153
- })
154
-
155
- test('copy-mode tracking never claims physical sharing savings', async () => {
156
- const { home, vault } = await makeEnv()
157
- const content = crypto.randomBytes(4096)
158
- const hash = sha256(content)
159
- const first = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), content)
160
- const second = await writeFile(path.resolve(home, 'api', 'appB', 'm.bin'), content)
161
- const firstStat = await fs.promises.stat(first)
162
- const secondStat = await fs.promises.stat(second)
163
- vault.registry.addBlob(hash, { size: content.length })
164
- vault.registry.addLink(first, { hash, app: 'appA', dev: firstStat.dev, ino: firstStat.ino, mode: 'copy' })
165
- vault.registry.addLink(second, { hash, app: 'appB', dev: secondStat.dev, ino: secondStat.ino, mode: 'copy' })
166
-
167
- const status = await vault.status()
168
- assert.strictEqual(status.bytes_on_disk, content.length * 2)
169
- assert.strictEqual(status.saved_by_sharing, 0)
170
- })
171
-
172
- test('repair is advanced, metrics distinguish detected and explicit savings, and refresh retries', async () => {
173
- const vaultPage = await fs.promises.readFile(path.resolve(__dirname, '..', 'server', 'views', 'vault.ejs'), 'utf8')
174
- assert.match(vaultPage, /already_shared:\s*"Already shared"/)
175
- assert.match(vaultPage, /saved_by_vault:\s*"Saved by Vault"/)
176
- assert.match(vaultPage, /repair_index:\s*"Repair Vault index"/)
177
- assert.match(vaultPage, /<details class='vault-advanced'/)
178
- assert.doesNotMatch(vaultPage, /id='btn-rebuild'/)
179
- const refreshBlock = vaultPage.match(/const refresh = async \(\) => \{[\s\S]*?\n\}/)
180
- assert.ok(refreshBlock)
181
- assert.match(refreshBlock[0], /finally/)
182
- assert.doesNotMatch(vaultPage, /priorScanning/)
183
- })
184
-
185
- test('vault uses compact desktop density without truncating duplicate matches', async () => {
186
- const vaultPage = await fs.promises.readFile(path.resolve(__dirname, '..', 'server', 'views', 'vault.ejs'), 'utf8')
187
- assert.match(vaultPage, /--vault-control-height:\s*28px/)
188
- assert.match(vaultPage, /--vault-row-height:\s*44px/)
189
- assert.match(vaultPage, /--vault-tree-row:\s*26px/)
190
- assert.match(vaultPage, /--vault-tree-path-row:\s*34px/)
191
- assert.match(vaultPage, /\.vault-shell \*,?[\s\S]*?box-sizing:\s*border-box/)
192
- assert.match(vaultPage, /grid-template-columns:\s*248px minmax\(0, 1fr\)/)
193
- assert.match(vaultPage, /\.vault-toolbar \{[\s\S]*?min-height:\s*44px/)
194
- assert.match(vaultPage, /\.vault-match-path \{[\s\S]*?overflow-wrap:\s*anywhere/)
195
- assert.doesNotMatch(vaultPage, /<header class='task-shell-header'>/)
196
- assert.doesNotMatch(vaultPage, /id='vault-(?:title|subtitle)'/)
197
- assert.match(vaultPage, /\.vault-overview \{[\s\S]*?padding:\s*7px var\(--vault-inline\)/)
198
- assert.match(vaultPage, /\.vault-metric \{[\s\S]*?border:\s*0;[\s\S]*?border-left:\s*1px solid var\(--task-border\)/)
199
- assert.match(vaultPage, /button\.vault-metric \{[\s\S]*?font:\s*inherit/)
200
- assert.match(vaultPage, /id='btn-add-source'/)
201
- assert.match(vaultPage, /<script src="\/Socket\.js"><\/script>/)
202
- assert.match(vaultPage, /action:\s*"add_source"/)
203
- assert.match(vaultPage, /identical_contents_at:\s*"Identical contents at"/)
204
- assert.doesNotMatch(vaultPage, /Matching locations/)
205
- assert.match(vaultPage, /scan_waiting:\s*"Waiting for scan results"/)
206
- assert.match(vaultPage, /view === "all" && !scanning \? `<button class="vault-button" type="button" id="btn-empty-scan"/)
207
- assert.match(vaultPage, /class="vault-progress-track"/)
208
- assert.match(vaultPage, /role="progressbar"/)
209
- assert.match(vaultPage, /scan\.walk_duration_ms == null/)
210
- assert.match(vaultPage, /const hashTotal = scan\.hash_total/)
211
- assert.match(vaultPage, /const hasEstimate = Number\.isFinite\(scan\.estimated_files\)/)
212
- assert.match(vaultPage, /scan_estimate_help:\s*"Estimated from your last completed scan"/)
213
- assert.match(vaultPage, /\$\{estimated \? "~" : ""\}\$\{progressValue\}%/)
214
- assert.match(vaultPage, /\.vault-progress-bar\.determinate \{[\s\S]*?transform:\s*scaleX/)
215
- assert.match(vaultPage, /\.vault-progress-bar\.indeterminate \{[\s\S]*?transform:\s*scaleX\(1\)[\s\S]*?animation:\s*vault-progress-pulse/)
216
- assert.doesNotMatch(vaultPage, /vault-progress-sweep/)
217
- assert.match(vaultPage, /body\.vault-page \.task-container \{[\s\S]*?overflow:\s*hidden/)
218
- assert.match(vaultPage, /\.vault-explorer \{[\s\S]*?flex:\s*1 1 auto[\s\S]*?overflow:\s*hidden/)
219
- assert.match(vaultPage, /\.vault-rail \{[\s\S]*?overflow-y:\s*auto[\s\S]*?overscroll-behavior:\s*contain/)
220
- assert.match(vaultPage, /\.vault-pane \{[\s\S]*?grid-template-rows:\s*auto minmax\(0, 1fr\) auto/)
221
- assert.doesNotMatch(vaultPage, /vault-pane-head/)
222
- assert.match(vaultPage, /const toolbarSummary = \(visibleItems\) =>/)
223
- assert.match(vaultPage, /id="vault-toolbar-summary"/)
224
- assert.match(vaultPage, /\.vault-table-wrap \{[\s\S]*?overflow:\s*auto[\s\S]*?overscroll-behavior:\s*contain/)
225
- assert.match(vaultPage, /#vault-locations \{[\s\S]*?flex-direction:\s*column[\s\S]*?justify-content:\s*flex-start[\s\S]*?gap:\s*0/)
226
- assert.match(vaultPage, /\.vault-source-line\.depth-2 \{ padding-left:\s*24px; \}/)
227
- assert.match(vaultPage, /vault-source-line depth-\$\{Math\.min\(depth, 2\)\} \$\{pathText \? "has-path" : ""\}/)
228
- })
229
-
230
- test('vault route provisions the dev requirements needed by its folder picker', async () => {
231
- const serverSource = await fs.promises.readFile(path.resolve(__dirname, '..', 'server', 'index.js'), 'utf8')
232
- const routeStart = serverSource.indexOf('this.app.get("/vault"')
233
- const routeEnd = serverSource.indexOf('this.app.post("/vault/action"', routeStart)
234
- assert.ok(routeStart >= 0 && routeEnd > routeStart, 'vault route must be present')
235
- const route = serverSource.slice(routeStart, routeEnd)
236
- assert.match(route, /this\.kernel\.bin\.check\(/)
237
- assert.match(route, /this\.kernel\.bin\.preset\("dev"\)/)
238
- assert.match(route, /requirements_pending \|\| install_required/)
239
- assert.match(route, /\/setup\/dev\?callback=\$\{encodeURIComponent\(req\.originalUrl\)\}/)
240
- assert.ok(route.indexOf('res.redirect') < route.indexOf('res.render("vault"'), 'requirements redirect must happen before rendering Vault')
241
- })
242
-
243
- test('deduplicate batches share a batch id and are undoable as one', async () => {
244
- const { home, vault } = await makeEnv()
245
- const c1 = crypto.randomBytes(4096)
246
- const c2 = crypto.randomBytes(4096)
247
- const a1 = await writeFile(path.resolve(home, 'api', 'appA', 'm1.bin'), c1)
248
- const a2 = await writeFile(path.resolve(home, 'api', 'appA', 'm2.bin'), c2)
249
- await vault.adopt(a1, sha256(c1), { app: 'appA' })
250
- await vault.adopt(a2, sha256(c2), { app: 'appA' })
251
- const b1 = await writeFile(path.resolve(home, 'api', 'appB', 'm1.bin'), c1)
252
- const b2 = await writeFile(path.resolve(home, 'api', 'appB', 'm2.bin'), c2)
253
- vault.registry.duplicates.set(b1, { hash: sha256(c1), size: c1.length, app: 'appB' })
254
- vault.registry.duplicates.set(b2, { hash: sha256(c2), size: c2.length, app: 'appB' })
255
-
256
- const summary = await vault.sweeper.convertPending('appB', { batch_id: 'batch-x' })
257
- assert.strictEqual(summary.converted, 2)
258
- assert.strictEqual(summary.bytes_saved, c1.length + c2.length)
259
-
260
- const undo = await vault.undoBatch('batch-x')
261
- assert.strictEqual(undo.undone, 2)
262
- assert.strictEqual((await fs.promises.stat(b1)).nlink, 1)
263
- assert.strictEqual((await fs.promises.stat(b2)).nlink, 1)
264
- })
265
-
266
- test('source-scoped batches never include another non-app location', async () => {
267
- const { home, vault } = await makeEnv()
268
- const content = crypto.randomBytes(4096)
269
- const hash = sha256(content)
270
- const original = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), content)
271
- const appCopy = await writeFile(path.resolve(home, 'api', 'appB', 'm.bin'), content)
272
- const cacheCopy = await writeFile(path.resolve(home, 'cache', 'models', 'm.bin'), content)
273
- await vault.refreshSources()
274
- const appA = vault.sources().find((item) => item.kind === 'app' && item.app === 'appA')
275
- const appB = vault.sources().find((item) => item.kind === 'app' && item.app === 'appB')
276
- const cache = vault.sources().find((item) => item.kind === 'folder' && item.label === 'cache')
277
- await vault.adopt(original, hash, { app: 'appA', source_id: appA.id })
278
- vault.registry.duplicates.set(appCopy, { hash, size: content.length, app: 'appB', source_id: appB.id })
279
- vault.registry.duplicates.set(cacheCopy, { hash, size: content.length, app: null, source_id: cache.id })
280
-
281
- const result = await vault.sweeper.convertPending(undefined, { scope_id: appB.id, batch_id: 'scope-app-b' })
282
- assert.strictEqual(result.converted, 1)
283
- assert.strictEqual((await fs.promises.stat(appCopy)).ino, (await fs.promises.stat(original)).ino)
284
- assert.strictEqual((await fs.promises.stat(cacheCopy)).nlink, 1)
285
- assert.ok(vault.registry.duplicates.has(cacheCopy), 'other non-app scope remains pending')
286
- })
287
-
288
- test('unavailable conversions remain pending for review', async () => {
289
- const { home, vault } = await makeEnv()
290
- const content = crypto.randomBytes(4096)
291
- const hash = sha256(content)
292
- const original = await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), content)
293
- const pending = await writeFile(path.resolve(home, 'api', 'appB', 'm.bin'), content)
294
- await vault.adopt(original, hash, { app: 'appA' })
295
- vault.registry.duplicates.set(pending, { hash, size: content.length, app: 'appB' })
296
- const realConvert = vault.convert.bind(vault)
297
- vault.convert = async () => ({ status: 'unavailable' })
298
- const result = await vault.sweeper.convertPending('appB')
299
- vault.convert = realConvert
300
- assert.strictEqual(result.unavailable, 1)
301
- assert.ok(vault.registry.duplicates.has(pending))
302
- assert.strictEqual(vault.registry.links.has(pending), false)
303
- })
304
-
305
- test('status separates app, Pinokio folder, and external source metadata', async (t) => {
306
- const { home, vault } = await makeEnv()
307
- const external = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-ui-external-'))
308
- homes.push(external)
309
- try {
310
- await fs.promises.symlink(external, path.resolve(home, 'api', 'linked-models'), process.platform === 'win32' ? 'junction' : 'dir')
311
- } catch (error) {
312
- t.skip(`directory links unavailable: ${error.message}`)
313
- return
314
- }
315
- await fs.promises.mkdir(path.resolve(home, 'api', 'local-app'), { recursive: true })
316
- await fs.promises.mkdir(path.resolve(home, 'cache'), { recursive: true })
317
- await vault.refreshSources()
318
- const status = await vault.status()
319
- const byKind = new Map(status.sources.map((source) => [source.id, source]))
320
- assert.ok([...byKind.values()].some((source) => source.kind === 'app' && source.label === 'local-app' && source.parent_id === 'apps'))
321
- assert.ok([...byKind.values()].some((source) => source.kind === 'folder' && source.label === 'cache' && source.parent_id === 'pinokio'))
322
- assert.ok([...byKind.values()].some((source) => source.kind === 'external' && source.label === 'linked-models' && source.parent_id === 'external'))
323
- })
324
- })