oxedyne/daimond/dev/verify_cappdelivery.mjs
22.8 KiB, 1 run
created by r2519314175:261, 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_cappdelivery.mjs — the guide's capp delivery button, and the update path |
| 2 | // that keeps what it delivered current. |
| 3 | // |
| 4 | // Not to be confused with `dev/verify_capp.mjs`, which is about what the app REFUSES |
| 5 | // an untrusted page. The guide's Capps page carries one button that asks the app for |
| 6 | // a furnished Diamond, and this drives that. What is asserted: |
| 7 | // |
| 8 | // * the ask is answered with a dialog in APP chrome, not silently obeyed; |
| 9 | // * saying yes leaves a Diamond called "Log Life" with the template's page in |
| 10 | // it, and opens it; |
| 11 | // * asking twice does not make a second one -- the entries are in the first; |
| 12 | // * a message from anywhere but the guide frame is ignored; |
| 13 | // * A NEWER SERVED TEMPLATE REACHES AN INSTANCE THAT HAS NOT BEEN TOUCHED, and |
| 14 | // `log/` is byte-identical afterwards; |
| 15 | // * A FILE THE USER HAS CHANGED IS LEFT ALONE while the rest updates, and they |
| 16 | // are told ONCE; |
| 17 | // * AN INSTANCE WITH NO DELIVERY RECORD IS NOT SILENTLY REWRITTEN -- it is asked |
| 18 | // about, and "no" means no. |
| 19 | // |
| 20 | // The template is CODE, so it gets fixes; the lanes are DATA the page rewrites, so |
| 21 | // they are the user's the moment he edits one. One rule covers both, and the rule is |
| 22 | // a measurement: a stored file whose SHA-256 still equals the hash recorded at |
| 23 | // delivery is one nobody has touched. See the "A capp, kept current" section of |
| 24 | // `www/js/daimond.js` and §11 of `dev/CAPP_CONTRACT.md`. |
| 25 | // |
| 26 | // EACH CHECK IS PROVED AGAINST BROKEN CODE FIRST. |
| 27 | // |
| 28 | // node dev/verify_cappdelivery.mjs --break stale # 5 fails: the version is never compared |
| 29 | // node dev/verify_cappdelivery.mjs --break clobber # 6 fails: the user's own file is overwritten |
| 30 | // node dev/verify_cappdelivery.mjs --break pushy # 7 fails: "no" rewrites the page anyway |
| 31 | // node dev/verify_cappdelivery.mjs --break logwritable # 5's log check fails: `log/` becomes writable |
| 32 | // node dev/verify_cappdelivery.mjs # and then, clean |
| 33 | // |
| 34 | // `logwritable` is why the served manifest in this file NAMES a path under `log/` |
| 35 | // and serves bytes for it. A guard nothing ever tries to cross is a guard that |
| 36 | // cannot be seen to hold: without a template that asks to write the user's entries, |
| 37 | // "the log is unchanged" would pass against an app with no log rule at all. |
| 38 | // |
| 39 | // It writes no path down: `harness.mjs` is imported relative to this file, and the |
| 40 | // profile and screenshot go to the harness scratch root — never into `www/`. |
| 41 | import fs from 'node:fs'; |
| 42 | import path from 'node:path'; |
| 43 | import { fileURLToPath } from 'node:url'; |
| 44 | import { open, connectMock, scratch } from './harness.mjs'; |
| 45 | |
| 46 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 47 | const WWW = path.join(HERE, '..', 'www'); |
| 48 | const TPL = path.join(WWW, 'capps', 'lifelog'); |
| 49 | |
| 50 | let failures = 0; |
| 51 | const check = (cond, msg, detail) => { |
| 52 | console.log((cond ? ' ok ' : ' FAIL ') + msg + (detail != null ? ' — ' + detail : '')); |
| 53 | if (!cond) failures++; |
| 54 | }; |
| 55 | |
| 56 | const BREAK = (() => { |
| 57 | const i = process.argv.indexOf('--break'); |
| 58 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 59 | })(); |
| 60 | |
| 61 | // Every break is a line of the APP's own guard, replaced in the file the browser is |
| 62 | // served. The anchor must appear exactly once or nothing was broken and the run |
| 63 | // below would prove nothing. |
| 64 | const BREAKS = { |
| 65 | // The served version is never compared, so no fix ever reaches an instance — |
| 66 | // which is the defect this whole path exists to close. |
| 67 | stale: { |
| 68 | file: 'js/daimond.js', |
| 69 | find: ' if (have >= man.v) return out; // current', |
| 70 | with: ' if (true) return out; // current', |
| 71 | }, |
| 72 | // The hash comparison goes, so a file the user has edited is overwritten by the |
| 73 | // template. The silent-overwrite failure, which is the expensive direction. |
| 74 | clobber: { |
| 75 | file: 'js/daimond.js', |
| 76 | find: ' if (!had || !files[rel] || had !== files[rel]) {', |
| 77 | with: ' if (false) {', |
| 78 | }, |
| 79 | // The answer to the legacy question is ignored, so an instance nothing is known |
| 80 | // about is rewritten whatever the person said. |
| 81 | pushy: { |
| 82 | file: 'js/daimond.js', |
| 83 | find: ' if (!go) { await writeCappRecord(id, { capp: key, offered: man.v }); return false; }', |
| 84 | with: ' if (!go) { await writeCappRecord(id, { capp: key, offered: man.v }); }', |
| 85 | }, |
| 86 | // `log/` becomes an ordinary template path, so a manifest that names one can lay |
| 87 | // bytes over the user's entries. |
| 88 | logwritable: { |
| 89 | file: 'js/daimond.js', |
| 90 | find: " return p === 'capp.json' || /^log(\\/|$)/.test(p);", |
| 91 | with: " return p === 'capp.json';", |
| 92 | }, |
| 93 | }; |
| 94 | |
| 95 | /// The real template on disk, which is what an unbroken run delivers at version 2. |
| 96 | const real = (rel) => fs.readFileSync(path.join(TPL, rel), 'utf8'); |
| 97 | |
| 98 | /// What the bundle SERVES, when this file wants it to be something else. |
| 99 | /// |
| 100 | /// `null` lets every request through to the dev server, which is how the delivery |
| 101 | /// half of this file runs against the shipped template. Setting it is how a new |
| 102 | /// build is put in front of a running app without a reload: the app re-fetches the |
| 103 | /// manifest on every open, deliberately and uncached, so that this is possible. |
| 104 | let plan = null; |
| 105 | |
| 106 | /// The manifest's file list. It NAMES A PATH UNDER `log/`, which the app must |
| 107 | /// refuse whatever else it does — see `logwritable` above. |
| 108 | const MANIFEST = [ |
| 109 | 'crystal.html', 'index.json', |
| 110 | 'lanes/diet.json', 'lanes/gym.json', 'lanes/body.json', |
| 111 | 'cat/diet.json', 'cat/gym.json', |
| 112 | 'log/gym/2026-08.jsonl', |
| 113 | ]; |
| 114 | |
| 115 | const s = await open({ |
| 116 | name: 'cappprobe', |
| 117 | profile: scratch('pw', 'cappprobe-' + process.pid), |
| 118 | route: async (page) => { |
| 119 | if (BREAK) { |
| 120 | const spec = BREAKS[BREAK]; |
| 121 | if (!spec) { console.error('no such break: ' + BREAK); process.exit(2); } |
| 122 | const src = fs.readFileSync(path.join(WWW, spec.file), 'utf8'); |
| 123 | const n = src.split(spec.find).length - 1; |
| 124 | if (n !== 1) { |
| 125 | console.error(`break '${BREAK}': the anchor appears ${n} times in ${spec.file}, ` |
| 126 | + 'so nothing was broken and the run below would prove nothing.'); |
| 127 | process.exit(2); |
| 128 | } |
| 129 | const body = src.replace(spec.find, spec.with); |
| 130 | await page.route('**/' + spec.file, (r) => r.fulfill({ |
| 131 | status: 200, contentType: 'application/javascript', body, |
| 132 | })); |
| 133 | } |
| 134 | await page.route('**/capps/lifelog/**', async (r) => { |
| 135 | if (!plan) return r.continue(); |
| 136 | const rel = new URL(r.request().url()).pathname.replace(/^.*\/capps\/lifelog\//, ''); |
| 137 | if (rel === 'capp.json') { |
| 138 | return r.fulfill({ |
| 139 | status: 200, contentType: 'application/json', |
| 140 | body: JSON.stringify({ v: plan.v, files: MANIFEST }), |
| 141 | }); |
| 142 | } |
| 143 | if (plan.files[rel] != null) { |
| 144 | return r.fulfill({ status: 200, contentType: 'text/plain', body: plan.files[rel] }); |
| 145 | } |
| 146 | return r.continue(); |
| 147 | }); |
| 148 | }, |
| 149 | }); |
| 150 | const p = s.page; |
| 151 | |
| 152 | /// The Diamonds as the store holds them, and what is inside one. |
| 153 | const diamonds = (p) => p.evaluate(async () => { |
| 154 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 155 | const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true); |
| 156 | try { return JSON.parse(await app.list_diamonds()); } catch (e) { return []; } |
| 157 | }); |
| 158 | |
| 159 | /// One stored file of an instance, or `null`. The page comes through the app's own |
| 160 | /// reader; everything else straight off the store, so what is asserted on is what is |
| 161 | /// on disk rather than what the app believes it wrote. |
| 162 | const stored = (id, rel) => p.evaluate(async ({ id, rel }) => { |
| 163 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 164 | if (rel === 'crystal.html') { |
| 165 | const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true); |
| 166 | try { return await app.read_crystal_page(id); } catch (e) { return null; } |
| 167 | } |
| 168 | try { return await m.store_read('diamonds/' + id + '/' + rel); } catch (e) { return null; } |
| 169 | }, { id, rel }); |
| 170 | |
| 171 | /// Write a file into an instance, as the user's page does through `save`. |
| 172 | const put = (id, rel, text) => p.evaluate(async ({ id, rel, text }) => { |
| 173 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 174 | await m.store_write('diamonds/' + id + '/' + rel, text); |
| 175 | return true; |
| 176 | }, { id, rel, text }); |
| 177 | |
| 178 | /// EVERYTHING under `log/`, path → contents. The user's entries: the one thing here |
| 179 | /// that cannot be re-derived from anything. |
| 180 | const logTree = (id) => p.evaluate(async (id) => { |
| 181 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 182 | const out = {}; |
| 183 | const walk = async (rel) => { |
| 184 | let lines = ''; |
| 185 | const dir = ('diamonds/' + id + '/' + rel).replace(/\/$/, ''); |
| 186 | try { lines = await m.store_list(dir); } catch (e) { return; } |
| 187 | for (const ln of String(lines).split('\n').filter(Boolean)) { |
| 188 | const bits = ln.split('\t'); |
| 189 | if (bits[1] === 'dir') { await walk(rel + bits[0] + '/'); continue; } |
| 190 | try { out[rel + bits[0]] = await m.store_read('diamonds/' + id + '/' + rel + bits[0]); } |
| 191 | catch (e) { out[rel + bits[0]] = 'UNREADABLE'; } |
| 192 | } |
| 193 | }; |
| 194 | await walk('log/'); |
| 195 | return out; |
| 196 | }, id); |
| 197 | |
| 198 | /// Put a file's TRUE hash into the delivery record, as though it had been |
| 199 | /// delivered. |
| 200 | /// |
| 201 | /// Used on a path under `log/`. It writes through the WASM DOOR, `store_write`, which |
| 202 | /// is what the app itself uses -- not through the page's `save`, which cannot reach |
| 203 | /// `capp.json` at all: `PAGE_NEVER_WRITES` covers it since 2026-08-17 |
| 204 | /// (`www/js/crystal.js:755`). This comment said the opposite until 2026-08-28, and |
| 205 | /// justified the fixture by that gap; the fixture never depended on it, but a reader |
| 206 | /// would have reasoned from a fence that had already been closed. |
| 207 | /// |
| 208 | /// What it stands in for now is the shape that is still reachable: a corrupted or a |
| 209 | /// half-migrated record, or one written by a build that predates the guard. |
| 210 | /// |
| 211 | /// It exists because without it the log assertions pass for the WRONG REASON. A log |
| 212 | /// file has no recorded hash, so the divergence rule already refuses to replace it |
| 213 | /// and the `log/` guard is never the thing being tested — running `--break |
| 214 | /// logwritable` against the first version of this file showed every log check still |
| 215 | /// green. Claiming the file puts the path refusal on its own, which is the only way |
| 216 | /// to see it hold. |
| 217 | const claimInRecord = (id, rel) => p.evaluate(async ({ id, rel }) => { |
| 218 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 219 | const body = await m.store_read('diamonds/' + id + '/' + rel); |
| 220 | const rec = JSON.parse(await m.store_read('diamonds/' + id + '/capp.json')); |
| 221 | rec.files[rel] = await DaimondCloud.sha256(body); |
| 222 | await m.store_write('diamonds/' + id + '/capp.json', JSON.stringify(rec)); |
| 223 | return rec.files[rel]; |
| 224 | }, { id, rel }); |
| 225 | |
| 226 | /// The delivery record beside an instance, parsed. |
| 227 | const record = (id) => p.evaluate(async (id) => { |
| 228 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 229 | let t = ''; |
| 230 | try { t = await m.store_read('diamonds/' + id + '/capp.json'); } catch (e) { return null; } |
| 231 | try { return t ? JSON.parse(t) : null; } catch (e) { return null; } |
| 232 | }, id); |
| 233 | |
| 234 | /// Answer whichever confirm box is on screen. |
| 235 | const answer = async (p, yes) => { |
| 236 | await p.waitForSelector('.dlg-card', { timeout: 8000 }); |
| 237 | const said = await p.evaluate(() => { |
| 238 | const c = [...document.querySelectorAll('.dlg-card')].filter(x => x.getClientRects().length).pop(); |
| 239 | return c ? (c.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 240) : ''; |
| 240 | }); |
| 241 | await p.evaluate((y) => { |
| 242 | const c = [...document.querySelectorAll('.dlg-card')].filter(x => x.getClientRects().length).pop(); |
| 243 | const b = c.querySelector(y ? '.dlg-ok' : '.dlg-cancel') || c.querySelector('.dlg-ok'); |
| 244 | b.click(); |
| 245 | }, yes); |
| 246 | await p.waitForTimeout(1500); |
| 247 | return said; |
| 248 | }; |
| 249 | |
| 250 | /// Press the guide's own button, inside the guide frame. |
| 251 | const pressInGuide = async (p) => { |
| 252 | const f = p.frames().find(fr => /guide\/capps\.html/.test(fr.url())); |
| 253 | if (!f) throw new Error('the guide frame is not showing capps.html'); |
| 254 | await f.click('#make-lifelog'); |
| 255 | }; |
| 256 | |
| 257 | /// OPEN THE CAPP, the way a person does: off its face and back onto it. Both |
| 258 | /// buttons go through `selectDiamond`, which is the one path to `renderCrystal` — |
| 259 | /// so this is the real open and not a poke at an internal. |
| 260 | const reopen = async () => { |
| 261 | await p.evaluate(() => { const b = document.getElementById('dview-chat'); if (b) b.click(); }); |
| 262 | await p.waitForTimeout(500); |
| 263 | await p.evaluate(() => { const b = document.getElementById('dview-crystal'); if (b) b.click(); }); |
| 264 | await p.waitForTimeout(1800); |
| 265 | }; |
| 266 | |
| 267 | /// The one quiet line that says which files were left as the user has them. |
| 268 | const noteText = () => p.evaluate(() => { |
| 269 | const els = [...document.querySelectorAll('#capp-note, .capp-note')]; |
| 270 | return { n: els.length, text: els.length ? (els[0].textContent || '').trim() : '' }; |
| 271 | }); |
| 272 | |
| 273 | try { |
| 274 | await connectMock(s); |
| 275 | await p.evaluate(() => DaimondWeb.guide('capps.html')); |
| 276 | await p.waitForTimeout(2500); |
| 277 | const framed = p.frames().some(f => /guide\/capps\.html/.test(f.url())); |
| 278 | check(framed, 'the guide is showing its Capps page'); |
| 279 | |
| 280 | // ── A stranger's message is not an instruction. |
| 281 | await p.evaluate(() => window.postMessage({ daimondGuide: 'make', what: 'lifelog' }, '*')); |
| 282 | await p.waitForTimeout(900); |
| 283 | check(!(await p.$('.dlg-card')), 'a message from the page itself is ignored'); |
| 284 | |
| 285 | // ── Refused. |
| 286 | await pressInGuide(p); |
| 287 | const said = await answer(p, false); |
| 288 | console.log(' dialog: ' + said); |
| 289 | check(/Log Life/.test(said), 'the ask is answered with a dialog naming what it will make'); |
| 290 | check(!(await diamonds(p)).some(d => d.name === 'Log Life'), 'saying no makes nothing'); |
| 291 | |
| 292 | // ── Accepted. |
| 293 | await pressInGuide(p); |
| 294 | await answer(p, true); |
| 295 | const made = (await diamonds(p)).filter(d => d.name === 'Log Life'); |
| 296 | check(made.length === 1, 'saying yes makes exactly one Log Life', made.length + ' found'); |
| 297 | |
| 298 | const id = made.length ? made[0].id : ''; |
| 299 | if (id) { |
| 300 | const inside = await p.evaluate(async (id) => { |
| 301 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 302 | const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true); |
| 303 | const out = {}; |
| 304 | try { out.page = (await app.read_crystal_page(id) || '').length; } catch (e) { out.page = 'threw: ' + e; } |
| 305 | try { out.data = (await app.read_crystal_data(id) || '').length; } catch (e) { out.data = 'threw: ' + e; } |
| 306 | return out; |
| 307 | }, id); |
| 308 | console.log(' inside: ' + JSON.stringify(inside)); |
| 309 | check(typeof inside.page === 'number' && inside.page > 500, 'with the template page in it', inside.page); |
| 310 | check(typeof inside.data === 'number' && inside.data > 0, |
| 311 | 'AND A CRYSTAL, without which the face never mounts the page', inside.data); |
| 312 | } |
| 313 | |
| 314 | // It is on screen, which is what "delivered" means. |
| 315 | const showing = await p.evaluate(() => ({ |
| 316 | name: (document.getElementById('current-session-name') || {}).textContent || '', |
| 317 | frame: !!document.querySelector('#crystal-frame-wrap'), |
| 318 | })); |
| 319 | console.log(' showing: ' + JSON.stringify(showing)); |
| 320 | check(/Log Life/.test(showing.name), 'and it is the Diamond on screen'); |
| 321 | check(showing.frame, 'with its page mounted'); |
| 322 | await p.waitForTimeout(1200); |
| 323 | await p.screenshot({ path: scratch('capp-made.png') }); |
| 324 | |
| 325 | // ── Asked again: the first one, not a second. |
| 326 | await p.evaluate(() => DaimondWeb.guide('capps.html')); |
| 327 | await p.waitForTimeout(2000); |
| 328 | await pressInGuide(p); |
| 329 | const again = await answer(p, true); |
| 330 | console.log(' second dialog: ' + again); |
| 331 | check(/already have/i.test(again), 'the second ask offers to OPEN the one that exists'); |
| 332 | check((await diamonds(p)).filter(d => d.name === 'Log Life').length === 1, |
| 333 | 'and there is still exactly one'); |
| 334 | |
| 335 | // ══ 4. The delivery left a record of exactly what went in ══════════ |
| 336 | // |
| 337 | // Without it nothing later can tell the template's bytes from the user's, and |
| 338 | // every update afterwards would have to guess or ask. |
| 339 | const rec0 = id ? await record(id) : null; |
| 340 | console.log(' record: ' + JSON.stringify(rec0 && { capp: rec0.capp, v: rec0.v, files: Object.keys(rec0.files || {}) })); |
| 341 | check(!!rec0 && rec0.capp === 'lifelog', 'delivery writes a record naming the template'); |
| 342 | check(!!rec0 && rec0.v === JSON.parse(real('capp.json')).v, |
| 343 | 'at the version the bundle serves', rec0 ? rec0.v : 'no record'); |
| 344 | check(!!rec0 && !!(rec0.files || {})['crystal.html'] && !!(rec0.files || {})['lanes/gym.json'], |
| 345 | 'with a hash per delivered file'); |
| 346 | check(!!rec0 && !(rec0.files || {})['crystal.json'], |
| 347 | 'and NOT for the seeded crystal, which came from no served file'); |
| 348 | |
| 349 | // ══ 5. A newer template reaches an instance nobody has touched ═════ |
| 350 | // |
| 351 | // And the user's entries do not move. Both halves matter: an update that also |
| 352 | // took the log would be a worse defect than the one this closes. |
| 353 | await put(id, 'log/gym/2026-08.jsonl', '{"t":"2026-08-01","lift":"squat","kg":100}\n'); |
| 354 | await put(id, 'log/diet/2026-08.jsonl', '{"t":"2026-08-01","food":"porridge"}\n'); |
| 355 | const logBefore = await logTree(id); |
| 356 | console.log(' log before: ' + JSON.stringify(Object.keys(logBefore))); |
| 357 | // The record now CLAIMS one of the user's log files as delivered bytes, so the |
| 358 | // only thing standing between the template and a year of entries is the `log/` |
| 359 | // refusal itself. See `claimInRecord`. |
| 360 | const claimed = await claimInRecord(id, 'log/gym/2026-08.jsonl'); |
| 361 | check(/^[0-9a-f]{64}$/.test(String(claimed)), |
| 362 | 'the record is made to claim a log file, so the path guard stands alone', claimed); |
| 363 | |
| 364 | const GYM3 = JSON.stringify(Object.assign(JSON.parse(real('lanes/gym.json')), { v3: true })); |
| 365 | plan = { |
| 366 | v: 3, |
| 367 | files: { |
| 368 | 'crystal.html': real('crystal.html') + '\n<!-- delivered v3 -->\n', |
| 369 | 'lanes/gym.json': GYM3, |
| 370 | // A template that asks to write the user's entries. It must not be able to. |
| 371 | 'log/gym/2026-08.jsonl': '{"t":"1999-01-01","lift":"OVERWRITTEN"}\n', |
| 372 | }, |
| 373 | }; |
| 374 | await reopen(); |
| 375 | |
| 376 | const page3 = await stored(id, 'crystal.html'); |
| 377 | check(/<!-- delivered v3 -->/.test(String(page3 || '')), |
| 378 | 'a newer served version replaces the page of an untouched instance'); |
| 379 | check(String(await stored(id, 'lanes/gym.json')) === GYM3, |
| 380 | 'and its seeded data, which nobody had edited'); |
| 381 | const rec3 = await record(id); |
| 382 | check(!!rec3 && rec3.v === 3, 'and the stored version moves with it', rec3 ? rec3.v : 'no record'); |
| 383 | const logAfter = await logTree(id); |
| 384 | check(JSON.stringify(logAfter) === JSON.stringify(logBefore), |
| 385 | 'THE LOG IS BYTE-IDENTICAL, though the manifest named a path inside it', |
| 386 | JSON.stringify(logAfter)); |
| 387 | const n3 = await noteText(); |
| 388 | check(n3.n === 0, 'and nothing was said, because nothing was left behind', n3.text); |
| 389 | |
| 390 | // ══ 6. A file the user has changed is left alone, and said so ONCE ══ |
| 391 | const MINE = JSON.stringify({ id: 'diet', mine: true, note: 'the user edited this' }); |
| 392 | await put(id, 'lanes/diet.json', MINE); |
| 393 | const GYM4 = JSON.stringify(Object.assign(JSON.parse(real('lanes/gym.json')), { v4: true })); |
| 394 | plan = { |
| 395 | v: 4, |
| 396 | files: { |
| 397 | 'crystal.html': real('crystal.html') + '\n<!-- delivered v4 -->\n', |
| 398 | 'lanes/gym.json': GYM4, |
| 399 | 'lanes/diet.json': JSON.stringify({ id: 'diet', fromTemplate: 'v4' }), |
| 400 | 'log/gym/2026-08.jsonl': '{"t":"1999-01-01","lift":"OVERWRITTEN"}\n', |
| 401 | }, |
| 402 | }; |
| 403 | await reopen(); |
| 404 | |
| 405 | check(String(await stored(id, 'lanes/diet.json')) === MINE, |
| 406 | 'A FILE THE USER CHANGED IS LEFT EXACTLY AS IT IS'); |
| 407 | check(String(await stored(id, 'lanes/gym.json')) === GYM4, |
| 408 | 'while the files nobody touched still update'); |
| 409 | check(/<!-- delivered v4 -->/.test(String(await stored(id, 'crystal.html'))), |
| 410 | 'including the page, which is code and not data'); |
| 411 | const n4 = await noteText(); |
| 412 | console.log(' note: ' + n4.text); |
| 413 | check(n4.n === 1, 'and the person is told ONCE, not once per file', n4.n + ' notes'); |
| 414 | check(/lanes\/diet\.json/.test(n4.text), 'naming what was kept', n4.text); |
| 415 | const rec4 = await record(id); |
| 416 | check(!!rec4 && rec4.v === 4, 'the version moves even though a file did not', rec4 ? rec4.v : 'no record'); |
| 417 | check(JSON.stringify(await logTree(id)) === JSON.stringify(logBefore), 'and the log is still untouched'); |
| 418 | |
| 419 | // ══ 7. An instance with NO record is not silently rewritten ════════ |
| 420 | // |
| 421 | // The owner's own Log Life is this case: made before capps carried a version, |
| 422 | // so nothing is known about what was delivered and no file can be shown to be |
| 423 | // ours. The only honest move is to ask. |
| 424 | const LEGACY = real('crystal.html') + '\n<!-- the user\'s own page -->\n'; |
| 425 | await p.evaluate(async ({ id, page }) => { |
| 426 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 427 | const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true); |
| 428 | await app.write_crystal_page(id, page); |
| 429 | // No record at all, which is what an instance made before this existed has. |
| 430 | try { |
| 431 | let dir = await DaimondCloud.opfsRoot(); |
| 432 | for (const seg of ['diamonds', id]) dir = await dir.getDirectoryHandle(seg); |
| 433 | await dir.removeEntry('capp.json'); |
| 434 | } catch (e) { await m.store_write('diamonds/' + id + '/capp.json', ''); } |
| 435 | }, { id, page: LEGACY }); |
| 436 | check(!(await record(id)), 'the instance now has no delivery record'); |
| 437 | |
| 438 | plan = { v: 5, files: { 'crystal.html': real('crystal.html') + '\n<!-- delivered v5 -->\n' } }; |
| 439 | await reopen(); |
| 440 | const asked = await p.$('.dlg-card'); |
| 441 | check(!!asked, 'a record-less instance is ASKED about rather than updated'); |
| 442 | check(String(await stored(id, 'crystal.html')) === LEGACY, |
| 443 | 'and nothing has been written while the question is on screen'); |
| 444 | const legacySaid = asked ? await answer(p, false) : ''; |
| 445 | console.log(' legacy dialog: ' + legacySaid); |
| 446 | check(/before capps carried a version/i.test(legacySaid), |
| 447 | 'the question says why it is being asked', legacySaid); |
| 448 | check(String(await stored(id, 'crystal.html')) === LEGACY, |
| 449 | 'SAYING NO LEAVES THE PAGE EXACTLY AS IT WAS'); |
| 450 | const rec5 = await record(id); |
| 451 | check(!!rec5 && rec5.offered === 5 && !rec5.files, |
| 452 | 'the refusal is remembered, at the version it was offered at', JSON.stringify(rec5)); |
| 453 | |
| 454 | await reopen(); |
| 455 | check(!(await p.$('.dlg-card')), 'and it is not asked again at the same version'); |
| 456 | check(String(await stored(id, 'crystal.html')) === LEGACY, 'nor rewritten behind the refusal'); |
| 457 | |
| 458 | // Taken, this time: the page moves, everything else stays, and the instance |
| 459 | // joins the automatic path. |
| 460 | plan = { v: 6, files: { 'crystal.html': real('crystal.html') + '\n<!-- delivered v6 -->\n' } }; |
| 461 | await reopen(); |
| 462 | const asked6 = await p.$('.dlg-card'); |
| 463 | check(!!asked6, 'something newer asks again'); |
| 464 | if (asked6) await answer(p, true); |
| 465 | check(/<!-- delivered v6 -->/.test(String(await stored(id, 'crystal.html'))), |
| 466 | 'and saying yes brings the page up to the current one'); |
| 467 | check(String(await stored(id, 'lanes/diet.json')) === MINE, |
| 468 | 'while the lanes it knows nothing about are left alone'); |
| 469 | check(JSON.stringify(await logTree(id)) === JSON.stringify(logBefore), |
| 470 | 'and so are the entries'); |
| 471 | const rec6 = await record(id); |
| 472 | check(!!rec6 && rec6.v === 6 && !!(rec6.files || {})['crystal.html'] |
| 473 | && !(rec6.files || {})['lanes/diet.json'], |
| 474 | 'a record is written claiming the PAGE only, so no later version overwrites a lane', |
| 475 | JSON.stringify(rec6 && { v: rec6.v, files: Object.keys(rec6.files || {}) })); |
| 476 | await p.screenshot({ path: scratch('capp-updated.png') }); |
| 477 | } catch (e) { |
| 478 | console.log(' FAIL threw — ' + (e && e.message)); |
| 479 | failures++; |
| 480 | } finally { |
| 481 | const errs = s.errs.filter(e => !/favicon|manifest|502|Bad Gateway|gateway/i.test(e)); |
| 482 | if (errs.length) console.log(' console errors: ' + errs.slice(0, 6).join(' | ')); |
| 483 | await s.close(); |
| 484 | } |
| 485 | console.log(failures ? failures + ' failure(s)' : 'all checks passed'); |
| 486 | process.exit(failures ? 1 : 0); |