diff --git a/README.md b/README.md index 1d6fcd8..39502ac 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j | verify-layer | 44/44 with the AIP-23 reference verifier (`AERE_VERIFY_PROOF= node proba-sidecar.mjs`); without it 39 run, 5 are reported as skipped and the exit code is 2 | 9/9 in this repository (`node control-negativ-sidecar.mjs`; its tenth case compares with the version from the development history and is skipped here) | | proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test | | readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) | -| control-plane | planner 30/30, command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), console 8/8 | remediation 7/7, compliance report 3/3 in this repository | +| control-plane | planner 30/30, command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) | | identity | 43/43 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), measured 2026-09-30 | 46/46 (`node control-negativ-identity.mjs`) | | agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 51/51, approval and revocation 39/39, command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`x402/proba-x402-testnet.mjs`, `x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc `) | 26/26 (`node control-negativ-aprobare.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository | @@ -45,4 +45,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ... ## Licence -MIT, see [LICENSE](LICENSE). Files: 137 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 8, proof-kinds 3, control-plane 17, agents 34, identity 5, readiness 4). +MIT, see [LICENSE](LICENSE). Files: 139 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 8, proof-kinds 3, control-plane 19, agents 34, identity 5, readiness 4). diff --git a/control-plane/README.md b/control-plane/README.md index 55af649..da72d2e 100644 --- a/control-plane/README.md +++ b/control-plane/README.md @@ -92,6 +92,8 @@ computes nothing in the browser, and a bundle pasted there directly is shown as node proba-raport-conformitate.mjs # 27: on the real outputs of the inventory and the scanner; the envelope through the AIP-23 verifier node control-negativ-raport-conformitate.mjs # 3 plantings, each turns the named check red node proba-consola.mjs # 8: a good bundle and four negative controls + node proba-consola-web.mjs # 26: the viewer in a real Chromium, phone (390 px) and desktop (1440 px); needs playwright-core and a Chromium + node control-negativ-consola-web.mjs # 8 defects planted in a copy of the page, each turns its named check red The report test runs the envelope through the AIP-23 reference verifier (`AERE_VERIFY_PROOF=`). No third party has reviewed any of this. diff --git a/control-plane/consola-web/README.md b/control-plane/consola-web/README.md index 33e117f..36cddf2 100644 --- a/control-plane/consola-web/README.md +++ b/control-plane/consola-web/README.md @@ -8,5 +8,9 @@ verification layer, and a sidecar bundle pasted directly is shown as `UNCHECKED` The data shown on opening is an example generated by the tools (host `aws-eu-prod-1`, three entries). Paste your own output to render it. The page loads the IBM Plex fonts from Google Fonts; everything else is in the file. -Checked on 2026-09-29 by running its script in a minimal DOM (no browser): the example, a real console state (OK), a real broken -state (BROKEN) and a raw bundle (UNCHECKED) render without error. The layout was not measured in a real browser in that pass. +Checked on 2026-09-30 in a real Chromium (`../proba-consola-web.mjs`), on a phone (390 px, touch, media queries applied) and on a +desktop (1440 px), with the network cut: the page is exactly as wide as the screen, nothing scrolls sideways, and the example, a +real console state (OK), a real broken state (BROKEN), a raw bundle (UNCHECKED) and invalid JSON render as they should, with no +script error. That pass found two defects of the first version, fixed: without a viewport tag a phone laid the page out 980 px +wide, and every pasted state was shown with the finality step "not notarized" marked as current, which the console never +checks. Now no step is marked for pasted output, and the finality tile says it is not checked here. diff --git a/control-plane/consola-web/index.html b/control-plane/consola-web/index.html index a373140..abeb397 100644 --- a/control-plane/consola-web/index.html +++ b/control-plane/consola-web/index.html @@ -1,3 +1,8 @@ + + + + + AERE Verification Console @@ -118,6 +123,8 @@ button.ghost{border:1px solid var(--line2);background:transparent;color:var(--mu .foot code{font-family:var(--mono);color:var(--mut)} .warnbox{margin-top:12px;border:1px solid color-mix(in srgb,var(--crit) 45%,transparent);background:color-mix(in srgb,var(--crit) 10%,transparent);color:var(--crit);border-radius:10px;padding:11px 13px;font-size:12.5px} + +
@@ -181,7 +188,7 @@ button.ghost{border:1px solid var(--line2);background:transparent;color:var(--mu
- A rendering of the output of the AUTHORITATIVE tools (consola.mjs and the sidecar). The verdict and the integrity of the chain and of the envelopes are computed by the tools, not in the browser: this page shows them as they are. The audit chain is an append-only hash chain where every entry covers sha256(seq | prev | envelope); its head can be given post-quantum finality by notarizing it on AereNotary (Aere Network). The envelopes follow the AERE Proof Protocol (AIP-23). The data shown on opening is an example deployment generated by the tools; the finality step shown is the one of that example (a head that can be notarized and is not yet). + A rendering of the output of the AUTHORITATIVE tools (consola.mjs and the sidecar). The verdict and the integrity of the chain and of the envelopes are computed by the tools, not in the browser: this page shows them as they are. The audit chain is an append-only hash chain where every entry covers sha256(seq | prev | envelope); its head can be given post-quantum finality by notarizing it on AereNotary (Aere Network). The envelopes follow the AERE Proof Protocol (AIP-23). The data shown on opening is an example deployment generated by the tools; the finality step marked is the one of that example (a head that can be notarized and is not yet). For output you paste, the console does not check finality, so no step is marked.
@@ -237,7 +244,9 @@ function copyBtn(text){ return b; } -function render(d){ +// `exemplu`: numai exemplul are o treapta de finalitate cunoscuta (capul lui nu e notarizat); pentru o iesire lipita consola nu masoara +// finalitatea, deci nu se marcheaza nicio treapta si caseta spune ca nu e verificata aici (2026-09-30, B-24) +function render(d, exemplu=false){ d=normalize(d); const ok = d.verdict==="OK"; document.getElementById("host").textContent=d.host||"?"; @@ -252,7 +261,8 @@ function render(d){ {k:"AIP-23 envelopes",pin:bad?"crit":"ok",v:(pl.intact??0)+"/"+(pl.total??0),n:bad?"intact · bad: "+pl.bad.map(x=>x.seq).join(","):"intact",s:bad?"s-crit":"s-ok"}, {k:"PQ migration",pin:mg?(mg.critical?"crit":"acc"):"acc",v:mg&&mg.total!=null?String(mg.total):"—",n:mg?(mg.total==null?(mg.error||"plan not readable"):(mg.critical+" critical · "+mg.auto+" automatic")):"no plan attached",s:mg&&mg.critical?"s-crit":"s-acc"}, ex?{k:"Execution",pin:ex.chainOk?(ex.failed?"crit":"ok"):"crit",v:ex.chainOk?"INTACT":"BROKEN",n:ex.actions+" actions · "+ex.ok+" OK · "+ex.failed+" failed",s:ex.chainOk&&!ex.failed?"s-ok":"s-crit"} - :{k:"Finality",pin:"warn",v:"ready",n:"the head can be notarized (not notarized)",s:"s-warn"}, + :(exemplu?{k:"Finality",pin:"warn",v:"ready",n:"example: the head can be notarized (not notarized)",s:"s-warn"} + :{k:"Finality",pin:"warn",v:"not checked",n:"the console does not check finality: notarize the head, then check it with the AIP-23 verifier",s:"s-warn"}), ]; const T=document.getElementById("tiles");T.innerHTML=""; for(const t of tiles){ @@ -302,7 +312,7 @@ function render(d){ ["3","anchor without certificate","in a block, under an anchor without a complete post-quantum certificate"], ["4","post-quantum","under an anchor with a Falcon / SLH-DSA certificate"], ]; - const cur = 1; + const cur = exemplu ? 1 : 0; const L=document.getElementById("ladder");L.innerHTML=""; steps.forEach((s,i)=>{ const st=el("div","step"+(i+1===cur?" cur":"")); @@ -334,8 +344,10 @@ document.getElementById("render").addEventListener("click",()=>{ }); document.getElementById("reset").addEventListener("click",()=>{ document.getElementById("in").value=JSON.stringify(SAMPLE,null,1); - document.getElementById("perr").hidden=true; render(SAMPLE); + document.getElementById("perr").hidden=true; render(SAMPLE, true); }); -render(SAMPLE); +render(SAMPLE, true); + + diff --git a/control-plane/control-negativ-consola-web.mjs b/control-plane/control-negativ-consola-web.mjs new file mode 100644 index 0000000..00dcdfd --- /dev/null +++ b/control-plane/control-negativ-consola-web.mjs @@ -0,0 +1,52 @@ +// Controlul negativ al probei consolei web in browser (proba-consola-web.mjs): fiecare defect se planteaza intr-o COPIE a paginii, +// proba ruleaza pe copie (primeste calea ei) si proba NUMITA trebuie sa iasa rosie, cu proba chiar rulata; pe copia neatinsa, verde. +// Trei stari: un tipar care nu apare exact o data sau o proba care nu ajunge la rezumat e STRICAT si se numara esec. +// node control-negativ-consola-web.mjs iesire 0 = martorul verde si toate plantarile rosii pe proba lor +import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const PAGINA = path.join(AICI, 'consola-web', 'index.html'); +const PLANTARI = [ + // [nume, tipar, inlocuire, proba (o parte din numele ei)] + ['fara viewport: telefonul primeste pagina de desktop (forma din 29 sept)', '\n', '', '390px: latimea interna = latimea ecranului'], + ['o treapta de finalitate marcata pentru orice stare lipita (forma din 29 sept)', 'const cur = exemplu ? 1 : 0;', 'const cur = 1;', 'nicio treapta de finalitate nu e aratata'], + ['caseta de finalitate spune "ready" pentru orice stare lipita (forma din 29 sept)', ':(exemplu?{k:"Finality"', ':(true?{k:"Finality"', 'caseta de finalitate spune'], + ['o eroare de script dupa prima redare', 'render(SAMPLE, true);\n', 'render(SAMPLE, true); nuExistaFunctia();\n', 'nicio eroare de consola sau de pagina'], + ['un buraf brut aratat drept verificat', 'verdict:"UNCHECKED",', 'verdict:"OK",', 'un buraf brut redat UNCHECKED'], + ['un continut mai lat decat telefonul', '.wrap{max-width:1000px;', '.wrap{min-width:600px;max-width:1000px;', '390px: latimea interna = latimea ecranului'], + ['verdictul BROKEN aratat in culoarea buna', 'v.className="verdict "+(ok?"ok":"bad");', 'v.className="verdict ok";', 'redata BROKEN, in culoarea rea'], + ['un JSON stricat redat in tacere', 'e.hidden=false; e.textContent="invalid JSON: "+err.message;', '', 'un JSON stricat'], +]; +function ruleaza(pagina) { + return new Promise((resolve) => { + const c = spawn(process.execPath, [path.join(AICI, 'proba-consola-web.mjs'), pagina]); let out = ''; + const ceas = setTimeout(() => c.kill(), 240000); + c.stdout.on('data', (x) => { out += x; }); c.stderr.on('data', (x) => { out += x; }); + c.on('close', (cod) => { clearTimeout(ceas); resolve({ cod, rulat: /consola-web: \d+\/\d+/.test(out), rosii: out.split('\n').filter((l) => l.includes('[RAU ]')), nemasurat: /^NEMASURAT/m.test(out) }); }); + }); +} +const T = fs.mkdtempSync(path.join(os.tmpdir(), 'cons-web-ctl-')); +let rele = 0; +try { + const m0 = path.join(T, 'martor.html'); fs.copyFileSync(PAGINA, m0); + const m = await ruleaza(m0); + if (m.nemasurat) { console.log('NEMASURAT: proba nu poate rula aici (fara browser)'); process.exit(2); } + if (m.rulat && m.cod === 0 && !m.rosii.length) console.log(' OK martorul: copia neatinsa verde'); + else { rele++; console.log(` STRICAT martorul nu e verde (cod ${m.cod}, ${m.rosii.length} rosii)`); } + const src = fs.readFileSync(PAGINA, 'utf8'); + const planteaza = async ([nume, din, inl, tinta], i) => { + if (src.split(din).length !== 2) return [false, ` STRICAT ${nume}: tiparul apare de ${src.split(din).length - 1} ori`]; + const f = path.join(T, `p${i}.html`); fs.writeFileSync(f, src.replace(din, inl)); + const r = await ruleaza(f); + if (!r.rulat) return [false, ` STRICAT ${nume}: proba nu a ajuns la rezumat (cod ${r.cod})`]; + if (r.rosii.some((l) => l.includes(tinta))) return [true, ` ROSU cum trebuia ${nume} (proba '${tinta}' pica)`]; + return [false, ` CONTROL CAZUT ${nume}: proba '${tinta}' a ramas verde (${r.rosii.length} rosii altundeva)`]; + }; + // cate doua browsere deodata (fiecare cateva sute de MB): laptopul e impartit cu constructiile celuilalt agent + const rez = new Array(PLANTARI.length); let k = 0; + await Promise.all(Array.from({ length: 2 }, async () => { while (k < PLANTARI.length) { const i = k++; rez[i] = await planteaza(PLANTARI[i], i); } })); + for (const [bun, linie] of rez) { console.log(linie); if (!bun) rele++; } +} finally { fs.rmSync(T, { recursive: true, force: true }); } +console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `DOVEDIT: martorul verde, ${PLANTARI.length} din ${PLANTARI.length} defecte plantate -> proba lor rosie`); +process.exitCode = rele ? 1 : 0; diff --git a/control-plane/proba-consola-web.mjs b/control-plane/proba-consola-web.mjs new file mode 100644 index 0000000..79ab1b8 --- /dev/null +++ b/control-plane/proba-consola-web.mjs @@ -0,0 +1,82 @@ +// Proba consolei web (consola-web/index.html) intr-un Chromium ADEVARAT (playwright-core + Chromium-ul instalat de Playwright), pe +// telefon (390x844, isMobile, cu media queries aplicate) si pe desktop (1440x900). Inchide randul NEMASURAT din 29 sept ("aspectul +// consolei web intr-un browser adevarat"): pana atunci pagina fusese rulata numai intr-un DOM minim, fara browser. +// Cere, pe fiecare latime: nicio eroare de consola sau de pagina; latimea interna = latimea ecranului (pagina nu iese din telefon); +// nicio derulare orizontala; exemplul redat (OK); o stare REALA a consolei (facuta chiar de consola.mjs pe un jurnal real al +// sidecar-ului) redata cu verdictul ei, OK si apoi BROKEN; un buraf brut redat UNCHECKED cu nota; un JSON stricat -> mesajul de eroare; +// si finalitatea: consola nu o masoara, deci pentru o stare lipita nu are voie sa arate o treapta ca fiind cea curenta. +// Reteaua e oprita (fonturile Google se cer si se refuza): proba nu atinge internetul. +// node proba-consola-web.mjs [cale index.html] iesire 0 = toate cum trebuia, 1 = una rosie, 2 = nu s-a putut rula (fara browser) +import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto'; +import { execFileSync } from 'node:child_process'; import { fileURLToPath, pathToFileURL, } from 'node:url'; +import { createRequire } from 'node:module'; +import { evalueaza, caleaSidecarului } from './consola.mjs'; + +const AICI = path.dirname(fileURLToPath(import.meta.url)); +const PAGINA = path.resolve(process.argv[2] || path.join(AICI, 'consola-web', 'index.html')); +const EXE = process.env.CHROME || path.join(os.homedir(), 'AppData', 'Local', 'ms-playwright', 'chromium-1228', 'chrome-win64', 'chrome.exe'); +let chromium; +try { ({ chromium } = createRequire(import.meta.url)('playwright-core')); } catch { /* mai jos */ } +if (!chromium || !fs.existsSync(EXE)) { console.log(`NEMASURAT: lipseste playwright-core (npm i playwright-core, in dosarul acesta sau mai sus) sau Chromium (${EXE}); CHROME= pentru altul`); process.exit(2); } + +let rele = 0, bune = 0; const cer = (n, c, d = '') => { console.log(` [${c ? 'OK ' : 'RAU '}] ${n}${c || !d ? '' : ' -- ' + d}`); if (c) bune++; else rele++; }; + +// stari reale, facute de uneltele adevarate: un jurnal al sidecar-ului, un buraf, consola pe el; apoi acelasi jurnal atins +const T = fs.mkdtempSync(path.join(os.tmpdir(), 'cons-web-')); +const SIDE = caleaSidecarului(); +const side = (args) => { try { execFileSync(process.execPath, [SIDE, ...args], { encoding: 'utf8', stdio: 'pipe' }); } catch { /* verificat prin rezultat */ } }; +const log = path.join(T, 'audit.log'); const bin = path.join(T, 'node'); fs.writeFileSync(bin, 'BIN'); +const sh = '0x' + crypto.createHash('sha256').update(fs.readFileSync(bin)).digest('hex'); +side(['record', '--kind', 'runtime', '--artifact', bin, '--attested', sh, '--host', 'h1', '--log', log, '--at', '2026-09-26T09:00:00Z']); +side(['record', '--kind', 'deployment', '--name', 'app', '--version', '1.0', '--content-file', bin, '--log', log, '--at', '2026-09-26T09:01:00Z']); +const bf = path.join(T, 'b.json'); side(['bundle', '--log', log, '--out', bf, '--host', 'h1', '--at', '2026-09-26T09:02:00Z']); +const bundle = JSON.parse(fs.readFileSync(bf, 'utf8')); +const stareOk = await evalueaza({ bundle }); +const atins = JSON.parse(JSON.stringify(bundle)); atins.entries[0].proof.statement.host = 'h2'; +const stareRupta = await evalueaza({ bundle: atins }); +if (stareOk.verdict !== 'OK' || stareRupta.verdict !== 'BROKEN') { console.log(`NEMASURAT: starile de proba nu sunt cele asteptate (${stareOk.verdict}, ${stareRupta.verdict})`); process.exit(2); } + +const browser = await chromium.launch({ executablePath: EXE, headless: true }); +try { + for (const vp of [{ w: 390, h: 844, mobil: true }, { w: 1440, h: 900, mobil: false }]) { + const et = `${vp.w}px`; + const ctx = await browser.newContext({ viewport: { width: vp.w, height: vp.h }, deviceScaleFactor: 1, isMobile: vp.mobil, hasTouch: vp.mobil }); + const page = await ctx.newPage(); + const erori = []; const cereri = []; + page.on('console', (m) => { if (m.type() === 'error' && !/ERR_(BLOCKED|FAILED|INTERNET|NAME)|net::|Failed to load resource/.test(m.text())) erori.push(m.text().slice(0, 160)); }); + page.on('pageerror', (e) => erori.push('pageerror: ' + String(e.message).slice(0, 160))); + await page.route('**/*', (r) => { const u = r.request().url(); if (u.startsWith('file:')) return r.continue(); cereri.push(u); return r.abort(); }); + await page.goto(pathToFileURL(PAGINA).href, { waitUntil: 'load' }); + const m = await page.evaluate(() => ({ iw: window.innerWidth, sw: document.documentElement.scrollWidth, vtext: document.getElementById('vtext').textContent, tiles: document.querySelectorAll('#tiles .tile').length })); + cer(`${et}: latimea interna = latimea ecranului (${m.iw})`, m.iw === vp.w, `innerWidth ${m.iw}`); + cer(`${et}: nicio derulare orizontala (scrollWidth ${m.sw})`, m.sw <= m.iw, `scrollWidth ${m.sw} > ${m.iw}`); + cer(`${et}: exemplul redat: verdict OK, patru casete`, m.vtext === 'OK' && m.tiles === 4, JSON.stringify(m)); + // un buton pe care un om nu il poate apasa (acoperit, in afara ecranului) e un defect al paginii, deci iese RAU, nu opreste proba + const lipeste = async (o) => { try { await page.fill('#in', typeof o === 'string' ? o : JSON.stringify(o), { timeout: 5000 }); await page.click('#render', { timeout: 5000 }); } + catch (e) { cer(`${et}: campul si butonul Render se pot folosi`, false, String(e.message).split('\n')[0].slice(0, 140)); return {}; } + return page.evaluate(() => ({ + vtext: document.getElementById('vtext').textContent, bad: document.getElementById('verdict').classList.contains('bad'), alert: document.getElementById('alert').textContent, + perr: document.getElementById('perr').hidden ? null : document.getElementById('perr').textContent, cur: document.querySelectorAll('#ladder .step.cur').length, + fin: [...document.querySelectorAll('#tiles .tile')].map((t) => t.textContent).find((x) => /Finality/.test(x)) || null, sw: document.documentElement.scrollWidth })); }; + const a = await lipeste(stareOk); + cer(`${et}: o stare reala OK (consola.mjs pe un jurnal real) redata OK`, a.vtext === 'OK' && !a.bad, JSON.stringify(a)); + cer(`${et}: pentru o stare lipita nicio treapta de finalitate nu e aratata drept cea curenta (consola nu o masoara)`, a.cur === 0, `${a.cur} treapta marcata curenta`); + cer(`${et}: caseta de finalitate spune ca finalitatea nu e masurata aici`, a.fin && /not checked/i.test(a.fin), a.fin); + const b = await lipeste(stareRupta); + cer(`${et}: o stare reala BROKEN (jurnal atins) redata BROKEN, in culoarea rea`, b.vtext === 'BROKEN' && b.bad, JSON.stringify(b)); + const c = await lipeste(bundle); + cer(`${et}: un buraf brut redat UNCHECKED, cu nota`, c.vtext === 'UNCHECKED' && /checks nothing/.test(c.alert), JSON.stringify(c).slice(0, 200)); + const d = await lipeste('{ nu e json'); + cer(`${et}: un JSON stricat -> mesajul de eroare, nimic redat peste`, d.perr && /invalid JSON/.test(d.perr) && d.vtext === 'UNCHECKED', JSON.stringify(d).slice(0, 200)); + cer(`${et}: tot fara derulare orizontala dupa redari (scrollWidth ${c.sw})`, c.sw <= vp.w, `scrollWidth ${c.sw}`); + try { await page.click('#reset', { timeout: 5000 }); } catch (e) { cer(`${et}: butonul Example se poate apasa`, false, String(e.message).split('\n')[0].slice(0, 140)); } + const e = await page.evaluate(() => ({ vtext: document.getElementById('vtext').textContent, cur: document.querySelectorAll('#ladder .step.cur').length })); + cer(`${et}: butonul Example reface exemplul (verdict OK)`, e.vtext === 'OK', JSON.stringify(e)); + cer(`${et}: nicio eroare de consola sau de pagina`, !erori.length, erori.join(' | ')); + cer(`${et}: nicio cerere in afara fisierului a ajuns la retea (${cereri.length} cerute, toate refuzate: fonturile)`, cereri.every((u) => /fonts\.(googleapis|gstatic)\.com/.test(u)), cereri.join(', ')); + if (process.env.CAPTURI) { fs.mkdirSync(process.env.CAPTURI, { recursive: true }); await page.screenshot({ path: path.join(process.env.CAPTURI, `consola-${vp.w}.png`), fullPage: true }); } + await ctx.close(); + } +} finally { await browser.close(); fs.rmSync(T, { recursive: true, force: true }); } +console.log(`\nconsola-web: ${bune}/${bune + rele} cum trebuia (Chromium ${path.basename(path.dirname(path.dirname(EXE)))})`); +process.exitCode = rele ? 1 : 0;