2026-09-09 12:59:55 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Toetst de testcliënt zonder relay: tools/relay-client/src/client.js
|
|
|
|
|
//
|
|
|
|
|
// Draaien: node tests/test_client_lokaal.mjs
|
|
|
|
|
//
|
|
|
|
|
// Anders dan de rest van deze map heeft deze toets wél afhankelijkheden nodig,
|
|
|
|
|
// namelijk die van tools/relay-client. Staat `node_modules` daar niet, dan slaat
|
|
|
|
|
// hij zichzelf over en meldt dat, zoals de certificaattoetsen doen als de
|
|
|
|
|
// certificaatwinkel van het besturingssysteem ontbreekt. Installeren gaat met
|
|
|
|
|
// `npm install` in tools/relay-client.
|
|
|
|
|
//
|
|
|
|
|
// Waarom dit bestand er is: `@evolu/nodejs` bevat geen cliënt, dus de
|
|
|
|
|
// afhankelijkheden voor `createEvolu` worden in src/evolu-node.js zelf
|
|
|
|
|
// samengesteld. Dat is de plek die bij een versieverhoging van Evolu als eerste
|
|
|
|
|
// omvalt, en hij faalt stil: alle vier de fouten die dat bestand tijdens de bouw
|
|
|
|
|
// opleverde gaven een instantie die het lijkt te doen, tot de eerste
|
|
|
|
|
// schrijfactie. **Deze toets doet dus een schrijfactie**, en aanmaken alleen is
|
|
|
|
|
// geen bewijs.
|
|
|
|
|
//
|
|
|
|
|
// Geen relay: `openStore` zonder `relayUrl`. Wat hier getoetst wordt is de
|
|
|
|
|
// cliënt, en die is los van het netwerk te beproeven. Het toegangsbeleid van de
|
|
|
|
|
// relay staat in test_limiter.mjs.
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
|
|
import { tmpdir } from 'node:os';
|
|
|
|
|
import { join } from 'node:path';
|
|
|
|
|
|
|
|
|
|
// Een wachthond, en die is hier niet voor de sier. Gaat er iets mis in de
|
|
|
|
|
// samenstelling, dan komt dat er meestal uit als een schrijfactie die nooit
|
|
|
|
|
// antwoordt in plaats van een fout: het defect zit in een worker en de
|
|
|
|
|
// `loadQuery` erna blijft hangen. Zonder deze wachthond blijft de suite dus
|
|
|
|
|
// staan in plaats van rood te worden, en dat is bij de mutatietoets van
|
|
|
|
|
// 09-09-2026 letterlijk gebeurd.
|
|
|
|
|
const WACHTHOND_SECONDEN = 30;
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
console.log(`FOUT: niets meer gehoord na ${WACHTHOND_SECONDEN} seconden.`);
|
|
|
|
|
console.log(' Dat wijst op een defect in een worker; zie tools/relay-client/src/evolu-node.js.');
|
|
|
|
|
console.log('');
|
|
|
|
|
console.log('afgebroken');
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}, WACHTHOND_SECONDEN * 1000).unref();
|
|
|
|
|
|
|
|
|
|
let goed = 0;
|
|
|
|
|
let fout = 0;
|
|
|
|
|
|
|
|
|
|
const toets = (omschrijving, voorwaarde) => {
|
|
|
|
|
if (voorwaarde) {
|
|
|
|
|
goed += 1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fout += 1;
|
|
|
|
|
console.log(`FOUT: ${omschrijving}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── Zijn de afhankelijkheden er? ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
let client;
|
|
|
|
|
try {
|
|
|
|
|
client = await import('../tools/relay-client/src/client.js');
|
|
|
|
|
} catch (oorzaak) {
|
|
|
|
|
console.log('OVERGESLAGEN: de pakketten van tools/relay-client ontbreken.');
|
|
|
|
|
console.log(` Draai daar eerst \`npm install\`. (${oorzaak.message})`);
|
|
|
|
|
console.log('');
|
|
|
|
|
console.log('0 goed, 0 fout');
|
|
|
|
|
process.exit(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { eigenaarUitMnemonic, nieuweEigenaar, openStore } = client;
|
|
|
|
|
|
|
|
|
|
// ── Een mnemonic levert altijd hetzelfde OwnerId op ───────────────────────────
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
// Dit is de toets die omvalt als Evolu onder ons iets anders gaat doen met de
|
|
|
|
|
// afleiding van een eigenaar. Vaste mnemonic, vast antwoord. Het is geen
|
|
|
|
|
// geheim: deze eigenaar bestaat alleen in deze toets, en het is bovendien de
|
|
|
|
|
// bekendste testmnemonic die er is.
|
|
|
|
|
const MNEMONIC =
|
|
|
|
|
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
|
|
|
|
|
|
|
|
|
const eerste = eigenaarUitMnemonic(MNEMONIC);
|
|
|
|
|
const tweede = eigenaarUitMnemonic(MNEMONIC);
|
|
|
|
|
|
|
|
|
|
toets('dezelfde mnemonic geeft hetzelfde OwnerId', eerste.id === tweede.id);
|
|
|
|
|
toets('en een OwnerId is een niet-lege string', typeof eerste.id === 'string' && eerste.id.length > 0);
|
|
|
|
|
toets('de eigenaar houdt zijn mnemonic bij zich', eerste.mnemonic === MNEMONIC);
|
|
|
|
|
toets('een verse eigenaar krijgt een ander OwnerId', nieuweEigenaar().id !== eerste.id);
|
|
|
|
|
|
|
|
|
|
let geweigerd = false;
|
|
|
|
|
try {
|
|
|
|
|
eigenaarUitMnemonic('dit is geen mnemonic');
|
|
|
|
|
} catch {
|
|
|
|
|
geweigerd = true;
|
|
|
|
|
}
|
|
|
|
|
// Het niet-gelukkige pad, en dat is hier het belangrijkste: stil een andere
|
|
|
|
|
// eigenaar maken zou een proef opleveren die nergens over gaat.
|
|
|
|
|
toets('een onleesbare mnemonic wordt geweigerd', geweigerd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Schrijven en teruglezen, zonder relay ─────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const map = mkdtempSync(join(tmpdir(), 'relay-client-toets-'));
|
|
|
|
|
let store = null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const defecten = [];
|
2026-09-09 13:39:47 +02:00
|
|
|
// Vastgehouden, want verderop wordt met dezelfde eigenaar heropend.
|
|
|
|
|
const eigenaar = nieuweEigenaar();
|
2026-09-09 12:59:55 +02:00
|
|
|
store = await openStore({
|
|
|
|
|
directory: map,
|
2026-09-09 13:39:47 +02:00
|
|
|
owner: eigenaar,
|
2026-09-09 12:59:55 +02:00
|
|
|
onDefect: (defect) => {
|
|
|
|
|
defecten.push(defect);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
toets('zonder relayUrl is er geen transport', store.transportUrl === null);
|
|
|
|
|
|
|
|
|
|
const bytes = new Uint8Array([0, 1, 2, 253, 254, 255]);
|
2026-09-09 13:39:47 +02:00
|
|
|
const geschreven = await store.schrijf('toets', bytes);
|
2026-09-09 12:59:55 +02:00
|
|
|
toets('een blob wegschrijven geeft een id terug', typeof geschreven?.id === 'string');
|
|
|
|
|
|
|
|
|
|
const rijen = await store.lees();
|
|
|
|
|
toets('en die blob komt er weer uit', rijen.length === 1);
|
|
|
|
|
toets('met hetzelfde label', rijen[0]?.label === 'toets');
|
|
|
|
|
toets(
|
|
|
|
|
'en met de bytes ongeschonden',
|
|
|
|
|
rijen[0]?.body?.length === bytes.length
|
|
|
|
|
&& [...(rijen[0]?.body ?? [])].every((waarde, plek) => waarde === bytes[plek]),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const sluiten = store.sluit;
|
|
|
|
|
store = null;
|
|
|
|
|
await sluiten();
|
|
|
|
|
|
2026-09-09 13:39:47 +02:00
|
|
|
// Opnieuw openen met dezelfde eigenaar en dezelfde map. Dit is de toets die
|
|
|
|
|
// een echte fout van 09-09-2026 vasthoudt: `evolu.insert` zet de schrijfactie
|
|
|
|
|
// in de wachtrij van de database-worker en geeft meteen een id terug, dus wie
|
|
|
|
|
// vlak daarna afsluit is de rij kwijt. Het symptoom was een `lees` die "nog
|
|
|
|
|
// geen blobs" meldde over een blob die zojuist bevestigd was, en dát is precies
|
|
|
|
|
// het soort fout dat een toets binnen één instantie niet vindt.
|
|
|
|
|
store = await openStore({ directory: map, owner: eigenaar });
|
|
|
|
|
const naHeropenen = await store.lees();
|
|
|
|
|
toets('de blob overleeft het afsluiten en heropenen', naHeropenen.length === 1);
|
|
|
|
|
toets('met hetzelfde label', naHeropenen[0]?.label === 'toets');
|
|
|
|
|
|
|
|
|
|
const nogmaalsSluiten = store.sluit;
|
|
|
|
|
store = null;
|
|
|
|
|
await nogmaalsSluiten();
|
|
|
|
|
|
2026-09-09 12:59:55 +02:00
|
|
|
// Afsluiten hoort stil te zijn. Ging dat mis, dan bleef er een worker draaien
|
|
|
|
|
// die zijn eigen run al kwijt was, en dat merk je pas als een proces blijft
|
|
|
|
|
// hangen. Dat was de vierde valstrik uit PLAN.md §4e.
|
|
|
|
|
toets(`afsluiten levert geen defect op (${defecten.length} gezien)`, defecten.length === 0);
|
|
|
|
|
} finally {
|
|
|
|
|
if (store) await store.sluit();
|
|
|
|
|
rmSync(map, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('');
|
|
|
|
|
console.log(`${goed} goed, ${fout} fout`);
|
|
|
|
|
process.exit(fout === 0 ? 0 : 1);
|