1import assert from 'node:assert/strict';
2import { readFile } from 'node:fs/promises';
3import test from 'node:test';
4import vm from 'node:vm';
5
6async function browser(folder) {
7 const source = await readFile(new URL('../../crates/snowbound/web/glue.js', import.meta.url), 'utf8');
8 const context = vm.createContext({ folder, queueMicrotask, console: { error() {} } });
9 vm.runInContext(source.replace(/\bexport /g, '').replaceAll('import.meta.url', JSON.stringify(import.meta.url)), context);
10 vm.runInContext(`
11 storage = { postMessage() { queueMicrotask(() => settling.shift().resolve()); } };
12 wasm = { refreshed() {} };
13 folders.set('/Folders/Notebook', { handle: folder, known: new Map() });
14 `, context);
15 return context;
16}
17
18test('a failed destination write refuses source deletion and retries before later writes', async () => {
19 const attempts = [];
20 const written = [];
21 let failing = true;
22 const context = await browser({
23 async getFileHandle(name) {
24 return {
25 async getFile() { return { size: 0, lastModified: 0 }; },
26 async createWritable() {
27 attempts.push(name);
28 if (failing) throw Error('Destination unavailable');
29 return {
30 async write({ data }) { written.push([name, [...data]]); },
31 async truncate() {},
32 async close() {},
33 };
34 },
35 };
36 },
37 });
38 vm.runInContext(`
39 storeFiles([
40 ['/Folders/Notebook/destination.one', 1, [[0, new Uint8Array([1])]]],
41 ['/Folders/Notebook/following.one', 1, [[0, new Uint8Array([2])]]],
42 ]);
43 `, context);
44 let sourceDeleted = false;
45 const move = vm.runInContext('settled()', context).then(() => { sourceDeleted = true; });
46 await assert.rejects(move, /Destination unavailable/);
47 assert.equal(sourceDeleted, false);
48 assert.deepEqual(attempts, ['destination.one']);
49 assert.deepEqual(written, []);
50 failing = false;
51 await vm.runInContext('settled()', context).then(() => { sourceDeleted = true; });
52 assert.equal(sourceDeleted, true);
53 assert.deepEqual(attempts, ['destination.one', 'destination.one', 'following.one']);
54 assert.deepEqual(written, [['destination.one', [1]], ['following.one', [2]]]);
55 vm.runInContext("storeFiles([['/Folders/Notebook/later.one', 1, [[0, new Uint8Array([3])]]]])", context);
56 await vm.runInContext('settled()', context);
57 assert.deepEqual(written, [['destination.one', [1]], ['following.one', [2]], ['later.one', [3]]]);
58});
59
60test('folder deletion retains permission failures and accepts an already missing entry', async () => {
61 const attempts = [];
62 let failing = true;
63 const context = await browser({
64 async removeEntry(name) {
65 attempts.push(name);
66 const error = Error(failing ? 'Permission refused' : 'Already removed');
67 error.name = failing ? 'NotAllowedError' : 'NotFoundError';
68 throw error;
69 },
70 });
71 vm.runInContext("storeFiles([['/Folders/Notebook/deleted.one']])", context);
72 await assert.rejects(vm.runInContext('settled()', context), /Permission refused/);
73 failing = false;
74 await vm.runInContext('settled()', context);
75 assert.deepEqual(attempts, ['deleted.one', 'deleted.one']);
76});
77
78
79test('a destination receipt completes without waiting for later folder writes', async () => {
80 let release;
81 const first = new Promise((resolve) => { release = resolve; });
82 const context = await browser({});
83 context.first = first;
84 vm.runInContext('serially(() => first)', context);
85 const receipt = vm.runInContext('settled()', context);
86 vm.runInContext("serially(() => Promise.reject(Error('Later failure')))", context);
87 release();
88 await receipt;
89 await assert.rejects(vm.runInContext('settled()', context), /Later failure/);
90});