Files
UmbrelApps/tests/test_paginas_parsen.mjs
HarmenandClaude Fable 5.1 973b24a23f Gate 0.1.0: eigen image, het command-blok is een script, de app-map is leeg
Plan Eigenimage, fase 1 tot en met 3. De vier templates verhuizen naar
tools/electrum-gate/ zonder extensie; daarnaast Dockerfile (nginx:1.30-alpine
plus python3), entrypoint.sh (het command-blok van de compose, zonder $$) en
build.sh naar het voorbeeld van Evolu Relay. Een image voor beide containers,
gebouwd op de Umbrel; open punt 2 en 3 daarmee beslist.

Inhoudelijk anders dan alleen verplaatst: het log_format staat in stream.conf
zelf, het backend-adres komt via twee plaatshouders zonder dollarteken uit de
omgeving (ook in de server-service), en de pagina haalt versie en adres uit
status.json via GATE_APP_VERSION.

Tests mee verhuisd en uitgebreid: entrypoint.sh en Dockerfile in plaats van het
command-blok, en de tag in de compose gelijk aan VERSION in build.sh voor elke
eigen image. Mutatie-getest met drie ingrepen.

Nog niet gebouwd: er is hier geen Docker. De tag staat ongepind tot de eerste
push; dat is fase 4 en die is van de gebruiker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 20:20:42 +02:00

174 lines
6.9 KiB
JavaScript

// ═══════════════════════════════════════════════════════════════════════════════
// Toetst dat de JavaScript in de statuspagina's parseert, en dat hij dat ook nog
// doet nadat umbreld de template heeft ingevuld.
//
// Draaien: node tests/test_paginas_parsen.mjs
//
// Waarom dit bestand er is. De pagina's zijn met bijna drieduizend regels het
// grootste deel van deze repo en er staat geen enkele toets op: alles eraan is
// handwerk in een browser. Dat blijft zo voor wat het doet, maar één klasse fouten
// hoeft niet op het apparaat gevonden te worden: een tikfout waardoor het script
// helemaal niet loopt. Het gevolg daarvan is een pagina die stil op "unknown"
// blijft staan, en dat lijkt op een agent die niet antwoordt.
//
// Aanleiding: op 30-08-2026 is de pagina van Evolu Relay in zijn geheel opnieuw
// geschreven, negenhonderd regels in één keer. Zo'n wijziging hoort niet ongetoetst
// naar een apparaat te gaan.
//
// De invulling wordt hier nagedaan en niet alleen de bron gelezen, want umbreld
// haalt elke *.template door envsubst en wat er draait is dus niet wat er staat.
// Dat vindt de gevallen waar het weghalen van een accolade-vorm de syntaxis breekt.
//
// LET OP wat deze helft NIET vindt, want dat is precies de fout die dit project
// vreest: een template-literal als `iets ${x}` wordt na de invulling `iets ` en dat
// parseert prima. Hij is dan stil kapot en niet luidruchtig. Wat dát dichthoudt is
// de dollarteken-toets in tests/test_appstore_vorm.py, die élke accolade-vorm
// vlagt die umbrelOS niet zelf invult. Deze toets is de tweede zeef en niet de
// eerste.
//
// Wat dit verder NIET toetst: of het script het juiste doet. Alleen dat het loopt.
// ═══════════════════════════════════════════════════════════════════════════════
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import vm from 'node:vm';
const HIER = dirname(fileURLToPath(import.meta.url));
const REPO = join(HIER, '..');
let goed = 0;
let fout = 0;
const toets = (omschrijving, voorwaarde, uitleg) => {
if (voorwaarde) {
goed += 1;
return;
}
fout += 1;
console.log(`FOUT: ${omschrijving}${uitleg ? ` - ${uitleg}` : ''}`);
};
/** Elke map in de repo-root met een umbrel-app.yml erin. Zelf gevonden, zoals de
* Python-toets over de store dat ook doet: een derde app valt er dan vanzelf
* onder. */
const appMappen = () =>
readdirSync(REPO, { withFileTypes: true })
.filter((regel) => regel.isDirectory())
.map((regel) => regel.name)
.filter((naam) => existsSync(join(REPO, naam, 'umbrel-app.yml')))
.sort();
/** De inhoud van elk <script>-blok zonder src-attribuut. */
const scriptBlokken = (html) => {
const blokken = [];
const patroon = /<script(?![^>]*\bsrc=)[^>]*>([\s\S]*?)<\/script>/gi;
let treffer = patroon.exec(html);
while (treffer !== null) {
blokken.push(treffer[1]);
treffer = patroon.exec(html);
}
return blokken;
};
/**
* Doet na wat umbreld met een template doet.
*
* envsubst vervangt `${NAAM}` en `$NAAM` door de waarde uit de omgeving, en door
* een lege string als die variabele niet bestaat. Wat wij hier invullen is een
* plausibele waarde voor de variabelen die de pagina's gebruiken; voor de rest de
* lege string, want dat is precies wat er op het apparaat zou gebeuren.
*/
const vulIn = (tekst) => {
const WAARDEN = { APP_VERSION: '0.5.0' };
return tekst.replace(/\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g, (heel, naam) =>
Object.prototype.hasOwnProperty.call(WAARDEN, naam) ? WAARDEN[naam] : '',
);
};
/** Parseert het script zonder het te draaien. Geeft de foutmelding of null. */
const parseert = (bron) => {
try {
// new vm.Script parseert bij het aanmaken en draait niets. Dat is precies wat
// hier nodig is: draaien zou een document en een window vragen die er niet
// zijn, en dan toets je je eigen namaakbrowser.
new vm.Script(bron);
return null;
} catch (error) {
return String(error && error.message ? error.message : error);
}
};
/**
* Waar de pagina van een app staat, en of umbreld hem invult.
*
* Twee plekken: als *.template in de app-map (dan gaat hij door envsubst), of als
* gewoon bestand in het bouwrecept onder tools/<app-id zonder store-id>/ (dan zit
* hij in een eigen image en wordt hij door niemand ingevuld). Electrum Gate is
* sinds 0.1.0 het tweede geval; zie het plan Eigenimage.
*/
const vindPagina = (app) => {
const template = join(REPO, app, 'index.html.template');
if (existsSync(template)) {
return { pad: template, template: true };
}
const kort = app.includes('-') ? app.slice(app.indexOf('-') + 1) : app;
const inImage = join(REPO, 'tools', kort, 'index.html');
if (existsSync(inImage)) {
return { pad: inImage, template: false };
}
return null;
};
for (const app of appMappen()) {
const pagina = vindPagina(app);
if (pagina === null) {
// Geen pagina is geen fout: een app hoeft er geen te hebben.
continue;
}
const bron = readFileSync(pagina.pad, 'utf8');
const blokken = scriptBlokken(bron);
toets(`${app}: de pagina heeft een scriptblok`, blokken.length > 0);
blokken.forEach((blok, index) => {
const naam = `${app}: scriptblok ${index + 1}`;
const rauw = parseert(blok);
toets(`${naam} parseert zoals hij in de repo staat`, rauw === null, rauw);
if (pagina.template) {
const ingevuld = parseert(vulIn(blok));
toets(`${naam} parseert ook nadat umbreld hem invult`, ingevuld === null, ingevuld);
}
});
if (!pagina.template) {
// Een pagina in een image gaat door geen envsubst meer, dus wat er staat is
// wat de browser krijgt. Een accolade-variabele is dan een restant uit de
// template-tijd die letterlijk op de pagina zou belanden.
toets(`${app}: de pagina in de image bevat geen accolade-variabele`, !bron.includes('${'));
}
// De HTML zelf: geen parser, maar wel de fout die je in een bestand van
// tweeduizend regels echt maakt. Een dialog of een header die niet gesloten is,
// laat de rest van de pagina in dat element hangen.
for (const tag of ['html', 'head', 'body', 'header', 'script', 'style', 'dialog', 'details']) {
const open = (bron.match(new RegExp(`<${tag}[\\s>]`, 'gi')) || []).length;
const dicht = (bron.match(new RegExp(`</${tag}>`, 'gi')) || []).length;
if (open === 0 && dicht === 0) {
continue;
}
toets(
`${app}: elke <${tag}> is gesloten`,
open === dicht,
`${open} keer open, ${dicht} keer dicht`,
);
}
}
console.log('');
console.log(`${goed} goed, ${fout} fout`);
process.exit(fout === 0 ? 0 : 1);