source: Klonkt/test/guardian-help-state.test.js@ e667d26

main
Last change on this file since e667d26 was e667d26, checked in by roboburr <roboburr@…>, 4 weeks ago

Een levende klok in het antwoord maakte de lange poll een lus (9-8)

Barts melding: de browser komt in een feedback-lus van ongeveer een seconde, en
de scrollpositie wordt vermalen. Dat is mijn fout van een uur eerder, en de
oorzaak is scherper dan hij lijkt.

Het paneel kreeg gisteren een inhouds-ETag, en de paneelstate bevat -- bevatte --
ageMs: de leeftijd van de oudste oppik, uitgerekend als now - tijdstip.
Daardoor verschilde elk antwoord een paar milliseconden van het vorige, kon de
ETag nooit gelijk zijn, kwam de 304 nooit, keerde de lange poll meteen terug, en
tekende het paneel zich stuk terwijl Bart scrollde.

EEN LEVENDE KLOK MAAKT EEN ANTWOORD ONVERGELIJKBAAR MET ZICHZELF. Dat is de les,
en hij raakt elk veld dat we ooit nog toevoegen. Er staat nu een toets op die
hetzelfde antwoord twee keer opvraagt met een halve seconde ertussen en eist dat
het identiek is.

De reparatie zit bij de BRON, niet in de ETag: het antwoord draagt nu het
TIJDSTIP (oldestPickupAt) en de client rekent de leeftijd uit. Dat is ook wat het
altijd al was -- een weergavedetail. De ETag uitzonderingen laten maken zou een
tweede beschrijving zijn van "wat telt als veranderd", precies wat ik gisteren
als argument tegen een cursor gebruikte.

TWEE GRENDELS ERBIJ, aan de kant die de gebruiker merkt:

  • Zelfde inhoud wordt niet hertekend. De 304 hoort dit af te vangen, maar een enkel onstabiel veld zette het scherm aan het knipperen; dit vangt de volgende.
  • Een lange poll die binnen twee seconden terugkeert wacht drie seconden voordat hij opnieuw vraagt. Een poll die niet wacht mag nooit een lus worden -- anders vervangt een defect een tik per 45 seconden door duizend per minuut.

En de scrollpositie overleeft een verversing: meten en terugzetten rond
renderAll. Dat is het verschil tussen een scherm dat bijwerkt en een scherm dat
onder je handen wegschuift.

Suite 782/782; met de klok terug valt hij om.

  • Property mode set to 100644
File size: 7.7 KB
Line 
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
15import { test } from 'node:test';
16import assert from 'node:assert/strict';
17
18process.env.DATABASE_PATH = ':memory:';
19process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
20
21const dbMod = await import('../src/config/database.js');
22const db = dbMod.default;
23dbMod.initializeDatabase();
24const help = await import('../src/services/guardianship/help.js');
25
26const NOTE = 'https://kind.test/ap/notes/hulp-1';
27const OMA = 'https://oma.test/ap/users/oma';
28const OPA = 'https://opa.test/ap/users/opa';
29
30test('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
37test('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
47test('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
53test('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
58test('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
71test('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
79test('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
87test('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
96test('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
104test('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
110test('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
120test('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
129test('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
137test('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
146test('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
152test('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});
Note: See TracBrowser for help on using the repository browser.