source: Klonkt/test/conversations.test.js@ fc9ca9d

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

Een kopje draagt ook het nieuwste bericht VAN de ander

Op het toestel gezien: de hemel noemde mensen opeens 'boiert' en
'hotelbreakfast' in plaats van hun naam, en tekende letters in plaats van
gezichten. Oorzaak: is het nieuwste bericht in een gesprek van MIJ, dan draagt
het kopje mijn byline, en de hemel zoekt naam en gezicht van de ander in een
bericht VAN de ander. Vond hij dat niet, dan viel hij terug op het staartje van
de actor-uri.

conversationHeads geeft nu twee rijen per persoon: het nieuwste bericht (dat
bepaalt waar iemand hangt) en het nieuwste bericht van hem. Vallen ze samen,
dan blijft het een rij. Geen clientwijziging nodig -- conversationsFrom maakt er
dezelfde Conversation van.

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

  • Property mode set to 100644
File size: 8.0 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 arrived = (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 sent = (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++) arrived(`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.
39arrived('https://elders/n/tante-1', TANTE, '2026-08-01T09:00:00Z');
40sent('mijn-1', TANTE, '2026-08-01T09:30:00Z');
41// Een PUBLIEK antwoord aan een vreemde is geen gesprek.
42sent('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 heads = AP.conversationHeads('kind');
52 assert.deepEqual([...new Set(heads.map((k) => k.other))], [OMA, TANTE], 'nieuwste gesprek eerst, allebei aanwezig');
53});
54
55test('is het nieuwste bericht van MIJ, dan gaat dat van hem ook mee', () => {
56 // Anders draagt het kopje mijn byline en zoekt de hemel de naam en het
57 // gezicht van de ander tevergeefs -- die viel dan terug op het staartje van
58 // de actor-uri. Bij tante is mijn antwoord het nieuwste.
59 const vanTante = AP.conversationHeads('kind').filter((k) => k.other === TANTE);
60 assert.equal(vanTante.length, 2);
61 assert.deepEqual(vanTante.map((k) => k.direction).sort(), ['in', 'out']);
62 // Bij oma is het nieuwste al van haar: dan is een rij genoeg.
63 assert.equal(AP.conversationHeads('kind').filter((k) => k.other === OMA).length, 1);
64});
65
66test('een publiek antwoord is geen gesprek en wordt geen gezicht', () => {
67 assert.ok(!AP.conversationHeads('kind').some((k) => k.other === VREEMDE));
68});
69
70test('het gesprek draagt beide kanten onder EEN limiet', () => {
71 const talk = AP.conversationHistory('kind', TANTE, { limit: 60 });
72 assert.equal(talk.rows.length, 2);
73 assert.deepEqual(talk.rows.map((r) => r.direction), ['out', 'in'], 'nieuwste eerst, mijn antwoord bovenaan');
74 assert.equal(talk.more, false);
75});
76
77test('load more: er is een cursor en een eerlijk antwoord op "is er meer"', () => {
78 // De 80 berichten van oma delen stempels (i % 60), en dat is met opzet:
79 // twee berichten in dezelfde seconde is bij DM's een gesprek, geen randgeval.
80 const first = AP.conversationHistory('kind', OMA, { limit: 30 });
81 assert.equal(first.rows.length, 30);
82 assert.equal(first.more, true, 'er is meer, en dat mag de client weten');
83
84 const second = AP.conversationHistory('kind', OMA, { limit: 30, before: first.oldest });
85 assert.equal(second.rows.length, 30);
86 // Geen overlap en geen gat: de second pagina begint waar de first ophield.
87 const ids = new Set(first.rows.map((r) => r.ref));
88 assert.ok(second.rows.every((r) => !ids.has(r.ref)), 'geen dubbele');
89
90 const third = AP.conversationHistory('kind', OMA, { limit: 30, before: second.oldest });
91 assert.equal(third.rows.length, 20, 'de staart');
92 assert.equal(third.more, false, 'en dan is het op');
93 // Alles bij elkaar: precies 80, geen dubbele en niets overgeslagen. Met een
94 // cursor op alleen de stempel zou de grensseconde hier stil wegvallen.
95 const allRefs = new Set([...first.rows, ...second.rows, ...third.rows].map((r) => r.ref));
96 assert.equal(allRefs.size, 80, 'elke berichtje precies een keer');
97});
98
99// ── Door de routes heen, want daar wordt de vorm beslist ────────────
100test('de gesprekslezingen: een rij per persoon, en een gesprek met next', async (t) => {
101 const crypto = await import('crypto');
102 const express = (await import('express')).default;
103 const routes = (await import('../src/routes/activitypub.js')).default;
104
105 const bearer = 'test-token-' + 'e'.repeat(24);
106 db.prepare('INSERT INTO oauth_tokens (token_hash, client_id, user_id, site_slug, scope) VALUES (?,?,?,?,?)')
107 .run(crypto.createHash('sha256').update(bearer).digest('base64url'), 'c', 'u1', 'kind', 'read write');
108
109 const app = express();
110 app.use(routes);
111 const server = app.listen(0);
112 t.after(() => server.close());
113 await new Promise((r) => server.once('listening', r));
114 const get = async (pad) => {
115 const r = await fetch(`http://127.0.0.1:${server.address().port}${pad}`,
116 { headers: { Authorization: `Bearer ${bearer}` } });
117 return { status: r.status, body: await r.json() };
118 };
119 const slug = '/ap/users/kind';
120
121 const list = await get(`${slug}/conversations`);
122 assert.equal(list.status, 200);
123 // Drie items voor twee mensen: bij tante gaat haar eigen bericht mee omdat
124 // het nieuwste van mij is (anders mist de hemel haar naam en gezicht).
125 assert.equal(list.body.totalItems, 3);
126 assert.ok(!JSON.stringify(list.body).includes(VREEMDE), 'de publieke vreemde is geen gesprek');
127 const vanTante = list.body.orderedItems.filter((i) => i.actor === TANTE);
128 assert.equal(vanTante.length, 1, 'en dat bericht van tante draagt haar byline');
129 assert.ok(vanTante[0].object.attributedTo.name);
130 // De byline zit in de ingesloten actor, net als overal sinds shaer-nmw.
131 assert.ok(list.body.orderedItems.every((i) => i.object.attributedTo));
132
133 const first = await get(`${slug}/messages?with=${encodeURIComponent(OMA)}&limit=30`);
134 assert.equal(first.body.orderedItems.length, 30);
135 assert.ok(first.body.next, 'er is meer, en de standaardvorm zegt het');
136 assert.ok(first.body.next.includes('limit=30'), 'de paginagrootte reist mee');
137
138 // De next-link volgen doet wat hij belooft: geen dubbele, en uiteindelijk op.
139 const path2 = first.body.next.replace(/^https?:\/\/[^/]+/, '');
140 const second = await get(path2);
141 const firstIds = new Set(first.body.orderedItems.map((i) => i.object.id));
142 assert.ok(second.body.orderedItems.every((i) => !firstIds.has(i.object.id)), 'geen overlap');
143 const third = await get(second.body.next.replace(/^https?:\/\/[^/]+/, ''));
144 assert.equal(third.body.orderedItems.length, 20);
145 assert.equal(third.body.next, undefined, 'op is op, en dat staat er ook');
146
147 // Een gesprek draagt beide kanten; het eigen antwoord aan tante hoort erbij.
148 const withAunt = await get(`${slug}/messages?with=${encodeURIComponent(TANTE)}`);
149 assert.equal(withAunt.body.orderedItems.length, 2);
150 assert.ok(withAunt.body.orderedItems.some((i) => i.actor === 'https://klonkt.test/ap/users/kind'), 'mijn eigen kant zit erin');
151
152 const broken = await get(`${slug}/messages?with=nonsens`);
153 assert.equal(broken.status, 400, 'geen actor-uri, geen gesprek');
154});
Note: See TracBrowser for help on using the repository browser.