source: Klonkt/test/conversations.test.js@ 09fc5fb

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

Gesprekken: eerst wie, dan pas wat

shaer-frontend-yso, stap 1 van 3: de serverkant NAAST het bestaande pad, want
de apps in het veld lezen de oude inbox-lezing nog.

De oude lezing geeft de nieuwste 60 berichten over ALLE gesprekken samen. Bij
DM's is dat veel erger dan bij posts -- meer en kortere berichten, dus een druk
gesprek eet de 60 in zijn eentje op en duwt de rest eruit. Viel het laatste
bericht van iemand erbuiten, dan verdween die persoon HELEMAAL uit Messages:
de avatarhemel plaatst mensen op de leeftijd van hun laatste bericht, dus geen
bericht is geen gezicht. test/conversations.test.js legt die bug eerst vast.

Nu twee lezingen:

GET /ap/users/:slug/conversations een rij per tegenpartij, compleet van

vorm -- het aantal rijen is het aantal
mensen, niet het aantal berichten

GET /ap/users/:slug/messages?with= het gesprek zelf, beide kanten onder EEN

limiet, met next zolang er meer is

Beide kanten onder een limiet, want in de oude lezing werden jouw kant
(getSentNotes) en hun kant apart afgekapt en kon een gesprek eenzijdig lijken.

DE CURSOR IS SAMENGESTELD (stempel|ref) en niet alleen de stempel. Twee
berichten in dezelfde seconde is bij DM's een gesprek en geen randgeval; met
'stamp < before' viel alles wat die grensseconde deelde stil weg. De testen
vonden dat, en tellen nu dat de drie paginas samen precies 80 berichten zijn.
De paginagrootte reist mee in next, anders wordt pagina twee stilletjes de
standaard.

Om te voorkomen dat de kaartvorm twee keer beschreven staat: de poorten
(leesPoorten) en de vorm (berichtItem/verzondenItem) zijn uit de inbox-lezing
gehesen en worden nu door allebei gebruikt. Zelfde gedrag, 843/843 groen.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 6.9 KB
Line 
1// Gesprekken: eerst wie, dan pas wat (shaer-frontend-yso).
2//
3// De oude lezing gaf 60 berichten over ALLE gesprekken samen. Deze toetsen
4// leggen vast wat daaraan mis was en niet meer mag terugkomen: dat een druk
5// gesprek de rest wegdrukt, dat iemand daardoor uit de hemel verdwijnt, en dat
6// jouw kant en hun kant apart afgekapt werden.
7//
8// In-memory SQLite. Run: npm test
9import { test } from 'node:test';
10import assert from 'node:assert/strict';
11
12process.env.DATABASE_PATH = ':memory:';
13process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
14
15const dbMod = await import('../src/config/database.js');
16const db = dbMod.default;
17dbMod.initializeDatabase();
18const AP = await import('../src/services/ActivityPubService.js');
19
20db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
21 .run('u1', 'u1', 'u1@t', 'x', 'god');
22db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'kind', 'Kind', 'u1');
23
24const TANTE = 'https://elders/u/tante';
25const OMA = 'https://elders/u/oma';
26const VREEMDE = 'https://elders/u/vreemde';
27
28const binnen = (uri, van, stamp) =>
29 db.prepare(`INSERT INTO ap_mentions (slug, object_uri, actor_uri, actor_name, content, published)
30 VALUES ('kind', ?, ?, 'iemand', '<p>hoi</p>', ?)`).run(uri, van, stamp);
31const verstuurd = (id, naar, stamp, visibility = 'direct') =>
32 db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, to_actor, to_actors, content, visibility, created_at)
33 VALUES (?, 'kind', 'p1', ?, ?, '<p>terug</p>', ?, ?)`)
34 .run(id, naar, JSON.stringify([naar]), visibility, stamp);
35
36// Een DRUK gesprek met oma: genoeg om in de oude lezing de rest weg te drukken.
37for (let i = 0; i < 80; i++) binnen(`https://elders/n/oma-${i}`, OMA, `2026-08-09T10:${String(i % 60).padStart(2, '0')}:00Z`);
38// En een stil gesprek met tante, ouder dan al die drukte.
39binnen('https://elders/n/tante-1', TANTE, '2026-08-01T09:00:00Z');
40verstuurd('mijn-1', TANTE, '2026-08-01T09:30:00Z');
41// Een PUBLIEK antwoord aan een vreemde is geen gesprek.
42verstuurd('mijn-publiek', VREEMDE, '2026-08-09T12:00:00Z', 'public');
43
44test('de oude lezing verliest tante achter een druk gesprek -- dat is de bug', () => {
45 const oud = AP.getDirectMessages('kind', 60);
46 assert.equal(oud.length, 60);
47 assert.ok(!oud.some((m) => m.actor_uri === TANTE), 'tante valt buiten de 60');
48});
49
50test('een rij per tegenpartij, hoe druk de drukste ook is', () => {
51 const koppen = AP.conversationHeads('kind');
52 assert.deepEqual(koppen.map((k) => k.other), [OMA, TANTE], 'nieuwste gesprek eerst, allebei aanwezig');
53});
54
55test('een publiek antwoord is geen gesprek en wordt geen gezicht', () => {
56 assert.ok(!AP.conversationHeads('kind').some((k) => k.other === VREEMDE));
57});
58
59test('het gesprek draagt beide kanten onder EEN limiet', () => {
60 const g = AP.conversationHistory('kind', TANTE, { limit: 60 });
61 assert.equal(g.rijen.length, 2);
62 assert.deepEqual(g.rijen.map((r) => r.richting), ['uit', 'in'], 'nieuwste eerst, mijn antwoord bovenaan');
63 assert.equal(g.meer, false);
64});
65
66test('load more: er is een cursor en een eerlijk antwoord op "is er meer"', () => {
67 // De 80 berichten van oma delen stempels (i % 60), en dat is met opzet:
68 // twee berichten in dezelfde seconde is bij DM's een gesprek, geen randgeval.
69 const eerste = AP.conversationHistory('kind', OMA, { limit: 30 });
70 assert.equal(eerste.rijen.length, 30);
71 assert.equal(eerste.meer, true, 'er is meer, en dat mag de client weten');
72
73 const tweede = AP.conversationHistory('kind', OMA, { limit: 30, before: eerste.oudste });
74 assert.equal(tweede.rijen.length, 30);
75 // Geen overlap en geen gat: de tweede pagina begint waar de eerste ophield.
76 const ids = new Set(eerste.rijen.map((r) => r.ref));
77 assert.ok(tweede.rijen.every((r) => !ids.has(r.ref)), 'geen dubbele');
78
79 const derde = AP.conversationHistory('kind', OMA, { limit: 30, before: tweede.oudste });
80 assert.equal(derde.rijen.length, 20, 'de staart');
81 assert.equal(derde.meer, false, 'en dan is het op');
82 // Alles bij elkaar: precies 80, geen dubbele en niets overgeslagen. Met een
83 // cursor op alleen de stempel zou de grensseconde hier stil wegvallen.
84 const alles = new Set([...eerste.rijen, ...tweede.rijen, ...derde.rijen].map((r) => r.ref));
85 assert.equal(alles.size, 80, 'elke berichtje precies een keer');
86});
87
88// ── Door de routes heen, want daar wordt de vorm beslist ────────────
89test('de gesprekslezingen: een rij per persoon, en een gesprek met next', async (t) => {
90 const crypto = await import('crypto');
91 const express = (await import('express')).default;
92 const routes = (await import('../src/routes/activitypub.js')).default;
93
94 const bearer = 'test-token-' + 'e'.repeat(24);
95 db.prepare('INSERT INTO oauth_tokens (token_hash, client_id, user_id, site_slug, scope) VALUES (?,?,?,?,?)')
96 .run(crypto.createHash('sha256').update(bearer).digest('base64url'), 'c', 'u1', 'kind', 'read write');
97
98 const app = express();
99 app.use(routes);
100 const server = app.listen(0);
101 t.after(() => server.close());
102 await new Promise((r) => server.once('listening', r));
103 const haal = async (pad) => {
104 const r = await fetch(`http://127.0.0.1:${server.address().port}${pad}`,
105 { headers: { Authorization: `Bearer ${bearer}` } });
106 return { status: r.status, body: await r.json() };
107 };
108 const slug = '/ap/users/kind';
109
110 const g = await haal(`${slug}/conversations`);
111 assert.equal(g.status, 200);
112 assert.equal(g.body.totalItems, 2, 'oma en tante, en niet de publieke vreemde');
113 // De byline zit in de ingesloten actor, net als overal sinds shaer-nmw.
114 assert.ok(g.body.orderedItems.every((i) => i.object.attributedTo));
115
116 const eerste = await haal(`${slug}/messages?with=${encodeURIComponent(OMA)}&limit=30`);
117 assert.equal(eerste.body.orderedItems.length, 30);
118 assert.ok(eerste.body.next, 'er is meer, en de standaardvorm zegt het');
119 assert.ok(eerste.body.next.includes('limit=30'), 'de paginagrootte reist mee');
120
121 // De next-link volgen doet wat hij belooft: geen dubbele, en uiteindelijk op.
122 const pad2 = eerste.body.next.replace(/^https?:\/\/[^/]+/, '');
123 const tweede = await haal(pad2);
124 const eersteIds = new Set(eerste.body.orderedItems.map((i) => i.object.id));
125 assert.ok(tweede.body.orderedItems.every((i) => !eersteIds.has(i.object.id)), 'geen overlap');
126 const derde = await haal(tweede.body.next.replace(/^https?:\/\/[^/]+/, ''));
127 assert.equal(derde.body.orderedItems.length, 20);
128 assert.equal(derde.body.next, undefined, 'op is op, en dat staat er ook');
129
130 // Een gesprek draagt beide kanten; het eigen antwoord aan tante hoort erbij.
131 const tante = await haal(`${slug}/messages?with=${encodeURIComponent(TANTE)}`);
132 assert.equal(tante.body.orderedItems.length, 2);
133 assert.ok(tante.body.orderedItems.some((i) => i.actor === 'https://klonkt.test/ap/users/kind'), 'mijn eigen kant zit erin');
134
135 const stuk = await haal(`${slug}/messages?with=nonsens`);
136 assert.equal(stuk.status, 400, 'geen actor-uri, geen gesprek');
137});
Note: See TracBrowser for help on using the repository browser.