Oregami
Repositories/oxedyne/daimond

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.
18import { open, shot } from './harness.mjs';
19
20let ok = 0, bad = 0;
21const 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
26const s = await open({ name: 'crystaltheme', signIn: true, connect: true, defaults: false });
27const { page } = s;
28
29// The exact block every page written before the fix carries.
30const 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 ────────────────
39const 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
59check('the app exposes the page upgrade at all', !pure.missing);
60check('an old page is recognised and changed', pure.changed === true);
61check('THE WIDGETS SURVIVE — only the theme block is substituted', pure.keptWidget === true);
62check('the inline-style theme is gone', pure.oldGone === true);
63check('and the style-element theme is in its place', pure.newThere === true);
64check('A PAGE IT DOES NOT RECOGNISE IS RETURNED UNTOUCHED', pure.strangerUntouched === true);
65check('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.
73await page.click('#new-diamond-btn', { force: true }).catch(() => {});
74await page.waitForTimeout(900);
75await page.fill('.dlg-input', 'ThemeMigrate').catch(() => {});
76await page.click('.dlg-ok', { force: true }).catch(() => {});
77await page.waitForTimeout(2500);
78
79const 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);
103check('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.
106await page.evaluate(() => { const c = document.getElementById('dview-chat'); if (c) c.click(); });
107await page.waitForTimeout(1200);
108await page.evaluate(() => { const c = document.getElementById('dview-crystal'); if (c) c.click(); });
109await page.waitForTimeout(3000);
110
111const 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
122check('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');
125check('and the widget in it survived the upgrade',
126 after.onDisk.indexOf('<select id="mine">') !== -1);
127check('the page still runs after being rewritten', after.mode === 'frame', after.mode);
128
129await shot(s, 'crystaltheme-upgraded');
130
131// ── Proving the checks against broken code ───────────────────────────
132console.log('\n--- self-test: break the migration and require the checks to notice');
133const 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});
141if (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
160console.log(`\n${ok} ok, ${bad} failed`);
161await s.browser.close();
162process.exit(bad ? 1 : 0);