| 1 | // Oppikken en afhandelen van een hulpvraag (shaer-lgo).
|
|---|
| 2 | //
|
|---|
| 3 | // Een hulpvraag gaat naar ALLE guardians van een kind, op verschillende servers.
|
|---|
| 4 | // Zonder gedeelde staat denken er twee dat de ander het oppakt -- en dat is
|
|---|
| 5 | // precies het scenario waar de reddingsboei voor bestaat.
|
|---|
| 6 | //
|
|---|
| 7 | // DE FAALSTAND IS HIER NIET VEILIG. Bij een gate is 'dicht' het veilige
|
|---|
| 8 | // antwoord; hier is de faalstand 'iedereen denkt dat het geregeld is', en dat is
|
|---|
| 9 | // gevaarlijker dan geen markering. Vandaar dat de meeste tests hieronder gaan
|
|---|
| 10 | // over wat er gebeurt als er IETS niet klopt: dan hoort de hulpvraag OPEN te
|
|---|
| 11 | // staan.
|
|---|
| 12 | //
|
|---|
| 13 | // Run: npm test
|
|---|
| 14 |
|
|---|
| 15 | import { test } from 'node:test';
|
|---|
| 16 | import assert from 'node:assert/strict';
|
|---|
| 17 |
|
|---|
| 18 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 19 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 20 |
|
|---|
| 21 | const dbMod = await import('../src/config/database.js');
|
|---|
| 22 | const db = dbMod.default;
|
|---|
| 23 | dbMod.initializeDatabase();
|
|---|
| 24 | const help = await import('../src/services/guardianship/help.js');
|
|---|
| 25 |
|
|---|
| 26 | const NOTE = 'https://kind.test/ap/notes/hulp-1';
|
|---|
| 27 | const OMA = 'https://oma.test/ap/users/oma';
|
|---|
| 28 | const OPA = 'https://opa.test/ap/users/opa';
|
|---|
| 29 |
|
|---|
| 30 | test('een verse hulpvraag staat open', () => {
|
|---|
| 31 | const s = help.statusOf(NOTE);
|
|---|
| 32 | assert.equal(s.open, true);
|
|---|
| 33 | assert.deepEqual(s.pickedUpBy, []);
|
|---|
| 34 | assert.equal(s.handled, null);
|
|---|
| 35 | });
|
|---|
| 36 |
|
|---|
| 37 | test('oppikken mag STAPELEN', () => {
|
|---|
| 38 | // Twee mensen die tegelijk reageren op een kind dat om hulp vraagt is geen
|
|---|
| 39 | // probleem. Twee die allebei niets doen omdat de ander het geclaimd had, wel.
|
|---|
| 40 | help.record(NOTE, OMA, 'pickup', '@oma@oma.test');
|
|---|
| 41 | help.record(NOTE, OPA, 'pickup', '@opa@opa.test');
|
|---|
| 42 | const s = help.statusOf(NOTE);
|
|---|
| 43 | assert.equal(s.pickedUpBy.length, 2);
|
|---|
| 44 | assert.equal(s.open, true, 'opgepikt is nog niet afgehandeld');
|
|---|
| 45 | });
|
|---|
| 46 |
|
|---|
| 47 | test('en het staat erbij DOOR WIE, want dat was de hele vraag', () => {
|
|---|
| 48 | const s = help.statusOf(NOTE);
|
|---|
| 49 | assert.ok(s.pickedUpBy.some((p) => p.handle === '@oma@oma.test'));
|
|---|
| 50 | assert.ok(s.pickedUpBy.every((p) => p.at), 'met een tijdstip, anders kun je niet zien dat het oud wordt');
|
|---|
| 51 | });
|
|---|
| 52 |
|
|---|
| 53 | test('twee keer hetzelfde oppikken telt een keer', () => {
|
|---|
| 54 | help.record(NOTE, OMA, 'pickup', '@oma@oma.test');
|
|---|
| 55 | assert.equal(help.statusOf(NOTE).pickedUpBy.length, 2);
|
|---|
| 56 | });
|
|---|
| 57 |
|
|---|
| 58 | test('een oppik VERVALT NIET maar veroudert wel zichtbaar', () => {
|
|---|
| 59 | // Barts keuze: niets verdwijnt, want een signaal dat wegvalt laat een
|
|---|
| 60 | // hulpvraag er onaangeroerd uitzien terwijl er iemand mee bezig is. Het scherm
|
|---|
| 61 | // toont in plaats daarvan hoe oud het is.
|
|---|
| 62 | const rijen = [{ kind: 'pickup', guardian_uri: OMA, created_at: '2026-08-05T10:00:00.000Z' }];
|
|---|
| 63 | const s = help.helpStatus(rijen, Date.parse('2026-08-07T10:00:00.000Z'));
|
|---|
| 64 | assert.equal(s.open, true);
|
|---|
| 65 | // Een TIJDSTIP, geen leeftijd: een verschil met `now` maakt het antwoord elke
|
|---|
| 66 | // milliseconde anders, en dan kan de ETag van het paneel nooit gelijk zijn --
|
|---|
| 67 | // dat leverde een lus op waarin de browser zichzelf bleef verversen.
|
|---|
| 68 | assert.equal(s.oldestPickupAt, '2026-08-05T10:00:00.000Z');
|
|---|
| 69 | });
|
|---|
| 70 |
|
|---|
| 71 | test('afhandelen sluit hem, met naam', () => {
|
|---|
| 72 | help.record(NOTE, OPA, 'handled', '@opa@opa.test');
|
|---|
| 73 | const s = help.statusOf(NOTE);
|
|---|
| 74 | assert.equal(s.open, false);
|
|---|
| 75 | assert.equal(s.handled.handle, '@opa@opa.test');
|
|---|
| 76 | assert.equal(s.oldestPickupAt, null, 'het tijdstip van een oppik zegt niets meer als het klaar is');
|
|---|
| 77 | });
|
|---|
| 78 |
|
|---|
| 79 | test('er is GEEN terugdraai', () => {
|
|---|
| 80 | // Besluit van Bart: sluiten gebeurt met een stevige bevestiging, en leeft de
|
|---|
| 81 | // vraag daarna nog, dan wordt hij OPNIEUW GESTELD -- een nieuwe hulpvraag. Er
|
|---|
| 82 | // wordt niets herschreven.
|
|---|
| 83 | assert.equal(typeof help.default.undo, 'undefined');
|
|---|
| 84 | assert.equal(typeof help.default.reopen, 'undefined');
|
|---|
| 85 | });
|
|---|
| 86 |
|
|---|
| 87 | test('bij twijfel staat een hulpvraag OPEN', () => {
|
|---|
| 88 | // Alles wat geen expliciete afsluiting is telt als open. Een lege lijst, rommel,
|
|---|
| 89 | // een onbekende soort: allemaal 'er wacht nog iemand'. De omgekeerde fout --
|
|---|
| 90 | // iets als afgehandeld tonen dat het niet is -- is de gevaarlijke.
|
|---|
| 91 | for (const rijen of [[], null, [{ kind: 'iets', guardian_uri: OMA }], [{ kind: 'pickup', guardian_uri: OMA }]]) {
|
|---|
| 92 | assert.equal(help.helpStatus(rijen).open, true);
|
|---|
| 93 | }
|
|---|
| 94 | });
|
|---|
| 95 |
|
|---|
| 96 | test('een markering wordt herkend, en iets anders niet', () => {
|
|---|
| 97 | assert.deepEqual(help.parseMarker({ 'shaer:helpPickup': NOTE }), { kind: 'pickup', noteUri: NOTE });
|
|---|
| 98 | assert.deepEqual(help.parseMarker({ 'shaer:helpHandled': NOTE }), { kind: 'handled', noteUri: NOTE });
|
|---|
| 99 | assert.equal(help.parseMarker({ content: '<p>hoi</p>' }), null);
|
|---|
| 100 | assert.equal(help.parseMarker(null), null);
|
|---|
| 101 | assert.equal(help.parseMarker({ 'shaer:helpPickup': '' }), null, 'leeg is geen markering');
|
|---|
| 102 | });
|
|---|
| 103 |
|
|---|
| 104 | test('afhandelen wint van oppikken in hetzelfde bericht', () => {
|
|---|
| 105 | // Kan niet ontstaan uit onze eigen verzending, maar wel uit een andere
|
|---|
| 106 | // implementatie. Het zwaarste signaal telt.
|
|---|
| 107 | assert.equal(help.parseMarker({ 'shaer:helpPickup': NOTE, 'shaer:helpHandled': NOTE }).kind, 'handled');
|
|---|
| 108 | });
|
|---|
| 109 |
|
|---|
| 110 | test('een lijst hulpvragen kost EEN query', () => {
|
|---|
| 111 | const N2 = 'https://kind.test/ap/notes/hulp-2';
|
|---|
| 112 | help.record(N2, OMA, 'pickup', '@oma@oma.test');
|
|---|
| 113 | const m = help.statusFor([NOTE, N2, 'https://kind.test/ap/notes/leeg']);
|
|---|
| 114 | assert.equal(m.size, 3);
|
|---|
| 115 | assert.equal(m.get(NOTE).open, false);
|
|---|
| 116 | assert.equal(m.get(N2).pickedUpBy.length, 1);
|
|---|
| 117 | assert.equal(m.get('https://kind.test/ap/notes/leeg').open, true, 'onbekend is open');
|
|---|
| 118 | });
|
|---|
| 119 |
|
|---|
| 120 | test('een hulpvraag van een OUD-ward staat niet meer open', () => {
|
|---|
| 121 | // Het loslaat-scherm belooft dit al: "je krijgt geen hulpvragen meer van ze".
|
|---|
| 122 | // Nieuwe komen niet meer binnen, maar wat er al lag bleef staan -- en was niet
|
|---|
| 123 | // af te sluiten, want de markeerroute eist dat het nog je ward is.
|
|---|
| 124 | const st = help.withWardship(help.helpStatus([]), false);
|
|---|
| 125 | assert.equal(st.open, false);
|
|---|
| 126 | assert.equal(st.formerWard, true);
|
|---|
| 127 | });
|
|---|
| 128 |
|
|---|
| 129 | test('maar hij is NIET afgehandeld', () => {
|
|---|
| 130 | // Dat zou een claim zijn over een kind waar je niets meer over te zeggen hebt,
|
|---|
| 131 | // en die claim wordt ook nog rondgestuurd naar de andere guardians. Er is een
|
|---|
| 132 | // derde uitkomst: niet meer van jou.
|
|---|
| 133 | const st = help.withWardship(help.helpStatus([]), false);
|
|---|
| 134 | assert.equal(st.handled, null);
|
|---|
| 135 | });
|
|---|
| 136 |
|
|---|
| 137 | test('een lopende oppik blijft leesbaar als je losgelaten hebt', () => {
|
|---|
| 138 | // Je was erbij. Dat je nu geen guardian meer bent maakt niet dat het nooit
|
|---|
| 139 | // gebeurd is -- de geschiedenis hoort te kloppen.
|
|---|
| 140 | const rijen = [{ kind: 'pickup', guardian_uri: OMA, guardian_handle: '@oma', created_at: '2026-08-05T10:00:00.000Z' }];
|
|---|
| 141 | const st = help.withWardship(help.helpStatus(rijen, Date.parse('2026-08-05T11:00:00.000Z')), false);
|
|---|
| 142 | assert.equal(st.pickedUpBy.length, 1);
|
|---|
| 143 | assert.equal(st.pickedUpBy[0].handle, '@oma');
|
|---|
| 144 | });
|
|---|
| 145 |
|
|---|
| 146 | test('zolang het WEL je ward is verandert er niets', () => {
|
|---|
| 147 | const rijen = [{ kind: 'pickup', guardian_uri: OMA, guardian_handle: '@oma', created_at: '2026-08-05T10:00:00.000Z' }];
|
|---|
| 148 | const basis = help.helpStatus(rijen, Date.parse('2026-08-05T11:00:00.000Z'));
|
|---|
| 149 | assert.deepEqual(help.withWardship(basis, true), basis);
|
|---|
| 150 | });
|
|---|
| 151 |
|
|---|
| 152 | test('het antwoord verandert NIET als je het twee keer opvraagt', () => {
|
|---|
| 153 | // De fout die Barts browser in een lus bracht (9-8): hier stond ageMs, een
|
|---|
| 154 | // verschil met `now`. Daardoor was elk antwoord anders, kon de ETag van het
|
|---|
| 155 | // paneel nooit gelijk zijn, kwam de 304 nooit, en keerde de lange poll meteen
|
|---|
| 156 | // terug -- inclusief een hertekening die de scrollpositie vermaalde.
|
|---|
| 157 | //
|
|---|
| 158 | // Een levende klok in een antwoord maakt dat antwoord onvergelijkbaar met
|
|---|
| 159 | // zichzelf. Deze toets bewaakt precies dat.
|
|---|
| 160 | const rijen = [{ kind: 'pickup', guardian_uri: OMA, created_at: '2026-08-05T10:00:00.000Z' }];
|
|---|
| 161 | const a = JSON.stringify(help.helpStatus(rijen, Date.parse('2026-08-05T12:00:00.000Z')));
|
|---|
| 162 | const b = JSON.stringify(help.helpStatus(rijen, Date.parse('2026-08-05T12:00:00.500Z')));
|
|---|
| 163 | assert.equal(a, b, 'een halve seconde later is hetzelfde antwoord');
|
|---|
| 164 | });
|
|---|