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.
- package/kernel/api/hf/index.js +2 -2
- package/kernel/connect/index.js +2 -2
- package/kernel/connect/providers/huggingface/index.js +14 -5
- package/kernel/index.js +0 -12
- package/kernel/shell.js +1 -1
- package/kernel/shells.js +6 -0
- package/package.json +1 -1
- package/server/index.js +0 -120
- package/server/views/connect/huggingface.ejs +7 -9
- package/server/views/partials/main_sidebar.ejs +0 -1
- package/test/hf-api.test.js +4 -2
- package/test/huggingface-connect.test.js +7 -11
- package/test/huggingface-token-validity.test.js +135 -0
- package/kernel/vault/hash_worker.js +0 -24
- package/kernel/vault/index.js +0 -820
- package/kernel/vault/registry.js +0 -169
- package/kernel/vault/sweeper.js +0 -470
- package/server/views/vault.ejs +0 -1556
- package/test/vault-engine.test.js +0 -360
- package/test/vault-sweep.test.js +0 -336
- package/test/vault-ui.test.js +0 -324
package/test/vault-sweep.test.js
DELETED
|
@@ -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
|
-
})
|
package/test/vault-ui.test.js
DELETED
|
@@ -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
|
-
})
|