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. |
| 15 | import { open, signInAs } from './harness.mjs'; |
| 16 | |
| 17 | const ok = [], bad = []; |
| 18 | const check = (name, pass, detail) => { |
| 19 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 20 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 21 | }; |
| 22 | |
| 23 | const ID = 'ancient1'; |
| 24 | |
| 25 | const s = await open({ name: 'diamondroot', connect: false }); |
| 26 | const p = s.page; |
| 27 | await p.waitForTimeout(1200); |
| 28 | |
| 29 | // ── Seed a pre-rename workspace, and the two localStorage keys with it ── |
| 30 | const 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 }); |
| 54 | check('a pre-rename workspace was seeded under foci/', seeded === true); |
| 55 | |
| 56 | // ── Boot it the way a user does ───────────────────────────────────────── |
| 57 | await p.reload({ waitUntil: 'domcontentloaded' }); |
| 58 | await signInAs(s, 'diamondroot'); |
| 59 | await p.waitForTimeout(2500); |
| 60 | |
| 61 | // ── What the user sees ────────────────────────────────────────────────── |
| 62 | const listed = await p.$eval('#diamond-list', e => e.textContent); |
| 63 | check('the Diamond survived the root move', /Ancient pursuit/.test(listed), listed.trim().slice(0, 60)); |
| 64 | |
| 65 | // ── What is on disk ───────────────────────────────────────────────────── |
| 66 | const 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 | |
| 85 | check('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. |
| 94 | check('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); |
| 97 | check('and the intermediate crystal.md is kept, because the conversion is not proven', |
| 98 | disk.staleMd !== null, 'crystal.md=' + disk.staleMd); |
| 99 | check('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)); |
| 101 | check('the crystal and its markdown snapshots came across', |
| 102 | /An ancient pursuit/.test(disk.crystal || '') && /An ancient pursuit/.test(disk.version || '')); |
| 103 | check('the retained delta came across', |
| 104 | disk.delta === 'ANCIENT-DELTA: what that fold consumed.'); |
| 105 | check('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. |
| 110 | check('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 ──────────────────── |
| 116 | const 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 | })); |
| 122 | check('the Diamond counter carried over', keys.counter === '7', String(keys.counter)); |
| 123 | check('the per-Diamond model choices carried over', |
| 124 | /someid/.test(keys.models || ''), (keys.models || '').slice(0, 40)); |
| 125 | check('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 ──────────────── |
| 129 | const 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 }); |
| 138 | check('the history reads back whole', history.recs === 2, history.recs + ' records'); |
| 139 | check('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 ────────────────── |
| 143 | await p.reload({ waitUntil: 'domcontentloaded' }); |
| 144 | await signInAs(s, 'diamondroot'); |
| 145 | await p.waitForTimeout(2500); |
| 146 | const again = await p.$eval('#diamond-list', e => e.textContent); |
| 147 | check('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. |
| 158 | const 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 | }); |
| 178 | check('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)); |
| 180 | check('and it is in the list, so the user can actually see it', |
| 181 | /Latecomer/.test(merged.listed), merged.listed.slice(0, 120)); |
| 182 | check('the emptied old root is gone, not left as a second place to look', |
| 183 | merged.leftOld === null, String(merged.leftOld)); |
| 184 | check('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. |
| 203 | const logsBefore = s.logs.length; |
| 204 | const 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 | }); |
| 227 | check('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)); |
| 229 | check('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)); |
| 231 | check('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 | } |
| 242 | check('and the store says an older root is still waiting, so nothing seeds over it', |
| 243 | collide.waiting === true, String(collide.waiting)); |
| 244 | check('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 | |
| 248 | await s.close(); |
| 249 | |
| 250 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 251 | if (bad.length) console.log('FAILED:\n ' + bad.join('\n ')); |
| 252 | process.exit(bad.length ? 1 : 0); |