De pagina van Evolu Relay op de lijst van de gebruiker
Negen punten uit echt gebruik met 0.4.0, alle negen gedaan. Twee ervan waren onderzoeksvragen en die staan onderaan. Evolu Relay 0.5.0 - Een tijdvenster van twee minuten voor nieuwe eigenaars, met een stopknop. De teller zit in het relay-proces en niet in de pagina: een teller in een tabblad dat je sluit, sluit de deur niet. policy.js kreeg learningUntil, isLearningOpen en expireLearning. - decideOwner kijkt naar isLearningOpen en niet naar het veld learning. De lus die een verlopen venster opruimt loopt elke twee seconden, en in dat gat zou een onbekende alsnog binnenkomen. - Zonder STATE_VERSION te verhogen, met een toets die dat verdedigt: een verhoging zou de allowlist van de draaiende installatie laten afwijzen en de deur sluiten voor eigenaars die er al in stonden. - Labels op een eigenaar-id, in een eigen labels.json met de agent als enige schrijver. Een label zegt niets over toegang, dus de relay hoeft het niet te weten; het is daardoor meteen opgeslagen en werkt ook als de relay omligt. - Geblokkeerde en geweigerde eigenaars in een kader, met een badge die zegt welke van de twee het is. De badge staat buiten het hover-blok, anders is dat onderscheid onzichtbaar tenzij je over de regel gaat. - Maatvoering gelijk aan Electrum Gate: 1760px, hetzelfde raster, icoon van 64 pixels, dezelfde kop, versienummer erachter. Uitleg uit de kaders, knoppen pas bij hover, geen voetregel. Electrum Gate 0.0.24 - Menu-item "About this app", in beide apps. - De statuswidget zei "Answering" met "answered in 7 ms, from inside the app" en zegt nu "Running" met de meting eronder. De nuance dat de controle van container naar container loopt is verplaatst naar een eigen kopje in die dialoog, waar er ruimte voor is; vier woorden waren te weinig. Toetsen en gereedschap - tests/test_relay_agent.py (nieuw, 65 toetsen) en tests/test_paginas_parsen.mjs (nieuw). Muteertests gedraaid op de beslissende regels. - Een dollarteken-toets in test_appstore_vorm.py. Het commentaar in drie bestanden beweerde al dat die test bestond; nu is dat waar. - Een toets dat er geen werkbestanden in een app-map staan. umbreld kopieert de hele map naar het apparaat en in de back-up. - tools/voorbeeldpagina.mjs maakt van een *.template een pagina die je in een browser kunt openen. Dat vond meteen twee echte opmaakfouten. De twee onderzoeksvragen - Een geweigerde eigenaar komt niet in de database: isOwnerAllowed zit in de WebSocket-upgrade, dus het is een 401 en een gesloten socket. Het gewenste gevolg treedt wel op, via de client: die is local-first en levert bij toelating de hele geschiedenis. Blokkeren werkt daarentegen pas bij de volgende verbinding, en dat staat als open punt. - De blobs zijn niet met een xpub te ontcijferen; een OwnerId komt daar niet uit. Met de SLIP-21-node van het apparaat kan het wel, maar die geeft volledige zeggenschap, dus dat hoort niet in een relay. Als plan-punt opgenomen bij de tool in HomeGit/Trezor. Nog niet uitgerold: de image 0.5.0 moet gebouwd en geduwd worden. De digest staat daarom niet in de compose, want een oude digest onder een nieuwe tag levert stil de oude relay. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -316,6 +316,126 @@ def test_containernamen_zijn_volledig(u, app):
|
||||
not fout, "korte namen: %r" % fout)
|
||||
|
||||
|
||||
def test_geen_werkbestanden_in_de_app_map(u, app):
|
||||
"""In een app-map staat alleen wat umbreld nodig heeft.
|
||||
|
||||
umbreld kopieert bij een installatie de héle app-map naar
|
||||
`~/umbrel/app-data/<app-id>/` met `rsync --archive`. Alles wat daar staat
|
||||
belandt dus op het apparaat en in de back-up: een CLAUDE.md, een gelaagd
|
||||
bewerkbestand van een icoon, een testscript, een aantekening.
|
||||
|
||||
Dat is geen theorie. Op 30-08-2026 stond er een `icon.pdn` van 175 kB in de map
|
||||
van Evolu Relay, een Paint.NET-bestand van een icoon dat nog niet af was. Het
|
||||
apparaat kan er niets mee en het maakt elke back-up groter. Verplaatst naar
|
||||
`tools/icons/`; deze toets is wat voorkomt dat de volgende terugkomt.
|
||||
|
||||
De lijst hieronder is een whitelist en geen blacklist, en dat is met opzet: bij
|
||||
een nieuw soort bestand hoort iemand na te denken of het daar hoort, en een
|
||||
blacklist stelt die vraag nooit.
|
||||
"""
|
||||
TOEGESTAAN_EXACT = {
|
||||
"umbrel-app.yml", "docker-compose.yml", "exports.sh", "torrc",
|
||||
"icon.png", "icon.svg", ".gitkeep",
|
||||
}
|
||||
TOEGESTAANE_MAPPEN = {"data", "hooks"}
|
||||
|
||||
fout = []
|
||||
for naam in sorted(os.listdir(os.path.join(REPO, app))):
|
||||
pad = os.path.join(REPO, app, naam)
|
||||
|
||||
if os.path.isdir(pad):
|
||||
# __pycache__ is niet gecommit (het staat in .gitignore) en komt dus
|
||||
# nooit in de kloon die umbreld ophaalt. Het staat er in een werkboom
|
||||
# zodra een test de agent-template importeert, en daarover klagen zou
|
||||
# de toets rood zetten op iets dat het apparaat niet bereikt.
|
||||
if naam == "__pycache__":
|
||||
continue
|
||||
if naam not in TOEGESTAANE_MAPPEN:
|
||||
fout.append(naam + "/")
|
||||
continue
|
||||
|
||||
if naam in TOEGESTAAN_EXACT:
|
||||
continue
|
||||
# Alles wat umbreld bij een update ververst is een template, en die horen
|
||||
# er dus per definitie.
|
||||
if naam.endswith(".template"):
|
||||
continue
|
||||
fout.append(naam)
|
||||
|
||||
u.check("%s: er staan geen werkbestanden in de app-map" % app,
|
||||
not fout,
|
||||
"deze horen buiten de app-map: %r" % fout)
|
||||
|
||||
|
||||
# De variabelen die umbrelOS werkelijk invult. Alleen deze mogen in een template
|
||||
# staan; zie test_templates_hebben_geen_losse_dollars. Overgenomen uit de tabel in
|
||||
# Docs/Referenties/Umbrel-appstore-spec.md §3.
|
||||
UMBREL_VARIABELEN = {
|
||||
"APP_ID", "APP_VERSION", "APP_DATA_DIR", "APP_MANIFEST_FILE", "UMBREL_ROOT",
|
||||
"DEVICE_HOSTNAME", "DEVICE_DOMAIN_NAME", "APP_DOMAIN",
|
||||
"APP_PROXY_HOSTNAME", "APP_PROXY_PORT", "NETWORK_IP",
|
||||
"TOR_PROXY_IP", "TOR_PROXY_PORT", "TOR_DATA_DIR",
|
||||
"APP_HIDDEN_SERVICE", "APP_SEED", "APP_PASSWORD",
|
||||
}
|
||||
|
||||
# Een app krijgt daarnaast de exports van zijn afhankelijkheden, en die heten
|
||||
# APP_<AFHANKELIJKHEID>_<IETS>. Electrum Gate gebruikt APP_ELECTRS_NODE_IP en
|
||||
# APP_ELECTRS_NODE_PORT. Die kunnen niet in een vaste lijst staan, want welke er
|
||||
# zijn hangt af van de afhankelijkheid; vandaar een vorm in plaats van een naam.
|
||||
EXPORT_VORM = r"^APP_[A-Z0-9]+_[A-Z0-9_]+$"
|
||||
|
||||
|
||||
def test_templates_hebben_geen_losse_dollars(u, app):
|
||||
"""In een *.template staat geen dollarteken dat umbrelOS niet invult.
|
||||
|
||||
Dit is de valstrik van dit hele project, en tot 30-08-2026 beweerde het
|
||||
commentaar in drie bestanden dat déze test hem dichthield terwijl dat niet zo
|
||||
was.
|
||||
|
||||
Wat er gebeurt: umbreld haalt elke `*.template` bij het starten door envsubst.
|
||||
Dat vervangt élke accolade-vorm, ook een variabele die niet bestaat, en die
|
||||
wordt dan leeg. Gevolgen per bestandstype:
|
||||
|
||||
- in een pagina sloopt het een JavaScript-template-literal, want die gebruikt
|
||||
accolades achter een dollarteken. Vandaar dat de pagina's overal strings met
|
||||
een plus aan elkaar plakken;
|
||||
- in een nginx-config verdwijnen `host`, `log_format` en elke variabele in een
|
||||
access_log;
|
||||
- in Python-code verdwijnt stilzwijgend een stuk code.
|
||||
|
||||
Er komt geen foutmelding. Het bestand wordt gewoon anders dan je schreef.
|
||||
|
||||
Beide vormen die envsubst kent worden gevlagd: `${NAAM}` en het kale `$NAAM`.
|
||||
Dat tweede is de vorm waarin een nginx-variabele als `$host` geschreven wordt,
|
||||
en die is hier dus net zo fout als de eerste.
|
||||
|
||||
Wat NIET gevlagd wordt is een los dollarteken zonder naam erachter. Dat laat
|
||||
envsubst staan, en het staat in dit project in de commentaarregels die deze
|
||||
regel juist uitleggen: een toets die zijn eigen uitleg rood zet, wordt
|
||||
uitgezet.
|
||||
"""
|
||||
import re
|
||||
|
||||
for bestandsnaam in sorted(os.listdir(os.path.join(REPO, app))):
|
||||
if not bestandsnaam.endswith(".template"):
|
||||
continue
|
||||
|
||||
tekst = lees(os.path.join(REPO, app, bestandsnaam))
|
||||
onbekend = []
|
||||
for regelnummer, regel in enumerate(tekst.splitlines(), start=1):
|
||||
for treffer in re.finditer(r"\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?", regel):
|
||||
naam = treffer.group(1)
|
||||
if naam in UMBREL_VARIABELEN:
|
||||
continue
|
||||
if re.match(EXPORT_VORM, naam):
|
||||
continue
|
||||
onbekend.append("regel %d: %r" % (regelnummer, treffer.group(0)))
|
||||
|
||||
u.check("%s/%s: geen dollartekens die umbrelOS leegmaakt" % (app, bestandsnaam),
|
||||
not onbekend,
|
||||
"gevonden: %r" % onbekend[:4])
|
||||
|
||||
|
||||
def rapporteer_pinstatus(apps):
|
||||
"""Afdrukken, niet toetsen. Zie de uitleg bovenaan dit bestand."""
|
||||
print()
|
||||
@@ -346,6 +466,8 @@ def main():
|
||||
test_compose_bestaat_en_hangt_samen(u, app)
|
||||
test_data_onder_data(u, app)
|
||||
test_containernamen_zijn_volledig(u, app)
|
||||
test_geen_werkbestanden_in_de_app_map(u, app)
|
||||
test_templates_hebben_geen_losse_dollars(u, app)
|
||||
|
||||
rapporteer_pinstatus(apps)
|
||||
return u.rapport()
|
||||
|
||||
@@ -13,11 +13,14 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import {
|
||||
MAX_LEARNING_SECONDS,
|
||||
MAX_OWNER_ID_LENGTH,
|
||||
MAX_REJECTED,
|
||||
applyCommand,
|
||||
createEmptyState,
|
||||
decideOwner,
|
||||
expireLearning,
|
||||
isLearningOpen,
|
||||
normalizeState,
|
||||
} from '../tools/evolu-relay/src/policy.js';
|
||||
|
||||
@@ -35,8 +38,12 @@ const toets = (omschrijving, voorwaarde) => {
|
||||
|
||||
const NU = '2026-08-28T12:00:00.000Z';
|
||||
const LATER = '2026-08-28T13:00:00.000Z';
|
||||
// Binnen en buiten een venster van twee minuten dat op NU begint.
|
||||
const BINNEN = '2026-08-28T12:01:00.000Z';
|
||||
const NA = '2026-08-28T12:02:30.000Z';
|
||||
const EIGEN = 'owner-van-de-gebruiker';
|
||||
const VREEMD = 'owner-van-een-ander';
|
||||
const TWEE_MINUTEN = 120;
|
||||
|
||||
// ── De leerstand ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -164,6 +171,160 @@ const VREEMD = 'owner-van-een-ander';
|
||||
);
|
||||
}
|
||||
|
||||
// ── Het tijdvenster voor nieuwe eigenaars ─────────────────────────────────────
|
||||
//
|
||||
// De pagina zet de deur twee minuten open in plaats van een schakelaar om te
|
||||
// zetten die je moet onthouden. De teller hoort in dít bestand en niet in de
|
||||
// browser: een teller in een tabblad dat je sluit, sluit de deur niet.
|
||||
|
||||
{
|
||||
const vers = createEmptyState();
|
||||
toets('een verse staat heeft geen tijdslot', vers.learningUntil === null);
|
||||
toets('en staat dus open zonder dat er een klok loopt', isLearningOpen(vers, LATER) === true);
|
||||
|
||||
const open = applyCommand(
|
||||
vers,
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
NU,
|
||||
);
|
||||
toets('openzetten met seconden is een wijziging', open.changed === true);
|
||||
toets('en zet een tijdslot', open.state.learningUntil === '2026-08-28T12:02:00.000Z');
|
||||
toets('binnen het venster staat de deur open', isLearningOpen(open.state, BINNEN) === true);
|
||||
toets('erna niet meer', isLearningOpen(open.state, NA) === false);
|
||||
}
|
||||
|
||||
// De guard die er het meest toe doet, en dit is de reden dat `decideOwner` naar
|
||||
// `isLearningOpen` kijkt en niet naar het veld `learning`: tussen het aflopen van
|
||||
// het venster en de ronde die het bestand opruimt zitten een paar seconden. In dat
|
||||
// gat mag er niemand binnenkomen.
|
||||
{
|
||||
const open = applyCommand(
|
||||
createEmptyState(),
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
NU,
|
||||
).state;
|
||||
|
||||
toets('in het bestand staat de leerstand nog aan', open.learning === true);
|
||||
|
||||
const binnen = decideOwner(open, EIGEN, BINNEN);
|
||||
toets('binnen het venster wordt een onbekende geleerd', binnen.allowed === true);
|
||||
|
||||
const erna = decideOwner(open, VREEMD, NA);
|
||||
toets(
|
||||
'na het venster wordt hij geweigerd, ook al staat learning nog op true',
|
||||
erna.allowed === false,
|
||||
);
|
||||
toets('met de reden dat er niet geleerd wordt', erna.reason === 'not-learning');
|
||||
toets('en de poging wordt onthouden voor de pagina', erna.state.rejected.length === 1);
|
||||
|
||||
// En wie al binnen was, blijft binnen. Het venster gaat over onbekenden.
|
||||
const bekend = decideOwner(binnen.state, EIGEN, LATER);
|
||||
toets('een geleerde eigenaar mag er na het venster nog steeds in', bekend.allowed === true);
|
||||
}
|
||||
|
||||
{
|
||||
const open = applyCommand(
|
||||
createEmptyState(),
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
NU,
|
||||
).state;
|
||||
|
||||
// Opnieuw indrukken verlengt. Zonder deze uitzondering zou `applyCommand`
|
||||
// 'changed: false' teruggeven omdat learning al true was, en dan liep het
|
||||
// venster af terwijl de gebruiker net verlengde.
|
||||
const opnieuw = applyCommand(
|
||||
open,
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
BINNEN,
|
||||
);
|
||||
toets('opnieuw openzetten is een wijziging', opnieuw.changed === true);
|
||||
toets('en zet de klok terug', opnieuw.state.learningUntil === '2026-08-28T12:03:00.000Z');
|
||||
toets('dus is de deur na het oude venster nog open', isLearningOpen(opnieuw.state, NA) === true);
|
||||
|
||||
// Vroegtijdig sluiten haalt het tijdslot mee weg. Zou het blijven staan, dan
|
||||
// erfde een volgende 'open zonder seconden' een venster dat niet gevraagd is.
|
||||
const dicht = applyCommand(open, { action: 'set-learning', value: false }, BINNEN);
|
||||
toets('vroegtijdig sluiten kan', dicht.state.learning === false);
|
||||
toets('en haalt het tijdslot weg', dicht.state.learningUntil === null);
|
||||
toets('de deur is dan dicht', isLearningOpen(dicht.state, BINNEN) === false);
|
||||
|
||||
const zonderSlot = applyCommand(dicht.state, { action: 'set-learning', value: true }, BINNEN);
|
||||
toets('openzetten zonder seconden blijft bestaan', zonderSlot.state.learning === true);
|
||||
toets('en heeft dan geen tijdslot', zonderSlot.state.learningUntil === null);
|
||||
}
|
||||
|
||||
// Wat de agent hoort tegen te houden, houdt dit bestand ook tegen. Twee zeven, en
|
||||
// dat is met opzet: dit is de kant die van buiten komt.
|
||||
{
|
||||
const vers = createEmptyState();
|
||||
const rommel = [
|
||||
{ action: 'set-learning', value: true, seconds: 0 },
|
||||
{ action: 'set-learning', value: true, seconds: -60 },
|
||||
{ action: 'set-learning', value: true, seconds: 1.5 },
|
||||
{ action: 'set-learning', value: true, seconds: '120' },
|
||||
{ action: 'set-learning', value: true, seconds: true },
|
||||
{ action: 'set-learning', value: true, seconds: MAX_LEARNING_SECONDS + 1 },
|
||||
];
|
||||
for (const opdracht of rommel) {
|
||||
const uitkomst = applyCommand(vers, opdracht, NU);
|
||||
toets(
|
||||
`een onbruikbaar aantal seconden wordt geweigerd: ${JSON.stringify(opdracht.seconds)}`,
|
||||
uitkomst.error === 'malformed-command',
|
||||
);
|
||||
toets('en verandert niets', uitkomst.changed === false);
|
||||
}
|
||||
|
||||
toets(
|
||||
'de bovengrens zelf mag wel',
|
||||
applyCommand(vers, { action: 'set-learning', value: true, seconds: MAX_LEARNING_SECONDS }, NU)
|
||||
.error === null,
|
||||
);
|
||||
|
||||
// Een onleesbaar `now` mag geen venster zonder einde opleveren. Dat is de reden
|
||||
// dat `addSeconds` null teruggeeft in plaats van iets te verzinnen.
|
||||
const kapotteKlok = applyCommand(
|
||||
vers,
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
'geen tijdstip',
|
||||
);
|
||||
toets('met een onleesbare klok wordt de opdracht geweigerd', kapotteKlok.error === 'malformed-command');
|
||||
toets('en blijft de staat zoals hij was', kapotteKlok.changed === false);
|
||||
}
|
||||
|
||||
// Het opruimen. Correctheid hangt hier niet aan, want `isLearningOpen` weigert al;
|
||||
// wat dit oplevert is dat het bestand en de pagina hetzelfde zeggen als de klok.
|
||||
{
|
||||
const open = applyCommand(
|
||||
createEmptyState(),
|
||||
{ action: 'set-learning', value: true, seconds: TWEE_MINUTEN },
|
||||
NU,
|
||||
).state;
|
||||
|
||||
const nogNiet = expireLearning(open, BINNEN);
|
||||
toets('binnen het venster wordt er niets opgeruimd', nogNiet.changed === false);
|
||||
toets('en de staat blijft dezelfde', nogNiet.state === open);
|
||||
|
||||
const opgeruimd = expireLearning(open, NA);
|
||||
toets('na het venster gaat de leerstand uit', opgeruimd.state.learning === false);
|
||||
toets('en het tijdslot weg', opgeruimd.state.learningUntil === null);
|
||||
toets('en dat is een wijziging', opgeruimd.changed === true);
|
||||
|
||||
toets(
|
||||
'nog een keer opruimen doet niets',
|
||||
expireLearning(opgeruimd.state, LATER).changed === false,
|
||||
);
|
||||
toets(
|
||||
'een leerstand zonder tijdslot wordt niet opgeruimd',
|
||||
expireLearning(createEmptyState(), LATER).changed === false,
|
||||
);
|
||||
|
||||
// Een onleesbaar tijdstip in het bestand is dicht, niet open. Dezelfde regel als
|
||||
// bij een onleesbare owners.json in store.js: bij twijfel weigert de app.
|
||||
const kapot = { ...createEmptyState(), learningUntil: 'ergens volgende week' };
|
||||
toets('een onleesbaar tijdslot leest als dicht', isLearningOpen(kapot, NU) === false);
|
||||
toets('en een onleesbare klok ook', isLearningOpen(open, 'geen tijdstip') === false);
|
||||
}
|
||||
|
||||
// ── Lezen van schijf ──────────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
@@ -185,6 +346,28 @@ const VREEMD = 'owner-van-een-ander';
|
||||
for (const invoer of rommel) {
|
||||
toets(`onbegrepen staat wordt geweigerd: ${JSON.stringify(invoer)}`, normalizeState(invoer) === null);
|
||||
}
|
||||
|
||||
// Het tijdslot is toegevoegd zonder STATE_VERSION te verhogen, en dat is de
|
||||
// toets die dat verdedigt: een bestand van een draaiende installatie mag niet
|
||||
// ineens onleesbaar worden. Zou dat gebeuren, dan schuift store.js de allowlist
|
||||
// opzij en gaat de deur dicht voor eigenaars die er al in stonden.
|
||||
const oud = { version: 1, learning: true, owners: [{ id: EIGEN, allowed: true }], rejected: [] };
|
||||
const gelezen = normalizeState(oud);
|
||||
toets('een bestand van vóór het tijdslot leest nog', gelezen !== null);
|
||||
toets('met de leerstand die erin stond', gelezen.learning === true);
|
||||
toets('en zonder tijdslot', gelezen.learningUntil === null);
|
||||
toets('en de eigenaar er nog in', gelezen.owners[0].id === EIGEN);
|
||||
|
||||
const metSlot = normalizeState({ ...oud, learningUntil: '2026-08-28T12:02:00.000Z' });
|
||||
toets('een geldig tijdslot blijft staan', metSlot.learningUntil === '2026-08-28T12:02:00.000Z');
|
||||
|
||||
for (const onzin of ['morgen', '', 42, {}, null]) {
|
||||
toets(
|
||||
`een onleesbaar tijdslot wordt null en geen weigering: ${JSON.stringify(onzin)}`,
|
||||
normalizeState({ ...oud, learningUntil: onzin }) !== null
|
||||
&& normalizeState({ ...oud, learningUntil: onzin }).learningUntil === null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
for (const app of appMappen()) {
|
||||
const pad = join(REPO, app, 'index.html.template');
|
||||
if (!existsSync(pad)) {
|
||||
// Geen pagina is geen fout: een app hoeft er geen te hebben.
|
||||
continue;
|
||||
}
|
||||
|
||||
const bron = readFileSync(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);
|
||||
|
||||
const ingevuld = parseert(vulIn(blok));
|
||||
toets(`${naam} parseert ook nadat umbreld hem invult`, ingevuld === null, ingevuld);
|
||||
});
|
||||
|
||||
// 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);
|
||||
@@ -0,0 +1,359 @@
|
||||
"""Toetst de agent van Evolu Relay: de labels en het tijdvenster.
|
||||
|
||||
Waarom deze test bestaat. De agent van deze app beslist niets over toegang, en dat
|
||||
is precies waarom hij tot 30-08-2026 ongetoetst kon blijven: hij las een bestand en
|
||||
legde opdrachten in een postbus, en het beleid eromheen staat in `policy.js` met
|
||||
`tests/test_limiter.mjs` erop. Met deze release doet hij twee dingen erbij die
|
||||
eigen logica hebben:
|
||||
|
||||
- **hij schrijft zelf een bestand**, `labels.json`, en is daar de enige schrijver
|
||||
van. Dat is de eerste plek waar de agent staat bijhoudt in plaats van doorgeeft,
|
||||
en labelen is lezen-wijzigen-schrijven, dus er zit een slot om;
|
||||
- **hij rekent het tijdvenster uit** dat de pagina toont. Een verlopen venster moet
|
||||
hij als dicht rapporteren, ook al staat er in owners.json nog dat de leerstand
|
||||
aan is: het relay-proces ruimt dat op zijn eigen moment op.
|
||||
|
||||
Wat deze test NIET dekt: de HTTP-laag. De handlers zitten in een
|
||||
BaseHTTPRequestHandler en die is zonder socket niet aan te roepen; wat eronder
|
||||
hangt, de validatie en het schrijven, is hier wél getoetst. De pagina zelf blijft
|
||||
handwerk in een browser.
|
||||
|
||||
Draaien:
|
||||
|
||||
python tests/test_relay_agent.py
|
||||
|
||||
De test laadt `agent.py.template` rechtstreeks. Dat kan omdat dat bestand geen
|
||||
accolade-variabelen bevat en de invulling door umbreld hem dus onveranderd laat;
|
||||
de eerste toets hieronder controleert precies dat.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# Vóór de imports, want anders is het te laat: Python legt bytecode naast
|
||||
# agent.py.template zodra die geïmporteerd wordt, en die rommel hoort niet in de
|
||||
# app-map. Een keer is zo'n .pyc meegegaan in een commit.
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
import importlib.machinery # noqa: E402
|
||||
import importlib.util # noqa: E402
|
||||
import json # noqa: E402
|
||||
import os # noqa: E402
|
||||
import tempfile # noqa: E402
|
||||
from datetime import datetime, timedelta, timezone # noqa: E402
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
APP = os.path.join(HERE, os.pardir, "whatsnext-evolu-relay")
|
||||
TEMPLATE = os.path.join(APP, "agent.py.template")
|
||||
|
||||
|
||||
def load_agent(state_dir):
|
||||
"""Laadt agent.py.template als module, met zijn staat in een tijdelijke map.
|
||||
|
||||
De agent leest `RELAY_STATE_DIR` op moduleniveau, dus die moet vóór het laden
|
||||
in de omgeving staan. Met een expliciete loader, want importlib kijkt normaal
|
||||
naar de extensie en .template staat daar niet tussen.
|
||||
"""
|
||||
os.environ["RELAY_STATE_DIR"] = state_dir
|
||||
loader = importlib.machinery.SourceFileLoader("relay_agent", TEMPLATE)
|
||||
spec = importlib.util.spec_from_file_location("relay_agent", TEMPLATE, loader=loader)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class Uitslag:
|
||||
def __init__(self):
|
||||
self.goed = 0
|
||||
self.fout = []
|
||||
|
||||
def check(self, naam, gelukt, uitleg=""):
|
||||
if gelukt:
|
||||
self.goed += 1
|
||||
else:
|
||||
self.fout.append(naam + ((" - " + uitleg) if uitleg else ""))
|
||||
|
||||
def rapport(self):
|
||||
print()
|
||||
print("%d goed, %d fout" % (self.goed, len(self.fout)))
|
||||
for f in self.fout:
|
||||
print(" FOUT: " + f)
|
||||
return 0 if not self.fout else 1
|
||||
|
||||
|
||||
def iso(seconden_vanaf_nu):
|
||||
"""Een tijdstip in de vorm die het relay-proces schrijft: ISO, met een Z."""
|
||||
when = datetime.now(timezone.utc) + timedelta(seconds=seconden_vanaf_nu)
|
||||
return when.isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def schrijf_owners(state_dir, staat):
|
||||
with open(os.path.join(state_dir, "owners.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(staat, f)
|
||||
|
||||
|
||||
# ── De aanname waar deze hele test op rust ───────────────────────────────────
|
||||
|
||||
def test_template_is_invulbaar_zonder_schade(u):
|
||||
with open(TEMPLATE, "r", encoding="utf-8") as f:
|
||||
inhoud = f.read()
|
||||
u.check("template bevat geen dollartekens",
|
||||
"$" not in inhoud,
|
||||
"umbreld zou die invullen en de Python-code slopen")
|
||||
|
||||
|
||||
# ── Het tijdvenster ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_venster_dat_nog_loopt(agent, u):
|
||||
feiten = agent.learning_facts({"learning": True, "learningUntil": iso(90)})
|
||||
u.check("een lopend venster leest als open", feiten["learning"] is True)
|
||||
u.check("met een resterende tijd", isinstance(feiten["learningSecondsLeft"], int))
|
||||
u.check("die ergens rond de negentig seconden ligt",
|
||||
85 <= feiten["learningSecondsLeft"] <= 90,
|
||||
"kreeg %r" % feiten["learningSecondsLeft"])
|
||||
|
||||
|
||||
def test_venster_dat_verlopen_is(agent, u):
|
||||
"""De toets die er het meest toe doet aan deze kant.
|
||||
|
||||
In het bestand staat `learning: true` en een tijdstip dat voorbij is. Het
|
||||
relay-proces ruimt dat op in zijn lus van twee seconden, dus er is een moment
|
||||
waarop het bestand nog "aan" zegt terwijl de deur dicht is. De pagina hoort dan
|
||||
"closed" te tonen en geen open deur.
|
||||
"""
|
||||
feiten = agent.learning_facts({"learning": True, "learningUntil": iso(-30)})
|
||||
u.check("een verlopen venster leest als dicht", feiten["learning"] is False)
|
||||
u.check("met nul seconden over", feiten["learningSecondsLeft"] == 0)
|
||||
u.check("en zonder tijdstip", feiten["learningUntil"] is None)
|
||||
|
||||
|
||||
def test_venster_zonder_tijdslot(agent, u):
|
||||
feiten = agent.learning_facts({"learning": True, "learningUntil": None})
|
||||
u.check("open zonder tijdslot blijft open", feiten["learning"] is True)
|
||||
u.check("en heeft geen teller", feiten["learningSecondsLeft"] is None)
|
||||
|
||||
|
||||
def test_onleesbaar_tijdslot(agent, u):
|
||||
"""Onleesbaar hoort niet als "voor altijd open" te lezen, maar wel als open.
|
||||
|
||||
Anders dan bij `policy.js`, en dat is met opzet: dit is de weergave en niet de
|
||||
beslissing. Het relay-proces beslist, en dáár is een onleesbaar tijdstip dicht.
|
||||
De pagina die iets anders toont dan het bestand zegt zou verwarrender zijn dan
|
||||
een pagina die de leerstand toont die er staat.
|
||||
"""
|
||||
for onzin in ["morgen", "", 42, {}, []]:
|
||||
feiten = agent.learning_facts({"learning": True, "learningUntil": onzin})
|
||||
u.check("een onleesbaar tijdslot valt terug op de leerstand zelf: %r" % (onzin,),
|
||||
feiten["learning"] is True and feiten["learningSecondsLeft"] is None)
|
||||
|
||||
|
||||
def test_leerstand_onbekend(agent, u):
|
||||
"""Onbekend is geen synonym voor uit.
|
||||
|
||||
Ontbreekt owners.json, dan is er nog nooit iets geschreven en weet niemand wat
|
||||
de leerstand is. Uit is een keuze, onbekend is een reden om te kijken, en de
|
||||
pagina hoort dat verschil te tonen.
|
||||
"""
|
||||
feiten = agent.learning_facts({})
|
||||
u.check("een ontbrekende leerstand blijft onbekend", feiten["learning"] is None)
|
||||
u.check("en heeft geen teller", feiten["learningSecondsLeft"] is None)
|
||||
|
||||
uit = agent.learning_facts({"learning": False, "learningUntil": None})
|
||||
u.check("en uit blijft uit", uit["learning"] is False)
|
||||
|
||||
|
||||
# ── De labels ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_label_zetten_en_weghalen(agent, u):
|
||||
u.check("een label zetten lukt", agent.apply_label("owner-a", "Laptop") is None)
|
||||
u.check("en staat er daarna", agent.read_labels().get("owner-a") == "Laptop")
|
||||
|
||||
u.check("overschrijven lukt", agent.apply_label("owner-a", "Werklaptop") is None)
|
||||
u.check("en vervangt de vorige", agent.read_labels().get("owner-a") == "Werklaptop")
|
||||
|
||||
u.check("een leeg label haalt hem weg", agent.apply_label("owner-a", "") is None)
|
||||
u.check("en dan is hij er niet meer", "owner-a" not in agent.read_labels())
|
||||
|
||||
u.check("nog een keer weghalen is geen fout",
|
||||
agent.apply_label("owner-a", "") is None)
|
||||
|
||||
|
||||
def test_label_blijft_op_schijf(agent, u, state_dir):
|
||||
"""Het bestand is JSON en te lezen zonder de agent.
|
||||
|
||||
Dat is geen formaliteit: dit bestand staat onder de app-datamap en gaat mee in
|
||||
de back-up van umbrelOS. Een gebruiker die zijn labels kwijt is, moet ze daar
|
||||
kunnen terugvinden.
|
||||
"""
|
||||
agent.apply_label("owner-b", "Telefoon")
|
||||
with open(os.path.join(state_dir, "labels.json"), "r", encoding="utf-8") as f:
|
||||
op_schijf = json.load(f)
|
||||
u.check("het label staat in labels.json", op_schijf.get("owner-b") == "Telefoon")
|
||||
|
||||
|
||||
def test_label_validatie(agent, u):
|
||||
goed = agent.valid_label({"ownerId": "owner-c", "label": " Mijn laptop "})
|
||||
u.check("witruimte wordt samengetrokken", goed[1] == "Mijn laptop")
|
||||
u.check("en er is geen fout", goed[2] is None)
|
||||
|
||||
# Regeleindes eruit: dit is één regel naast een id, en een label met een nieuwe
|
||||
# regel erin zou de lijst uit elkaar trekken.
|
||||
plat = agent.valid_label({"ownerId": "owner-c", "label": "een\ntwee"})
|
||||
u.check("een regeleinde wordt een spatie", plat[1] == "een twee")
|
||||
|
||||
leeg = agent.valid_label({"ownerId": "owner-c"})
|
||||
u.check("een ontbrekend label is leeg en geen fout", leeg[1] == "" and leeg[2] is None)
|
||||
|
||||
for rommel, waarom in [
|
||||
({}, "geen ownerId"),
|
||||
({"ownerId": ""}, "leeg ownerId"),
|
||||
({"ownerId": 42}, "ownerId is geen tekst"),
|
||||
({"ownerId": "x" * (agent.MAX_OWNER_ID_LENGTH + 1)}, "ownerId te lang"),
|
||||
({"ownerId": "owner-c", "label": 42}, "label is geen tekst"),
|
||||
({"ownerId": "owner-c", "label": "x" * (agent.MAX_LABEL_LENGTH + 1)}, "label te lang"),
|
||||
("geen object", "geen object"),
|
||||
]:
|
||||
u.check("een onbruikbaar label wordt geweigerd: %s" % waarom,
|
||||
agent.valid_label(rommel)[2] is not None)
|
||||
|
||||
|
||||
def test_labels_lopen_niet_vol(agent, u):
|
||||
"""Er zit een plafond op. Niet tegen een aanvaller, want deze pagina zit achter
|
||||
de inlog van umbrelOS, maar tegen een lus die per ongeluk blijft schrijven.
|
||||
"""
|
||||
for i in range(agent.MAX_LABELS):
|
||||
agent.apply_label("bulk-%d" % i, "label %d" % i)
|
||||
|
||||
u.check("het plafond is bereikt", len(agent.read_labels()) == agent.MAX_LABELS)
|
||||
u.check("en een label erboven wordt geweigerd",
|
||||
agent.apply_label("een-te-veel", "nog een") is not None)
|
||||
u.check("maar een bestaand label mag nog wél gewijzigd worden",
|
||||
agent.apply_label("bulk-0", "gewijzigd") is None,
|
||||
"anders kun je bij een vol bestand niets meer verbeteren")
|
||||
|
||||
|
||||
def test_onleesbare_labels_zijn_geen_ramp(agent, u, state_dir):
|
||||
"""Anders dan owners.json: daar hangt aan een half begrepen bestand de vraag wie
|
||||
er binnen mag, en dan is weigeren het antwoord. Hier gaat het om een naam naast
|
||||
een id, en het ergste gevolg is dat je de rauwe ids ziet.
|
||||
"""
|
||||
with open(os.path.join(state_dir, "labels.json"), "w", encoding="utf-8") as f:
|
||||
f.write("dit is geen json {{{")
|
||||
u.check("een onleesbaar labelbestand leest als leeg", agent.read_labels() == {})
|
||||
|
||||
with open(os.path.join(state_dir, "labels.json"), "w", encoding="utf-8") as f:
|
||||
json.dump({"owner-d": "Goed", "owner-e": 42, "": "geen id", "owner-f": " "}, f)
|
||||
labels = agent.read_labels()
|
||||
u.check("de goede regel blijft", labels.get("owner-d") == "Goed")
|
||||
u.check("een label dat geen tekst is valt weg", "owner-e" not in labels)
|
||||
u.check("een leeg id valt weg", "" not in labels)
|
||||
u.check("een label van alleen witruimte valt weg", "owner-f" not in labels)
|
||||
|
||||
|
||||
# ── De opdracht met seconden ────────────────────────────────────────────────
|
||||
|
||||
def test_set_learning_met_seconden(agent, u):
|
||||
goed, problem = agent.valid_command({"action": "set-learning", "value": True, "seconds": 120})
|
||||
u.check("openzetten met seconden mag", problem is None)
|
||||
u.check("en de seconden gaan mee naar de relay", goed.get("seconds") == 120)
|
||||
|
||||
zonder, problem = agent.valid_command({"action": "set-learning", "value": True})
|
||||
u.check("openzetten zonder seconden mag ook", problem is None)
|
||||
u.check("en dan staat er geen seconds in de opdracht", "seconds" not in zonder)
|
||||
|
||||
for seconden, waarom in [
|
||||
(0, "nul"),
|
||||
(-60, "negatief"),
|
||||
(1.5, "geen heel getal"),
|
||||
("120", "tekst"),
|
||||
# isinstance(True, int) is in Python waar, dus zonder de uitsluiting in de
|
||||
# agent zou een boolean hier als aantal seconden doorglippen.
|
||||
(True, "een boolean"),
|
||||
(agent.MAX_LEARNING_SECONDS + 1, "boven de bovengrens"),
|
||||
]:
|
||||
_, problem = agent.valid_command(
|
||||
{"action": "set-learning", "value": True, "seconds": seconden})
|
||||
u.check("een onbruikbaar aantal seconden wordt geweigerd: %s" % waarom,
|
||||
problem is not None)
|
||||
|
||||
_, problem = agent.valid_command(
|
||||
{"action": "set-learning", "value": True, "seconds": agent.MAX_LEARNING_SECONDS})
|
||||
u.check("de bovengrens zelf mag wel", problem is None)
|
||||
|
||||
# Seconden bij dichtzetten is onzin en hoort dus geweigerd te worden in plaats
|
||||
# van stil genegeerd: stil negeren zou een sluitopdracht met een tikfout laten
|
||||
# slagen terwijl er iets anders gebeurt dan er staat.
|
||||
_, problem = agent.valid_command(
|
||||
{"action": "set-learning", "value": False, "seconds": 120})
|
||||
u.check("seconden bij dichtzetten wordt geweigerd", problem is not None)
|
||||
|
||||
|
||||
def test_status_hangt_labels_aan_de_regels(agent, u, state_dir):
|
||||
"""De pagina krijgt het label bij de eigenaar, en het bestand van de relay
|
||||
blijft ongemoeid. Dat tweede is de reden dat `met_label` een kopie maakt.
|
||||
"""
|
||||
schrijf_owners(state_dir, {
|
||||
"version": 1,
|
||||
"learning": False,
|
||||
"learningUntil": None,
|
||||
"owners": [
|
||||
{"id": "owner-g", "allowed": True, "firstSeen": iso(-600), "lastSeen": iso(-60)},
|
||||
{"id": "owner-h", "allowed": False, "firstSeen": iso(-900), "lastSeen": None},
|
||||
],
|
||||
"rejected": [{"id": "owner-i", "firstSeen": iso(-300), "lastSeen": iso(-10),
|
||||
"attempts": 3}],
|
||||
})
|
||||
agent.apply_label("owner-g", "Laptop")
|
||||
agent.apply_label("owner-i", "Onbekend apparaat")
|
||||
|
||||
status = agent.build_status()
|
||||
owners = status["owners"]
|
||||
|
||||
u.check("de toegelaten eigenaar krijgt zijn label",
|
||||
owners["allowed"][0]["label"] == "Laptop")
|
||||
u.check("de geblokkeerde staat in blocked", owners["blocked"][0]["id"] == "owner-h")
|
||||
u.check("en heeft geen label", owners["blocked"][0]["label"] is None)
|
||||
u.check("de geweigerde poging staat in rejected", owners["rejected"][0]["id"] == "owner-i")
|
||||
u.check("en krijgt zijn label", owners["rejected"][0]["label"] == "Onbekend apparaat")
|
||||
u.check("het aantal pogingen gaat mee", owners["rejected"][0]["attempts"] == 3)
|
||||
|
||||
u.check("de leerstand komt uit learning_facts", owners["learning"] is False)
|
||||
u.check("en de statusvelden voor de teller staan erin",
|
||||
"learningSecondsLeft" in owners and "learningUntil" in owners)
|
||||
|
||||
with open(os.path.join(state_dir, "owners.json"), "r", encoding="utf-8") as f:
|
||||
op_schijf = json.load(f)
|
||||
u.check("owners.json is niet aangeraakt",
|
||||
all("label" not in regel for regel in op_schijf["owners"]),
|
||||
"de agent hoort niet in het bestand van de relay te schrijven")
|
||||
|
||||
|
||||
def main():
|
||||
u = Uitslag()
|
||||
test_template_is_invulbaar_zonder_schade(u)
|
||||
|
||||
with tempfile.TemporaryDirectory() as state_dir:
|
||||
agent = load_agent(state_dir)
|
||||
|
||||
test_venster_dat_nog_loopt(agent, u)
|
||||
test_venster_dat_verlopen_is(agent, u)
|
||||
test_venster_zonder_tijdslot(agent, u)
|
||||
test_onleesbaar_tijdslot(agent, u)
|
||||
test_leerstand_onbekend(agent, u)
|
||||
|
||||
test_label_zetten_en_weghalen(agent, u)
|
||||
test_label_blijft_op_schijf(agent, u, state_dir)
|
||||
test_label_validatie(agent, u)
|
||||
test_onleesbare_labels_zijn_geen_ramp(agent, u, state_dir)
|
||||
|
||||
test_set_learning_met_seconden(agent, u)
|
||||
test_status_hangt_labels_aan_de_regels(agent, u, state_dir)
|
||||
|
||||
# Als laatste: deze vult het labelbestand tot het plafond en laat dus geen
|
||||
# bruikbare staat achter voor een toets erna.
|
||||
test_labels_lopen_niet_vol(agent, u)
|
||||
|
||||
return u.rapport()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user