Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_autoreload.mjs

24.6 KiB, 1 run

created by r2519314175:249, 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_autoreload.mjs — the standing instruction to buy your own credits.
2//
3// This one is driven against the REAL gateway on :9002, not a stub, because the thing worth
4// proving is not that the form renders. It is that the gateway REFUSES the settings that cannot
5// work, and that the browser shows the refusal rather than pretending it saved:
6//
7// * on, with no card -> 422. There is nothing to charge, and the user would not find out
8// until the balance ran dry.
9// * budget < one top-up -> 400. The first reload already crosses the cap, so the setting
10// would look on and never act.
11//
12// Both are the gateway's rules. The browser must not have its own copy of them — it must ask, and
13// say what it is told. A client that guesses is a client that will one day guess differently from
14// the till.
15//
16// AND THE PANEL HAS TO SAY WHAT IT WAS TOLD. Everything above was asserted at the API for a
17// month while the panel's own message line could not print a word: `note()` looked up an ID
18// `ar-note` that was only ever a CLASS, so `getElementById` returned null, the guard swallowed
19// every message, and four call sites about money -- a card that would not save, a gateway that
20// refused the settings -- went quiet. Nothing caught it, because an element that does not exist
21// reports itself to a browser locator as HIDDEN, which is exactly what a working guard produces.
22// So the checks here assert the line EXISTS and says the gateway's own sentence; never that no
23// error is showing.
24//
25// AND THE PANEL HAS TO BE THERE AT ALL. The last section drives a session whose gateway
26// bootstrap is still in flight when Credits is opened, which is the state a loaded machine
27// produces by itself. Proved red first:
28//
29// AND THE SAVE BUTTON HAS TO BE WIRED TO ANYTHING AT ALL. Driving the panel through the button
30// rather than the API found a third fault under the first two: `render()` held a local
31// `var save` for the card button, `var` is function-scoped, and so the module's `save()` was
32// shadowed where the Save button binds to it. `addEventListener` was handed a DOM element on one
33// branch and `undefined` on the other, accepted both without a word, and the button did NOTHING.
34// Every check that spoke to the API passed throughout.
35//
36// node dev/verify_autoreload.mjs --break latefill # the late fill is caught
37// node dev/verify_autoreload.mjs --break noteid # the class-for-an-id trap is caught
38// node dev/verify_autoreload.mjs --break noteorder # the note eaten by its own redraw
39// node dev/verify_autoreload.mjs --break deadsave # the Save button wired to nothing
40// node dev/verify_autoreload.mjs # and then, clean
41import fs from 'node:fs';
42import path from 'node:path';
43import { fileURLToPath } from 'node:url';
44import { open, shot, errors } from './harness.mjs';
45
46const HERE = path.dirname(fileURLToPath(import.meta.url));
47const AR_JS = path.join(HERE, '..', 'www', 'js', 'autoreload.js');
48
49const BREAK = (() => {
50 const i = process.argv.indexOf('--break');
51 return i > 0 ? String(process.argv[i + 1] || '') : '';
52})();
53const die = (why) => { console.error('ABORT: ' + why); process.exit(2); };
54
55// ── The patches that prove the message-line checks can fail ────────────
56//
57// Each is [anchor, replacement] against the shipped `www/js/autoreload.js`, applied to the file
58// the browser fetches. An anchor that no longer appears is a broken proof and not a passing one,
59// so it is checked against the file on disk before a browser is opened.
60//
61// noteid the exact defect that shipped: the id put back to being only a class, so
62// `getElementById('ar-note')` is null and every message is swallowed by the guard.
63// noteorder the SECOND defect on the same path: the confirmation written before `render()`
64// rather than after, so the redraw destroys it in the same tick. Fixing either one
65// alone leaves the panel silent, which is why both are proved separately.
66const BREAKS = {
67 noteid: {
68 what: 'the note div given a class where an id was wanted, exactly as it shipped',
69 patch: ["\t\tnoteEl.id = 'ar-note';\n", ''],
70 },
71 deadsave: {
72 what: 'the card button named `save` again, shadowing the handler the Save button binds to',
73 patch: [
74 "var addCard = el('button', 'ar-card-btn accent', t('autoreload.save_card'));\n"
75 + "\t\t\taddCard.title = t('autoreload.save_card_help');\n"
76 + "\t\t\taddCard.addEventListener('click', startCard);\n"
77 + "\t\t\tcardRow.appendChild(addCard);",
78 "var save = el('button', 'ar-card-btn accent', t('autoreload.save_card'));\n"
79 + "\t\t\tsave.title = t('autoreload.save_card_help');\n"
80 + "\t\t\tsave.addEventListener('click', startCard);\n"
81 + "\t\t\tcardRow.appendChild(save);",
82 ],
83 },
84 noteorder: {
85 what: 'the confirmation written before the redraw that wipes it',
86 patch: [
87 "\t\t\tawait render();\n"
88 + "\t\t\tnote(next.enabled ? t('autoreload.on_note') : t('autoreload.off_note'));\n",
89 "\t\t\tnote(next.enabled ? t('autoreload.on_note') : t('autoreload.off_note'));\n"
90 + "\t\t\tawait render();\n",
91 ],
92 },
93};
94if (BREAK && BREAK !== 'latefill' && !BREAKS[BREAK]) die(`no break called "${BREAK}"`);
95
96/// The damaged `autoreload.js`, or null when nothing is being broken here.
97const hurtSrc = (() => {
98 if (!BREAKS[BREAK]) return null;
99 const src = fs.readFileSync(AR_JS, 'utf8');
100 const [from, to] = BREAKS[BREAK].patch;
101 if (!src.includes(from)) die(`the "${BREAK}" break no longer matches autoreload.js`);
102 return src.split(from).join(to);
103})();
104if (hurtSrc) console.log(`BREAK ${BREAK}: ${BREAKS[BREAK].what}\n`);
105
106const ok = [], bad = [];
107const check = (name, pass, detail) => {
108 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
109 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
110};
111
112const s = await open({
113 name: 'autoreload', connect: false,
114 route: hurtSrc ? async (pg) => {
115 await pg.route('**/js/autoreload.js', (r) =>
116 r.fulfill({ status: 200, contentType: 'text/javascript', body: hurtSrc }));
117 } : null,
118});
119const p = s.page;
120await p.waitForTimeout(2000);
121
122// The gateway must be up, or nothing below means anything.
123const up = await p.evaluate(async () => {
124 const r = await fetch('/api/autoreload', { credentials: 'same-origin' });
125 return { status: r.status };
126});
127check('the gateway is up and the route is reachable',
128 up.status === 200 || up.status === 401, 'HTTP ' + up.status);
129
130// Open Credits — the panel lives with the credits, not in a settings page of its own.
131await p.evaluate(() => {
132 const row = document.getElementById('astat-credits')
133 || [...document.querySelectorAll('.astat-row')].find(r => /credit/i.test(r.textContent));
134 if (row) row.click();
135});
136// Wait for the panel, rather than sleeping at where it ought to be.
137//
138// This was a flat 1800ms, and on the gate of 2026-08-13 that was not enough: the gateway
139// session is taken asynchronously at boot — an account POST, a challenge, a signature, a
140// verify — and on a loaded machine it had not landed when the panel was read. Five checks
141// went red, including the self-test below, which correctly reported that there was nothing
142// for it to blind. A fixed sleep made this file's verdict a measurement of how busy the
143// machine was.
144//
145// BOUNDED, so a panel that never arrives still fails, and fails saying so. Waiting for the
146// condition is not the same as assuming it: the case where the session lands AFTER Credits
147// was opened is a property in its own right, and it is proved at the foot of this file.
148await p.waitForFunction(() => {
149 const h = document.getElementById('autoreload');
150 return !!h && h.textContent.trim().length > 0;
151}, null, { timeout: 15000 }).catch(() => { /* the check below reports it */ });
152
153const panel = await p.evaluate(() => {
154 const h = document.getElementById('autoreload');
155 return {
156 there: !!h && h.textContent.trim().length > 0,
157 text: h ? h.textContent : '',
158 // The LEAD, on its own. The panel's own furniture says "card" and "budget"
159 // in its labels, so a claim tested against the whole panel is answered by
160 // the widgets and passes whether or not a sentence explains anything.
161 lead: (h && h.querySelector('.cfg-lead') || {}).textContent || '',
162 hasCard: !!document.querySelector('.ar-card-has'),
163 noCard: !!document.querySelector('.ar-card-none'),
164 switchDisabled: !!(document.getElementById('ar-on') || {}).disabled,
165 };
166});
167check('auto-reload appears inside Credits, where the balance is',
168 panel.there, panel.there ? '(rendered)' : '(empty — is the account authed?)');
169check('a fresh account is told it has no card', panel.noCard === true);
170check('and the switch cannot be turned on without one',
171 panel.switchDisabled === true,
172 panel.switchDisabled ? 'disabled, and says why' : 'ENABLED WITH NO CARD');
173// ── The panel explains itself ──────────────────────────────────────────
174//
175// A standing instruction to spend the user's money must SAY, before it is armed, the three
176// things a person would otherwise find out from a statement: that the money comes off the
177// CARD, that it happens WITHOUT THEM BEING ASKED, and that their own LIMIT bounds it.
178//
179// Those three facts are the property. The sentence carrying them is not: this check used to
180// match `charges the card below, without asking` verbatim and went red on 2026-08-11 when a
181// concision pass over 222 catalogue strings dropped two commas — the panel explained itself
182// exactly as well before and after. A check that cannot survive a comma is measuring the
183// author, not the product.
184const SAYS = {
185 'what is charged': /\bcards?\b/i,
186 'that it is unasked': /without (being )?ask|without asking|automatic|on its own|by itself|unprompted/i,
187 'the user’s ceiling': /\b(limit|cap|ceiling|budget|no more than|up to)\b/i,
188 'that it spends': /\bcharg|\bbuy|\bbuys\b|\bbought\b|top[- ]?up|purchas/i,
189};
190const explains = (s) => Object.entries(SAYS).filter(([, re]) => !re.test(String(s || ''))).map(([k]) => k);
191const missing = explains(panel.lead);
192check('the panel says what it will do, in words',
193 panel.lead.trim().length > 0 && missing.length === 0,
194 missing.length ? 'the lead never says: ' + missing.join(', ') + ' — “' + panel.lead + '”'
195 : '“' + panel.lead.trim() + '”');
196
197// And the same check, over copy that genuinely explains nothing — read back out of the DOM
198// by the same path, so what is proved red is the whole check and not just its regexes.
199const blinded = await p.evaluate(() => {
200 const el = document.querySelector('#autoreload .cfg-lead');
201 if (!el) return null;
202 const was = el.textContent;
203 el.textContent = 'Auto-reload settings.';
204 const seen = (document.querySelector('#autoreload .cfg-lead') || {}).textContent || '';
205 el.textContent = was;
206 return { seen, restored: (document.querySelector('#autoreload .cfg-lead') || {}).textContent };
207});
208check('and it is a check that can fail — a lead that explains nothing is caught',
209 !!blinded && explains(blinded.seen).length === Object.keys(SAYS).length
210 && blinded.restored === panel.lead,
211 blinded ? 'a bare “' + blinded.seen + '” is missing all ' + Object.keys(SAYS).length
212 : 'there was no lead paragraph to blind');
213
214await shot(s, 'autoreload');
215
216// ── The gateway's refusals, seen from the browser ──────────────────────
217//
218// The switch is disabled in the UI, so the "on with no card" case is forced at the API — which is
219// where it must hold anyway, since a client can always be lied to but the gateway cannot.
220
221const noCard = await p.evaluate(async () => {
222 const r = await fetch('/api/autoreload', {
223 method: 'POST', credentials: 'same-origin',
224 headers: { 'content-type': 'application/json' },
225 body: JSON.stringify({ enabled: true, threshold_minor: 500, topup_minor: 2000, monthly_budget_minor: 10000 }),
226 });
227 return { status: r.status, body: await r.text() };
228});
229check('turning it on with no card is refused by the gateway',
230 noCard.status === 422, 'HTTP ' + noCard.status);
231check('and the refusal says what to do about it',
232 /save a card/i.test(noCard.body), noCard.body.slice(0, 90));
233
234const tooSmall = await p.evaluate(async () => {
235 const r = await fetch('/api/autoreload', {
236 method: 'POST', credentials: 'same-origin',
237 headers: { 'content-type': 'application/json' },
238 // Off, so the card rule does not fire first — but the budget is still nonsense.
239 body: JSON.stringify({ enabled: false, threshold_minor: 500, topup_minor: 2000, monthly_budget_minor: 1000 }),
240 });
241 return { status: r.status, body: await r.text() };
242});
243// With `enabled: false` the gateway saves it: an instruction that is off cannot misfire, and
244// refusing to let a user write down a plan before switching it on would be officious.
245check('settings that are OFF are saved without argument, nonsense or not',
246 tooSmall.status === 200, 'HTTP ' + tooSmall.status);
247
248// The same nonsense, ON. It is refused — but for the CARD, not the budget: the card is asked
249// about first, because it is the refusal the user can act on. Told the budget was too small while
250// also having no card, they would fix the budget and be refused all over again.
251//
252// Which means the budget rule is unreachable from here, and from any browser test: it lies behind
253// the card check, and no test can put a real card on a real Stripe customer. It is therefore
254// tested where it CAN be — `autoreload::tests::test_a_budget_under_one_topup_is_refused`, against
255// the pure `refuse()` the handler calls. This check exists to pin the ORDER, so that if the two
256// rules are ever swapped, this fails and says so.
257const nonsenseOn = await p.evaluate(async () => {
258 const r = await fetch('/api/autoreload', {
259 method: 'POST', credentials: 'same-origin',
260 headers: { 'content-type': 'application/json' },
261 body: JSON.stringify({ enabled: true, threshold_minor: 500, topup_minor: 2000, monthly_budget_minor: 1000 }),
262 });
263 return { status: r.status, body: await r.text() };
264});
265check('switching on with no card complains about the CARD, not the budget — the fixable thing first',
266 nonsenseOn.status === 422 && /saved card/i.test(nonsenseOn.body),
267 'HTTP ' + nonsenseOn.status + ' — ' + nonsenseOn.body.slice(0, 60));
268
269// ── What is saved is what comes back ──────────────────────────────────
270
271const roundTrip = await p.evaluate(async () => {
272 await fetch('/api/autoreload', {
273 method: 'POST', credentials: 'same-origin',
274 headers: { 'content-type': 'application/json' },
275 body: JSON.stringify({ enabled: false, threshold_minor: 750, topup_minor: 2500, monthly_budget_minor: 15000 }),
276 });
277 const r = await fetch('/api/autoreload', { credentials: 'same-origin' });
278 return await r.json();
279});
280check('what is written is what is read back',
281 roundTrip.threshold_minor === 750 && roundTrip.topup_minor === 2500
282 && roundTrip.monthly_budget_minor === 15000 && roundTrip.enabled === false,
283 `${roundTrip.threshold_minor}/${roundTrip.topup_minor}/${roundTrip.monthly_budget_minor} enabled=${roundTrip.enabled}`);
284check('and the month\'s spend is reported against the ceiling',
285 typeof roundTrip.spent_this_month_minor === 'number',
286 'spent ' + roundTrip.spent_this_month_minor + ' of ' + roundTrip.monthly_budget_minor);
287check('the card is always the same shape, never null',
288 roundTrip.card && typeof roundTrip.card.saved === 'boolean',
289 JSON.stringify(roundTrip.card));
290
291// The panel must show what the gateway now holds, not what it drew before.
292await p.evaluate(() => window.DaimondAutoReload.render());
293await p.waitForTimeout(900);
294const shown = await p.evaluate(() => {
295 const v = id => (document.getElementById(id) || {}).value;
296 return { th: v('ar-threshold'), tu: v('ar-topup'), bu: v('ar-budget') };
297});
298check('the panel shows the saved numbers, in whole units',
299 shown.th === '7.5' && shown.tu === '25' && shown.bu === '150',
300 `${shown.th} / ${shown.tu} / ${shown.bu}`);
301
302// ── The panel SAYS what the gateway said ───────────────────────────────
303//
304// Everything above this point is measured at the API. That is where the RULES live, and it is
305// also how this file could report a healthy panel for a month while the panel could not print a
306// word: `note()` asked for an element by an id that was only ever a class name, so it found
307// null every time and returned quietly. A user saving nonsense was shown nothing whatever.
308//
309// The first check here is therefore about EXISTENCE, and it is the one that matters. An element
310// that is missing and an element that is correctly empty look identical to a locator -- both
311// hidden -- so a check written as "no error is showing" passes on a panel that has been struck
312// dumb. Presence first, then content, and never absence alone.
313
314const line = await p.evaluate(() => {
315 const n = document.getElementById('ar-note');
316 const h = document.getElementById('autoreload');
317 return {
318 there: !!n,
319 tag: n ? n.tagName.toLowerCase() : '',
320 inPanel: !!(n && h && h.contains(n)),
321 live: n ? n.getAttribute('aria-live') : '',
322 };
323});
324check('the panel HAS a message line, under the id the code looks it up by',
325 line.there && line.inPanel,
326 line.there ? `<${line.tag}> inside #autoreload, aria-live="${line.live}"`
327 : 'NO #ar-note — every message this panel can make is swallowed by its own guard');
328
329// A save the gateway ACCEPTS. Its confirmation has to survive the redraw the save itself sets
330// off: `render()` blanks the host and re-appends an EMPTY note line, so a message written
331// before it is destroyed in the same tick and the user is told nothing at all.
332const okSave = await p.evaluate(() => {
333 const want = window.DaimondI18n ? DaimondI18n.t('autoreload.off_note') : '';
334 const box = document.getElementById('ar-on');
335 if (box) box.checked = false; // off is saved without argument
336 const btn = document.getElementById('ar-save');
337 if (btn) btn.click();
338 return { want, clicked: !!btn };
339});
340check('there is a Save button to drive', okSave.clicked === true);
341const settled = await p.waitForFunction(() => {
342 const b = document.getElementById('ar-save');
343 const n = document.getElementById('ar-note');
344 return !!b && !b.disabled && !!n && n.textContent.trim().length > 0;
345}, null, { timeout: 8000 }).then(() => true).catch(() => false);
346await p.waitForTimeout(800); // and then the panel is left alone a moment
347const said = await p.evaluate(() => {
348 const n = document.getElementById('ar-note');
349 return {
350 there: !!n,
351 text: n ? n.textContent.trim() : '',
352 bad: !!(n && n.classList.contains('bad')),
353 };
354});
355check('a save the gateway accepts is confirmed, and the confirmation outlives its own redraw',
356 settled && said.there && said.text.length > 0 && said.text === okSave.want && !said.bad,
357 said.there ? `“${said.text || '(empty — the redraw ate the confirmation)'}”`
358 : 'there is no message line at all');
359
360// And a save the gateway REFUSES. The switch is disabled without a card, so the refusal is
361// reached the way a client that ignored the disabled attribute would reach it — which is the
362// case the panel was silent about: a 422 carrying a sentence written for a person to read,
363// thrown away by a guard that looked like it was working.
364const refusal = (() => {
365 try { return String(JSON.parse(noCard.body).error || ''); } catch (e) { return ''; }
366})();
367check('the gateway’s refusal is a sentence a person can act on, not a code',
368 refusal.length > 20 && /card/i.test(refusal), `“${refusal}”`);
369
370await p.evaluate(() => {
371 const box = document.getElementById('ar-on');
372 if (box) box.checked = true; // there is no card; the gateway will refuse
373 const btn = document.getElementById('ar-save');
374 if (btn) btn.click();
375});
376const heard = await p.waitForFunction((want) => {
377 const n = document.getElementById('ar-note');
378 return !!n && n.textContent.trim() === want;
379}, refusal, { timeout: 8000 }).then(() => true).catch(() => false);
380await p.waitForTimeout(900); // the panel settles; the refusal must remain
381const after = await p.evaluate(() => {
382 const n = document.getElementById('ar-note');
383 return {
384 there: !!n,
385 text: n ? n.textContent.trim() : '',
386 bad: !!(n && n.classList.contains('bad')),
387 seen: !!(n && n.offsetParent !== null),
388 };
389});
390check('the gateway’s refusal is SHOWN, in the gateway’s own words',
391 heard && after.there && after.text === refusal,
392 after.there ? `“${after.text || '(the panel said nothing)'}”` : 'there is no message line at all');
393check('and it reads as a failure, and is still on screen once the panel has settled',
394 after.bad && after.seen && after.text === refusal,
395 `bad=${after.bad} on-screen=${after.seen}`);
396await shot(s, 'autoreload-note');
397
398// The 422s are the refusals this test went looking for, so they are not faults — the browser logs
399// every non-2xx fetch as a console error.
400// 402 joins the ambient list: a free account's idle sync push is refused
401// (sync is Pro), which the browser logs while this test is about auto-reload.
402const errs = errors(s).filter(e => !/favicon|404|401|402|422|Unprocessable|net::ERR/.test(e));
403console.log('\nconsole errors:', errs.slice(0, 4));
404check('nothing throws', errs.length === 0, errs[0] || '');
405
406await s.close();
407
408// ── A session that lands AFTER Credits was opened fills the panel in ────
409//
410// Everything above is measured on a session that had already landed. This is the case where
411// it has not, and until 2026-08-13 it was a permanent blank: `render` draws nothing without
412// `state.authed`, the flag is set five round trips into the boot, and NOTHING redrew the
413// panel when it flipped. A user who reached Credits a second early was shown an empty space
414// where the standing instruction to spend their money should be, for the life of the page.
415// The only way back was a reload, and nothing on screen said so.
416//
417// That is also what made the gate red rather than this file: the failure arrived as a timing
418// flake, so it read as a slow machine instead of a missing panel.
419//
420// Driven by holding the LAST leg of the bootstrap, so the app is in exactly the state a
421// loaded machine puts it in: unlocked, drawn, and not yet authed. The precondition is
422// ASSERTED and not assumed — a run where the session beat the click proves nothing, and must
423// say so rather than going green.
424const HOLD_MS = 8000;
425const late = await open({
426 name: 'autoreload-late', connect: false,
427 route: async (pg) => {
428 await pg.route('**/api/auth/verify', async (r) => {
429 await new Promise((f) => setTimeout(f, HOLD_MS));
430 await r.continue();
431 });
432 if (BREAK === 'latefill') {
433 // The listener taken back out, and only that: the panel is drawn by
434 // exactly the code that drew it before, and nothing redraws it late.
435 const src = fs.readFileSync(AR_JS, 'utf8');
436 const hurt = src.replace(/\twindow\.addEventListener\('daimond:authed'[\s\S]*?\n\t\}\);\n/, '');
437 if (hurt === src) die('the latefill break did not reach the daimond:authed listener');
438 await pg.route('**/js/autoreload.js', (r) =>
439 r.fulfill({ status: 200, contentType: 'text/javascript', body: hurt }));
440 }
441 },
442});
443const lp = late.page;
444await lp.waitForTimeout(1500);
445await lp.evaluate(() => {
446 const row = document.getElementById('astat-credits')
447 || [...document.querySelectorAll('.astat-row')].find(r => /credit/i.test(r.textContent));
448 if (row) row.click();
449});
450await lp.waitForTimeout(600);
451const early = await lp.evaluate(() => {
452 const h = document.getElementById('autoreload');
453 return {
454 empty: !h || h.textContent.trim().length === 0,
455 authed: !!(window.DaimondGateway && DaimondGateway.state().authed),
456 };
457});
458// The panel AND the session, so a blank that stays blank cannot be read as a session that
459// never came.
460const filled = await lp.waitForFunction(() => {
461 const h = document.getElementById('autoreload');
462 return !!h && !!h.querySelector('.cfg-lead')
463 && !!(window.DaimondGateway && DaimondGateway.state().authed);
464}, null, { timeout: 20000 }).then(() => true).catch(() => false);
465const lateAuthed = await lp.evaluate(() =>
466 !!(window.DaimondGateway && DaimondGateway.state().authed));
467check('Credits opened before the session lands is filled in when it does',
468 early.empty && !early.authed && filled,
469 !early.empty || early.authed
470 ? 'nothing was proved: the session had already landed when Credits was opened'
471 : filled ? 'blank while unauthed, drawn once authed'
472 : `still blank ${lateAuthed ? 'WITH' : 'without'} a session, 20s on`);
473await shot(late, 'autoreload-late');
474await late.close();
475console.log(`\n${ok.length} passed, ${bad.length} failed`);
476if (bad.length) console.log('FAILED:\n ' + bad.join('\n '));
477if (BREAK) {
478 if (bad.length) { console.log('the break was caught, as it should be'); process.exit(0); }
479 console.log('THE BREAK WAS NOT CAUGHT: this check proves nothing');
480 process.exit(1);
481}
482process.exit(bad.length ? 1 : 0);