Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_diamondmodels.mjs

40.1 KiB, 45 runs

created by r2519314175:361, 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_diamondmodels.mjs — phase D: the models a Diamond runs on, and the fold.
2//
3// Five properties, each one a thing notes2 asks for and none of which could be
4// seen from the app before this phase:
5//
6// 1. A Diamond SHOWS the model it thinks with. It has been stored since
7// Diamonds had models and drawn nowhere, so two Diamonds deliberately put
8// on different models were indistinguishable on the rail.
9// 2. Changing the daimon's model is a deliberate two-step: the pulldown reveals
10// a "Change" button ONLY while the pick differs, and pressing it takes effect,
11// is written into the crystal's own version history — the discontinuity notes2
12// calls "a fold and a new daimon" — and LEAVES THE CONVERSATION WHERE IT IS.
13// There is no confirm, and the absence is the property: the modal that stood
14// here warned that the thread would not carry over, which was true for the
15// twenty-two minutes before a daimon had a thread at all. So the two halves are
16// checked together, because either one alone can be satisfied by the wrong
17// thing: a confirm nobody answers also leaves the session intact, and a silent
18// handler that empties it also asks nothing.
19// 3. The secondary is a map keyed by modality. A task naming an image is
20// dispatched on the vision model; a task naming none is not. Measured from
21// the RUN, which is what carries the model and the key, rather than from
22// the pulldown that was set.
23// 4. The context meter says where the fold happens. The number is `FOLD_AT`
24// in compact.rs and has never been anywhere a user could see it.
25// 5. The fold can be asked for, and reports honestly when there is nothing to
26// fold. That a hand fold does not SHRINK the window is proved in Rust, where
27// the window lives — `learn_from_refusal` moves it down and never up, so a
28// hand fold routed through the refusal arm would cost a quarter of the
29// context every three presses, and no screenshot of a meter can tell a
30// window that shrank from a conversation that grew.
31//
32// node dev/verify_diamondmodels.mjs
33// node dev/verify_diamondmodels.mjs --break novision # dispatch ignores modality
34// node dev/verify_diamondmodels.mjs --break nomark # the meter loses the fold mark
35// node dev/verify_diamondmodels.mjs --break wipes # the model change empties the session
36// node dev/verify_diamondmodels.mjs --break asks # the confirm comes back
37// node dev/verify_diamondmodels.mjs --break appguard # the fold asks for an engine again
38// node dev/verify_diamondmodels.mjs --break refuses # the fold refuses before it asks
39//
40// `wipes` and `asks` are SOURCE breaks: the handler they target is a closure inside
41// daimond.js and cannot be reached from the page, so the file is patched in memory
42// and served through `page.route` — the same trick verify_daimonface.mjs uses. Each
43// one asserts its anchor lands exactly once; an anchor that has drifted is a break
44// that quietly stopped applying, and a green run under it would prove nothing.
45//
46// Needs dev/serve.mjs and dev/mockllm.mjs (dev/world.sh N --up gives both).
47import fs from 'node:fs';
48import path from 'node:path';
49import os from 'node:os';
50import { fileURLToPath } from 'node:url';
51import { open, newChat, connectMock, scratch, shot, signInAs, steerDiamond, storedChats } from './harness.mjs';
52
53// The mock offers three models, and exactly one of them is in the price TABLE with
54// a context window. That is the one this run connects under, because `mock/fast`
55// has no published window: the meter is then not drawn at all, and the checks on
56// the fold mark would pass vacuously against a meter that is not there.
57const MODEL = 'accounts/fireworks/models/glm-5p2';
58
59const OUT = path.join(os.homedir(), '.cache/daimond/diamondmodels-shots');
60fs.mkdirSync(OUT, { recursive: true });
61
62// `--break x` or `--break=x`. The index form has to be guarded: `indexOf` of an
63// absent flag is -1, and -1 + 1 is 0, which is the path to node.
64const BI = process.argv.indexOf('--break');
65const BEQ = process.argv.find(a => a.startsWith('--break='));
66const BREAK = BEQ ? BEQ.split('=')[1] : (BI >= 0 ? (process.argv[BI + 1] || '') : '');
67
68// ── The two source breaks ────────────────────────────────────────────
69//
70// Both live in the daimon's model-change handler, which is a closure: nothing the
71// page can reach from outside gets at it, so the only honest way to break it is to
72// serve a different file.
73const HERE = path.dirname(fileURLToPath(import.meta.url));
74const SRC = 'js/daimond.js';
75const SRC_BREAKS = {
76 // THE LOSS THE OLD CONFIRM WARNED ABOUT, made real: the conversation discarded as
77 // the model moves. Both halves of it, because the two are stored separately and a
78 // break that took only one would leave the other check green on a conversation
79 // half gone -- the model's own copy, which is what the next turn carries, and the
80 // transcript the reader is looking at.
81 wipes: {
82 find: "\t\t\tresetDiamondApps();\n\t\t\tvar logged = true;",
83 with: "\t\t\tresetDiamondApps();\n"
84 + "\t\t\tvar _lost = (chats || []).find(function (c) { return c.diamondId === opts.id; });\n"
85 + "\t\t\tif (_lost) {\n"
86 + "\t\t\t\t_lost.messages = [];\n"
87 + "\t\t\t\t_lost.session = { v: 1, msgs: [], upto: '', uptoTs: 0 };\n"
88 + "\t\t\t\tpersistChats();\n"
89 + "\t\t\t\tif (current && current.id === _lost.id) renderHistory(_lost.messages);\n"
90 + "\t\t\t}\n"
91 + "\t\t\tvar logged = true;",
92 },
93 // THE OLD FOLD GUARD, restored: `chat.app`, which asks whether an engine was built
94 // during THIS page load and not whether anything was ever said. After a reload it
95 // is null for every chat there is, so a thread of any length is refused.
96 appguard: {
97 find: "\t\tif (!chatSaid(chat)) { toast(t('tile.fold_unavailable'), true); return; }",
98 with: "\t\tvar _a = chat.app;\n"
99 + "\t\tif (!_a || typeof _a.fold_now !== 'function') {\n"
100 + "\t\t\ttoast(t('tile.fold_unavailable'), true); return;\n"
101 + "\t\t}",
102 },
103 // THE FOLD REFUSING BEFORE IT ASKS, which is the shape that trashed a chat.
104 // `appguard` above produces it only after a reload; this one produces it on the
105 // first press, where section 5a stands. It is here so the SCOPING in 5a and 5b is
106 // held by something: unscoped, this break does not merely fail, it deletes the
107 // conversation the rest of the section is about and then reports on what is left.
108 refuses: {
109 find: "\t\tif (!chatSaid(chat)) { toast(t('tile.fold_unavailable'), true); return; }",
110 with: "\t\tif (true) { toast(t('tile.fold_unavailable'), true); return; }",
111 },
112 // The confirm back, in its plainest form. Nothing answers it, so the model does
113 // not move either — which is the point: a confirm is a thing that stands there.
114 // It now lives on the Change button's click, where the switch was moved.
115 asks: {
116 find: "\t\t\tsetDiamondModel(opts.id, { provider: p.provider, model: p.model });",
117 with: "\t\t\tvar _ok = await confirmDialog('Change the model?', 'Change',\n"
118 + "\t\t\t\t{ title: 'Change the daimon', danger: false });\n"
119 + "\t\t\tif (!_ok) return;\n"
120 + "\t\t\tsetDiamondModel(opts.id, { provider: p.provider, model: p.model });",
121 },
122};
123let patched = null;
124if (SRC_BREAKS[BREAK]) {
125 const spec = SRC_BREAKS[BREAK];
126 patched = fs.readFileSync(path.join(HERE, '..', 'www', SRC), 'utf8');
127 const n = patched.split(spec.find).length - 1;
128 if (n !== 1) {
129 console.error(`break '${BREAK}': the anchor appears ${n} times in ${SRC}, so nothing was `
130 + 'broken and the run below would prove nothing. The file has moved on; move the '
131 + 'anchor with it.\n ' + spec.find.split('\n')[0].trim());
132 process.exit(2);
133 }
134 patched = patched.replace(spec.find, spec.with);
135}
136
137let failures = 0;
138const check = (cond, msg, detail) => {
139 console.log((cond ? ' ok ' : ' FAIL ') + msg + (detail != null ? ' — ' + detail : ''));
140 if (!cond) failures++;
141};
142
143/// The topmost visible dialog card.
144///
145/// NOT the first: the tile dialog's own card also carries `.dlg-card`, and its
146/// Done button also carries `.dlg-cancel` — so `find(first visible)` reaches past
147/// a confirm that is drawn over it and presses Done on the dialog underneath,
148/// which closes the wrong thing and leaves the confirm standing.
149/// Make a Diamond through the dialog a person uses.
150async function create(p, name) {
151 await p.evaluate(() => document.getElementById('new-diamond-btn').click());
152 await p.waitForSelector('.dlg-card', { timeout: 8000 });
153 await p.evaluate((nm) => {
154 const card = [...document.querySelectorAll('.dlg-card')]
155 .filter(c => c.getClientRects().length).pop();
156 const inp = card.querySelector('input.dlg-input');
157 inp.value = nm;
158 inp.dispatchEvent(new Event('input', { bubbles: true }));
159 card.querySelector('.dlg-ok').click();
160 }, name);
161 await p.waitForTimeout(900);
162}
163
164/// Open a Diamond tile's cog dialog by the Diamond's name.
165async function openCog(p, name) {
166 const found = await p.evaluate((nm) => {
167 const box = [...document.querySelectorAll('#diamond-list .diamond-box')]
168 .find(b => ((b.querySelector('.session-box-name') || {}).textContent || '').trim() === nm);
169 if (!box) return false;
170 const cog = box.querySelector('.tile-cog');
171 if (!cog) return false;
172 cog.click();
173 return true;
174 }, name);
175 if (!found) return false;
176 await p.waitForSelector('.tile-dlg-card', { timeout: 8000 });
177 return true;
178}
179
180/// Close whatever dialogs are standing. The tile dialog's way out is the closer
181/// cross, which still carries `tile-dlg-done` — that class means "the control
182/// that finishes with this dialog" and never meant the word Done, so this path
183/// is unchanged by the cross replacing the button.
184const closeDialogs = (p) => p.evaluate(() => {
185 document.querySelectorAll('.tile-dlg .tile-dlg-done, .dlg-card .dlg-cancel')
186 .forEach(b => { try { b.click(); } catch {} });
187});
188
189/// How much every tile draws. Simple and Max are global (notes3), so this is one
190/// call and not a visit to each tile's dialog: the per-tile level control is
191/// gone. Set live rather than by writing `localStorage` and reloading — the
192/// reload was costing the wasm a fresh boot on every check, and what is under
193/// test here is the rail as the user sees it change.
194async function setView(p, level) {
195 await p.evaluate((lv) => window.DaimondView.set(lv), level);
196 await p.waitForTimeout(250);
197}
198
199/// Open one tile's dialog from its cog. False when that tile has no cog, which
200/// is a failure worth naming rather than a selector timeout.
201async function openTile(p, box) {
202 const opened = await p.evaluate((sel) => {
203 const b = document.querySelector(sel);
204 const cog = b && b.querySelector('.tile-cog');
205 if (!cog) return false;
206 cog.click();
207 return true;
208 }, box);
209 if (!opened) return false;
210 await p.waitForSelector('.tile-dlg-card', { timeout: 8000 });
211 return true;
212}
213
214const s = await open({
215 name: 'diamondmodels', profile: scratch('pw', 'models-' + process.pid),
216 route: patched ? async (page) => {
217 await page.route('**/' + SRC, r => r.fulfill({
218 status: 200, contentType: 'application/javascript', body: patched,
219 }));
220 } : null,
221});
222const { page: p } = s;
223try {
224 await connectMock(s, { model: MODEL });
225
226 if (BREAK) console.log(` .. running with --break ${BREAK}`);
227 if (BREAK === 'novision') {
228 // Exactly the pre-phase-D dispatch: one model for the whole fan-out,
229 // modality ignored. Applied to the live rule, so what is disabled is the
230 // thing under test rather than a flag beside it.
231 await p.evaluate(() => {
232 const W = window.DaimondWorkers;
233 W.routeFor = function (task, text) {
234 return { provider: text.provider || '', model: text.model || '', sees: false };
235 };
236 });
237 }
238 if (BREAK === 'nomark') {
239 await p.addStyleTag({ content: '.tile-ctx-fold { display: none !important; }' });
240 }
241 // ══ 1. A Diamond shows its model ══════════════════════════════════
242 await create(p, 'Dee');
243 const dId = await p.evaluate(() =>
244 ([...document.querySelectorAll('#diamond-list .diamond-box')]
245 .find(b => ((b.querySelector('.session-box-name') || {}).textContent || '').trim() === 'Dee') || {}).dataset?.id || '');
246 check(!!dId, 'the Diamond was made', dId);
247 const dSel = `#diamond-list .diamond-box[data-id="${dId}"]`;
248 check(await openTile(p, dSel), 'the tile dialog opens from the cog');
249 await closeDialogs(p);
250 await setView(p, 'max');
251
252 const chip = await p.evaluate((sel) => {
253 const c = document.querySelector(sel + ' .tile-model-chip.diamond-model');
254 if (!c) return null;
255 return { text: (c.textContent || '').trim(), title: c.title || '',
256 shown: c.getClientRects().length > 0 };
257 }, dSel);
258 check(chip !== null, 'the Diamond tile carries a model chip');
259 check(!!(chip && chip.shown && chip.text), 'the chip is drawn and says something',
260 chip && chip.text);
261 check(!!(chip && chip.title.includes(MODEL)),
262 'the chip names the full model, not only the short form', chip && chip.title);
263
264 // ── And Simple hides it, because a model is detail. ──
265 await setView(p, 'simple');
266 const hidden = await p.evaluate((sel) => {
267 const c = document.querySelector(sel + ' .tile-model-chip.diamond-model');
268 return !c || c.getClientRects().length === 0;
269 }, dSel);
270 check(hidden, 'Simple hides the model chip with the other model controls');
271 await setView(p, 'max');
272 await p.waitForTimeout(300);
273
274 // ══ 2. The model changes on Change, and the thread comes with it ═══
275 //
276 // GIVE THE DAIMON A CONVERSATION FIRST. Without one, "the session is the same
277 // length afterwards" is 0 === 0 — a check with no subject, which passes against
278 // anything, including a handler that empties the session on its way past.
279 await p.evaluate((sel) => { document.querySelector(sel).click(); }, dSel);
280 await p.waitForTimeout(900);
281 await steerDiamond(s, 'remember this sentence');
282 await p.waitForSelector('.chat-spinner', { state: 'detached', timeout: 40000 }).catch(() => {});
283 await p.waitForTimeout(1200);
284 /// How many messages the MODEL's own copy of this daimon's conversation holds.
285 ///
286 /// Read from IndexedDB rather than from the screen: `session.msgs` is what the next
287 /// turn is built from and carries the provider's tool-call ids, and it is invisible
288 /// from the transcript either way. The transcript is the other half of the same
289 /// claim and is checked beside it.
290 const sessLen = async () => {
291 const all = await storedChats(s);
292 const rec = (all || []).find(c => c && c.diamondId === dId);
293 return rec ? (((rec.session || {}).msgs) || []).length : -1;
294 };
295 const sessBefore = await sessLen();
296 check(sessBefore > 0, 'the daimon has a conversation, so what follows has a subject',
297 'session msgs: ' + sessBefore);
298
299 check(await openCog(p, 'Dee'), 'the cog opens the tile dialog');
300 // The label is a real `<label>` now, bound to its pulldown by `for`/`id`.
301 // It used to wear `.tile-model-chip`, which is the mono pill a model NAME is
302 // drawn in on the tile — so three form labels looked like three values, and
303 // the word did not focus the control it named.
304 const rows = await p.evaluate(() => {
305 const card = document.querySelector('.tile-dlg-card');
306 return [...card.querySelectorAll('.tile-dlg-model')].map(r => {
307 const lab = r.querySelector('label.tile-dlg-label');
308 const sel = r.querySelector('select');
309 return {
310 label: lab ? (lab.textContent || '').trim() : '',
311 // Which of the three settings this row IS, taken from the control's
312 // own id rather than from the row's position: a row list read by
313 // index says nothing about which setting moved when one is added.
314 which: sel ? (/(daimon|workers|vision)/.exec(sel.id) || [''])[0] : '',
315 bound: !!(lab && sel && lab.htmlFor && lab.htmlFor === sel.id),
316 options: [...((sel || { options: [] }).options)].map(o => o.value),
317 };
318 });
319 });
320 const rowFor = (w) => rows.find(r => r.which === w);
321 check(['daimon', 'workers', 'vision'].every(w => !!rowFor(w)),
322 'the dialog carries a row for each of the three settings: the daimon, the workers, and the workers’ eyes',
323 rows.map(r => r.which || '(unnamed)').join(', '));
324 check(rows.length > 0 && rows.every(r => r.label && r.bound),
325 'each is a real label bound to its own pulldown, so the word focuses the control it names',
326 JSON.stringify(rows.map(r => [r.label, r.bound])));
327 check(!!rowFor('workers') && !!rowFor('vision')
328 && rowFor('workers').options.includes('') && rowFor('vision').options.includes(''),
329 'the two worker rows offer "same as the text model" as a real choice, which is what absent MEANS here');
330
331 // Each of the three reads below reaches the DAIMON's pulldown by the setting
332 // it carries -- `/daimon/` in the control's own id -- rather than by taking
333 // the first row on screen. The daimon is the only one of the three written
334 // into the crystal's history, so a check that found a row by position would
335 // end up asserting that against a setting which deliberately has none.
336
337 // A second model to move to. The mock serves whatever it is asked for, so any
338 // second name in the pulldown will do.
339 const other = await p.evaluate(() => {
340 const sel = [...document.querySelectorAll('.tile-dlg-card .tile-dlg-model select')]
341 .find(s => /daimon/.test(s.id));
342 if (!sel) return '';
343 const opts = [...sel.options].map(o => o.value).filter(Boolean);
344 return opts.find(v => v !== sel.value) || '';
345 });
346 if (!other) {
347 console.log(' .. only one model is configured; the change checks are skipped');
348 } else {
349 const before = await p.evaluate((id) =>
350 (JSON.parse(localStorage.getItem('daimond-diamond-models') || '{}')[id] || {}).model || '', dId);
351 const move = (v) => p.evaluate((val) => {
352 const sel = [...document.querySelectorAll('.tile-dlg-card .tile-dlg-model select')]
353 .find(s => /daimon/.test(s.id));
354 sel.value = val;
355 sel.dispatchEvent(new Event('change', { bubbles: true }));
356 }, v);
357 /// Did a dialog appear OVER the tile dialog, within `ms`?
358 ///
359 /// Measured as a bounding rectangle -- `getClientRects().length` -- and never as
360 /// a computed `display`. `display: none` does not cascade: a child of a hidden
361 /// parent computes its own `display` quite happily and reports itself drawn, and
362 /// a check written that way is green against a dialog nobody can see and green
363 /// against one everybody can.
364 ///
365 /// The tile dialog's own card is excluded by class. It is a `.dlg-card` too, and
366 /// it is standing throughout, so an unscoped read answers "yes" every time and
367 /// the check never had a chance of being false.
368 const confirmWithin = async (ms) => {
369 try {
370 await p.waitForFunction(() =>
371 [...document.querySelectorAll('.dlg-card')]
372 .filter(c => c.getClientRects().length && !c.classList.contains('tile-dlg-card'))
373 .length > 0, null, { timeout: ms });
374 return true;
375 } catch { return false; }
376 };
377
378 // ── The pulldown reveals Change; pressing it moves the daimon. ──
379 //
380 // The switch is two steps now, on purpose: a daimon's model is persistent, so
381 // it is not moved by a pulldown brushed past. The Change button beside it shows
382 // ONLY while the pick differs from the model in force, and pressing it is what
383 // switches -- reusing the conversation, never a fresh daimon.
384 /// Is the daimon row's Change button drawn (a real bounding box), and what does
385 /// it read? Measured as `getClientRects()`, never a computed `display`, for the
386 /// reason `confirmWithin` states about itself.
387 const changeState = () => p.evaluate(() => {
388 const row = [...document.querySelectorAll('.tile-dlg-card .tile-dlg-model')]
389 .find(r => { const s = r.querySelector('select'); return s && /daimon/.test(s.id); });
390 const b = row ? row.querySelector('.tile-dlg-apply') : null;
391 return { shown: !!(b && b.getClientRects().length), text: b ? (b.textContent || '').trim() : '' };
392 });
393 const pressChange = () => p.evaluate(() => {
394 const row = [...document.querySelectorAll('.tile-dlg-card .tile-dlg-model')]
395 .find(r => { const s = r.querySelector('select'); return s && /daimon/.test(s.id); });
396 const b = row ? row.querySelector('.tile-dlg-apply') : null;
397 if (b) b.click();
398 });
399 const hiddenAtRest = await changeState();
400 check(!hiddenAtRest.shown,
401 'the Change button is hidden until a different model is picked',
402 'shown=' + hiddenAtRest.shown);
403 await move(other);
404 await p.waitForTimeout(150);
405 const shownOnDiff = await changeState();
406 check(shownOnDiff.shown && /change/i.test(shownOnDiff.text),
407 'picking a different model reveals the button, reading "Change"',
408 JSON.stringify(shownOnDiff));
409 await pressChange();
410 // THE RECEIPT IS CAUGHT AS IT APPEARS. It used to be read at the bottom of
411 // this block, and that read was a coin toss.
412 //
413 // `toast()` fades its box at 3600ms and REMOVES it at 4200ms
414 // (daimond.js:29857). The read below it stood behind `confirmWithin(3000)`,
415 // which burns its whole window on the healthy path where no confirm ever
416 // comes, then a 1200ms settle, then a localStorage read, an IndexedDB round
417 // trip and a transcript read. So the earliest it could sample was 4200ms
418 // plus three round trips: the removal boundary itself, missed or hit on a
419 // few milliseconds of jitter. It came up "(nothing said)" once in ten runs
420 // on 2026-08-21 — red for the right reason, in the one shape that cannot be
421 // attributed to anything, and a gate of 253 checks that reports a different
422 // count each time is a gate nobody can read.
423 //
424 // The fade is not the problem and a shorter wait would not have fixed it:
425 // an opacity-0 box still answers `getClientRects()`, so the window is sharp
426 // rather than soft. What is wanted is not an earlier guess at when to look
427 // but a watch that fires the moment the toast exists, which is this.
428 const receiptEarly = await p.waitForFunction(() => {
429 const said = [...document.querySelectorAll('.toast, .toast-msg, [class*="toast"]')]
430 .filter(n => n.getClientRects().length)
431 .map(n => (n.textContent || '').trim()).filter(Boolean).join(' | ');
432 return (/tile\.model_changed/.test(said) || /moved from .* to /i.test(said)) ? said : false;
433 }, null, { timeout: 4000 }).then(h => h.jsonValue()).catch(() => '');
434 const asked = await confirmWithin(3000);
435 check(!asked, 'NOTHING IS ASKED: a change this cheap to undo takes a receipt, not a modal',
436 asked ? 'a confirm was drawn over the tile dialog' : 'no dialog appeared');
437 await p.waitForTimeout(1200);
438 const afterYes = await p.evaluate((id) =>
439 (JSON.parse(localStorage.getItem('daimond-diamond-models') || '{}')[id] || {}).model || '', dId);
440 check(afterYes === other, 'and the daimon really is on the new model',
441 `${before} → ${afterYes}`);
442
443 // ── AND THE CONVERSATION IS STILL THERE. ──
444 //
445 // The claim the deleted modal made was that it would not be. Both halves are
446 // read: the model's own copy, which is what the next turn carries and is
447 // invisible from the screen, and the transcript, which is what the reader sees.
448 const sessAfter = await sessLen();
449 check(sessAfter === sessBefore,
450 'THE MODEL\'S OWN CONVERSATION IS UNTOUCHED by the change of model',
451 `${sessBefore} → ${sessAfter} messages`);
452 const stillDrawn = await p.evaluate(() => {
453 const co = document.getElementById('chat-output');
454 return co ? (co.textContent || '') : '';
455 });
456 check(/remember this sentence/.test(stillDrawn),
457 'and so is the transcript the reader is looking at',
458 stillDrawn.replace(/\s+/g, ' ').slice(0, 70));
459
460 // ── The receipt. ──
461 //
462 // `tile.model_changed` is added by the locale files, so until those land `t()`
463 // hands back the key itself and the toast reads `tile.model_changed`. Either
464 // form is accepted; what is asserted is that the app SAID something when it
465 // moved, rather than moving in silence.
466 // The watch above is what answers this; the late read stays as the fallback,
467 // so a build whose toast lingers still reports what it said rather than an
468 // empty string, and so a failure names whatever IS on screen instead of
469 // nothing at all.
470 const receiptLate = await p.evaluate(() =>
471 [...document.querySelectorAll('.toast, .toast-msg, [class*="toast"]')]
472 .filter(n => n.getClientRects().length)
473 .map(n => (n.textContent || '').trim()).filter(Boolean).join(' | '));
474 const receipt = receiptEarly || receiptLate;
475 check(/tile\.model_changed/.test(receipt) || /moved from .* to /i.test(receipt),
476 'a receipt says what moved, in place of the question that used to be asked',
477 receipt || '(nothing said)');
478
479 // ── And it is written into the crystal's own history, which is what
480 // notes2 means by "it requires a fold and a new daimon": a change that
481 // only flipped a browser setting would leave the Diamond with no record
482 // that its daimon is not the one that wrote the crystal above it. ──
483 await closeDialogs(p);
484 await p.waitForTimeout(400);
485 await p.evaluate((sel) => { document.querySelector(sel).click(); }, dSel);
486 await p.waitForTimeout(900);
487 // Onto the CRYSTAL face: the steer above put this Diamond on its chat face and
488 // `setDiamondView` remembers, so selecting it again lands on the conversation and
489 // the history control -- which belongs to the crystal -- is simply not on screen.
490 await p.click('#dview-crystal', { force: true });
491 await p.waitForTimeout(900);
492 const opened = await p.evaluate(() => {
493 const b = [...document.querySelectorAll('.crystal-act')]
494 .find(x => /history/i.test(x.textContent || ''));
495 if (!b) return false;
496 b.click();
497 return true;
498 });
499 check(opened, 'the crystal has a history to open');
500 await p.waitForTimeout(900);
501 const hist = await p.evaluate(() => [...document.querySelectorAll('.hist-row')].map(r => ({
502 kind: (r.querySelector('.hist-kind') || {}).textContent || '',
503 note: (r.querySelector('.hist-note') || {}).textContent || '',
504 })));
505 const mrow = hist.find(h => h.kind.trim() === 'model');
506 check(!!mrow, 'the model change is a version in the crystal\'s history',
507 hist.map(h => h.kind.trim()).join(', ') || '(no rows)');
508 check(!!(mrow && /moved from .* to /i.test(mrow.note)),
509 'and the row says which model it moved from and to',
510 mrow && mrow.note);
511 await shot(s, 'history');
512 }
513 await closeDialogs(p);
514
515 // ══ 3. The secondary is keyed by modality ═════════════════════════
516 // Asked of `Workers.routeFor`, which is the function `dispatch` itself calls —
517 // not a copy of the rule written here. A rule about how money is spent that
518 // could only be observed by spending it would be checked by nobody.
519 const routed = await p.evaluate(() => {
520 const W = window.DaimondWorkers;
521 if (!W || !W.routeFor) return null;
522 const text = { provider: 'p', model: 'the-text-one' };
523 const eyes = { provider: 'p', model: 'the-eyes-one' };
524 const r = (task, supplied) => W.routeFor(task, text, eyes, !!supplied);
525 return {
526 plain: r('read src/main.rs and summarise it'),
527 image: r('look at shots/home.png and say what is wrong'),
528 jpeg: r('compare a.jpg with b.jpeg'),
529 gif: r('what happens in loop.gif'),
530 webp: r('open hero.webp'),
531 nearMiss: r('read notes.pngx, it is not an image'),
532 tiff: r('open scan.tiff'),
533 supplied: r('look at shots/home.png', true),
534 };
535 });
536 if (!routed) {
537 check(false, 'the dispatch routing rule can be asked about without dispatching');
538 } else {
539 check(routed.plain.model === 'the-text-one',
540 'a task naming no image runs on the text model', routed.plain.model);
541 check(routed.image.model === 'the-eyes-one',
542 'a task naming a .png runs on the image model', routed.image.model);
543 check(routed.jpeg.model === 'the-eyes-one' && routed.gif.model === 'the-eyes-one'
544 && routed.webp.model === 'the-eyes-one',
545 '.jpg, .jpeg, .gif and .webp count as images — exactly what ImageMedia sniffs');
546 check(routed.tiff.model === 'the-text-one',
547 'a format this app cannot hand a model as an image is not one', routed.tiff.model);
548 check(routed.nearMiss.model === 'the-text-one',
549 'an extension that merely starts with an image name is not one', routed.nearMiss.model);
550 check(routed.supplied.model === 'the-text-one' && routed.supplied.sees === false,
551 'a model supplied for the whole fan-out is not second-guessed by modality',
552 routed.supplied.model);
553
554 // ── And through the store: with no image model set, an image task falls
555 // back to the TEXT worker model rather than failing — §1.4's stated
556 // behaviour — and still records that it was an image task, so the
557 // fallback shows on the run instead of looking chosen. ──
558 const fell = await p.evaluate((id) => {
559 const all = JSON.parse(localStorage.getItem('daimond-diamond-models') || '{}');
560 all[id] = Object.assign({}, all[id], { visionProvider: '', visionModel: '' });
561 localStorage.setItem('daimond-diamond-models', JSON.stringify(all));
562 const W = window.DaimondWorkers;
563 return {
564 img: W.routeForDiamond(id, 'look at shots/home.png'),
565 plain: W.routeForDiamond(id, 'read main.rs'),
566 };
567 }, dId);
568 check(!!fell.img.model && fell.img.model === fell.plain.model,
569 'with no image model set, an image task falls back to the text model',
570 fell.img.model);
571 check(fell.img.sees === true && fell.plain.sees === false,
572 'and the run still records which of the two it was', String(fell.img.sees));
573 }
574
575 // ══ 4. The meter says where the fold happens ══════════════════════
576 await newChat(s);
577 await p.fill('#chat-input', 'hello');
578 await p.click('#chat-send');
579 await p.waitForTimeout(2500);
580 // Max, or the meter is hidden by the view's CSS rather than absent.
581 const cId = await p.evaluate(() =>
582 (document.querySelector('#session-list .chat-box') || {}).dataset?.id || '');
583 await setView(p, 'max');
584 await p.waitForTimeout(300);
585
586 const meter = await p.evaluate(() => {
587 const bar = document.querySelector('#session-list .chat-box .tile-ctx-bar');
588 if (!bar) return null;
589 const mark = bar.querySelector('.tile-ctx-fold');
590 const ctx = bar.closest('.tile-ctx');
591 return {
592 hasMark: !!mark,
593 left: mark ? mark.style.left : '',
594 drawn: mark ? mark.getClientRects().length > 0 : false,
595 title: ctx ? (ctx.title || '') : '',
596 };
597 });
598 if (meter === null) {
599 console.log(' .. this model publishes no window, so there is no meter to mark');
600 } else {
601 check(meter.hasMark, 'the context bar carries a fold mark');
602 check(meter.drawn, 'the fold mark is actually drawn', meter.left);
603 check(/^\d+%$/.test(meter.left) && parseInt(meter.left, 10) > 0
604 && parseInt(meter.left, 10) < 100,
605 'the mark sits inside the bar at the fold fraction', meter.left);
606 check(/\bFolds at\b|\d+%/.test(meter.title),
607 'and the meter says so in words as well as in ink', meter.title);
608 }
609 await shot(s, 'meter');
610
611 // ══ 5. A fold the user can ask for, that reports honestly ═════════
612 //
613 // That a hand fold does not SHRINK the window is proved in Rust, where the
614 // window lives (`test_a_hand_fold_leaves_the_window_where_it_was_00`). What
615 // only the browser can prove is that the control exists, is reachable, and
616 // tells the truth about a conversation too short to fold — the failure mode
617 // of every "do it now" button ever written.
618 const cogOpened = await p.evaluate((id) => {
619 const box = document.querySelector(`#session-list .chat-box[data-id="${id}"]`);
620 const cog = box && box.querySelector('.tile-cog');
621 if (!cog) return false;
622 cog.click();
623 return true;
624 }, cId);
625 check(cogOpened, 'the chat tile has a cog');
626 await p.waitForSelector('.tile-dlg-card', { timeout: 8000 });
627 const ctxSec = await p.evaluate(() => {
628 const card = document.querySelector('.tile-dlg-card');
629 const heads = [...card.querySelectorAll('.tile-dlg-head')].map(h => h.textContent.trim());
630 // `.tile-dlg-level` is a BOX, not a meaning: the colour reset in this same
631 // dialog wears it too. The fold is the one inside the Context section, so
632 // the reset is excluded by class and the words settle which of what is
633 // left is being pressed.
634 const btn = [...card.querySelectorAll('.tile-dlg-level:not(.tile-dlg-clear)')]
635 .find(b => /fold/i.test(b.textContent || ''));
636 const notes = [...card.querySelectorAll('.tile-dlg-note')].map(n => n.textContent.trim());
637 return { heads, hasBtn: !!btn, notes };
638 });
639 check(ctxSec.heads.some(h => /context/i.test(h)),
640 'the chat dialog has a Context section', ctxSec.heads.join(' | '));
641 check(ctxSec.hasBtn, 'and a control that folds it now');
642 check(ctxSec.notes.some(n => /folds at \d+%/i.test(n)),
643 'which says where the fold happens, in words',
644 ctxSec.notes.find(n => /folds/i.test(n)) || ctxSec.notes.join(' | '));
645
646 // ── Press it on a one-turn conversation. There is no tail to cut below
647 // MIN_KEEP_MESSAGES, so the honest answer is that nothing moved. ──
648 await p.evaluate(() => {
649 const card = document.querySelector('.tile-dlg-card');
650 [...card.querySelectorAll('.tile-dlg-level:not(.tile-dlg-clear)')]
651 .find(b => /fold/i.test(b.textContent || '')).click();
652 });
653 // THE CONFIRM, SCOPED PAST THE TILE DIALOG'S OWN CARD — and this is the
654 // second place in this file to need saying so.
655 //
656 // What stood here was `waitForSelector('.dlg-card .dlg-ok')` followed by a
657 // click on the last visible `.dlg-card`. The tile dialog is a `.dlg-card`
658 // too (`modal-card dlg-card tile-dlg-card`), it is standing at this point
659 // because the Fold button that was just pressed is inside it, and the only
660 // `.dlg-ok` it carries is DELETE at its foot — which `deleteChat` acts on
661 // with no second question. So the wait was answered by the dialog already on
662 // screen, and the click was a coin toss settled by document order.
663 //
664 // It came up tails on 2026-08-20/21. Under a break where the fold refuses
665 // before any confirm is drawn, this block pressed Delete and the run said
666 // "Moved 'the chat from just now' to the trash" — a check destroying the
667 // fixture it was in the middle of measuring, and then reporting on the
668 // wreckage. 5b was scoped that night; this one was left because it passed,
669 // and it passed only because a confirm happens to be drawn LAST in document
670 // order on the healthy path. That is not a reason, it is a coincidence.
671 //
672 // Scoped by `:not(.tile-dlg-card)` — the same guard 5b uses — and a missing
673 // confirm is now a NAMED failure rather than a timeout thrown out of the
674 // block: the fold is a destructive-enough act to be worth asking about, so
675 // its absence is a fact about the product and belongs in the list.
676 //
677 // Read the toast once BEFORE waiting the confirm out, for the reason 5b gives
678 // below: a refusal is instant and its toast is gone in a few seconds, so a run
679 // that waits the full timeout first reports the break as "(nothing said)" —
680 // red for the right reason and unreadable, which is how a red run gets blamed
681 // on the harness.
682 await p.waitForTimeout(1200);
683 const earlySaid = await p.evaluate(() =>
684 [...document.querySelectorAll('.toast, .toast-msg, [class*="toast"]')]
685 .filter(n => n.getClientRects().length)
686 .map(n => (n.textContent || '').trim()).filter(Boolean).join(' | '));
687 const askedFirst = await p.waitForFunction(() =>
688 [...document.querySelectorAll('.dlg-card')]
689 .filter(c => c.getClientRects().length && !c.classList.contains('tile-dlg-card'))
690 .length > 0, null, { timeout: 8000 }).then(() => true).catch(() => false);
691 check(askedFirst, 'pressing Fold asks first, in a card of its own',
692 askedFirst ? 'a confirm was drawn over the tile dialog' : 'no confirm appeared, so nothing was pressed');
693 if (askedFirst) {
694 await p.evaluate(() => {
695 const card = [...document.querySelectorAll('.dlg-card')]
696 .filter(c => c.getClientRects().length && !c.classList.contains('tile-dlg-card')).pop();
697 card.querySelector('.dlg-ok').click();
698 });
699 }
700 await p.waitForTimeout(2500);
701 const said = (await p.evaluate(() =>
702 [...document.querySelectorAll('.toast, .toast-msg, [class*="toast"]')]
703 .map(n => (n.textContent || '').trim()).filter(Boolean).join(' | '))) || earlySaid;
704 check(/nothing to fold|already as short/i.test(said),
705 'a fold of a conversation that cannot be folded says so', said || '(nothing said)');
706 // And the transcript is not quietly shorter for having pressed it.
707 const foldRows = await p.evaluate(() =>
708 document.querySelectorAll('.chat-msg-compacted').length);
709 check(foldRows === 0, 'and no fold notice was written for a fold that did not happen',
710 String(foldRows));
711 await shot(s, 'fold-dialog');
712 await closeDialogs(p);
713
714 // ── 5b. AND IT IS STILL ASKABLE AFTER A RELOAD, which is the ordinary case.
715 //
716 // `chat.app` is an engine instance for THIS page load, and `hydrateChat` nulls it on
717 // every boot. The guard used to read it, so the first thing a fold said about a
718 // two-hundred-message thread reopened tomorrow was that the chat had nothing in it.
719 // Nothing could see this from a single-page run, which is why the reload is the
720 // check: the conversation is real, the engine is not, and the fold has to build one.
721 //
722 // What is asserted is the HONEST answer for a one-turn chat -- nothing to fold --
723 // rather than the absence of the refusal. Asserting on the refusal's own words would
724 // be a check written against a string the locales are in the middle of rewording.
725 await p.reload({ waitUntil: 'domcontentloaded' });
726 await signInAs(s, 'diamondmodels');
727 await p.waitForTimeout(2000);
728 await setView(p, 'max');
729 const reopened = await p.evaluate((id) => {
730 const box = document.querySelector(`#session-list .chat-box[data-id="${id}"]`);
731 if (!box) return false;
732 box.click();
733 return true;
734 }, cId);
735 check(reopened, 'the chat is still on the rail after a reload');
736 await p.waitForTimeout(900);
737 const drawnBack = await p.evaluate(() => {
738 const co = document.getElementById('chat-output');
739 return co ? (co.textContent || '').replace(/\s+/g, ' ') : '';
740 });
741 check(/hello/.test(drawnBack),
742 'and it still holds the conversation, so the fold below has a subject',
743 drawnBack.slice(0, 60));
744 const cogAgain = await p.evaluate((id) => {
745 const box = document.querySelector(`#session-list .chat-box[data-id="${id}"]`);
746 const cog = box && box.querySelector('.tile-cog');
747 if (!cog) return false;
748 cog.click();
749 return true;
750 }, cId);
751 check(cogAgain, 'its cog still opens');
752 await p.waitForSelector('.tile-dlg-card', { timeout: 8000 });
753 await p.evaluate(() => {
754 const card = document.querySelector('.tile-dlg-card');
755 [...card.querySelectorAll('.tile-dlg-level:not(.tile-dlg-clear)')]
756 .find(b => /fold/i.test(b.textContent || '')).click();
757 });
758 /// Whatever the app is saying in a toast, right now.
759 const toasts = () => p.evaluate(() =>
760 [...document.querySelectorAll('.toast, .toast-msg, [class*="toast"]')]
761 .filter(n => n.getClientRects().length)
762 .map(n => (n.textContent || '').trim()).filter(Boolean).join(' | '));
763 // Read once BEFORE waiting for the confirm. A refusal is instant and its toast is
764 // gone in a few seconds, so a run that waited out the confirm timeout first would
765 // report the break as "(nothing said)" -- red for the right reason, and unreadable.
766 await p.waitForTimeout(1200);
767 const early = await toasts();
768 // The confirm, if one comes: SCOPED past the tile dialog's own card, which carries
769 // `.dlg-card` and whose Delete at the foot carries `.dlg-ok`. An unscoped wait is
770 // satisfied by the dialog already standing and an unscoped click then trashes the
771 // chat -- which is what happened here, under the break, where a refusal returns
772 // before any confirm is drawn. A missing confirm is not a failure at this point;
773 // the toast is what is being judged.
774 const askedToFold = await p.waitForFunction(() =>
775 [...document.querySelectorAll('.dlg-card')]
776 .filter(c => c.getClientRects().length && !c.classList.contains('tile-dlg-card'))
777 .length > 0, null, { timeout: 8000 }).then(() => true).catch(() => false);
778 if (askedToFold) {
779 await p.evaluate(() => {
780 const card = [...document.querySelectorAll('.dlg-card')]
781 .filter(c => c.getClientRects().length && !c.classList.contains('tile-dlg-card')).pop();
782 card.querySelector('.dlg-ok').click();
783 });
784 }
785 await p.waitForTimeout(3000);
786 const saidAgain = (await toasts()) || early;
787 check(/nothing to fold|already as short/i.test(saidAgain),
788 'A FOLD ASKED FOR AFTER A RELOAD REACHES THE CONVERSATION, and answers about it',
789 saidAgain || '(nothing said)');
790 await closeDialogs(p);
791
792} catch (e) {
793 check(false, 'the run finished', String(e && e.message || e));
794 try { await shot(s, 'threw'); } catch {}
795} finally {
796 await s.close();
797}
798
799console.log(failures === 0
800 ? `\nverify_diamondmodels: all checks pass.`
801 : `\nverify_diamondmodels: ${failures} failed.`);
802process.exit(failures === 0 ? 0 : 1);