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>
174 lines
6.9 KiB
JavaScript
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);
|