pinokiod 8.0.37 → 8.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/kernel/vault/hash_worker.js +6 -1
- package/kernel/vault/index.js +238 -69
- package/kernel/vault/registry.js +2 -2
- package/kernel/vault/sweeper.js +208 -47
- package/package.json +1 -1
- package/server/index.js +29 -0
- package/server/views/vault.ejs +479 -198
- package/test/vault-engine.test.js +96 -9
- package/test/vault-sweep.test.js +103 -0
- package/test/vault-ui.test.js +108 -0
|
@@ -188,6 +188,11 @@ describe('vault engine (phase 1)', () => {
|
|
|
188
188
|
assert.strictEqual(fresh.registry.blobs.get(hash).orphan, false)
|
|
189
189
|
assert.strictEqual(fresh.registry.links.get(a).hash, hash)
|
|
190
190
|
assert.strictEqual(fresh.registry.links.get(a).app, 'appA')
|
|
191
|
+
assert.strictEqual(
|
|
192
|
+
fresh.registry.links.has(fresh.storePathFor(hash)),
|
|
193
|
+
false,
|
|
194
|
+
'the internal vault name is never exposed as a tracked location'
|
|
195
|
+
)
|
|
191
196
|
})
|
|
192
197
|
|
|
193
198
|
test('item 9: torn events line tolerated; corrupt registry.json triggers rebuild, not failure', async () => {
|
|
@@ -224,7 +229,31 @@ describe('vault engine (phase 1)', () => {
|
|
|
224
229
|
assert.strictEqual(fs.existsSync(path.resolve(vault.root, 'registry.json.tmp')), false)
|
|
225
230
|
})
|
|
226
231
|
|
|
227
|
-
test('
|
|
232
|
+
test('manual index repair preserves user decisions, history, and pending review state', async () => {
|
|
233
|
+
const h = await home()
|
|
234
|
+
const vault = await makeVault(h)
|
|
235
|
+
const content = crypto.randomBytes(4096)
|
|
236
|
+
const hash = sha256(content)
|
|
237
|
+
const original = await writeFile(path.resolve(h, 'api', 'appA', 'm.bin'), content)
|
|
238
|
+
const pending = await writeFile(path.resolve(h, 'api', 'appB', 'm.bin'), content)
|
|
239
|
+
const independent = await writeFile(path.resolve(h, 'cache', 'keep.bin'), crypto.randomBytes(2048))
|
|
240
|
+
await vault.adopt(original, hash, { app: 'appA', source_urls: ['https://example.test/model'] })
|
|
241
|
+
vault.registry.duplicates.set(pending, { hash, size: content.length, app: 'appB', discovered: Date.now() })
|
|
242
|
+
vault.registry.excluded.set(independent, { ts: Date.now(), size: 2048 })
|
|
243
|
+
vault.registry.totals.lifetime_bytes_saved = 12345
|
|
244
|
+
vault.registry.lastScan = { ts: 111, dirs: 2, files: 3, bytes_total: 999 }
|
|
245
|
+
|
|
246
|
+
await vault.rebuild([])
|
|
247
|
+
|
|
248
|
+
assert.ok(vault.registry.excluded.has(independent), 'keep-independent decision survives')
|
|
249
|
+
assert.strictEqual(vault.registry.excluded.get(independent).size, 2048)
|
|
250
|
+
assert.strictEqual(vault.registry.totals.lifetime_bytes_saved, 12345)
|
|
251
|
+
assert.deepStrictEqual(vault.registry.lastScan, { ts: 111, dirs: 2, files: 3, bytes_total: 999 })
|
|
252
|
+
assert.ok(vault.registry.duplicates.has(pending), 'pending review item survives when still valid')
|
|
253
|
+
assert.deepStrictEqual(vault.registry.blobs.get(hash).source_urls, ['https://example.test/model'])
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('rebuild walks every directory without name heuristics and still skips symlinks', async () => {
|
|
228
257
|
const h = await home()
|
|
229
258
|
const vault = await makeVault(h)
|
|
230
259
|
const big = Buffer.alloc(Vault.SIZE_THRESHOLD, 3)
|
|
@@ -234,19 +263,77 @@ describe('vault engine (phase 1)', () => {
|
|
|
234
263
|
await fs.promises.unlink(path.resolve(vault.root, 'registry.json')).catch(() => {})
|
|
235
264
|
const fresh = await makeVault(h)
|
|
236
265
|
await fresh.rebuild()
|
|
237
|
-
// Blob is found (store side), but the env/ path must NOT be re-associated.
|
|
238
266
|
assert.ok(fresh.registry.blobs.has(hash))
|
|
239
|
-
assert.strictEqual(fresh.registry.links.has(inEnv),
|
|
267
|
+
assert.strictEqual(fresh.registry.links.has(inEnv), true, 'directory names never hide tracked files')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test('hash worker computes sha256 and is reused across a scan batch', async () => {
|
|
271
|
+
const h = await home()
|
|
272
|
+
const vault = await makeVault(h)
|
|
273
|
+
const first = crypto.randomBytes(1024 * 1024)
|
|
274
|
+
const second = crypto.randomBytes(1024 * 1024)
|
|
275
|
+
const firstFile = await writeFile(path.resolve(h, 'api', 'appA', 'x.bin'), first)
|
|
276
|
+
const secondFile = await writeFile(path.resolve(h, 'api', 'appA', 'y.bin'), second)
|
|
277
|
+
const firstResult = await vault.hashFile(firstFile)
|
|
278
|
+
const secondResult = await vault.hashFile(secondFile)
|
|
279
|
+
assert.strictEqual(firstResult.hash, sha256(first))
|
|
280
|
+
assert.strictEqual(firstResult.size, first.length)
|
|
281
|
+
assert.strictEqual(secondResult.hash, sha256(second))
|
|
282
|
+
assert.strictEqual(vault.workerStarts, 1, 'worker startup is amortized across files')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('unsupported conversion leaves the independent copy pending and unregistered', async () => {
|
|
286
|
+
const h = await home()
|
|
287
|
+
const vault = await makeVault(h)
|
|
288
|
+
const content = crypto.randomBytes(4096)
|
|
289
|
+
const hash = sha256(content)
|
|
290
|
+
const original = await writeFile(path.resolve(h, 'api', 'appA', 'm.bin'), content)
|
|
291
|
+
const pending = await writeFile(path.resolve(h, 'api', 'appB', 'm.bin'), content)
|
|
292
|
+
await vault.adopt(original, hash, { app: 'appA' })
|
|
293
|
+
const realLink = fs.promises.link
|
|
294
|
+
fs.promises.link = async (source, target) => {
|
|
295
|
+
if (target === pending + Vault.TMP_SUFFIX) {
|
|
296
|
+
const error = new Error('different volume')
|
|
297
|
+
error.code = 'EXDEV'
|
|
298
|
+
throw error
|
|
299
|
+
}
|
|
300
|
+
return realLink(source, target)
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const result = await vault.convert(pending, hash, { app: 'appB' })
|
|
304
|
+
assert.strictEqual(result.status, 'unavailable')
|
|
305
|
+
} finally {
|
|
306
|
+
fs.promises.link = realLink
|
|
307
|
+
}
|
|
308
|
+
assert.strictEqual(vault.registry.links.has(pending), false)
|
|
309
|
+
assert.deepStrictEqual(await fs.promises.readFile(pending), content)
|
|
240
310
|
})
|
|
241
311
|
|
|
242
|
-
test('
|
|
312
|
+
test('permission errors during conversion are retryable locks, not unsupported disks', async () => {
|
|
243
313
|
const h = await home()
|
|
244
314
|
const vault = await makeVault(h)
|
|
245
|
-
const content = crypto.randomBytes(
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
315
|
+
const content = crypto.randomBytes(4096)
|
|
316
|
+
const hash = sha256(content)
|
|
317
|
+
const original = await writeFile(path.resolve(h, 'api', 'appA', 'm.bin'), content)
|
|
318
|
+
const pending = await writeFile(path.resolve(h, 'api', 'appB', 'm.bin'), content)
|
|
319
|
+
await vault.adopt(original, hash, { app: 'appA' })
|
|
320
|
+
const realLink = fs.promises.link
|
|
321
|
+
fs.promises.link = async (source, target) => {
|
|
322
|
+
if (target === pending + Vault.TMP_SUFFIX) {
|
|
323
|
+
const error = new Error('file is in use')
|
|
324
|
+
error.code = 'EPERM'
|
|
325
|
+
throw error
|
|
326
|
+
}
|
|
327
|
+
return realLink(source, target)
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const result = await vault.convert(pending, hash, { app: 'appB' })
|
|
331
|
+
assert.strictEqual(result.status, 'locked')
|
|
332
|
+
} finally {
|
|
333
|
+
fs.promises.link = realLink
|
|
334
|
+
}
|
|
335
|
+
assert.strictEqual(vault.registry.links.has(pending), false)
|
|
336
|
+
assert.deepStrictEqual(await fs.promises.readFile(pending), content)
|
|
250
337
|
})
|
|
251
338
|
|
|
252
339
|
test('item 11: exFAT volume enters copy mode (macOS loopback; skipped elsewhere)', { skip: process.platform !== 'darwin' }, async (t) => {
|
package/test/vault-sweep.test.js
CHANGED
|
@@ -47,6 +47,11 @@ describe('vault manual scan (phase 3)', () => {
|
|
|
47
47
|
assert.ok(vault.registry.blobs.has(sha256(content)))
|
|
48
48
|
assert.ok(result.bytes_total >= content.length + 4, 'folder total includes small files')
|
|
49
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')
|
|
50
55
|
})
|
|
51
56
|
|
|
52
57
|
test('scans NEVER convert: every byte-identical copy is pending, even in HF caches', async () => {
|
|
@@ -123,6 +128,28 @@ describe('vault manual scan (phase 3)', () => {
|
|
|
123
128
|
assert.strictEqual(status.last_scan.source_bytes[source.id], content.length, 'external totals remain attributable')
|
|
124
129
|
})
|
|
125
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
|
+
|
|
126
153
|
test('excluded paths are respected: no adoption, no pending, no re-listing', async () => {
|
|
127
154
|
const { home, vault } = await makeEnv()
|
|
128
155
|
const content = crypto.randomBytes(4096)
|
|
@@ -140,8 +167,84 @@ describe('vault manual scan (phase 3)', () => {
|
|
|
140
167
|
await writeFile(path.resolve(home, 'api', 'appA', 'm.bin'), crypto.randomBytes(4096))
|
|
141
168
|
await vault.sweeper.scan()
|
|
142
169
|
const after = vault.sweeper.stats.hashes
|
|
170
|
+
const priorFiles = vault.registry.lastScan.files
|
|
143
171
|
await vault.sweeper.scan()
|
|
144
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')
|
|
145
248
|
})
|
|
146
249
|
|
|
147
250
|
test('conversion tmp files are never candidates', async () => {
|
package/test/vault-ui.test.js
CHANGED
|
@@ -67,6 +67,7 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
67
67
|
assert.deepStrictEqual(await fs.promises.readFile(b), content)
|
|
68
68
|
assert.strictEqual((await fs.promises.stat(a)).nlink, 2)
|
|
69
69
|
assert.ok(vault.registry.excluded.has(b))
|
|
70
|
+
assert.strictEqual(vault.registry.excluded.get(b).size, content.length)
|
|
70
71
|
assert.strictEqual(fs.existsSync(b + '.pinokio-dedup-tmp'), false)
|
|
71
72
|
|
|
72
73
|
// A scan neither re-adopts nor re-lists the pinned path.
|
|
@@ -95,6 +96,7 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
95
96
|
assert.strictEqual(result.status, 'ignored')
|
|
96
97
|
assert.strictEqual(vault.registry.duplicates.has(b), false)
|
|
97
98
|
assert.ok(vault.registry.excluded.has(b))
|
|
99
|
+
assert.strictEqual(vault.registry.excluded.get(b).size, content.length)
|
|
98
100
|
})
|
|
99
101
|
|
|
100
102
|
test('status(): disk figures, scan metadata, excluded list', async () => {
|
|
@@ -107,6 +109,7 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
107
109
|
assert.strictEqual(status.bytes_on_disk, content.length)
|
|
108
110
|
assert.ok(status.last_scan && status.last_scan.files > 0)
|
|
109
111
|
assert.strictEqual(status.excluded.length, 1)
|
|
112
|
+
assert.strictEqual(status.excluded[0].size, content.length)
|
|
110
113
|
assert.strictEqual(status.blobs.length, 1)
|
|
111
114
|
assert.deepStrictEqual(status.blobs[0].apps, ['appA'])
|
|
112
115
|
assert.ok(status.scan && status.scan.active === false)
|
|
@@ -149,6 +152,94 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
149
152
|
assert.doesNotMatch(style[0], /text-overflow:\s*ellipsis/)
|
|
150
153
|
})
|
|
151
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
|
+
|
|
152
243
|
test('deduplicate batches share a batch id and are undoable as one', async () => {
|
|
153
244
|
const { home, vault } = await makeEnv()
|
|
154
245
|
const c1 = crypto.randomBytes(4096)
|
|
@@ -194,6 +285,23 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
194
285
|
assert.ok(vault.registry.duplicates.has(cacheCopy), 'other non-app scope remains pending')
|
|
195
286
|
})
|
|
196
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
|
+
|
|
197
305
|
test('status separates app, Pinokio folder, and external source metadata', async (t) => {
|
|
198
306
|
const { home, vault } = await makeEnv()
|
|
199
307
|
const external = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-ui-external-'))
|