Files
UmbrelApps/tests/test_limiter.mjs
T
HarmenandClaude Opus 5 c57197637f Wat de eerste echte gebruiksdag opleverde: logging, geschiedenis, uitlijning
Drie dingen die vandaag misgingen of ontbraken, en die alle drie uit gebruik
kwamen en niet uit nadenken.

De allowlist logde niets als een bekende eigenaar terugkwam en niets als er een
geweigerd werd. Daardoor stond er een tweede eigenaar drieentwintig keer aan de
deur zonder dat het log er iets over zei; het stond alleen op de pagina. Nu is er
een regel per eigenaar per keer dat het proces draait, dus ook bij terugkomst en
bij weigering, met een bovengrens omdat een weigering een id bevat dat de ander
zelf verzint. Een geweigerde schrijfactie logt voortaan hoeveel bytes er gevraagd
werden en welke grens gold, met de naam van de variabele erbij.

Allow gooide de geschiedenis weg. Een eigenaar die je alsnog toeliet kreeg "first
seen" op het moment van de klik, terwijl hij al twintig minuten stond te
kloppen. Juist daar wil je zien sinds wanneer. Met een toets erop.

En de knoppen stonden niet recht: id, gegevens en knoppen stonden naast elkaar,
dus de lengte van de gegevensregel bepaalde of de rij afbrak. Een eigenaar met
een last seen duwde zijn knoppen naar de volgende regel en dus naar links.
Nu staan naam en gegevens onder elkaar in een kolom en worden de knoppen altijd
naar rechts geduwd.

Inhoudelijk het belangrijkste: "de eerste eigenaar wint" is te smal. Een enkele
app kan meer dan een eigenaar gebruiken, en de pagina adviseerde de deur te
sluiten zodra de eerste binnen was. Daarmee sluit je je eigen tweede eigenaar
buiten. De tekst zegt nu te wachten tot er een minuut niets nieuws meer bij komt.

Image naar 0.3.0. De digest van 0.2.0 is uit de compose gehaald in plaats van
blijven staan: die zou naar de vorige image wijzen terwijl de tag iets anders
belooft.

Tests: alle vier groen (34, 54, 39 en 61 goed, 0 fout).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:08:19 +02:00

193 lines
8.7 KiB
JavaScript

// ═══════════════════════════════════════════════════════════════════════════════
// Toetst het toegangsbeleid van de relay: tools/evolu-relay/src/policy.js
//
// Draaien: node tests/test_limiter.mjs
//
// Geen testrunner en geen afhankelijkheden, net als de Python-tests in deze map.
// Geeft 0 terug als alles goed is en 1 bij de eerste fout in de telling.
//
// Waarom dit bestand er is: dit beleid is de enige plek die bepaalt wie er op de
// relay mag schrijven. De duurste fout zit hier niet in een berekening maar in
// een guard, en dat is precies wat hieronder getoetst wordt: een geblokkeerde
// eigenaar mag niet alsnog binnenkomen doordat de leerstand aanstaat.
// ═══════════════════════════════════════════════════════════════════════════════
import {
MAX_OWNER_ID_LENGTH,
MAX_REJECTED,
applyCommand,
createEmptyState,
decideOwner,
normalizeState,
} from '../tools/evolu-relay/src/policy.js';
let goed = 0;
let fout = 0;
const toets = (omschrijving, voorwaarde) => {
if (voorwaarde) {
goed += 1;
return;
}
fout += 1;
console.log(`FOUT: ${omschrijving}`);
};
const NU = '2026-08-28T12:00:00.000Z';
const LATER = '2026-08-28T13:00:00.000Z';
const EIGEN = 'owner-van-de-gebruiker';
const VREEMD = 'owner-van-een-ander';
// ── De leerstand ──────────────────────────────────────────────────────────────
{
const vers = createEmptyState();
toets('een verse staat staat in de leerstand', vers.learning === true);
const eerste = decideOwner(vers, EIGEN, NU);
toets('de eerste eigenaar wordt toegelaten', eerste.allowed === true);
toets('en onthouden als toegelaten', eerste.state.owners.length === 1);
toets('met de reden dat hij geleerd is', eerste.reason === 'learned');
const nogEens = decideOwner(eerste.state, EIGEN, LATER);
toets('dezelfde eigenaar blijft toegelaten', nogEens.allowed === true);
toets('en komt er niet dubbel in', nogEens.state.owners.length === 1);
toets('de reden is nu dat hij bekend is', nogEens.reason === 'known-allowed');
}
// ── De leerstand uit ──────────────────────────────────────────────────────────
{
const geleerd = decideOwner(createEmptyState(), EIGEN, NU).state;
const dicht = applyCommand(geleerd, { action: 'set-learning', value: false }, NU).state;
const eigen = decideOwner(dicht, EIGEN, LATER);
toets('met de leerstand uit mag de bekende eigenaar er nog steeds in', eigen.allowed === true);
const vreemd = decideOwner(dicht, VREEMD, LATER);
toets('maar een onbekende wordt geweigerd', vreemd.allowed === false);
toets('de weigering wordt onthouden voor de pagina', vreemd.state.rejected.length === 1);
toets('en hij belandt niet in de allowlist', vreemd.state.owners.length === 1);
}
// ── De guard die er het meest toe doet ────────────────────────────────────────
{
const geleerd = decideOwner(createEmptyState(), EIGEN, NU).state;
const geblokkeerd = applyCommand(geleerd, { action: 'block', ownerId: EIGEN }, NU).state;
toets('de leerstand staat hier nog aan', geblokkeerd.learning === true);
const opnieuw = decideOwner(geblokkeerd, EIGEN, LATER);
toets(
'een geblokkeerde eigenaar komt er niet alsnog in via de leerstand',
opnieuw.allowed === false,
);
toets('en de reden zegt dat hij geblokkeerd is', opnieuw.reason === 'known-blocked');
}
// ── Onbruikbare invoer ────────────────────────────────────────────────────────
{
const vers = createEmptyState();
for (const rommel of ['', null, undefined, 42, {}, 'x'.repeat(MAX_OWNER_ID_LENGTH + 1)]) {
const uitkomst = decideOwner(vers, rommel, NU);
toets(`een onbruikbaar id (${typeof rommel}) wordt geweigerd`, uitkomst.allowed === false);
toets('en wordt niet onthouden', uitkomst.state.owners.length === 0);
toets('ook niet als geweigerde poging', uitkomst.state.rejected.length === 0);
}
}
// ── De lijst met weigeringen loopt niet vol ───────────────────────────────────
{
let staat = { ...createEmptyState(), learning: false };
for (let i = 0; i < MAX_REJECTED + 5; i += 1) {
staat = decideOwner(staat, `indringer-${i}`, NU).state;
}
toets('de lijst met weigeringen wordt afgekapt', staat.rejected.length === MAX_REJECTED);
toets('en de nieuwste staat vooraan', staat.rejected[0].id === `indringer-${MAX_REJECTED + 4}`);
const herhaling = decideOwner(staat, `indringer-${MAX_REJECTED + 4}`, LATER);
toets('een herhaalde poging telt op', herhaling.state.rejected[0].attempts === 2);
toets('en voegt geen tweede regel toe', herhaling.state.rejected.length === MAX_REJECTED);
}
// ── De opdrachten van de pagina ───────────────────────────────────────────────
{
const geleerd = decideOwner(createEmptyState(), EIGEN, NU).state;
const uit = applyCommand(geleerd, { action: 'set-learning', value: false }, NU);
toets('de leerstand gaat uit', uit.state.learning === false);
toets('en dat is een wijziging', uit.changed === true);
const nogEensUit = applyCommand(uit.state, { action: 'set-learning', value: false }, NU);
toets('nog een keer uitzetten verandert niets', nogEensUit.changed === false);
toets('en is geen fout', nogEensUit.error === null);
const vergeten = applyCommand(geleerd, { action: 'forget', ownerId: EIGEN }, NU);
toets('vergeten haalt de eigenaar uit de lijst', vergeten.state.owners.length === 0);
const onbekend = applyCommand(geleerd, { action: 'forget', ownerId: VREEMD }, NU);
toets('een onbekende vergeten is een fout', onbekend.error === 'unknown-owner');
toets('en verandert niets', onbekend.changed === false);
const blokkeerOnbekend = applyCommand(geleerd, { action: 'block', ownerId: VREEMD }, NU);
toets('een onbekende blokkeren is een fout', blokkeerOnbekend.error === 'unknown-owner');
const dicht = { ...createEmptyState(), learning: false };
const geweigerd = decideOwner(dicht, VREEMD, NU).state;
const alsnog = applyCommand(geweigerd, { action: 'allow', ownerId: VREEMD }, LATER);
toets('een geweigerde poging kan alsnog toegelaten worden', alsnog.state.owners.length === 1);
toets('en verdwijnt dan uit de weigeringen', alsnog.state.rejected.length === 0);
toets(
'en houdt het moment waarop hij zich meldde, niet dat van de klik',
alsnog.state.owners[0].firstSeen === NU,
);
toets(
'en mag er daarna in, ook met de leerstand uit',
decideOwner(alsnog.state, VREEMD, LATER).allowed === true,
);
toets(
'een onbekende actie is een fout',
applyCommand(geleerd, { action: 'sudo' }, NU).error === 'unknown-action',
);
toets(
'een opdracht zonder vorm is een fout',
applyCommand(geleerd, 'zet alles open', NU).error === 'malformed-command',
);
toets(
'een leerstand-opdracht zonder boolean is een fout',
applyCommand(geleerd, { action: 'set-learning', value: 'ja' }, NU).error === 'malformed-command',
);
}
// ── Lezen van schijf ──────────────────────────────────────────────────────────
{
const goedeStaat = decideOwner(createEmptyState(), EIGEN, NU).state;
const rondgang = normalizeState(JSON.parse(JSON.stringify(goedeStaat)));
toets('een eigen staat overleeft de rondgang door JSON', rondgang !== null);
toets('met de eigenaar er nog in', rondgang.owners[0].id === EIGEN);
const rommel = [
null,
'tekst',
[],
{ version: 999, learning: true, owners: [], rejected: [] },
{ version: 1, learning: 'ja', owners: [], rejected: [] },
{ version: 1, learning: true, owners: {}, rejected: [] },
{ version: 1, learning: true, owners: [{ id: '', allowed: true }], rejected: [] },
{ version: 1, learning: true, owners: [{ id: EIGEN }], rejected: [] },
];
for (const invoer of rommel) {
toets(`onbegrepen staat wordt geweigerd: ${JSON.stringify(invoer)}`, normalizeState(invoer) === null);
}
}
console.log('');
console.log(`${goed} goed, ${fout} fout`);
process.exit(fout === 0 ? 0 : 1);