Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_reachlegible.mjs

24.2 KiB, 1 run

created by r2519314175:629, 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_reachlegible.mjs — the Workspace panel says what a daimon may reach, and
2// the control that widens it says so in a word.
3//
4// ── WHY ──────────────────────────────────────────────────────────────────────
5//
6// Two faults, both found by the owner failing to use his own app on 2026-08-23,
7// both in the Workspace panel, and both invisible to every check that existed.
8//
9// 1. THE SCOPE CHIP READ AS A PERMISSION SETTING AND IS A VIEW FILTER. He
10// worked an evening on `Everything`, where the panel drew the workspace at
11// large and said nothing whatever about the Diamond in front of him: the
12// Toolchains row was not drawn, the marks were in the other tree, and there
13// was no sentence anywhere. Nothing was marked into that Diamond, so
14// `diamond_bounds` gave it one `OnlyWriteUnder` — its own folder — and a
15// `file_search` naming no path started there, because `walk_starts`
16// (src/tools.rs) takes its default from exactly those prefixes. His daimon
17// reported a function present ten times as absent. Every fact needed to see
18// that coming was in the store and none of it was on the screen.
19//
20// 2. TWO `+` BUTTONS WITH IDENTICAL TEXT. One marked a folder into a fence and
21// one put a file in front of a model, and the only difference on screen was
22// a tooltip. Worse, the crystal footer's `+` was labelled "Attach" while
23// doing the marking, because on a Diamond `Files.attachAdd` writes a `holds`
24// link and `Files.bounds` turns every one of those into an `OnlyWriteUnder`.
25//
26// ── WHAT IT ASSERTS, WHICH IS THE PROPERTY AND NOT THE FIX ───────────────────
27//
28// Nothing here asks whether an element exists. Six properties, and each can hold
29// while another breaks:
30//
31// 1. THE REACH ROW IS DRAWN IN BOTH TREES. `Everything` is where the fault
32// happened, so `Everything` is where it is measured first.
33// 2. IT AGREES WITH THE FENCE, path for path and in order, against
34// `DaimondDiamond.bounds` — the same call `scopeAgentTo` hands the engine.
35// A row built from the panel's own cache would pass an existence check and
36// fail this one.
37// 3. THE ZERO STATE IS SAID OUT LOUD, IN INK, WITHOUT A HOVER. This is the
38// state the owner was in and could not see. Asserted as a laid-out element
39// carrying a non-empty sentence that names the Diamond — a `title` would
40// not do, which is the whole complaint.
41// 4. THE TOOLCHAINS ROW IS OFFERED ON `Everything` TOO.
42// 5. ONE PRESS MARKS THE FOLDER ON SCREEN, and the fence and the row both
43// move. He had to open a picker and walk a tree back down to the folder he
44// was already standing in.
45// 6. A `+` THAT WIDENS A FENCE SAYS SO, and does not read the same as a `+`
46// that does not. Measured as TEXT, because the tooltip was the fault.
47// 7. A FOLDER'S MARK CONTROL IS NOT BEHIND A HOVER. A grant nobody can see is
48// a grant nobody takes back.
49//
50// EACH CHECK IS PROVED AGAINST BROKEN CODE FIRST. `--break <name>` serves a
51// deliberately damaged copy of a real file to the real page through
52// `page.route`, and the run is then EXPECTED TO FAIL. A break whose anchor is
53// not found aborts rather than passing quietly.
54//
55// node dev/verify_reachlegible.mjs --break noreach # 1,2,3: the row only in the Diamond tree
56// node dev/verify_reachlegible.mjs --break reachlies # 2: the marks dropped from the row
57// node dev/verify_reachlegible.mjs --break reachown # 2: the own folder dropped from it
58// node dev/verify_reachlegible.mjs --break quietzero # 3: the zero state left unsaid
59// node dev/verify_reachlegible.mjs --break scopeonly # 4: Toolchains back in one tree
60// node dev/verify_reachlegible.mjs --break nomarkhere # 5: no one-press mark
61// node dev/verify_reachlegible.mjs --break bareplus # 6: the `+` back to a bare `+`
62// node dev/verify_reachlegible.mjs --break sameword # 6: the two `+` given one word again
63// node dev/verify_reachlegible.mjs --break hoveronly # 7: the mark hidden until hover
64// node dev/verify_reachlegible.mjs --break noclass # 7: the row control never marked as a grant
65// node dev/verify_reachlegible.mjs --break panelmute # 8: both rows silent in both trees
66// node dev/verify_reachlegible.mjs # and then, clean
67//
68// BRING THE WORLD DOWN AND UP BETWEEN RUNS. The mock keeps state across a run,
69// so a re-run is not a repeat — see the header of dev/verify_twodepth.mjs for
70// the hour that cost.
71//
72// bash dev/world.sh 4 --down && eval "$(bash dev/world.sh 4 --up)"
73//
74// Needs dev/serve.mjs and dev/mockllm.mjs. No gateway and no real provider:
75// nothing here sends a turn, because every fault is a thing on the screen. The
76// mock is connected only because the New Diamond form refuses to create one
77// without a model it can resolve a key for.
78
79import fs from 'node:fs';
80import path from 'node:path';
81import { fileURLToPath } from 'node:url';
82import { open, shot, scratch, errors } from './harness.mjs';
83
84const HERE = path.dirname(fileURLToPath(import.meta.url));
85const WWW = path.join(HERE, '..', 'www');
86
87const BREAK = (() => {
88 const i = process.argv.indexOf('--break');
89 return i > 0 ? String(process.argv[i + 1] || '') : '';
90})();
91
92const PROFILE = scratch('pw', 'reachlegible' + (BREAK ? '-' + BREAK : ''));
93fs.rmSync(PROFILE, { recursive: true, force: true });
94
95const ok = [], bad = [];
96const check = (name, pass, detail) => {
97 (pass ? ok : bad).push(name);
98 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail != null ? ' — ' + detail : ''));
99};
100const sleep = ms => new Promise(r => setTimeout(r, ms));
101
102// ── The breaks ───────────────────────────────────────────────────────────────
103//
104// Each is one line and is scoped to survive every check but the ones it proves.
105// `noreach` and `scopeonly` are the two defects exactly as they stood.
106const BREAKS = {
107 // The row put back behind the scope chip, which is where every fact about the
108 // fence used to live.
109 noreach: [{
110 file: 'js/daimond.js',
111 find: " if (!currentDiamond) { reachEl.innerHTML = ''; reachEl.style.display = 'none'; return; }",
112 with: " if (!currentDiamond || !diamondScope()) { reachEl.innerHTML = ''; reachEl.style.display = 'none'; return; }",
113 }],
114 // The row drawn, and drawn from nothing: the marks dropped, so it reports a
115 // fence narrower than the one the turn is given. Existence checks all pass.
116 reachlies: [{
117 file: 'js/daimond.js',
118 find: " var marks = b.attached || [], ro = b.read_only || [];",
119 with: " var marks = [], ro = b.read_only || [];",
120 }],
121 // The Diamond's own folder left off the row, so the panel under-reports the
122 // fence by exactly the place a bare search starts when nothing else is marked.
123 // `reachlies` cannot prove 2, because at that point there is nothing to drop.
124 reachown: [{
125 file: 'js/daimond.js',
126 find: " reachEl.appendChild(reachChip(t('dws.reach_own'), own, false));",
127 with: " void own;",
128 }],
129 // The state the owner was in, left unsaid again.
130 quietzero: [{
131 file: 'js/daimond.js',
132 find: " : t('dws.reach_none', { name: currentDiamond.name });",
133 with: " : '';",
134 }],
135 // Toolchains back in the Diamond tree only — absent rather than explained.
136 scopeonly: [{
137 file: 'js/daimond.js',
138 find: " if (!currentDiamond) { kitsEl.style.display = 'none'; return; }",
139 with: " if (!currentDiamond || !diamondScope()) { kitsEl.style.display = 'none'; return; }",
140 }],
141 // The one-press mark taken away, so the only route back is the picker.
142 nomarkhere: [{
143 file: 'js/daimond.js',
144 find: " if (!currentDiamond || !curDir) return null;",
145 with: " if (true) return null;",
146 }],
147 // The `+` back to what it was: one glyph, two meanings, a tooltip between them.
148 bareplus: [{
149 file: 'js/daimond.js',
150 find: " add.textContent = '+ ' + t(grants ? 'attach.add_mark' : 'attach.add');",
151 with: " add.textContent = '+';",
152 }],
153 // The class never applied, so nothing on the row says it is a grant and the
154 // rule that lifts it has nothing to match. `hoveronly` breaks the rule and
155 // this breaks the mark, which are two different ways to lose the same thing.
156 noclass: [{
157 file: 'js/daimond.js',
158 find: " btn.classList.toggle('mark', marks);",
159 with: " btn.classList.toggle('mark', false);",
160 }],
161 // The two `+` buttons given one word again. Nothing about the page changes;
162 // the only thing that breaks is that they can no longer be told apart, which
163 // is fault 2 exactly.
164 sameword: [{
165 file: 'i18n/en.js',
166 find: " 'attach.add_mark': 'Mark a folder in',",
167 with: " 'attach.add_mark': 'Attach',",
168 }],
169 // Both rows muted at once: the panel back to saying nothing about the Diamond
170 // in either tree, which is what makes 8 a check about the OTHER tree rather
171 // than a restatement of 1 and 4.
172 panelmute: [
173 {
174 file: 'js/daimond.js',
175 find: " async function paintReach() {\n if (!reachEl) return;",
176 with: " async function paintReach() {\n if (reachEl) { reachEl.style.display = 'none'; reachEl.innerHTML = ''; }\n if (true) return;",
177 },
178 {
179 file: 'js/daimond.js',
180 find: " if (!currentDiamond) { kitsEl.style.display = 'none'; return; }",
181 with: " if (true) { kitsEl.style.display = 'none'; return; }",
182 },
183 ],
184 // The grant on a folder row hidden until the pointer finds it.
185 hoveronly: [{
186 file: 'css/files.css',
187 find: '.attach-btn.mark { visibility: visible; }',
188 with: '.attach-btn.mark { visibility: hidden; }',
189 }],
190};
191
192function damagedFiles() {
193 const byFile = new Map();
194 for (const spec of (BREAKS[BREAK] || [])) {
195 // Off the WORKING copy, not off disk: a break with two edits to one file
196 // would otherwise keep only the last of them and quietly test half of itself.
197 const src = byFile.has(spec.file)
198 ? byFile.get(spec.file)
199 : fs.readFileSync(path.join(WWW, spec.file), 'utf8');
200 const n = src.split(spec.find).length - 1;
201 if (n !== 1) {
202 console.error(`--break ${BREAK}: anchor appears ${n} time(s) in ${spec.file}, wanted 1.`);
203 console.error('The break is stale. A break that patches nothing launders a plain run as proof.');
204 process.exit(1);
205 }
206 byFile.set(spec.file, src.replace(spec.find, spec.with));
207 }
208 return byFile;
209}
210
211async function serveBreaks(page) {
212 if (!BREAK) return;
213 if (!BREAKS[BREAK]) {
214 console.error(`--break ${BREAK}: no such break. Known: ${Object.keys(BREAKS).join(', ')}`);
215 process.exit(1);
216 }
217 for (const [file, body] of damagedFiles()) {
218 await page.route('**/' + file, r => r.fulfill({
219 status: 200,
220 contentType: file.endsWith('.css') ? 'text/css' : 'application/javascript',
221 body,
222 }));
223 }
224}
225
226const s = await open({ name: 'reachlegible', profile: PROFILE, route: serveBreaks });
227const p = s.page;
228
229// Every expected word is asked of the running app rather than spelled here: this
230// app ships eight languages and a test that hard-codes English passes in one.
231const T = (k, v) => p.evaluate(([k, v]) => DaimondI18n.t(k, v || undefined), [k, v || null]);
232
233// ── The fixture ──────────────────────────────────────────────────────────────
234//
235// Written through the engine's own door, which is not fenced because it is not a
236// turn — see the header of dev/harness.mjs.
237const seeded = await p.evaluate(async () => {
238 const M = await import('/pkg/oxedyne_daimond.js');
239 await M.write_file('code/parser/src/lib.rs', 'pub fn parse() {}\n');
240 await M.write_file('code/parser/README.md', '# parser\n');
241 await M.write_file('books/draft.md', '# draft\n');
242 return true;
243});
244check('0a the fixture is written', seeded === true);
245
246await p.click('#new-diamond-btn', { force: true });
247await p.waitForSelector('.dlg-input', { timeout: 10000 });
248await p.fill('.dlg-input', 'Ship a CSV parser');
249await p.click('.dlg-ok', { force: true });
250await sleep(1600);
251// Opened as well as made: `currentDiamond` is what every row of this panel is
252// about, and a Diamond that exists but is not in focus draws no scope row at all.
253await p.$$eval('.diamond-box', els => els[0] && els[0].click());
254await sleep(1200);
255
256const did = await p.evaluate(() => {
257 const d = window.DaimondDiamond && DaimondDiamond.current && DaimondDiamond.current();
258 return d ? d.id : '';
259}).catch(() => '');
260
261async function openPanel() {
262 await p.evaluate(() => window.DaimondPanels && DaimondPanels.show('work'));
263 await sleep(600);
264 await p.evaluate(() => {
265 const b = document.querySelector('#panel-work [data-act="refresh"]');
266 if (b) b.click(); // NOT page.click: a forced click is inert headless
267 });
268 await sleep(1100);
269}
270const setScope = async (which) => {
271 await p.click(`.files-scope-chip[data-scope="${which}"]`, { force: true });
272 await sleep(1100);
273};
274
275/// The reach row exactly as it is on the screen: laid out or not, its chips in
276/// order, and the sentence under them as INK.
277const reach = () => p.evaluate(() => {
278 const el = document.querySelector('#panel-work .files-reach');
279 if (!el) return { present: false };
280 const says = el.querySelector('.files-reach-says');
281 return {
282 present: true,
283 // `offsetParent` AND a non-zero box: a row that is `display:none` and one
284 // that is laid out at nothing are both invisible, and only one of them
285 // answers the attribute.
286 shown: !!el.offsetParent && el.getBoundingClientRect().height > 0,
287 label: (el.querySelector('.files-reach-label') || {}).textContent || '',
288 chips: [...el.querySelectorAll('.files-reach-chip')].map(c => ({
289 path: c.dataset.path || '', text: c.textContent || '',
290 ro: c.classList.contains('ro'),
291 })),
292 says: says ? (says.textContent || '') : null,
293 // The zero state has a class of its own so it can be coloured; read it
294 // back so "it said something" and "it said the alarming thing" are two
295 // different questions.
296 saysNone: !!(says && says.classList.contains('none')),
297 saysShown: !!(says && says.offsetParent && says.getBoundingClientRect().height > 0),
298 markHere: (() => {
299 const b = el.querySelector('[data-act="mark-here"]');
300 return b ? { path: b.dataset.path || '', text: b.textContent || '' } : null;
301 })(),
302 };
303});
304
305/// What the engine would actually be told, from the call `scopeAgentTo` makes.
306const bounds = (id) => p.evaluate((i) => window.DaimondDiamond.bounds(i), id);
307
308/// The Toolchains row, as furniture that is there or is not.
309const kits = () => p.evaluate(() => {
310 const el = document.querySelector('#panel-work .files-kits');
311 if (!el) return { present: false };
312 return {
313 present: true,
314 shown: !!el.offsetParent && el.getBoundingClientRect().height > 0,
315 chips: [...el.querySelectorAll('.files-kit-chip')].map(c => c.textContent),
316 };
317});
318
319await openPanel();
320
321// ── 1. The row is in the tree the fault happened in ──────────────────────────
322const scopeNow = await p.evaluate(() => window.DaimondFilesPanelScope || null);
323const active = await p.evaluate(() => {
324 const c = document.querySelector('.files-scope-chip.active');
325 return c ? c.dataset.scope : null;
326});
327check('1a the panel opens on Everything, as it always has', active === 'all',
328 `active chip = ${active}${scopeNow ? '' : ''}`);
329
330const r0 = await reach();
331check('1b THE REACH ROW IS DRAWN ON `Everything`, which is where the evening went',
332 !!r0 && r0.shown === true,
333 r0 ? `present=${r0.present} laid out=${r0.shown}` : 'no row');
334check('1c and it is labelled, so the chips above it read as a view control',
335 !!r0 && r0.label.trim().length > 0, r0 ? JSON.stringify(r0.label) : '');
336
337// ── 2. It agrees with the fence ──────────────────────────────────────────────
338//
339// Against `DaimondDiamond.bounds`, which is what `scopeAgentTo` hands the
340// engine. A row built from the panel's own cached `attached` would draw the same
341// pixels and fail here the moment the two differ.
342const b0 = await bounds(did);
343const wantPaths = [b0.own_dir].concat(b0.attached || []);
344check('2a THE CHIPS ARE THE FENCE, path for path and in order',
345 !!r0 && JSON.stringify(r0.chips.map(c => c.path)) === JSON.stringify(wantPaths),
346 `row ${JSON.stringify(r0 ? r0.chips.map(c => c.path) : null)} vs fence ${JSON.stringify(wantPaths)}`);
347check('2b and the Diamond’s own folder is the first of them, as diamond_bounds declares it',
348 !!r0 && r0.chips.length > 0 && r0.chips[0].path === b0.own_dir,
349 r0 && r0.chips[0] ? r0.chips[0].path : '');
350
351// ── 3. The zero state, said out loud ─────────────────────────────────────────
352check('3a nothing is marked in yet, which is the state under test',
353 Array.isArray(b0.attached) && b0.attached.length === 0,
354 JSON.stringify(b0.attached));
355const zeroWords = await T('dws.reach_none', { name: 'Ship a CSV parser' });
356check('3b THE PANEL SAYS SO, IN INK, WITH NOTHING HOVERED',
357 !!r0 && r0.saysNone === true && r0.saysShown === true && (r0.says || '').trim().length > 0,
358 r0 ? `none=${r0.saysNone} laid out=${r0.saysShown} text=${JSON.stringify((r0.says || '').slice(0, 60))}` : '');
359check('3c and it names the Diamond, so it is about the thing in front of him',
360 !!r0 && (r0.says || '').indexOf('Ship a CSV parser') !== -1,
361 r0 ? JSON.stringify((r0.says || '').slice(0, 90)) : '');
362check('3d it is the app’s own sentence for the state, not one this test invented',
363 !!r0 && (r0.says || '').trim() === (zeroWords || '').trim(),
364 JSON.stringify((zeroWords || '').slice(0, 60)));
365await shot(s, 'reach-everything-zero');
366
367// ── 4. Toolchains, in the tree the fault happened in ─────────────────────────
368const k0 = await kits();
369check('4a THE TOOLCHAINS ROW IS OFFERED ON `Everything` TOO',
370 !!k0 && k0.shown === true, k0 ? `present=${k0.present} laid out=${k0.shown}` : 'no row');
371check('4b and it offers the whole set, not a subset of it',
372 !!k0 && k0.chips.length === 5, k0 ? JSON.stringify(k0.chips) : '');
373
374// ── 5. One press, from the folder already on screen ──────────────────────────
375//
376// The route he had to take was: open the `+`, walk the picker's tree back down
377// to the folder he was standing in, tick it, press OK.
378const walked = await p.evaluate(() => {
379 const rows = [...document.querySelectorAll('#panel-work .files-tree .files-row')];
380 const hit = rows.find(r => ((r.querySelector('.files-name') || {}).textContent || '')
381 .replace(/^[^A-Za-z0-9._-]+/, '').trim() === 'code');
382 if (!hit) return false;
383 hit.click();
384 return true;
385});
386check('5a the tree walks into a folder', walked === true);
387await sleep(1100);
388
389const r1 = await reach();
390check('5b THE PANEL OFFERS TO MARK THE FOLDER ON SCREEN',
391 !!r1 && !!r1.markHere && r1.markHere.path === 'code',
392 r1 ? JSON.stringify(r1.markHere) : '');
393
394await p.evaluate(() => {
395 const b = document.querySelector('#panel-work .files-reach [data-act="mark-here"]');
396 if (b) b.click();
397});
398await sleep(1400);
399
400const b1 = await bounds(did);
401check('5c ONE PRESS AND THE FENCE MOVED',
402 Array.isArray(b1.attached) && b1.attached.indexOf('code') !== -1,
403 JSON.stringify(b1.attached));
404
405const r2 = await reach();
406check('5d and the row moved with it: a chip for the folder now stands in the fence',
407 !!r2 && r2.chips.some(c => c.path === 'code'),
408 r2 ? JSON.stringify(r2.chips.map(c => c.path)) : '');
409check('5e and the alarm is gone, because the state it described is gone',
410 !!r2 && r2.saysNone === false && (r2.says || '').trim().length > 0,
411 r2 ? `none=${r2.saysNone} text=${JSON.stringify((r2.says || '').slice(0, 60))}` : '');
412check('5f and it does not offer to mark a folder that is already inside the fence',
413 !!r2 && r2.markHere === null, r2 ? JSON.stringify(r2.markHere) : '');
414await shot(s, 'reach-everything-marked');
415
416// ── 6. The `+` says what it does ─────────────────────────────────────────────
417//
418// The crystal footer's `+` was labelled "Attach" while marking, because on a
419// Diamond attaching IS marking. TEXT, not `title`: the tooltip was the fault.
420const stripOpen = await p.evaluate(() => {
421 const strip = document.getElementById('arte-strip');
422 if (!strip) return false;
423 strip.click();
424 return true;
425});
426check('6a the artefact strip opens', stripOpen === true);
427await sleep(900);
428const plus = await p.evaluate(() => {
429 const b = document.querySelector('#arte-list [data-act="attach-add"]');
430 if (!b) return null;
431 return { text: (b.textContent || '').trim(), title: b.title || '', grants: b.dataset.grants || '' };
432});
433const markWord = (await T('attach.add_mark')) || '';
434const plainWord = (await T('attach.add')) || '';
435check('6b the Diamond’s `+` carries a WORD and not only a glyph',
436 !!plus && plus.text.replace(/^\+\s*/, '').length > 0, JSON.stringify(plus));
437check('6c AND IT IS THE MARKING WORD, because on a Diamond this `+` widens the fence',
438 !!plus && plus.text.indexOf(markWord) !== -1,
439 `button ${JSON.stringify(plus && plus.text)} vs ${JSON.stringify(markWord)}`);
440check('6d and it does not read as the one that merely attaches',
441 !!plus && markWord !== plainWord && plus.text.replace(/^\+\s*/, '').trim() !== plainWord.trim(),
442 `mark=${JSON.stringify(markWord)} plain=${JSON.stringify(plainWord)}`);
443check('6e and it says so to a screen reader as well as to an eye',
444 !!plus && plus.grants === '1' && plus.title.length > 0, JSON.stringify(plus));
445await shot(s, 'reach-plus-labelled');
446
447// ── 7. A grant on a folder row is not behind a hover ─────────────────────────
448//
449// Measured with the pointer nowhere near it, and as the COMPUTED visibility: a
450// class is not evidence, because the rule that reads it is in a file this test
451// can also damage.
452await p.mouse.move(5, 5);
453await sleep(200);
454const rowMark = await p.evaluate(() => {
455 const rows = [...document.querySelectorAll('#panel-work .files-tree .files-row.dir')];
456 const row = rows.find(r => r.querySelector('.attach-btn'));
457 if (!row) return null;
458 const b = row.querySelector('.attach-btn');
459 const cs = getComputedStyle(b);
460 return {
461 path: row.dataset.path || '',
462 mark: b.classList.contains('mark'),
463 vis: cs.visibility,
464 display: cs.display,
465 title: b.title || '',
466 };
467});
468check('7a a folder row carries the control that marks it in',
469 !!rowMark && rowMark.mark === true, JSON.stringify(rowMark));
470check('7b AND IT IS ON THE SCREEN WITH NOTHING HOVERED',
471 !!rowMark && rowMark.vis === 'visible' && rowMark.display !== 'none',
472 rowMark ? `visibility=${rowMark.vis} display=${rowMark.display}` : '');
473check('7c and it names the marking, not the attaching',
474 !!rowMark && rowMark.title.indexOf('Ship a CSV parser') !== -1,
475 rowMark ? JSON.stringify(rowMark.title) : '');
476
477// ── 8. And the row survives the switch it was hidden behind ──────────────────
478await setScope('diamond');
479const r3 = await reach();
480const b3 = await bounds(did);
481check('8a the row is drawn in the Diamond tree as well',
482 !!r3 && r3.shown === true, r3 ? `laid out=${r3.shown}` : 'no row');
483check('8b and still agrees with the fence there',
484 !!r3 && JSON.stringify(r3.chips.map(c => c.path))
485 === JSON.stringify([b3.own_dir].concat(b3.attached || [])),
486 r3 ? JSON.stringify(r3.chips.map(c => c.path)) : '');
487const k3 = await kits();
488check('8c and the Toolchains row did not move either', !!k3 && k3.shown === true);
489await shot(s, 'reach-diamond-tree');
490
491// ── 9. The console ───────────────────────────────────────────────────────────
492const errs = errors(s).filter(e => !/favicon|404|401|402|502|Bad Gateway|Account service|net::ERR/.test(e));
493check('9 nothing throws while all this happens', errs.length === 0, errs.slice(0, 2).join(' | '));
494
495await s.close();
496
497console.log(`\n${ok.length} passed, ${bad.length} failed`);
498if (BREAK) {
499 console.log(bad.length
500 ? `--break ${BREAK}: reddened ${bad.length} check(s), as it must`
501 : `--break ${BREAK}: CHANGED NOTHING — the check it names is not testing what it says`);
502 process.exit(bad.length ? 0 : 1);
503}
504if (bad.length) console.log('FAILED:\n ' + bad.join('\n '));
505process.exit(bad.length ? 1 : 0);