Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_droots_real.mjs

12.7 KiB, 1 run

created by r2519314175:387, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_droots_real.mjs — the adoption migration, over a REAL store rather than a fixture.
2//
3// dev/verify_droots.mjs proves the rules of the migration against data that
4// verifier wrote: four files, two Diamonds, names chosen to make the assertions
5// easy. That proves the logic and nothing about the corpus. A migration is a
6// promise about bytes somebody already has — deep directories, hundreds of
7// version snapshots, a log with a year in it, names with characters nobody on
8// this side thought of — and the only honest way to test it is against a store
9// this test did not write.
10//
11// So it needs a backup exported from a live install:
12//
13// node dev/verify_droots_real.mjs --backup ~/Downloads/daimond-backup-YYYY-MM-DD.json
14//
15// The backup's `workspace` entries are the whole of that account's browser
16// storage, byte for byte (`doExport` / `collectOpfsFiles` in www/js/daimond.js).
17// Everything under `diamonds/` is laid into the STAND-IN FOLDER — putting the
18// user's real store where yesterday's build would have stranded it — and the
19// adoption edge is asked to bring it home. Then every byte is compared back.
20//
21// WITHOUT --backup THIS EXITS 2 AND PROVES NOTHING. That is deliberate and it
22// is the whole point of the file: a skip that returned 0 would read as a pass
23// on a suite summary, and "the migration is proven" would then be a claim
24// resting on a test that never ran.
25//
26// Run with dev/serve.mjs (DAIMOND_PORT, default 8777) up. No gateway, no hand, no mock
27// model.
28import fs from 'node:fs';
29import { open, signInAs } from './harness.mjs';
30
31const arg = (name) => {
32 const i = process.argv.indexOf(name);
33 return i > -1 ? process.argv[i + 1] : null;
34};
35const backupPath = arg('--backup');
36
37if (!backupPath) {
38 console.log('');
39 console.log(' ────────────────────────────────────────────────────────────────');
40 console.log(' SKIPPED — and this is NOT a pass.');
41 console.log('');
42 console.log(' Nothing about the Diamond migration has been proved by running');
43 console.log(' this. It needs a backup exported from a real install:');
44 console.log('');
45 console.log(' Settings → Back up everything, then');
46 console.log(' node dev/verify_droots_real.mjs --backup <file.json>');
47 console.log('');
48 console.log(' dev/verify_droots.mjs covers the rules against a fixture it wrote');
49 console.log(' itself. That is a different claim, and a weaker one.');
50 console.log(' ────────────────────────────────────────────────────────────────');
51 console.log('');
52 process.exit(2);
53}
54
55if (!fs.existsSync(backupPath)) {
56 console.log(' no backup at ' + backupPath);
57 process.exit(2);
58}
59
60const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
61const all = Array.isArray(backup.workspace) ? backup.workspace : [];
62const store = all.filter((f) => /^diamonds\//.test(String(f.path)));
63const bytesOf = (f) => Buffer.from(String(f.b64 || ''), 'base64').length;
64const total = store.reduce((n, f) => n + bytesOf(f), 0);
65const ids = Array.from(new Set(store.map((f) => String(f.path).split('/')[1]))).filter(Boolean);
66
67console.log(' backup: ' + backupPath);
68console.log(' exported: ' + (backup.exported || 'unstated'));
69console.log(' store: ' + store.length + ' files, ' + total + ' bytes, '
70 + ids.length + ' Diamonds (' + all.length + ' workspace files in all)');
71console.log('');
72
73if (!store.length) {
74 console.log(' This backup carries no `diamonds/` files at all, so there is no');
75 console.log(' store in it to migrate. That is a real answer about this backup,');
76 console.log(' not a pass: find one taken after the Diamonds existed.');
77 process.exit(2);
78}
79
80const ok = [], bad = [];
81const check = (name, pass, detail) => {
82 (pass ? ok : bad).push(name);
83 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
84};
85
86const FOLDER = 'standin-folder';
87
88const s = await open({ name: 'drootsreal', connect: false });
89const p = s.page;
90await p.waitForTimeout(1500);
91
92const install = () => p.evaluate(async (folder) => {
93 const mod = await import('../pkg/oxedyne_daimond.js');
94 window.__d = {
95 mod,
96 app: new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true),
97 root: await navigator.storage.getDirectory(),
98 folder: null,
99 };
100 __d.folder = await __d.root.getDirectoryHandle(folder, { create: true });
101 const b64ToBytes = (b64) => {
102 const bin = atob(b64), out = new Uint8Array(bin.length);
103 for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
104 return out;
105 };
106 const dirFor = async (which, parts) => {
107 let cur = which === 'folder' ? __d.folder : __d.root;
108 for (let i = 0; i < parts.length - 1; i++) {
109 cur = await cur.getDirectoryHandle(parts[i], { create: true });
110 }
111 return cur;
112 };
113 // Written and read through the directory handle, never through the store:
114 // "did every byte arrive" must not be answered by the thing under test.
115 window.__put = async (which, path, b64) => {
116 const parts = path.split('/');
117 const dir = await dirFor(which, parts);
118 const fh = await dir.getFileHandle(parts[parts.length - 1], { create: true });
119 const w = await fh.createWritable();
120 await w.write(b64ToBytes(b64));
121 await w.close();
122 };
123 window.__digest = async (which, path) => {
124 let cur = which === 'folder' ? __d.folder : __d.root;
125 const parts = path.split('/');
126 try {
127 for (let i = 0; i < parts.length - 1; i++) cur = await cur.getDirectoryHandle(parts[i]);
128 const fh = await cur.getFileHandle(parts[parts.length - 1]);
129 const buf = await (await fh.getFile()).arrayBuffer();
130 const h = await crypto.subtle.digest('SHA-256', buf);
131 return [...new Uint8Array(h)].map((b) => b.toString(16).padStart(2, '0')).join('');
132 } catch (e) { return null; }
133 };
134}, FOLDER);
135
136await install();
137
138// ── The user's real store, where yesterday's build would have left it ────
139
140for (const f of store) {
141 await p.evaluate(([path, b64]) => __put('folder', path, b64), [f.path, f.b64]);
142}
143const digests = {};
144for (const f of store) {
145 digests[f.path] = await p.evaluate(([w, path]) => __digest(w, path), ['folder', f.path]);
146}
147check('every file of the real store was laid into the folder',
148 Object.values(digests).every((d) => !!d),
149 Object.values(digests).filter((d) => !d).length + ' unreadable');
150
151// ── Adoption, through the same edge the page calls ───────────────────────
152
153const t0 = Date.now();
154const report = await p.evaluate(async () => {
155 __d.mod.set_workspace_dir(__d.folder);
156 return JSON.parse(await __d.mod.adopt_folder_diamonds());
157});
158const took = Date.now() - t0;
159console.log(' adoption took ' + took + ' ms; ' + report.adopted.length + ' Diamonds, '
160 + (report.skipped || []).length + ' files skipped, '
161 + (report.left || []).length + ' entries left alone');
162
163check('adoption brought home every Diamond the store held',
164 ids.every((id) => report.adopted.some((d) => d.id === id)),
165 'missing: ' + ids.filter((id) => !report.adopted.some((d) => d.id === id)).join(' '));
166
167// ── Every byte, compared where it landed ─────────────────────────────────
168
169const wrong = [];
170for (const f of store) {
171 const here = await p.evaluate(([w, path]) => __digest(w, path), ['opfs', f.path]);
172 if (here !== digests[f.path]) wrong.push(f.path);
173}
174check('and every byte of it arrived, file for file',
175 wrong.length === 0, wrong.length + ' differ: ' + wrong.slice(0, 4).join(', '));
176
177const gone = [];
178for (const f of store) {
179 const there = await p.evaluate(([w, path]) => __digest(w, path), ['folder', f.path]);
180 if (there !== digests[f.path]) gone.push(f.path);
181}
182check('and nothing was taken out of the folder or changed there',
183 gone.length === 0, gone.length + ' differ: ' + gone.slice(0, 4).join(', '));
184
185// ── The Diamonds are Diamonds again ──────────────────────────────────────
186
187const rail = await p.evaluate(async () => JSON.parse(await __d.app.list_diamonds()));
188check('every Diamond in the backup is on the rail',
189 ids.every((id) => rail.some((r) => r.id === id)),
190 rail.length + ' listed, ' + ids.length + ' expected');
191
192const crystals = await p.evaluate(async (want) => {
193 const out = {};
194 for (const id of want) {
195 out[id] = await __d.app.read_crystal_data(id).then((t) => t).catch(() => null);
196 }
197 return out;
198}, ids);
199const unopenable = Object.keys(crystals).filter((id) => crystals[id] === null);
200check('and every one of them OPENS', unopenable.length === 0, unopenable.join(' '));
201
202/// The user's own characters in a crystal, whichever of its two forms it is in.
203///
204/// THE BYTE LENGTH CANNOT BE THE MEASURE ANY MORE. A backup exported before the
205/// crystal became data carries `crystal.md`, and the store converts it on the way
206/// in -- `# ` markers become JSON keys, quotes and newlines gain a backslash --
207/// so the file that arrives is a different length from the file that was sent
208/// even when not one word has been lost. What must survive the conversion is the
209/// user's text, so that is what is compared: structure, markers and whitespace
210/// stripped out of both sides, and every remaining character held to account.
211const bare = (text) => {
212 let words = String(text || '');
213 try {
214 const data = JSON.parse(words);
215 const seen = [];
216 (function walk(v) {
217 if (typeof v === 'string') seen.push(v);
218 else if (Array.isArray(v)) v.forEach(walk);
219 else if (v && typeof v === 'object') Object.keys(v).forEach((k) => walk(v[k]));
220 })(data);
221 words = seen.join('');
222 } catch (e) { /* markdown, which is its own text */ }
223 return words.replace(/^\s*#{1,6}\s+/gm, '').replace(/\s+/g, '');
224};
225
226// Whichever form the backup was written in. A Diamond mid-conversion holds both,
227// and the data is the one the store reads, so it is the one to compare against.
228const expectCrystal = {};
229for (const f of store) {
230 const parts = String(f.path).split('/');
231 if (parts.length !== 3) continue;
232 if (parts[2] !== 'crystal.json' && parts[2] !== 'crystal.md') continue;
233 if (parts[2] === 'crystal.md' && expectCrystal[parts[1]] !== undefined) continue;
234 expectCrystal[parts[1]] = Buffer.from(String(f.b64 || ''), 'base64').toString('utf8');
235}
236const shortened = Object.keys(expectCrystal)
237 .filter((id) => bare(crystals[id]) !== bare(expectCrystal[id]));
238check('carrying every character the user left in it',
239 shortened.length === 0,
240 shortened.map((id) => id + ': ' + bare(crystals[id]).length
241 + ' chars vs ' + bare(expectCrystal[id]).length).slice(0, 4).join(', '));
242
243// ── And it settles ───────────────────────────────────────────────────────
244
245const again = await p.evaluate(() => __d.mod.adopt_folder_diamonds().then(JSON.parse));
246check('running it a second time brings nothing home',
247 again.adopted.length === 0, JSON.stringify(again.adopted).slice(0, 120));
248
249// Spelled the way the engine spells it (`beside_path`, src/wasm/diamond.rs):
250// before the extension, and appended where there is none — `.daimond/log` has
251// no extension, and a regex that assumed one silently compared the file with
252// itself and called the result a second copy.
253const besidePath = (path) => {
254 const i = path.lastIndexOf('/');
255 const dir = i < 0 ? '' : path.slice(0, i + 1);
256 const leaf = path.slice(i + 1);
257 const d = leaf.lastIndexOf('.');
258 return d > 0 ? dir + leaf.slice(0, d) + '.from-machine' + leaf.slice(d)
259 : dir + leaf + '.from-machine';
260};
261const doubled = [];
262for (const f of store) {
263 const beside = besidePath(String(f.path));
264 const d = await p.evaluate(([w, path]) => __digest(w, path), ['opfs', beside]);
265 if (d) doubled.push(beside);
266}
267check('and made no second copy of anything, the store having been empty',
268 doubled.length === 0, doubled.slice(0, 4).join(', '));
269
270// ── Across a reload, which is where a store that is not really there shows ──
271
272await p.reload({ waitUntil: 'domcontentloaded' });
273await signInAs(s, 'drootsreal');
274await p.waitForTimeout(2500);
275await install();
276const afterReload = await p.evaluate(async () =>
277 JSON.parse(await __d.app.list_diamonds()).map((r) => r.id));
278check('and the whole store is still there after a reload',
279 ids.every((id) => afterReload.includes(id)),
280 afterReload.length + ' listed');
281
282const noise = s.errs.filter((e) =>
283 !/favicon|ERR_ABORTED|net::ERR|Failed to load resource/i.test(e));
284check('the page threw nothing along the way', noise.length === 0, noise.slice(0, 3).join(' | '));
285
286await s.close();
287console.log('\n' + ok.length + ' ok, ' + bad.length + ' failed'
288 + ' (over ' + store.length + ' real files, ' + total + ' bytes)');
289process.exit(bad.length ? 1 : 0);