Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_crystalgap.mjs

10.5 KiB, 1 run

created by r2519314175:329, 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_crystalgap.mjs — a Diamond that LISTS must OPEN.
2//
3// `list()` admits a Diamond on its metadata alone, deliberately: a Diamond the
4// user can see and cannot open is a bug they can report, while one that has
5// silently vanished is a bug they can only mourn (src/wasm/diamond.rs). The
6// other half of that bargain was missing. `read_crystal_data` was a bare read,
7// so a Diamond whose crystal was not beside its metadata threw NotFoundError at
8// the panel and could not be opened at all — and four live paths arrive there,
9// an interrupted `import_diamond` among them, because the export sorts its
10// paths and `.daimond/meta.json` sorts BEFORE `crystal.json`.
11//
12// The fallback it grew has four legs now that a crystal is data, and the ORDER
13// of them is the claim:
14//
15// 1. `crystal.json`, where it should be;
16// 2. the newest `versions/NNNN.json`, the store's own copy of the same bytes;
17// 3. a legacy markdown crystal — `crystal.md`, then the newest
18// `versions/NNNN.md` — converted on the way out;
19// 4. empty, and said so in the console.
20//
21// Leg 3 is why a Diamond that predates the migration still opens. It is the leg
22// most easily lost in a refactor, because everything above it works without it
23// right up until somebody restores a two-year-old backup.
24//
25// What is pinned:
26// * a Diamond with metadata and no crystal lists AND opens, empty;
27// * where a version snapshot survives, opening finds the NEWEST one — the
28// store's own redundancy, which nothing else ever read back;
29// * data beats markdown wherever both survive, whatever their numbers say;
30// * a crystal that is still markdown opens as data, converted;
31// * an import that fails part way leaves the Diamond invisible rather than
32// listed and broken, because the metadata is written LAST whatever order it
33// arrived in;
34// * a whole import still lands, which is what stops the fix above from being
35// "never write the metadata".
36//
37// Run with dev/serve.mjs (DAIMOND_PORT, default 8777) up. No gateway, no hand, no mock
38// model.
39//
40// node dev/verify_crystalgap.mjs
41//
42// Every check here has been proved against BROKEN code: revert
43// `read_crystal_data` to a bare read and the first two go red; drop its legacy
44// leg and the markdown ones do; revert the metadata-last sort in
45// `import_diamond` and the import one does.
46import { open } from './harness.mjs';
47
48const ok = [], bad = [];
49const check = (name, pass, detail) => {
50 (pass ? ok : bad).push(name);
51 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
52};
53
54const s = await open({ name: 'crystalgap', connect: false });
55const p = s.page;
56await p.waitForTimeout(1500);
57
58await p.evaluate(async () => {
59 const mod = await import('../pkg/oxedyne_daimond.js');
60 window.__d = {
61 mod,
62 app: new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true),
63 root: await navigator.storage.getDirectory(),
64 };
65 // Seeded through the directory handle, not through the store: the state
66 // under test is one the store's own writers cannot produce on purpose.
67 window.__put = async (path, body) => {
68 let cur = __d.root;
69 const parts = path.split('/');
70 for (let i = 0; i < parts.length - 1; i++) {
71 cur = await cur.getDirectoryHandle(parts[i], { create: true });
72 }
73 const fh = await cur.getFileHandle(parts[parts.length - 1], { create: true });
74 const w = await fh.createWritable();
75 await w.write(body);
76 await w.close();
77 };
78 window.__meta = (name) => JSON.stringify(
79 { name: name, crystal_version: 0, updated: 1, touched: 1 });
80});
81
82const listed = () => p.evaluate(async () => JSON.parse(await __d.app.list_diamonds()).map(d => d.id));
83const opens = (id) => p.evaluate(async (x) =>
84 __d.app.read_crystal_data(x).then((t) => ({ text: t })).catch((e) => ({ err: String(e) })), id);
85
86/// A crystal as the data file holds one, and the same words as the markdown a
87/// legacy Diamond holds. The conversion is asserted between the two, so both
88/// forms of every fixture below are written from one place.
89const json = (title) => JSON.stringify({ title: title });
90
91// ── A Diamond whose crystal is not there ────────────────────────────────
92
93const GAP = 'ba5eba11ca11';
94await p.evaluate(async (id) => {
95 await __put('diamonds/' + id + '/.daimond/meta.json', __meta('Metadata and nothing else'));
96}, GAP);
97
98const gapRows = await listed();
99check('a Diamond with metadata and no crystal is still on the rail',
100 gapRows.includes(GAP), gapRows.join(' '));
101const gapOpen = await opens(GAP);
102check('and it OPENS rather than throwing, which is the other half of that bargain',
103 gapOpen.err === undefined && gapOpen.text === '', JSON.stringify(gapOpen));
104
105// ── ...but its versions are the store's own redundancy ──────────────────
106
107const VER = 'c0deca11ab1e';
108await p.evaluate(async (arg) => {
109 await __put('diamonds/' + arg.id + '/.daimond/meta.json', __meta('Crystal lost, versions kept'));
110 await __put('diamonds/' + arg.id + '/versions/0001.json', arg.first);
111 await __put('diamonds/' + arg.id + '/versions/0009.json', arg.last);
112 await __put('diamonds/' + arg.id + '/versions/0002.json', arg.middle);
113 // A markdown snapshot NUMBERED ABOVE every one of them. The order of the legs
114 // is the claim, not the numbers: data is preferred wherever any data survives,
115 // and a walk that took the highest number whatever its extension would hand
116 // back the markdown here and look entirely reasonable doing it.
117 await __put('diamonds/' + arg.id + '/versions/0011.md', '# a newer markdown one\n');
118}, { id: VER, first: json('the first crystal'), last: json('the crystal as it was last left'),
119 middle: json('a middle one') });
120
121const verOpen = await opens(VER);
122check('a lost crystal is read back from the NEWEST version snapshot',
123 verOpen.text === json('the crystal as it was last left'), JSON.stringify(verOpen));
124check('and data wins over markdown even where the markdown is numbered higher',
125 !/a newer markdown one/.test(verOpen.text || ''), JSON.stringify(verOpen).slice(0, 90));
126
127// ── ...and a Diamond that predates the data entirely ────────────────────
128//
129// The leg that reaches a workspace older than this whole change. Whether the
130// conversion is done by the migration in `list()` or by the read itself, the
131// answer is the same data — which is the point: a Diamond that predates the
132// migration OPENS, and opens carrying its words.
133
134const OLD = '0lda11eddece';
135await p.evaluate(async (id) => {
136 await __put('diamonds/' + id + '/.daimond/meta.json', __meta('Written before the migration'));
137 await __put('diamonds/' + id + '/crystal.md', '# The old way\n\nWords a user typed years ago.\n');
138}, OLD);
139
140const oldOpen = await opens(OLD);
141check('a crystal still in markdown opens as data, converted, not empty',
142 oldOpen.err === undefined && /Words a user typed years ago/.test(oldOpen.text || '')
143 && oldOpen.text.trim().startsWith('{'),
144 JSON.stringify(oldOpen).slice(0, 120));
145
146// The same Diamond with only its HISTORY in markdown, which is what a
147// half-finished conversion leaves behind: the file gone, the snapshots not.
148const OLDV = '0ldve5510n55';
149await p.evaluate(async (id) => {
150 await __put('diamonds/' + id + '/.daimond/meta.json', __meta('History only, in markdown'));
151 await __put('diamonds/' + id + '/versions/0003.md', '# Older still\n\nAnd these words too.\n');
152}, OLDV);
153
154const oldVerOpen = await opens(OLDV);
155check('and so does one whose only surviving crystal is a markdown snapshot',
156 oldVerOpen.err === undefined && /And these words too/.test(oldVerOpen.text || ''),
157 JSON.stringify(oldVerOpen).slice(0, 120));
158
159// ── An import that fails half way ───────────────────────────────────────
160//
161// `a.md/b.md` cannot be written once `a.md` is a file, so the import fails
162// after some of its files are down. What matters is which ones: with the
163// metadata written last the Diamond is invisible and the next pull brings the
164// whole of it back, and with it written first the Diamond lists, opens, and
165// throws — the exact state the checks above are about.
166
167const HALF = 'facefeed1234';
168const half = await p.evaluate(async (arg) => {
169 // Object key order is insertion order, and this one puts the metadata first
170 // on purpose: an export sorts its paths, and that is where `.daimond/` sorts.
171 const files = {};
172 files['.daimond/meta.json'] = JSON.stringify(
173 { name: 'Arrived half way', crystal_version: 0, updated: 1, touched: 1 });
174 files['a.md'] = 'a file';
175 files['a.md/b.md'] = 'a file inside a file, which cannot be written';
176 files['crystal.json'] = arg.crystal;
177 const pack = JSON.stringify({ id: arg.id, touched: 1, files: files });
178 const err = await __d.app.import_diamond(pack).then(() => null).catch((e) => String(e));
179 return { err };
180}, { id: HALF, crystal: json('the crystal that never arrived') });
181
182check('an import that cannot write every file fails out loud',
183 !!half.err, JSON.stringify(half.err || '').slice(0, 90));
184const halfRows = await listed();
185check('and a half-arrived Diamond is invisible, not listed and broken',
186 !halfRows.includes(HALF), halfRows.join(' '));
187
188// ── A whole import still lands ──────────────────────────────────────────
189//
190// The control. Without it, "never write the metadata" would pass everything
191// above and break importing outright.
192
193const WHOLE = 'add511feed99';
194await p.evaluate(async (arg) => {
195 const files = {};
196 files['.daimond/meta.json'] = JSON.stringify(
197 { name: 'Arrived whole', crystal_version: 0, updated: 2, touched: 2 });
198 files['crystal.json'] = arg.crystal;
199 await __d.app.import_diamond(JSON.stringify({ id: arg.id, touched: 2, files: files }));
200}, { id: WHOLE, crystal: json('the whole crystal') });
201
202const wholeRows = await listed();
203const wholeOpen = await opens(WHOLE);
204check('a whole import lists and opens, at the crystal it arrived with',
205 wholeRows.includes(WHOLE) && wholeOpen.text === json('the whole crystal'),
206 wholeRows.join(' ') + ' | ' + JSON.stringify(wholeOpen));
207
208// A resource the browser could not load is the dev stack, not the page: no
209// gateway runs here, so its probes answer 401 or 502 and neither is a throw.
210const noise = s.errs.filter((e) =>
211 !/favicon|ERR_ABORTED|net::ERR|Failed to load resource/i.test(e));
212check('the page threw nothing along the way', noise.length === 0, noise.slice(0, 3).join(' | '));
213
214await s.close();
215console.log('\n' + ok.length + ' ok, ' + bad.length + ' failed');
216process.exit(bad.length ? 1 : 0);