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
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
const { test, describe, before, 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 { execSync } = require('child_process')
|
|
8
|
-
const Vault = require('../kernel/vault')
|
|
9
|
-
|
|
10
|
-
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex')
|
|
11
|
-
|
|
12
|
-
const makeHome = async () => {
|
|
13
|
-
const home = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-vault-test-'))
|
|
14
|
-
await fs.promises.mkdir(path.resolve(home, 'api'), { recursive: true })
|
|
15
|
-
return home
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const fakeKernel = (home) => ({
|
|
19
|
-
homedir: home,
|
|
20
|
-
platform: process.platform,
|
|
21
|
-
path: (...args) => path.resolve(home, ...args)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
const makeVault = async (home) => {
|
|
25
|
-
const vault = new Vault(fakeKernel(home))
|
|
26
|
-
await vault.init()
|
|
27
|
-
return vault
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const writeFile = async (p, content) => {
|
|
31
|
-
await fs.promises.mkdir(path.dirname(p), { recursive: true })
|
|
32
|
-
await fs.promises.writeFile(p, content)
|
|
33
|
-
return p
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe('vault engine (phase 1)', () => {
|
|
37
|
-
let homes = []
|
|
38
|
-
const home = async () => {
|
|
39
|
-
const h = await makeHome()
|
|
40
|
-
homes.push(h)
|
|
41
|
-
return h
|
|
42
|
-
}
|
|
43
|
-
after(async () => {
|
|
44
|
-
for (const h of homes) {
|
|
45
|
-
await fs.promises.rm(h, { recursive: true, force: true }).catch(() => {})
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('item 1: PINOKIO_VAULT=false creates nothing and disables all ops', async () => {
|
|
50
|
-
const h = await home()
|
|
51
|
-
await writeFile(path.resolve(h, 'ENVIRONMENT'), 'PINOKIO_VAULT=false\n')
|
|
52
|
-
const vault = new Vault(fakeKernel(h))
|
|
53
|
-
const result = await vault.init()
|
|
54
|
-
assert.strictEqual(result.enabled, false)
|
|
55
|
-
assert.strictEqual(fs.existsSync(path.resolve(h, 'vault')), false)
|
|
56
|
-
const file = await writeFile(path.resolve(h, 'api', 'app', 'model.bin'), 'data')
|
|
57
|
-
assert.strictEqual((await vault.adopt(file, sha256(Buffer.from('data')))).status, 'disabled')
|
|
58
|
-
assert.strictEqual(fs.existsSync(path.resolve(h, 'vault')), false)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
test('kill switch: process.env override wins', async () => {
|
|
62
|
-
const h = await home()
|
|
63
|
-
process.env.PINOKIO_VAULT = 'false'
|
|
64
|
-
try {
|
|
65
|
-
const vault = new Vault(fakeKernel(h))
|
|
66
|
-
const result = await vault.init()
|
|
67
|
-
assert.strictEqual(result.enabled, false)
|
|
68
|
-
} finally {
|
|
69
|
-
delete process.env.PINOKIO_VAULT
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
test('adopt: metadata-only, store name shares the inode', async () => {
|
|
74
|
-
const h = await home()
|
|
75
|
-
const vault = await makeVault(h)
|
|
76
|
-
const content = crypto.randomBytes(4096)
|
|
77
|
-
const hash = sha256(content)
|
|
78
|
-
const file = await writeFile(path.resolve(h, 'api', 'appA', 'models', 'm.safetensors'), content)
|
|
79
|
-
const result = await vault.adopt(file, hash, { app: 'appA' })
|
|
80
|
-
assert.strictEqual(result.status, 'adopted')
|
|
81
|
-
const st = await fs.promises.stat(file)
|
|
82
|
-
const storeStat = await fs.promises.stat(vault.storePathFor(hash))
|
|
83
|
-
assert.strictEqual(st.ino, storeStat.ino)
|
|
84
|
-
assert.strictEqual(st.nlink, 2)
|
|
85
|
-
assert.ok(vault.registry.blobs.has(hash))
|
|
86
|
-
assert.strictEqual(vault.registry.links.get(file).hash, hash)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
test('item 5: convert links duplicate to blob, content intact, other copies untouched', async () => {
|
|
90
|
-
const h = await home()
|
|
91
|
-
const vault = await makeVault(h)
|
|
92
|
-
const content = crypto.randomBytes(8192)
|
|
93
|
-
const hash = sha256(content)
|
|
94
|
-
const a = await writeFile(path.resolve(h, 'api', 'appA', 'm.bin'), content)
|
|
95
|
-
const b = await writeFile(path.resolve(h, 'api', 'appB', 'm.bin'), content)
|
|
96
|
-
await vault.adopt(a, hash, { app: 'appA' })
|
|
97
|
-
const result = await vault.convert(b, hash, { app: 'appB', batch_id: 'batch1' })
|
|
98
|
-
assert.strictEqual(result.status, 'converted')
|
|
99
|
-
const stA = await fs.promises.stat(a)
|
|
100
|
-
const stB = await fs.promises.stat(b)
|
|
101
|
-
assert.strictEqual(stA.ino, stB.ino)
|
|
102
|
-
assert.strictEqual(stB.nlink, 3)
|
|
103
|
-
assert.deepStrictEqual(await fs.promises.readFile(b), content)
|
|
104
|
-
assert.strictEqual(vault.registry.totals.lifetime_bytes_saved, content.length)
|
|
105
|
-
const events = await vault.registry.readEvents()
|
|
106
|
-
assert.ok(events.some((e) => e.kind === 'convert' && e.batch_id === 'batch1'))
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
test('item 5: simulated crash between link and rename leaves target intact; verify cleans stray tmp', async () => {
|
|
110
|
-
const h = await home()
|
|
111
|
-
const vault = await makeVault(h)
|
|
112
|
-
const content = crypto.randomBytes(4096)
|
|
113
|
-
const hash = sha256(content)
|
|
114
|
-
const a = await writeFile(path.resolve(h, 'api', 'appA', 'm.bin'), content)
|
|
115
|
-
await vault.adopt(a, hash, { app: 'appA' })
|
|
116
|
-
// Simulate a crash mid-convert: tmp link exists next to a registered path.
|
|
117
|
-
await fs.promises.link(vault.storePathFor(hash), a + Vault.TMP_SUFFIX)
|
|
118
|
-
assert.deepStrictEqual(await fs.promises.readFile(a), content)
|
|
119
|
-
await vault.verify()
|
|
120
|
-
assert.strictEqual(fs.existsSync(a + Vault.TMP_SUFFIX), false)
|
|
121
|
-
assert.deepStrictEqual(await fs.promises.readFile(a), content)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
test('verify: orphan detection via nlink, dead link pruning, store re-adoption', async () => {
|
|
125
|
-
const h = await home()
|
|
126
|
-
const vault = await makeVault(h)
|
|
127
|
-
const content = crypto.randomBytes(4096)
|
|
128
|
-
const hash = sha256(content)
|
|
129
|
-
const a = await writeFile(path.resolve(h, 'api', 'appA', 'm.bin'), content)
|
|
130
|
-
await vault.adopt(a, hash, { app: 'appA' })
|
|
131
|
-
|
|
132
|
-
// Delete the app copy: blob becomes an orphan, link is pruned.
|
|
133
|
-
await fs.promises.unlink(a)
|
|
134
|
-
await vault.verify()
|
|
135
|
-
assert.strictEqual(vault.registry.links.has(a), false)
|
|
136
|
-
assert.strictEqual(vault.registry.blobs.get(hash).orphan, true)
|
|
137
|
-
|
|
138
|
-
// Recreate the content as a NEW inode: adopt refuses (duplicate), convert
|
|
139
|
-
// links it. Then delete the STORE name: verify re-adopts from the link.
|
|
140
|
-
const b = await writeFile(path.resolve(h, 'api', 'appB', 'm.bin'), content)
|
|
141
|
-
assert.strictEqual((await vault.adopt(b, hash, { app: 'appB' })).status, 'duplicate')
|
|
142
|
-
assert.strictEqual((await vault.convert(b, hash, { app: 'appB' })).status, 'converted')
|
|
143
|
-
await fs.promises.unlink(vault.storePathFor(hash))
|
|
144
|
-
await vault.verify()
|
|
145
|
-
assert.strictEqual(fs.existsSync(vault.storePathFor(hash)), true)
|
|
146
|
-
const st = await fs.promises.stat(b)
|
|
147
|
-
assert.strictEqual(st.nlink, 2)
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
test('reclaim: refuses while in use, frees orphans', async () => {
|
|
151
|
-
const h = await home()
|
|
152
|
-
const vault = await makeVault(h)
|
|
153
|
-
const content = crypto.randomBytes(4096)
|
|
154
|
-
const hash = sha256(content)
|
|
155
|
-
const a = await writeFile(path.resolve(h, 'api', 'appA', 'm.bin'), content)
|
|
156
|
-
await vault.adopt(a, hash)
|
|
157
|
-
assert.strictEqual((await vault.reclaim(hash)).status, 'in-use')
|
|
158
|
-
await fs.promises.unlink(a)
|
|
159
|
-
const result = await vault.reclaim(hash)
|
|
160
|
-
assert.strictEqual(result.status, 'reclaimed')
|
|
161
|
-
assert.strictEqual(result.bytes_freed, content.length)
|
|
162
|
-
assert.strictEqual(fs.existsSync(vault.storePathFor(hash)), false)
|
|
163
|
-
assert.strictEqual(vault.registry.blobs.has(hash), false)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test('item 9: registry deleted => rebuild reproduces blobs, links, orphans from disk', async () => {
|
|
167
|
-
const h = await home()
|
|
168
|
-
const vault = await makeVault(h)
|
|
169
|
-
// Rebuild's ino-match walk only considers files >= SIZE_THRESHOLD, so
|
|
170
|
-
// lower the threshold surrogate by using large-enough sparse-ish files.
|
|
171
|
-
const big = Buffer.alloc(Vault.SIZE_THRESHOLD, 7)
|
|
172
|
-
const hash = sha256(big)
|
|
173
|
-
const a = await writeFile(path.resolve(h, 'api', 'appA', 'big.bin'), big)
|
|
174
|
-
await vault.adopt(a, hash, { app: 'appA' })
|
|
175
|
-
const orphanContent = Buffer.alloc(Vault.SIZE_THRESHOLD, 9)
|
|
176
|
-
const orphanHash = sha256(orphanContent)
|
|
177
|
-
const b = await writeFile(path.resolve(h, 'api', 'appB', 'big2.bin'), orphanContent)
|
|
178
|
-
await vault.adopt(b, orphanHash, { app: 'appB' })
|
|
179
|
-
await fs.promises.unlink(b)
|
|
180
|
-
|
|
181
|
-
await fs.promises.unlink(path.resolve(vault.root, 'registry.json')).catch(() => {})
|
|
182
|
-
await fs.promises.unlink(path.resolve(vault.root, 'events.ndjson')).catch(() => {})
|
|
183
|
-
const fresh = await makeVault(h)
|
|
184
|
-
await fresh.rebuild()
|
|
185
|
-
assert.ok(fresh.registry.blobs.has(hash))
|
|
186
|
-
assert.ok(fresh.registry.blobs.has(orphanHash))
|
|
187
|
-
assert.strictEqual(fresh.registry.blobs.get(orphanHash).orphan, true)
|
|
188
|
-
assert.strictEqual(fresh.registry.blobs.get(hash).orphan, false)
|
|
189
|
-
assert.strictEqual(fresh.registry.links.get(a).hash, hash)
|
|
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
|
-
)
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
test('item 9: torn events line tolerated; corrupt registry.json triggers rebuild, not failure', async () => {
|
|
199
|
-
const h = await home()
|
|
200
|
-
const vault = await makeVault(h)
|
|
201
|
-
const content = crypto.randomBytes(4096)
|
|
202
|
-
const hash = sha256(content)
|
|
203
|
-
const a = await writeFile(path.resolve(h, 'api', 'appA', 'm.bin'), content)
|
|
204
|
-
await vault.adopt(a, hash)
|
|
205
|
-
await vault.registry.flush()
|
|
206
|
-
// Torn final line
|
|
207
|
-
await fs.promises.appendFile(path.resolve(vault.root, 'events.ndjson'), '{"kind":"conv')
|
|
208
|
-
const events = await vault.registry.readEvents()
|
|
209
|
-
assert.ok(events.every((e) => typeof e === 'object'))
|
|
210
|
-
// Corrupt snapshot
|
|
211
|
-
await fs.promises.writeFile(path.resolve(vault.root, 'registry.json'), '{corrupted!!')
|
|
212
|
-
const fresh = new Vault(fakeKernel(h))
|
|
213
|
-
const result = await fresh.init()
|
|
214
|
-
assert.strictEqual(result.enabled, true)
|
|
215
|
-
assert.strictEqual(result.corrupt_recovered, true)
|
|
216
|
-
assert.ok(fresh.registry.blobs.has(hash))
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
test('registry snapshot flushes serialize without racing the shared temp file', async () => {
|
|
220
|
-
const h = await home()
|
|
221
|
-
const vault = await makeVault(h)
|
|
222
|
-
const content = crypto.randomBytes(4096)
|
|
223
|
-
const file = await writeFile(path.resolve(h, 'api', 'appA', 'm.bin'), content)
|
|
224
|
-
await vault.adopt(file, sha256(content), { app: 'appA' })
|
|
225
|
-
|
|
226
|
-
await Promise.all([vault.registry.flush(), vault.registry.flush(), vault.registry.flush()])
|
|
227
|
-
const snapshot = JSON.parse(await fs.promises.readFile(path.resolve(vault.root, 'registry.json'), 'utf8'))
|
|
228
|
-
assert.ok(snapshot.blobs[sha256(content)])
|
|
229
|
-
assert.strictEqual(fs.existsSync(path.resolve(vault.root, 'registry.json.tmp')), false)
|
|
230
|
-
})
|
|
231
|
-
|
|
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 () => {
|
|
257
|
-
const h = await home()
|
|
258
|
-
const vault = await makeVault(h)
|
|
259
|
-
const big = Buffer.alloc(Vault.SIZE_THRESHOLD, 3)
|
|
260
|
-
const hash = sha256(big)
|
|
261
|
-
const inEnv = await writeFile(path.resolve(h, 'api', 'appA', 'env', 'big.bin'), big)
|
|
262
|
-
await vault.adopt(inEnv, hash, { app: 'appA' })
|
|
263
|
-
await fs.promises.unlink(path.resolve(vault.root, 'registry.json')).catch(() => {})
|
|
264
|
-
const fresh = await makeVault(h)
|
|
265
|
-
await fresh.rebuild()
|
|
266
|
-
assert.ok(fresh.registry.blobs.has(hash))
|
|
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)
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
test('permission errors during conversion are retryable locks, not unsupported disks', async () => {
|
|
313
|
-
const h = await home()
|
|
314
|
-
const vault = await makeVault(h)
|
|
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)
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
test('item 11: exFAT volume enters copy mode (macOS loopback; skipped elsewhere)', { skip: process.platform !== 'darwin' }, async (t) => {
|
|
340
|
-
const h = await home()
|
|
341
|
-
const vault = await makeVault(h)
|
|
342
|
-
const dmg = path.resolve(h, 'exfat-test.dmg')
|
|
343
|
-
let mountPoint = null
|
|
344
|
-
try {
|
|
345
|
-
execSync(`hdiutil create -size 16m -fs exFAT -volname VAULTPROBE "${dmg}"`, { stdio: 'pipe' })
|
|
346
|
-
const out = execSync(`hdiutil attach "${dmg}" -nobrowse`, { encoding: 'utf8' })
|
|
347
|
-
const m = out.match(/(\/Volumes\/\S+)\s*$/m)
|
|
348
|
-
mountPoint = m && m[1]
|
|
349
|
-
} catch (e) {
|
|
350
|
-
t.skip('hdiutil exFAT loopback unavailable: ' + e.message)
|
|
351
|
-
return
|
|
352
|
-
}
|
|
353
|
-
try {
|
|
354
|
-
const mode = await vault.probe(mountPoint)
|
|
355
|
-
assert.strictEqual(mode, 'copy')
|
|
356
|
-
} finally {
|
|
357
|
-
if (mountPoint) execSync(`hdiutil detach "${mountPoint}" -force`, { stdio: 'pipe' })
|
|
358
|
-
}
|
|
359
|
-
})
|
|
360
|
-
})
|