Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_netchip.mjs

33.2 KiB, 1 run

created by r2519314175:547, 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_netchip.mjs — the permissions button says what it governs, shows whether
2// THIS chat's commands have the network, and lets that be granted and taken back.
3//
4// THE DEFECT, reported on 2026-08-19 by the owner, about a dialog he had been
5// approving for weeks: *"I always click approve. I never even understand the
6// message itself."* Three things were wrong and only the first is cosmetic.
7//
8// 1. THE HOVER NAMED THE CATEGORY. `permmode.chip_help` was "Permission mode:
9// what Daimond does without asking" — which is what somebody hovering a
10// button marked Guarded can already see. The sentence that answers them was
11// written, and sat a click away inside the popover as `guarded_blurb`.
12// 2. THE DIALOG SAID "THIS TURN" AND MEANT THIS CHAT. The answer is kept on the
13// chat's own engine object (`ensureApp`, daimond.js), which lasts until the
14// tab reloads — so a reader who took the word at face value expected to be
15// asked again next message and was not.
16// 3. AND THERE WAS NO WAY BACK. `set_net_consent` writes once and never
17// overwrites, which is right for the tool loop and wrong for a person: an
18// answer given in passing could not be changed, and a chat that had lost the
19// network showed nothing anywhere to say so.
20//
21// A FIRST ATTEMPT FIXED 1 AND 3 AND LEFT THE ASKING EXACTLY WHERE IT WAS, which
22// the same reporter said in the same words a day later: "I thought we got rid of
23// this bullshit!" The answer lived on the chat's engine object, built per chat and
24// gone on reload, so it had to be given again in every new chat and after every
25// refresh -- and it was only ever given by somebody who had gone into the menu
26// looking for it. It was made answerable, not answered. The standing choice is
27// what actually removes the interruption, and check 7 is the one that proves it.
28//
29// NINE PROPERTIES:
30//
31// 1. THE HOVER NAMES THE RUNG. Asserted as the pair — it carries the rung's own
32// word AND is not the old category sentence — because a tooltip that merely
33// changed would satisfy either half alone.
34// 2. A CLEAN CHAT SAYS SO AND IS NOT MARKED. The section is drawn, it says
35// nothing is withheld, and the chip carries no mark.
36// 3. A CHAT THAT HAS READ A STRANGER'S WORDS IS MARKED, on the button, where it
37// can be seen without opening anything.
38// 4. GRANTING IT MOVES THE ENGINE. Read from `net_state()` and not from the
39// button that was pressed: a control that relabels itself having changed
40// nothing is the failure this is for.
41// 5. AND IT CAN BE TAKEN BACK, which is the thing that was impossible. Both
42// directions, because a one-way control that happens to start in the other
43// state would satisfy check 4 on its own.
44// 6. THE SECTION IS NOT A FOURTH RUNG. Still exactly three radios in the ladder,
45// with the network under a head of its own -- the rungs are one policy for the
46// whole app and this is one answer, and giving it the ladder's shape would say
47// the two were the same kind of thing.
48// 7. AND A CHAT THAT DID NOT EXIST WHEN YOU ANSWERED IS NOT ASKED. This is the
49// property the whole thing is for. Measured at the engine, because
50// `NetStep::Ask` is the only branch that raises a dialog: a NEW chat that
51// reports `allowed` while marked has had the question answered before it could
52// be put. Watching for a dialog instead would also pass on a chat that simply
53// never got as far as a command.
54// 8. AND THE DIALOG CAN END THE ASKING IN ONE GESTURE -- a TICK the person makes,
55// clicked on the card that is actually up.
56// 9. WHILE A YES NOBODY TICKED CHANGES NOTHING ELSE. Added 2026-08-28, and it is
57// the one that matters most. Until then ANY yes here called
58// `setStandingNet('allow')`, which writes the app-wide standing answer and
59// pushes `allow` into every engine that exists. A NO said in one chat was
60// silently overwritten by a YES said in another about a DIFFERENT command:
61// the first chat's next command reached the network, nobody was asked, and the
62// permissions button read "Always" -- a setting the user never chose, standing
63// through every reload. The reasoning in the code argued that only a yes should
64// become standing because refusing once is not refusing for ever, which is true
65// and points the wrong way: the direction being made permanent and global was
66// the PERMISSIVE one. `src/tools.rs` says of `override_net_consent`, where that
67// path ends, that NOTHING IN THE TOOL LOOP MAY CALL IT.
68//
69// PROVED AGAINST BROKEN CODE FIRST, each break chosen to survive every check but
70// the ones under test:
71//
72// node dev/verify_netchip.mjs --break generic # 1-2: the hover names the category again
73// node dev/verify_netchip.mjs --break nomark # 3, 5: a cut chat looks like a clean one
74// node dev/verify_netchip.mjs --break stuck # 4-5: the choice is stored and never applied
75// node dev/verify_netchip.mjs --break perchat # 7: the answer dies with the chat
76// node dev/verify_netchip.mjs --break nosection# 8 checks, crudely
77// node dev/verify_netchip.mjs --break stickyyes# 9: every yes becomes standing again
78// node dev/verify_netchip.mjs --break notick # 8-9: no control to press at all
79// node dev/verify_netchip.mjs --break yesnotsticky # 8: the tick is drawn and does nothing
80// node dev/verify_netchip.mjs --break deafnote # 10: a command with no network says nothing
81// node dev/verify_netchip.mjs # and then, clean
82//
83// `perchat` is the SHARP one: it restores the reported defect exactly -- the
84// standing answer stored, applied to the engines that exist, and never handed to
85// the next one -- and it reddens check 7 and NOTHING ELSE. Every check before it
86// passes, because everything before it was working the day the defect was
87// reported. That is what makes 7 a test of its own and not a rider on the others.
88//
89// `stickyyes` is the SHARPEST, and it is sharp in the other direction: it restores
90// the consent defect exactly -- every yes made standing, whatever the box says --
91// and reddens check 9 and nothing else. Everything before it passes, because
92// everything before it is a property the standing answer is supposed to have and
93// still has. A defect that leaves every existing check green is what this file is
94// for; the previous one sat here for a week.
95//
96// `stuck` is its sibling one layer up: the choice recorded and never pushed into
97// any engine. A Rust break exists for the write-once rule that sits under both --
98// `override_net_consent` folded back into `set_net_consent` -- which no served file
99// can reach; it is held by `test_the_user_can_take_an_answer_back_though_the_
100// dialog_cannot`, run against that exact edit and red on "a no could not be taken
101// back".
102//
103// `nosection` is the crude one: eight checks at once, so it proves nothing about
104// any check after the first. It is kept because the section being absent is a real
105// way this can break, not because it tests anything sharply.
106//
107// THE MARK IS SET DIRECTLY, through `DaimondCore.markRead`. It is the same one-way
108// flag every real path ends at, so the STATE under test is the real state; which
109// reads produce it is a Rust question and is answered there.
110//
111// AND THE STANDING CHOICE IS ARMED OUTRIGHT at the start. It is `localStorage` and
112// the harness reuses its profile, so a run that ended on "always allow" left the
113// next one starting there -- which duly failed checks 3 and 4 against an app that
114// was working perfectly. A probe that assumes its starting state measures the last
115// run.
116//
117// eval "$(bash dev/world.sh 4 --up)"
118// node dev/verify_netchip.mjs
119//
120// Needs dev/serve.mjs and the mock. No gateway, no wasm rebuild.
121import fs from 'node:fs';
122import path from 'node:path';
123import { fileURLToPath } from 'node:url';
124import { open, newChat, scratch, shot } from './harness.mjs';
125
126const HERE = path.dirname(fileURLToPath(import.meta.url));
127const WWW = path.join(HERE, '..', 'www');
128
129const BREAK = (() => {
130 const i = process.argv.indexOf('--break');
131 return i > 0 ? String(process.argv[i + 1] || '') : '';
132})();
133
134// Each break is one real edit to one real file, served in its place.
135const BREAKS = {
136 // The tooltip as it stood: the category, which the word on the button already gives.
137 generic: {
138 file: 'js/handmode.js',
139 find: "\t\t\tchip.title = label(current) + ' — ' + blurb(current);",
140 with: "\t\t\tchip.title = t('permmode.chip_help');",
141 },
142 // The mark taken off, so a chat with no network looks exactly like one with it.
143 nomark: {
144 file: 'js/handmode.js',
145 find: "\t\t\tchip.classList.toggle('net-cut', cut);",
146 with: "\t\t\tchip.classList.toggle('net-cut', false);",
147 },
148 // The button that can only ever say yes — `override_net_consent` folded back
149 // into the write-once rule, restored where a served file can reach it.
150 // The standing answer stored but never pushed into an engine -- which is what
151 // "I granted it and it asked me again" actually looks like in code.
152 stuck: {
153 file: 'js/handmode.js',
154 find: "\t\tif (typeof cfg.netApplyAll === 'function') {",
155 with: "\t\tif (false) {",
156 },
157 // The dialog's yes answering this chat's engine and nothing else -- exactly
158 // what it did through three reports. The tick is on screen and does nothing.
159 yesnotsticky: {
160 file: 'js/daimond.js',
161 find: "\t\t\tif (netStanding && window.DaimondHandMode && DaimondHandMode.setStandingNet) {",
162 with: "\t\t\tif (false) {",
163 },
164 // THE SHARP ONE FOR CHECK 9, and the defect it restores is the worst this file
165 // has held: EVERY yes becomes the app-wide standing answer, whatever the box
166 // says. That is what the code did until 2026-08-28 -- `setStandingNet('allow')`
167 // on any yes, which writes `daimond-net-standing` AND pushes `allow` into every
168 // engine that exists (`netApplyAll`). A no said in one chat was overwritten by a
169 // yes said in another about a different command, with nothing on screen to say
170 // so. It reddens 9 and NOTHING ELSE: everything before it is a property the
171 // standing answer is supposed to have, and it still has them.
172 stickyyes: {
173 file: 'js/daimond.js',
174 find: "\t\t\tnetStanding = !!netAns.standing;",
175 with: "\t\t\tnetStanding = true;",
176 },
177 // And the other way: no control at all, so "one yes is enough" has no way to be
178 // said. Reddens 8, and 9's first check with it.
179 notick: {
180 file: 'js/daimond.js',
181 find: "\t\t\t\t\trow.appendChild(box);",
182 with: "\t\t\t\t\tif (false) row.appendChild(box);",
183 },
184 // Stored, applied to the engines that exist, and forgotten by the next one. The
185 // exact defect reported: an answer whose lifetime is one chat.
186 perchat: {
187 file: 'js/daimond.js',
188 find: "\t\t\tvar st = window.DaimondHandMode ? DaimondHandMode.standingNet() : '';",
189 with: "\t\t\tvar st = '';",
190 },
191 // The line saying a command had no network, drawn from a result that says it
192 // had none. Nothing else in this file touches that block, so it reddens 9 and
193 // only 9.
194 deafnote: {
195 file: 'js/daimond.js',
196 find: "\t\t\treturn !!(Wasm && Wasm.ran_without_net",
197 with: "\t\t\treturn false && !!(Wasm && Wasm.ran_without_net",
198 },
199 // No chat can be asked, so the whole section goes.
200 nosection: {
201 file: 'js/handmode.js',
202 find: "\t\tif (typeof cfg.netGet !== 'function') return '';",
203 with: "\t\tif (typeof cfg.netGet === 'function') return '';",
204 },
205};
206if (BREAK && !BREAKS[BREAK]) {
207 console.error(`unknown break '${BREAK}'; one of: ${Object.keys(BREAKS).join(', ')}`);
208 process.exit(2);
209}
210
211let bad = 0;
212const check = (pass, name, detail) => {
213 if (!pass) bad++;
214 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
215};
216
217const stub = async (page) => {
218 if (!BREAK) return;
219 const spec = BREAKS[BREAK];
220 const src = fs.readFileSync(path.join(WWW, spec.file), 'utf8');
221 // An anchor that is not there exactly once patches nothing and the run would
222 // pass quietly, which is worse than a red.
223 if (src.split(spec.find).length !== 2) {
224 console.error(`break '${BREAK}': its anchor is not in ${spec.file} exactly once`);
225 process.exit(2);
226 }
227 const body = src.replace(spec.find, spec.with);
228 await page.route('**/' + spec.file, (r) => r.fulfill({
229 status: 200, contentType: 'application/javascript', body,
230 }));
231};
232
233const s = await open({
234 name: 'netchip',
235 profile: scratch('pw', 'netchip' + (BREAK ? '-' + BREAK : '')),
236 route: stub,
237});
238const { page: p } = s;
239if (BREAK) console.log(`\n*** RUNNING UNDER --break ${BREAK}: failures below are the point ***\n`);
240
241// The popover is rebuilt on every open, so it is opened fresh for each reading
242// rather than left up: a stale one would report the state at the moment it opened.
243const popOpen = async () => {
244 await p.evaluate(() => {
245 const pop = document.getElementById('hand-mode-pop');
246 if (!pop.hidden) document.getElementById('hand-mode-chip').click();
247 });
248 await p.click('#hand-mode-chip');
249 await p.waitForTimeout(400);
250};
251const popClose = async () => {
252 await p.evaluate(() => {
253 const pop = document.getElementById('hand-mode-pop');
254 if (!pop.hidden) document.getElementById('hand-mode-chip').click();
255 });
256 await p.waitForTimeout(250);
257};
258const readPop = () => p.evaluate(() => {
259 const pop = document.getElementById('hand-mode-pop');
260 const chip = document.getElementById('hand-mode-chip');
261 const heads = [...pop.querySelectorAll('.pop-head')].map(e => e.textContent.trim());
262 const now = pop.querySelector('.net-now');
263 const opts = [...pop.querySelectorAll('.net-opt')];
264 return {
265 heads,
266 rungs: pop.querySelectorAll('.mode-row input[type=radio]').length,
267 now: now ? now.textContent.trim() : '',
268 btn: opts.map(e => e.textContent.trim()).join(' | '),
269 hasBtn: opts.length === 3,
270 chosen: (opts.find(e => e.getAttribute('aria-pressed') === 'true') || {}).textContent || '',
271 marked: chip.classList.contains('net-cut'),
272 title: chip.title || '',
273 aria: chip.getAttribute('aria-label') || '',
274 engine: (window.DaimondCore && DaimondCore.netState) ? DaimondCore.netState() : '',
275 };
276});
277
278try {
279 // ARM THE DEFAULT OUTRIGHT. The standing answer is `localStorage` and the
280 // harness reuses its profile, so a previous run that ended on "always allow"
281 // left this one starting there -- and checks 3 and 4 duly failed against a chat
282 // that was working correctly. A probe that assumes its starting state is a probe
283 // measuring the last run.
284 // No reload needed: `standing()` reads the key on every call, and no chat engine
285 // exists yet to be carrying an older answer.
286 await p.evaluate(() => { try { localStorage.removeItem('daimond-net-standing'); } catch (e) {} });
287
288 await newChat(s);
289 // A turn, so the chat has an engine to answer for. Nothing here reads anything
290 // from outside — that is check 2's whole point.
291 await p.fill('#chat-input', '@text hello');
292 await p.click('#chat-send');
293 await p.waitForTimeout(3500);
294
295 // ── 1. The hover names the rung ──────────────────────────────
296 await popOpen();
297 const clean = await readPop();
298 await popClose();
299 const rung = await p.evaluate(() =>
300 document.getElementById('hand-mode-chip-txt').textContent.trim());
301 // THE PAIR. "Names the rung" alone would pass on a tooltip that says "Guarded"
302 // and nothing else; "is not the old sentence" alone would pass on any rewording.
303 check(clean.title.includes(rung) && clean.title.length > rung.length + 12,
304 'THE HOVER NAMES THIS RUNG AND WHAT IT DOES',
305 JSON.stringify(clean.title.slice(0, 96)));
306 // AGAINST THE STRING ITSELF, not against the words it used to hold. Written the
307 // second way first, and `--break generic` did not redden it: the break restores
308 // `permmode.chip_help`, whose wording had also been shortened, so a literal from
309 // the old copy matched nothing and the check could not fail. Read the key.
310 const generic = await p.evaluate(() =>
311 (window.DaimondI18n ? DaimondI18n.t('permmode.chip_help') : ''));
312 check(!!generic && clean.title !== generic,
313 'and is not the category sentence, whatever that sentence now says',
314 `chip_help=${JSON.stringify(generic)}`);
315
316 // ── 2 and 6. A clean chat, and the section's altitude ────────
317 // Against the app's own string: the head was reworded once already, and a
318 // literal from the old copy would have made this check unable to fail.
319 const netHead = await p.evaluate(() => DaimondI18n.t('permmode.net_head'));
320 check(!!netHead && clean.heads.includes(netHead),
321 'THE POPOVER HAS A SECTION FOR THE NETWORK', JSON.stringify(clean.heads));
322 check(/nothing/i.test(clean.now) && clean.engine === 'open',
323 'A CLEAN CHAT SAYS NOTHING IS WITHHELD, and the engine agrees',
324 `${JSON.stringify(clean.now.slice(0, 60))} engine=${clean.engine}`);
325 check(!clean.marked, 'AND THE BUTTON IS NOT MARKED', `net-cut=${clean.marked}`);
326 // Not a fourth rung. A per-chat state sitting in the ladder would read as a
327 // setting that outlives the chat, which is the one thing it is not.
328 check(clean.rungs === 3,
329 'and the ladder is still three rungs, with the chat under a head of its own',
330 `${clean.rungs} radios, heads ${JSON.stringify(clean.heads)}`);
331
332 // ── 3. A chat that has read a stranger's words ───────────────
333 const marked = await p.evaluate(() =>
334 !!(window.DaimondCore && DaimondCore.markRead && DaimondCore.markRead()));
335 check(marked, 'the chat can be marked as having read outside content',
336 marked ? '' : 'DaimondCore.markRead did not take');
337 // A REAL TURN, and not a direct redraw. The mark goes on during a turn and the
338 // app refreshes the chip when that turn ends; calling the redraw from here would
339 // prove the drawing works while leaving the wiring that calls it unproved, and
340 // deleting that wiring would not redden anything.
341 await p.fill('#chat-input', '@text and again');
342 await p.click('#chat-send');
343 await p.waitForTimeout(3500);
344 // Read BEFORE anything is opened — that is the property.
345 const chipCut = await p.evaluate(() => ({
346 marked: document.getElementById('hand-mode-chip').classList.contains('net-cut'),
347 aria: document.getElementById('hand-mode-chip').getAttribute('aria-label') || '',
348 }));
349 await popOpen();
350 const cut = await readPop();
351 cut.marked = chipCut.marked;
352 cut.aria = chipCut.aria;
353 await shot(s, 'netchip-cut');
354 check(cut.engine === 'cut',
355 'a marked chat has lost the network', `engine=${cut.engine}`);
356 check(cut.marked,
357 'AND THE BUTTON SAYS SO WITHOUT ANYTHING BEING OPENED',
358 `net-cut=${cut.marked}, aria=${JSON.stringify(cut.aria)}`);
359 check(/no network|without.*network/i.test(cut.now) && cut.hasBtn,
360 'and the section says it in a sentence, with all three ways out beside it',
361 `${JSON.stringify(cut.now.slice(0, 70))} choices=${JSON.stringify(cut.btn)}`);
362
363 // ── 4. Granting it moves the ENGINE ──────────────────────────
364 const pick = async (label) => {
365 await p.evaluate((l) => {
366 const b = [...document.querySelectorAll('#hand-mode-pop .net-opt')]
367 .find(x => x.textContent.trim() === l);
368 if (b) b.click();
369 }, label);
370 await p.waitForTimeout(500);
371 };
372 const ALWAYS = await p.evaluate(() => DaimondI18n.t('permmode.net_always'));
373 const EACH = await p.evaluate(() => DaimondI18n.t('permmode.net_each'));
374 const NEVER = await p.evaluate(() => DaimondI18n.t('permmode.net_never'));
375 await pick(ALWAYS);
376 const on = await readPop();
377 check(on.engine === 'allowed',
378 'GRANTING IT MOVES THE ENGINE, not merely the label that was pressed',
379 `engine=${on.engine}`);
380 check(!on.marked,
381 'and the button stops saying the network is gone', `net-cut=${on.marked}`);
382
383 // ── 5. And it can be taken back ──────────────────────────────
384 await pick(NEVER);
385 const off = await readPop();
386 check(off.engine === 'refused',
387 'AND IT CAN BE TAKEN BACK — the thing that was impossible',
388 `engine=${off.engine}`);
389 check(off.marked,
390 'with the button marked again, so the two never disagree',
391 `net-cut=${off.marked}`);
392 // The default is reachable again, which a two-state toggle would have lost.
393 await pick(EACH);
394 const back = await readPop();
395 check(back.engine === 'cut',
396 'and "ask once per chat" is still reachable, so nothing is a one-way door',
397 `engine=${back.engine}`);
398 await popClose();
399
400 // ── 7. THE ONE THAT MATTERS: a NEW chat is not asked again ───
401 //
402 // The reported defect, in the reporter's words: "I thought we got rid of this
403 // bullshit!" It had not been got rid of. The answer lived on the chat's engine,
404 // which is built per chat and does not survive a reload, so every new chat put
405 // the question again however many times it had been answered.
406 //
407 // Measured at the ENGINE and not by watching for a dialog, because `NetStep::Ask`
408 // is the only branch that raises one: a chat that reports `allowed` while marked
409 // has had the question answered before it could be put. A dialog watcher would
410 // also pass on a chat that simply never ran a command.
411 await popOpen();
412 await pick(ALWAYS);
413 await popClose();
414 await newChat(s);
415 await p.fill('#chat-input', '@text a brand new chat');
416 await p.click('#chat-send');
417 await p.waitForTimeout(3500);
418 await p.evaluate(() => DaimondCore.markRead());
419 await p.fill('#chat-input', '@text and it reads something');
420 await p.click('#chat-send');
421 await p.waitForTimeout(3500);
422 const fresh = await p.evaluate(() => ({
423 engine: DaimondCore.netState(),
424 marked: document.getElementById('hand-mode-chip').classList.contains('net-cut'),
425 dialog: !![...document.querySelectorAll('.dlg-card')].filter(c => c.getClientRects().length).length,
426 }));
427 check(fresh.engine === 'allowed',
428 'A CHAT THAT NEVER EXISTED WHEN YOU ANSWERED IS NOT ASKED AGAIN',
429 `engine=${fresh.engine}`);
430 check(!fresh.marked && !fresh.dialog,
431 'and nothing on screen interrupts it',
432 `net-cut=${fresh.marked}, dialog=${fresh.dialog}`);
433
434 // ── 8. A YES *THE USER MAKES STANDING* IS THE LAST ONE ───────
435 //
436 // The standing choice above is only reachable by somebody who went looking for
437 // it in a menu. Nobody did: three reports came from a person answering the
438 // DIALOG, whose yes went to the chat's own engine and died with it. A person who
439 // has said yes has consented, and asking again in the next chat treats that
440 // answer as though it had never been given.
441 //
442 // SO THE DIALOG STILL ENDS IT IN ONE GESTURE -- it is now a TICK the person
443 // makes rather than something that happens to them for saying yes once. The box
444 // is ticked here, and check 9 is the other half: a yes with the box left alone
445 // changes nothing outside the command it was given about.
446 //
447 // DRIVEN THROUGH THE APP'S OWN GATE, `window.__daimondEgressAllowed` -- the
448 // global the wasm calls when a command wants the network -- and the dialog it
449 // raises is clicked like a person clicks it. The first version of this called
450 // `setStandingNet` directly instead, and `--break yesnotsticky` reddened
451 // NOTHING: the check proved the recorder worked and said nothing about whether
452 // the dialog ever reaches it, which is the entire defect. A break that does not
453 // go red is a finding, and this one's finding was about the check.
454 await p.evaluate(() => { try { localStorage.removeItem('daimond-net-standing'); } catch (e) {} });
455 await newChat(s);
456 await p.fill('#chat-input', '@text before the yes');
457 await p.click('#chat-send');
458 await p.waitForTimeout(3500);
459 const gate = p.evaluate(() => window.__daimondEgressAllowed(JSON.stringify({
460 tool: 'run_net', url: 'cargo build --release', detail: '/home/jason/usr/code',
461 })));
462 await p.waitForSelector('.dlg-card', { timeout: 8000 });
463 // THE TICK, then OK. Clicked as a person clicks it, on the box in the card that
464 // is actually up: a `standing: true` posted into the handler from here would
465 // prove the recorder works and say nothing about whether any control on screen
466 // reaches it, which is the same fault `--break yesnotsticky` was written to
467 // catch the first time.
468 const tickable = await p.evaluate(() => {
469 const card = [...document.querySelectorAll('.dlg-card')].filter(c => c.getClientRects().length).pop();
470 const box = card && card.querySelector('.net-standing-box');
471 if (!box) return false;
472 box.click();
473 return !!box.checked;
474 });
475 check(tickable,
476 'THE DIALOG OFFERS A CONTROL THE USER PRESSES to make an answer standing',
477 tickable ? '' : 'no .net-standing-box in the card, or it would not tick');
478 await p.evaluate(() => {
479 const card = [...document.querySelectorAll('.dlg-card')].filter(c => c.getClientRects().length).pop();
480 card.querySelector('.dlg-ok').click();
481 });
482 const verdict = await gate;
483 check(verdict === 'allow-net',
484 'the network dialog is raised and a yes comes back as a yes', String(verdict));
485 await newChat(s);
486 await p.fill('#chat-input', '@text a chat made after the yes');
487 await p.click('#chat-send');
488 await p.waitForTimeout(3500);
489 await p.evaluate(() => DaimondCore.markRead());
490 const after = await p.evaluate(() => DaimondCore.netState());
491 check(after === 'allowed',
492 'A YES THE USER TICKED IS THE LAST ONE — a later chat is never asked',
493 `engine=${after}`);
494
495 // ── 9. AND A YES NOBODY TICKED CHANGES NOTHING ELSE ──────────
496 //
497 // THE CHECK THAT MATTERS, and the one nothing above it could have caught. Until
498 // 2026-08-28 ANY yes in this dialog called `setStandingNet('allow')`, which
499 // writes `daimond-net-standing` -- the app-wide standing answer -- and pushes
500 // `allow` into every chat that already has an engine. So:
501 //
502 // two chats open; in one the user is asked about a command and says NO; in the
503 // other they are asked about a DIFFERENT command and say yes; and the first
504 // chat's refusal is gone. Its next command reaches the network, nobody is
505 // asked, nothing says the answer was reversed, and the permissions button
506 // reads "Always" -- a setting they never chose, surviving every reload.
507 //
508 // src/tools.rs says of `override_net_consent`, which that path ends at, that
509 // NOTHING IN THE TOOL LOOP MAY CALL IT. This dialog is the tool loop.
510 //
511 // TWO ASSERTIONS AND NOT ONE, because either alone is satisfied by half a fix:
512 // the stored policy is untouched, AND the other chat's engine still refuses. A
513 // build that stopped writing the key but went on calling `netApplyAll` would
514 // pass the first and reverse the user's no all the same.
515 //
516 // HOW THE REFUSING CHAT IS PUT IN THAT STATE: the popover's own "Never" chip,
517 // and then the stored key removed from under it. `setStanding` writes the key
518 // AND every engine, so taking the key away afterwards leaves exactly what a no
519 // in the dialog leaves -- one engine holding a refusal, and no stored policy.
520 // There is no other way in from a served page: a no at the dialog is answered to
521 // the WASM, which is not in the loop when the gate is driven from here.
522 const focusId = () => p.evaluate(() => {
523 try {
524 const f = window.DaimondAttach && window.DaimondAttach.focus();
525 return (f && f.kind === 'chat') ? String(f.id) : '';
526 } catch (e) { return ''; }
527 });
528 const goTo = async (id) => {
529 await p.evaluate((cid) => {
530 const esc = (window.CSS && CSS.escape) ? CSS.escape(cid) : cid;
531 const box = document.querySelector('.session-box[data-id="' + esc + '"]');
532 if (box) box.click();
533 }, id);
534 await p.waitForTimeout(800);
535 };
536
537 // Cleared BEFORE the chat is made: `ensureApp` reads the standing answer into
538 // each new engine, so a chat born under check 8's "always" would start granted.
539 await p.evaluate(() => { try { localStorage.removeItem('daimond-net-standing'); } catch (e) {} });
540 await newChat(s);
541 const chatNo = await focusId();
542 await p.fill('#chat-input', '@text the chat that says no');
543 await p.click('#chat-send');
544 await p.waitForTimeout(3500);
545 await p.evaluate(() => DaimondCore.markRead());
546 await popOpen();
547 await pick(NEVER);
548 await popClose();
549 await p.evaluate(() => { try { localStorage.removeItem('daimond-net-standing'); } catch (e) {} });
550 const noBefore = await p.evaluate(() => DaimondCore.netState());
551 check(noBefore === 'refused' && !!chatNo,
552 'a chat has said no, and nothing is stored — the state a no in the dialog leaves',
553 `engine=${noBefore} id=${JSON.stringify(chatNo)}`);
554
555 // The OTHER chat, a different command, and OK with the box LEFT ALONE.
556 await newChat(s);
557 await p.fill('#chat-input', '@text the chat that says yes');
558 await p.click('#chat-send');
559 await p.waitForTimeout(3500);
560 const gate2 = p.evaluate(() => window.__daimondEgressAllowed(JSON.stringify({
561 tool: 'run_net', url: 'curl https://elsewhere.test/x', detail: '/home/jason/usr',
562 })));
563 await p.waitForSelector('.dlg-card', { timeout: 8000 });
564 const untouched = await p.evaluate(() => {
565 const card = [...document.querySelectorAll('.dlg-card')].filter(c => c.getClientRects().length).pop();
566 const box = card && card.querySelector('.net-standing-box');
567 return !!card && (!box || box.checked === false);
568 });
569 await shot(s, 'netchip-oneoff');
570 await p.evaluate(() => {
571 const card = [...document.querySelectorAll('.dlg-card')].filter(c => c.getClientRects().length).pop();
572 card.querySelector('.dlg-ok').click();
573 });
574 const verdict2 = await gate2;
575 check(untouched && verdict2 === 'allow-net',
576 'the box starts UNTICKED and a plain yes is still a yes for this command',
577 `unticked=${untouched} verdict=${verdict2}`);
578 const stored = await p.evaluate(() => {
579 try { return localStorage.getItem('daimond-net-standing'); } catch (e) { return 'unreadable'; }
580 });
581 check(!stored,
582 'A ONE-OFF YES WRITES NO STANDING POLICY',
583 `daimond-net-standing=${JSON.stringify(stored)}`);
584 await goTo(chatNo);
585 const noAfter = await p.evaluate(() => DaimondCore.netState());
586 check(noAfter === 'refused',
587 "AND THE OTHER CHAT'S NO IS STILL A NO",
588 `engine=${noAfter}`);
589 // ── 10. AND THE COMMAND THAT RAN WITHOUT IT SAYS SO ──────────
590 //
591 // Everything above is about the CHAT's state, on a button in the header. None
592 // of it is what a person meets: they watch a `cargo build` stop halfway with
593 // "failed to fetch" and nothing anywhere says why. `src/tools.rs` writes a note
594 // into the result for exactly that reason -- and wrote it FOR THE MODEL, in
595 // English, inside brackets, so the person learned the reason only if the model
596 // chose to relay it. A capability with a model-facing surface and no
597 // user-facing one, recorded on 2026-08-13 and still standing on 2026-08-28.
598 //
599 // THE FIXTURE IS THE CONTRACT ITSELF. `NO_NET_MARK` is read out of the Rust,
600 // so this cannot go green against a mark the engine no longer writes; that the
601 // mark opens the note it stands for is held in Rust by
602 // `test_the_no_network_mark_opens_the_note_and_finds_it_in_a_result`, and the
603 // whole path -- a real command, a real hand, a real fence -- by
604 // `dev/verify_handrun.mjs`. What is asked here is the half neither of those
605 // can see: whether the person is told.
606 const MARK = (() => {
607 const rs = fs.readFileSync(path.join(WWW, '..', 'src/tools.rs'), 'utf8');
608 const m = rs.match(/pub const NO_NET_MARK: &str = "([^"]+)"/);
609 return m ? m[1] : '';
610 })();
611 check(!!MARK, 'the engine\'s own mark for a command that had no network was read',
612 JSON.stringify(MARK));
613
614 const said = await p.evaluate((mark) => {
615 const out = {};
616 DaimondCore.drawToolResult('run',
617 'error: failed to fetch `serde`\n[exit code: 101]\n' + mark + ' the turn read outside '
618 + 'content, so this command ran without it.]', 'done');
619 // The LAST block, by position rather than by `:last-of-type`, which is about
620 // element type and would answer about whatever div happens to be last.
621 const last = () => [...document.querySelectorAll('.tool-block')].pop();
622 out.blocks = document.querySelectorAll('.tool-block').length;
623 let line = last() ? last().querySelector('.tool-nonet') : null;
624 out.withNote = line ? (line.textContent || '').trim() : '';
625 out.shown = !!(line && line.getClientRects().length);
626 DaimondCore.drawToolResult('run', 'Compiling daimond v0.1.0\n[exit code: 0]', 'done');
627 line = last() ? last().querySelector('.tool-nonet') : null;
628 out.without = line ? (line.textContent || '').trim() : '';
629 out.sentence = window.DaimondI18n ? DaimondI18n.t('chat.tool_no_net') : '';
630 return out;
631 }, MARK);
632 check(!!said.withNote && said.shown,
633 'A COMMAND THAT RAN WITH NO NETWORK SAYS SO ON ITS OWN BLOCK, to the person',
634 `${said.blocks} block(s) drawn — ${JSON.stringify(said.withNote.slice(0, 80))}`);
635 // In the app's own words and not the model's: the bracketed note is written for
636 // a model to act on, and a person reading it is reading somebody else's post.
637 check(said.withNote === said.sentence && said.sentence.length > 40
638 && !said.withNote.includes(MARK),
639 'and in the app\'s own sentence, from the catalogue, not the model\'s note',
640 JSON.stringify(said.sentence.slice(0, 80)));
641 // The other half, and it is what stops the line becoming noise: a command that
642 // HAD the network says nothing at all.
643 check(said.without === '',
644 'and a command that had the network says nothing about it',
645 JSON.stringify(said.without));
646
647 // The sentence exists in all eight catalogues. `tOr` draws correct English when
648 // a key is in no table, so a missing translation looks exactly like a finished
649 // one and nothing reports it.
650 const LOCALES = ['en', 'es', 'de', 'fr', 'pt-BR', 'zh-Hans', 'ja', 'ko'];
651 const gaps = LOCALES.filter((code) => {
652 const src = fs.readFileSync(path.join(WWW, 'i18n', code + '.js'), 'utf8');
653 return !/'chat\.tool_no_net':\s*'[^']{20,}'/.test(src);
654 });
655 check(gaps.length === 0,
656 'and it is in all eight catalogues, not in English only',
657 gaps.join(', '));
658} finally {
659 await s.close();
660}
661
662console.log(bad ? `\n${bad} check(s) FAILED` : '\nall checks passed');
663process.exit(bad ? 1 : 0);