oxedyne/daimond/dev/verify_crystaltheme.mjs
8.1 KiB, 1 run
created by r2519314175:337, 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_crystaltheme — a page written before the palette fix is brought up to date, |
| 2 | // and a page nobody recognises is not touched. |
| 3 | // |
| 4 | // WHY THIS EXISTS. `theme()` lives inside the crystal page, not in the app: a page is copied |
| 5 | // from the shipped default when a Diamond first renders and is the user's own thereafter. So |
| 6 | // fixing the default reached no existing Diamond, and every page in a real workspace still |
| 7 | // applied the app's palette with `setProperty` on `documentElement` -- an inline style, which |
| 8 | // beats the page's own `:root{--bg:#fff}` rule. A user asked a daimon for a calendar widget and |
| 9 | // a white background in the same turn and got the widget: markup edits stuck, colour edits were |
| 10 | // overwritten a message later. |
| 11 | // |
| 12 | // The migration substitutes ONE block, matched byte for byte. The two properties that matter are |
| 13 | // therefore: it MUST reach an old page, and it MUST NOT touch a page it does not recognise -- |
| 14 | // a page a model rewrote in its own style has widgets in it that no substitution should guess at. |
| 15 | // |
| 16 | // Each check is proved against the broken code before it is trusted: the mutation section at the |
| 17 | // end breaks the migration in the live page and requires the checks to go red. |
| 18 | import { open, shot } from './harness.mjs'; |
| 19 | |
| 20 | let ok = 0, bad = 0; |
| 21 | const check = (name, cond, detail) => { |
| 22 | if (cond) { ok++; console.log(` ok ${name}${detail ? ' — ' + detail : ''}`); } |
| 23 | else { bad++; console.log(` FAIL ${name}${detail ? ' — ' + detail : ''}`); } |
| 24 | }; |
| 25 | |
| 26 | const s = await open({ name: 'crystaltheme', signIn: true, connect: true, defaults: false }); |
| 27 | const { page } = s; |
| 28 | |
| 29 | // The exact block every page written before the fix carries. |
| 30 | const OLD = [ |
| 31 | 'function theme(t){if(!t)return;var m={bg:"--bg",surface:"--sf",text:"--tx",', |
| 32 | 'muted:"--mu",border:"--bd",accent:"--ac",accentText:"--at",font:"--fo",', |
| 33 | 'mono:"--mo",size:"--fs",radius:"--rd"};', |
| 34 | 'for(var k in m)if(m.hasOwnProperty(k)&&t[k])', |
| 35 | 'document.documentElement.style.setProperty(m[k],t[k]);}', |
| 36 | ].join('\n'); |
| 37 | |
| 38 | // ── The pure function, before any Diamond is involved ──────────────── |
| 39 | const pure = await page.evaluate((OLD) => { |
| 40 | const C = window.DaimondCrystal; |
| 41 | if (!C || !C.upgrade) return { missing: true }; |
| 42 | // A page as it was written before the fix. |
| 43 | const before = '<!doctype html><html><head><style>:root{--bg:#fff}</style></head>' |
| 44 | + '<body><select id="mine"><option>A</option></select><script>' + OLD + '<\/script></body></html>'; |
| 45 | const after = C.upgrade(before); |
| 46 | return { |
| 47 | missing: false, |
| 48 | changed: after !== null, |
| 49 | keptWidget: after ? after.indexOf('<select id="mine">') !== -1 : false, |
| 50 | oldGone: after ? after.indexOf('documentElement.style.setProperty') === -1 : false, |
| 51 | newThere: after ? after.indexOf('dc-theme') !== -1 : false, |
| 52 | // A page that does not carry the block, byte for byte. |
| 53 | strangerUntouched: C.upgrade('<!doctype html><html><body>hand written<\/body></html>') === null, |
| 54 | // The shipped default is already current, so it is not rewritten on every render. |
| 55 | defaultUntouched: C.upgrade(C.DEFAULT_PAGE) === null, |
| 56 | }; |
| 57 | }, OLD); |
| 58 | |
| 59 | check('the app exposes the page upgrade at all', !pure.missing); |
| 60 | check('an old page is recognised and changed', pure.changed === true); |
| 61 | check('THE WIDGETS SURVIVE — only the theme block is substituted', pure.keptWidget === true); |
| 62 | check('the inline-style theme is gone', pure.oldGone === true); |
| 63 | check('and the style-element theme is in its place', pure.newThere === true); |
| 64 | check('A PAGE IT DOES NOT RECOGNISE IS RETURNED UNTOUCHED', pure.strangerUntouched === true); |
| 65 | check('the current default is not rewritten on every render', pure.defaultUntouched === true); |
| 66 | |
| 67 | // ── End to end: an old page on a real Diamond is upgraded on disk ──── |
| 68 | // |
| 69 | // The Diamond is made through the UI so the rail opens it, exactly as a person would; then its |
| 70 | // page is replaced with a pre-fix one and the face re-entered. Creating it through the wasm API |
| 71 | // alone leaves the rail unaware of it, the face never mounts, and the checks below then report |
| 72 | // on a Diamond nobody opened -- which reads as a broken migration and is a broken test. |
| 73 | await page.click('#new-diamond-btn', { force: true }).catch(() => {}); |
| 74 | await page.waitForTimeout(900); |
| 75 | await page.fill('.dlg-input', 'ThemeMigrate').catch(() => {}); |
| 76 | await page.click('.dlg-ok', { force: true }).catch(() => {}); |
| 77 | await page.waitForTimeout(2500); |
| 78 | |
| 79 | const made = await page.evaluate(async (OLD) => { |
| 80 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 81 | const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true); |
| 82 | const d = JSON.parse(await app.list_diamonds()).find(x => x.name === 'ThemeMigrate'); |
| 83 | if (!d) return ''; |
| 84 | await app.run_tool('file_write', JSON.stringify({ |
| 85 | path: 'diamonds/' + d.id + '/crystal.json', |
| 86 | content: JSON.stringify({ title: 'Theme migrate', summary: 'An old page.' }), |
| 87 | })); |
| 88 | const old = '<!doctype html><html><head><style>:root{--bg:#ffffff;--tx:#111}' |
| 89 | + 'html,body{background:var(--bg);color:var(--tx)}</style></head><body>' |
| 90 | + '<select id="mine"><option>A</option></select><div id="r"></div><script>' |
| 91 | + 'var D={};' + OLD |
| 92 | + 'function post(o){o.dc=1;o.v=1;parent.postMessage(o,"*");}' |
| 93 | + 'addEventListener("message",function(e){var m2=e.data||{};' |
| 94 | + 'if(m2.cmd==="data"){D=m2.data||{};theme(D._theme);' |
| 95 | + 'post({cmd:"rendered",keys:Object.keys(D).filter(function(k){return k[0]!=="_";})});' |
| 96 | + 'post({cmd:"height",px:200});}});post({cmd:"ready"});' |
| 97 | + '<\/script></body></html>'; |
| 98 | await app.run_tool('file_write', JSON.stringify({ |
| 99 | path: 'diamonds/' + d.id + '/crystal.html', content: old, |
| 100 | })); |
| 101 | return d.id; |
| 102 | }, OLD); |
| 103 | check('a Diamond was made carrying a pre-fix page', !!made); |
| 104 | |
| 105 | // Leave the face and come back, which is what re-reads the page from disk. |
| 106 | await page.evaluate(() => { const c = document.getElementById('dview-chat'); if (c) c.click(); }); |
| 107 | await page.waitForTimeout(1200); |
| 108 | await page.evaluate(() => { const c = document.getElementById('dview-crystal'); if (c) c.click(); }); |
| 109 | await page.waitForTimeout(3000); |
| 110 | |
| 111 | const after = await page.evaluate(async (id) => { |
| 112 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 113 | const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true); |
| 114 | const html = await app.run_tool('file_read', JSON.stringify({ |
| 115 | path: 'diamonds/' + id + '/crystal.html' })); |
| 116 | return { |
| 117 | onDisk: String(html), |
| 118 | mode: window.DaimondCrystal ? DaimondCrystal._state().mode : '?', |
| 119 | }; |
| 120 | }, made); |
| 121 | |
| 122 | check('OPENING THE DIAMOND REWROTE ITS PAGE ON DISK', |
| 123 | after.onDisk.indexOf('dc-theme') !== -1, |
| 124 | after.onDisk.indexOf('dc-theme') === -1 ? 'still the old theme' : 'upgraded'); |
| 125 | check('and the widget in it survived the upgrade', |
| 126 | after.onDisk.indexOf('<select id="mine">') !== -1); |
| 127 | check('the page still runs after being rewritten', after.mode === 'frame', after.mode); |
| 128 | |
| 129 | await shot(s, 'crystaltheme-upgraded'); |
| 130 | |
| 131 | // ── Proving the checks against broken code ─────────────────────────── |
| 132 | console.log('\n--- self-test: break the migration and require the checks to notice'); |
| 133 | const broke = await page.evaluate(() => { |
| 134 | const C = window.DaimondCrystal; |
| 135 | if (!C) return false; |
| 136 | C.__realUpgrade = C.upgrade; |
| 137 | // The commonest way to get this wrong: rewrite the whole page instead of one block. |
| 138 | Object.defineProperty(C, 'upgrade', { value: () => C.DEFAULT_PAGE, configurable: true }); |
| 139 | return true; |
| 140 | }); |
| 141 | if (broke) { |
| 142 | const mutated = await page.evaluate(() => { |
| 143 | const C = window.DaimondCrystal; |
| 144 | const after = C.upgrade('<html><body><select id="mine"></select></body></html>'); |
| 145 | return { keptWidget: after ? after.indexOf('<select id="mine">') !== -1 : false }; |
| 146 | }); |
| 147 | check('[self-test] a migration that rewrites the whole page LOSES the widget, and is caught', |
| 148 | mutated.keptWidget === false); |
| 149 | await page.evaluate(() => { |
| 150 | const C = window.DaimondCrystal; |
| 151 | Object.defineProperty(C, 'upgrade', { value: C.__realUpgrade, configurable: true }); |
| 152 | }); |
| 153 | const restored = await page.evaluate(() => { |
| 154 | const C = window.DaimondCrystal; |
| 155 | return C.upgrade('<html><body>nothing to do</body></html>') === null; |
| 156 | }); |
| 157 | check('[self-test] and the real one is quiet again once restored', restored === true); |
| 158 | } |
| 159 | |
| 160 | console.log(`\n${ok} ok, ${bad} failed`); |
| 161 | await s.browser.close(); |
| 162 | process.exit(bad ? 1 : 0); |