Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_diamondroot.mjs

14.3 KiB, 1 run

created by r2519314175:363, 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_diamondroot.mjs — a workspace written before the Focus → Diamond rename must open whole.
2//
3// The rename moved the root every pursuit lives under: `foci/` → `diamonds/`. That is the whole
4// store, not a corner of it, so getting it wrong loses everything at once — `list_dir("diamonds")`
5// finds nothing, the rail comes up empty, and a user with a year of crystals sees a new install.
6// It fails silently, too: no error, just an empty list, which is exactly how a user would fail
7// to notice until their work was gone.
8//
9// The seed here is the OLDEST shape a real workspace can have: `foci/<id>/.red/`, which needs
10// BOTH migrations, in order — the root move first, then the per-Diamond store move. That ordering
11// is the part worth pinning, because the store migration rewrites log paths that the root move
12// has already rewritten once.
13//
14// Run with dev/serve.mjs up. No gateway needed; nothing here talks to one.
15import { open, signInAs } from './harness.mjs';
16
17const ok = [], bad = [];
18const check = (name, pass, detail) => {
19 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
20 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
21};
22
23const ID = 'ancient1';
24
25const s = await open({ name: 'diamondroot', connect: false });
26const p = s.page;
27await p.waitForTimeout(1200);
28
29// ── Seed a pre-rename workspace, and the two localStorage keys with it ──
30const seeded = await p.evaluate(async ({ id }) => {
31 const mod = await import('../pkg/oxedyne_daimond.js');
32 const w = (path, content) => mod.write_file(path, content);
33
34 // Everything under the OLD root, with the OLD store name inside it.
35 await w(`foci/${id}/brief.md`, '# An ancient pursuit\n\nWritten before either rename.\n');
36 await w(`foci/${id}/versions/0000.md`, '');
37 await w(`foci/${id}/versions/0001.md`, '# An ancient pursuit\n\nWritten before either rename.\n');
38 await w(`foci/${id}/.red/meta.json`,
39 '{"name":"Ancient pursuit","brief_version":1,"updated":1740000000000}');
40 await w(`foci/${id}/.red/deltas/0001.md`, 'ANCIENT-DELTA: what that fold consumed.');
41 // The fold's delta_ref is an absolute path into the OLD root AND the OLD store.
42 await w(`foci/${id}/.red/log`,
43 '{"id":"a1","ts":1740000000000,"kind":"create","agent":"user","task":"create focus",'
44 + '"parent_brief_version":-1,"brief_version":0,"delta_ref":"","note":"Ancient pursuit"}\n'
45 + '{"id":"a2","ts":1740000000001,"kind":"fold","agent":"reducer","task":"fold delta",'
46 + '"parent_brief_version":0,"brief_version":1,'
47 + `"delta_ref":"foci/${id}/.red/deltas/0001.md","note":"folded long ago"}\n`);
48
49 // The keys a long-standing user holds, under their pre-rename names.
50 localStorage.setItem('daimond-focus-counter', '7');
51 localStorage.setItem('daimond-focus-models', '{"someid":{"provider":"p","model":"m"}}');
52 return true;
53}, { id: ID });
54check('a pre-rename workspace was seeded under foci/', seeded === true);
55
56// ── Boot it the way a user does ─────────────────────────────────────────
57await p.reload({ waitUntil: 'domcontentloaded' });
58await signInAs(s, 'diamondroot');
59await p.waitForTimeout(2500);
60
61// ── What the user sees ──────────────────────────────────────────────────
62const listed = await p.$eval('#diamond-list', e => e.textContent);
63check('the Diamond survived the root move', /Ancient pursuit/.test(listed), listed.trim().slice(0, 60));
64
65// ── What is on disk ─────────────────────────────────────────────────────
66const disk = await p.evaluate(async ({ id }) => {
67 const mod = await import('../pkg/oxedyne_daimond.js');
68 const read = async (path) => {
69 try { return await mod.read_file(path); } catch (e) { return null; }
70 };
71 return {
72 meta: await read(`diamonds/${id}/.daimond/meta.json`),
73 log: await read(`diamonds/${id}/.daimond/log`),
74 delta: await read(`diamonds/${id}/.daimond/deltas/0001.md`),
75 crystal: await read(`diamonds/${id}/crystal.json`),
76 version: await read(`diamonds/${id}/versions/0001.md`),
77 oldCrystal: await read(`foci/${id}/brief.md`),
78 staleCrystal: await read(`diamonds/${id}/brief.md`),
79 staleMd: await read(`diamonds/${id}/crystal.md`),
80 oldMeta: await read(`foci/${id}/.red/meta.json`),
81 oldDelta: await read(`foci/${id}/.red/deltas/0001.md`),
82 };
83}, { id: ID });
84
85check('the store landed at diamonds/<id>/.daimond/',
86 !!disk.meta && /Ancient pursuit/.test(disk.meta));
87// Two hops now, and they are different kinds of hop. `brief.md` -> `crystal.md`
88// is a RENAME and deletes what it moved. `crystal.md` -> `crystal.json` is a
89// CONVERSION and keeps its source: the round-trip self-check proves the bytes and
90// structurally cannot prove the structure, so the markdown stays until the
91// conversion has run against real workspaces. `brief.md` must still be gone --
92// nothing converts from it any more, so a leftover would be a second and older
93// answer nobody reads.
94check('brief.md became crystal.json, and brief.md itself is gone',
95 /An ancient pursuit/.test(disk.crystal || '') && disk.staleCrystal === null,
96 'brief.md=' + disk.staleCrystal);
97check('and the intermediate crystal.md is kept, because the conversion is not proven',
98 disk.staleMd !== null, 'crystal.md=' + disk.staleMd);
99check('a meta.json still saying brief_version reports the right version, not 0',
100 /"crystal_version":1|"brief_version":1/.test(disk.meta || ''), (disk.meta || '').slice(0, 70));
101check('the crystal and its markdown snapshots came across',
102 /An ancient pursuit/.test(disk.crystal || '') && /An ancient pursuit/.test(disk.version || ''));
103check('the retained delta came across',
104 disk.delta === 'ANCIENT-DELTA: what that fold consumed.');
105check('nothing was left behind under foci/',
106 disk.oldCrystal === null && disk.oldMeta === null && disk.oldDelta === null);
107
108// The check a directory move alone would fail, twice over: the log points at the delta BY PATH,
109// and that path named both the old root and the old store.
110check('the log’s delta_ref was rewritten through BOTH migrations',
111 !!disk.log && disk.log.includes(`diamonds/${ID}/.daimond/deltas/0001.md`)
112 && !disk.log.includes('foci/') && !disk.log.includes('.red/'),
113 (disk.log || '').split('\n')[1]?.slice(0, 84));
114
115// ── The localStorage keys a long-standing user holds ────────────────────
116const keys = await p.evaluate(() => ({
117 counter: localStorage.getItem('daimond-diamond-counter'),
118 models: localStorage.getItem('daimond-diamond-models'),
119 oldCounter: localStorage.getItem('daimond-focus-counter'),
120 oldModels: localStorage.getItem('daimond-focus-models'),
121}));
122check('the Diamond counter carried over', keys.counter === '7', String(keys.counter));
123check('the per-Diamond model choices carried over',
124 /someid/.test(keys.models || ''), (keys.models || '').slice(0, 40));
125check('the old keys were dropped, not left to rot',
126 keys.oldCounter === null && keys.oldModels === null);
127
128// ── The history the user opens, and the delta they click ────────────────
129const history = await p.evaluate(async ({ id }) => {
130 const mod = await import('../pkg/oxedyne_daimond.js');
131 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
132 const recs = JSON.parse(await app.log_read(id) || '[]');
133 const fold = recs.find(r => r.kind === 'fold');
134 if (!fold || !fold.delta_ref) return { recs: recs.length, delta: null };
135 return { recs: recs.length, ref: fold.delta_ref,
136 delta: await app.run_tool('file_read', JSON.stringify({ path: fold.delta_ref })) };
137}, { id: ID });
138check('the history reads back whole', history.recs === 2, history.recs + ' records');
139check('a fold made before either rename can still show its delta',
140 /ANCIENT-DELTA/.test(history.delta || ''), history.ref);
141
142// ── Idempotence: a second boot must not undo the first ──────────────────
143await p.reload({ waitUntil: 'domcontentloaded' });
144await signInAs(s, 'diamondroot');
145await p.waitForTimeout(2500);
146const again = await p.$eval('#diamond-list', e => e.textContent);
147check('a second boot leaves it alone', /Ancient pursuit/.test(again), again.trim().slice(0, 60));
148
149// ── Both roots present: MERGE what does not collide ─────────────────────
150//
151// This used to assert the opposite — "the old one is left alone rather than
152// merged" — and that refusal was the hazard, not the safeguard. `diamonds/`
153// exists the moment anyone creates a single Diamond, so a `foci/` arriving
154// afterwards (a restored backup, a sync from an older device, a folder adopted
155// later) went into a directory nothing reads and stayed there for ever. Moving
156// an id that is not already present overwrites nothing, so it is strictly safer
157// than leaving it unreachable.
158const merged = await p.evaluate(async () => {
159 const mod = await import('../pkg/oxedyne_daimond.js');
160 // An old root alongside the new one, holding a DIFFERENT Diamond, the way a
161 // restored backup arrives.
162 await mod.write_file('foci/latecomer/crystal.md', '# a pursuit that arrived afterwards\n');
163 await mod.write_file('foci/latecomer/.daimond/meta.json',
164 '{"name":"Latecomer","brief_version":0,"updated":1}');
165 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
166 const listed = await app.list_diamonds(); // triggers migrate_root again
167 const read = async (path) => { try { return await mod.read_file(path); } catch (e) { return null; } };
168 return {
169 listed,
170 // The root move lands it under `diamonds/`, and the same `list()` pass then
171 // converts it, so this is where it comes to rest -- not `crystal.md`.
172 moved: await read('diamonds/latecomer/crystal.json'),
173 leftOld: await read('foci/latecomer/crystal.md'),
174 // The original Diamond of this file, which must not have moved or changed.
175 ancient: await read('diamonds/ancient1/crystal.json'),
176 };
177});
178check('a Diamond that arrives in an old root AFTERWARDS is taken into diamonds/',
179 /a pursuit that arrived afterwards/.test(merged.moved || ''), String(merged.moved).slice(0, 60));
180check('and it is in the list, so the user can actually see it',
181 /Latecomer/.test(merged.listed), merged.listed.slice(0, 120));
182check('the emptied old root is gone, not left as a second place to look',
183 merged.leftOld === null, String(merged.leftOld));
184check('and the Diamond already in diamonds/ was not disturbed',
185 /An ancient pursuit/.test(merged.ancient || ''), String(merged.ancient).slice(0, 40));
186
187// ── A genuine id collision is the one case the merge does NOT attempt ───
188//
189// Which copy is the user's current work is not answerable from inside the
190// migration, and overwriting the one they can see with one they cannot is the
191// loss the whole function exists to avoid. So it stays where it is, and
192// `legacy_root_waiting` keeps saying so.
193// A collision is planted ALONGSIDE a clean entry, and that pairing is the point.
194//
195// `move_entry` refuses to clobber on its own, so a merge that simply tried every
196// entry would still overwrite nothing — but it would THROW on the collision and
197// abandon the walk, stranding whatever had not been reached. Recognising the
198// collision by name and stepping over it is what turns a failure into an
199// outcome. Measured as such: the migration must complete WITHOUT ERROR. That is
200// order-independent, where "the entry behind it still arrived" is not — OPFS
201// does not promise the order a directory iterates in, so the stranded entry may
202// or may not be the one this file happened to plant second.
203const logsBefore = s.logs.length;
204const collide = await p.evaluate(async () => {
205 const mod = await import('../pkg/oxedyne_daimond.js');
206 // The SAME id as the Diamond already in diamonds/, with different content...
207 await mod.write_file('foci/ancient1/crystal.md', '# a DIFFERENT pursuit under the same id\n');
208 await mod.write_file('foci/ancient1/.daimond/meta.json',
209 '{"name":"Impostor","brief_version":0,"updated":1}');
210 // ...and a clean one that sorts AFTER it, so a walk that stops on the
211 // collision never reaches it.
212 await mod.write_file('foci/zzlater/crystal.md', '# behind the collision in the walk\n');
213 await mod.write_file('foci/zzlater/.daimond/meta.json',
214 '{"name":"Behind it","brief_version":0,"updated":1}');
215 const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true);
216 const listed = await app.list_diamonds();
217 const read = async (path) => { try { return await mod.read_file(path); } catch (e) { return null; } };
218 return {
219 listed,
220 current: await read('diamonds/ancient1/crystal.json'),
221 // Untouched in the old root, so still the markdown it was written as.
222 old: await read('foci/ancient1/crystal.md'),
223 behind: await read('diamonds/zzlater/crystal.json'),
224 waiting: await app.legacy_diamond_root_waiting(),
225 };
226});
227check('a colliding id does NOT overwrite the copy the user can see',
228 /An ancient pursuit/.test(collide.current || ''), String(collide.current).slice(0, 40));
229check('and the old copy is still there, not deleted out from under them',
230 collide.old === '# a DIFFERENT pursuit under the same id\n', String(collide.old).slice(0, 50));
231check('the clean entry beside it still came across',
232 /behind the collision in the walk/.test(collide.behind || ''), String(collide.behind));
233{
234 // A COLLISION IS AN OUTCOME, NOT A FAILURE. If the walk throws on it instead
235 // of stepping over it, everything it had not yet reached is stranded — and
236 // which entries those are is down to an iteration order nothing promises.
237 const failed = s.logs.slice(logsBefore)
238 .filter((l) => /root could not be migrated/i.test(l));
239 check('and the migration completed without erroring out of the walk',
240 failed.length === 0, failed.length ? failed[0].slice(0, 160) : 'no migration error logged');
241}
242check('and the store says an older root is still waiting, so nothing seeds over it',
243 collide.waiting === true, String(collide.waiting));
244check('the rail shows one of them, not two rows for one id',
245 (collide.listed.match(/"id":"ancient1"/g) || []).length === 1,
246 collide.listed.slice(0, 120));
247
248await s.close();
249
250console.log(`\n${ok.length} passed, ${bad.length} failed`);
251if (bad.length) console.log('FAILED:\n ' + bad.join('\n '));
252process.exit(bad.length ? 1 : 0);