source: Klonkt/test/messages-conversations.test.js@ 99a7b40

main
Last change on this file since 99a7b40 was 971d00b, checked in by Claude (agent) <aiclaude@…>, 5 weeks ago

Eigen berichten renderen als post: media meegeven aan note-body

Berichten deelt note-body al met de Krant en de Guardian-PWA, dus opmaak, quotes
en embeds werkten -- maar alleen voor wat je ONTVANGT. Een sent-item kreeg geen
enkel renderveld mee, dus een foto of geluidsfragment dat jij meestuurde kwam
als kale tekst binnen terwijl hetzelfde bericht bij de ander wel een plaatje is.

De oorzaak is een vormverschil dat nooit vertaald werd: note-body leest
media_json met een type, ap_outbox bewaart attachments met een mediaType.
outboxMediaJson vertaalt dat, en geeft null bij niets of rommel -- kapotte JSON
in die kolom mag hooguit de media kosten, nooit het bericht zelf.

Emoji, quote en embed blijven leeg voor eigen berichten: ap_outbox bewaart die
kolommen niet. De HTML-inhoud zelf gaat wel door note-body heen, dus opmaak in
je eigen antwoord werkt al.

Suite 412/412, met een test op de vertaling en een op kapotte invoer.

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

  • Property mode set to 100644
File size: 8.5 KB
Line 
1// Berichten als gesprekken: antwoorden, mentions en je eigen verzonden
2// berichten vouwen samen tot draden, de rest van de stroom blijft ongemoeid.
3// Dit dekt de groepeerlogica zelf (zuiver, geen DB, geen netwerk); het
4// samenstellen van de stroom zit in getMessages en leunt op de database.
5//
6// Run: npm test
7
8import { test } from 'node:test';
9import assert from 'node:assert/strict';
10
11process.env.DATABASE_PATH = ':memory:';
12process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
13
14const dbMod = await import('../src/config/database.js');
15dbMod.initializeDatabase();
16const AP = await import('../src/services/ActivityPubService.js');
17
18const { threadKey, groupConversations } = AP;
19
20// Aflopend gesorteerd, zoals getMessages ze aanlevert.
21const ts = (n) => new Date(Date.UTC(2026, 0, 1, 12, n)).toISOString();
22
23test('threadKey: een post wint van een persoon', () => {
24 assert.equal(threadKey({ type: 'reply', post_slug: 'hallo', handle: '@anna@a.test' }), 'post:hallo');
25 assert.equal(threadKey({ type: 'sent', post_slug: 'hallo', to_handle: '@anna@a.test' }), 'post:hallo');
26 // Zonder post loopt de draad per tegenpartij, hoofdletter- en @-ongevoelig.
27 assert.equal(threadKey({ type: 'mention', handle: '@Anna@A.test' }), 'actor:anna@a.test');
28 assert.equal(threadKey({ type: 'sent', to_handle: 'Anna@a.test' }), 'actor:anna@a.test');
29});
30
31test('threadKey: alles wat geen gesprek is krijgt geen draad', () => {
32 for (const type of ['like', 'announce', 'follow', 'report', 'poll_done']) {
33 assert.equal(threadKey({ type, post_slug: 'hallo' }), null, `${type} hoort geen draad te krijgen`);
34 }
35 assert.equal(threadKey(null), null);
36 assert.equal(threadKey({ type: 'mention' }), null, 'een mention zonder afzender heeft geen sleutel');
37});
38
39test('ontvangen en verzonden op dezelfde post komen in EEN draad', () => {
40 const out = groupConversations([
41 { type: 'sent', post_slug: 'hallo', content: 'graag gedaan', created_at: ts(3) },
42 { type: 'reply', post_slug: 'hallo', post_title: 'Hallo fediverse', handle: '@anna@a.test', name: 'Anna', content: 'dank!', created_at: ts(2) },
43 { type: 'reply', post_slug: 'hallo', post_title: 'Hallo fediverse', handle: '@bo@b.test', name: 'Bo', content: 'mooi', created_at: ts(1) },
44 ]);
45 assert.equal(out.length, 1, 'drie berichten, één draad');
46 const t = out[0];
47 assert.equal(t.type, 'thread');
48 assert.equal(t.count, 3);
49 // De context hoort bij de draad: dit gesprek gaat over een post.
50 assert.deepEqual(t.post, { slug: 'hallo', title: 'Hallo fediverse' });
51 // Een gesprek leest naar beneden: oud → nieuw.
52 assert.deepEqual(t.messages.map((m) => m.content), ['mooi', 'dank!', 'graag gedaan']);
53 // De draad staat op de tijd van zijn NIEUWSTE bericht, zodat een levend
54 // gesprek bovenaan komt.
55 assert.equal(t.created_at, ts(3));
56 // Jij zit er niet bij als deelnemer, de anderen wel.
57 assert.deepEqual(t.people.map((p) => p.name), ['Bo', 'Anna']);
58 assert.equal(t.mine, true);
59});
60
61test('een mention zonder post loopt per persoon, niet per post', () => {
62 const out = groupConversations([
63 { type: 'sent', to_handle: '@cas@c.test', content: 'hoi terug', created_at: ts(2) },
64 { type: 'mention', handle: '@cas@c.test', name: 'Cas', content: 'hoi!', created_at: ts(1) },
65 ]);
66 assert.equal(out.length, 1);
67 assert.equal(out[0].key, 'actor:cas@c.test');
68 assert.equal(out[0].post, null, 'zonder post is er geen context-link');
69 assert.deepEqual(out[0].messages.map((m) => m.content), ['hoi!', 'hoi terug']);
70});
71
72test('twee posts blijven twee draden, op volgorde van hun nieuwste bericht', () => {
73 const out = groupConversations([
74 { type: 'reply', post_slug: 'twee', post_title: 'Twee', handle: '@anna@a.test', created_at: ts(4) },
75 { type: 'reply', post_slug: 'een', post_title: 'Een', handle: '@bo@b.test', created_at: ts(3) },
76 { type: 'reply', post_slug: 'twee', post_title: 'Twee', handle: '@bo@b.test', created_at: ts(2) },
77 ]);
78 assert.deepEqual(out.map((t) => t.key), ['post:twee', 'post:een']);
79 assert.equal(out[0].count, 2);
80 assert.equal(out[1].count, 1);
81});
82
83test('likes, boosts en follows stromen ongemoeid tussen de draden door', () => {
84 const out = groupConversations([
85 { type: 'like', post_slug: 'hallo', handle: '@anna@a.test', created_at: ts(4) },
86 { type: 'reply', post_slug: 'hallo', post_title: 'Hallo', handle: '@bo@b.test', created_at: ts(3) },
87 { type: 'follow', handle: '@cas@c.test', created_at: ts(2) },
88 { type: 'report', handle: '@dee@d.test', created_at: ts(1) },
89 ]);
90 assert.deepEqual(out.map((i) => i.type), ['like', 'thread', 'follow', 'report']);
91 // Een like op dezelfde post trekt die post NIET de draad in: het is geen
92 // gesprek, en Activiteit blijft zijn eigen chip houden.
93 assert.equal(out[0].post_slug, 'hallo');
94});
95
96test('de titel komt van welk bericht in de draad hem ook maar kent', () => {
97 // Een verzonden antwoord kent alleen de slug; een ontvangen antwoord de titel.
98 const out = groupConversations([
99 { type: 'sent', post_slug: 'hallo', created_at: ts(2) },
100 { type: 'reply', post_slug: 'hallo', post_title: 'Hallo fediverse', handle: '@anna@a.test', created_at: ts(1) },
101 ]);
102 assert.deepEqual(out[0].post, { slug: 'hallo', title: 'Hallo fediverse' });
103});
104
105test('lege en rommelige invoer levert geen kapotte draden op', () => {
106 assert.deepEqual(groupConversations([]), []);
107 assert.deepEqual(groupConversations(null), []);
108 // Een gesprekssoort zonder sleutel blijft een losse regel in plaats van te
109 // verdwijnen: nooit een bericht kwijtraken in de groepering.
110 const out = groupConversations([{ type: 'sent', content: 'aan niemand', created_at: ts(1) }]);
111 assert.equal(out.length, 1);
112 assert.equal(out[0].type, 'sent');
113});
114
115test('een direct bericht zonder to_handle valt terug op to_actors', () => {
116 // Precies het geval waarin het het onlogischst is dat er geen draad ontstaat:
117 // een gesprek dat JIJ begon. Zonder deze terugval bleef het een losse regel.
118 const key = threadKey({
119 type: 'sent',
120 to_actors: JSON.stringify(['https://a.test/users/anna', 'https://b.test/users/bo']),
121 created_at: ts(1),
122 });
123 assert.equal(key, 'actor:anna@a.test');
124 // Rommel in de kolom mag niets omgooien.
125 assert.equal(threadKey({ type: 'sent', to_actors: 'geen json' }), null);
126 assert.equal(threadKey({ type: 'sent', to_actors: '[]' }), null);
127});
128
129test('to_handle wint van to_actors, zodat een draad niet splitst', () => {
130 const a = threadKey({ type: 'sent', to_handle: '@anna@a.test' });
131 const b = threadKey({ type: 'sent', to_handle: '@anna@a.test', to_actors: JSON.stringify(['https://b.test/users/bo']) });
132 assert.equal(a, b, 'dezelfde tegenpartij hoort dezelfde sleutel te geven');
133});
134
135test('een eigen bericht met media rendert als een post, niet als kale tekst', async () => {
136 // note-body (gedeeld met de Krant) leest media_json met een `type`; ap_outbox
137 // bewaart attachments met een `mediaType`. Zonder vertaling viel een foto die
138 // JIJ meestuurde weg.
139 const db = (await import('../src/config/database.js')).default;
140 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
141 .run('mu', 'mu', 'mu@test', 'x', 'god');
142 db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('ms', 'media', 'Media', 'mu');
143 db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, to_handle, content, attachments, created_at)
144 VALUES ('om1','media','pm1','een-post','@anna@a.test','<p>kijk</p>',?,'2026-08-06 09:00:00')`)
145 .run(JSON.stringify([{ url: '/media/foto.png', mediaType: 'image/png', name: 'foto' }]));
146
147 const t = AP.getMessages('media', 50, 0).find((i) => i.type === 'thread');
148 const sent = t.messages.find((m) => m.type === 'sent');
149 assert.deepEqual(JSON.parse(sent.media_json), [{ url: '/media/foto.png', type: 'image/png', name: 'foto' }]);
150});
151
152test('een eigen bericht zonder of met kapotte media houdt gewoon zijn tekst', async () => {
153 const db = (await import('../src/config/database.js')).default;
154 db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('ms2', 'media2', 'M2', 'mu');
155 db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, to_handle, content, attachments, created_at)
156 VALUES ('om2','media2','pm2','p','@anna@a.test','<p>a</p>','geen json','2026-08-06 09:00:00'),
157 ('om3','media2','pm3','p','@bo@b.test','<p>b</p>',NULL,'2026-08-06 09:01:00')`).run();
158 const threads = AP.getMessages('media2', 50, 0).filter((i) => i.type === 'thread');
159 const alle = threads.flatMap((t) => t.messages);
160 assert.equal(alle.length, 2, 'geen bericht mag verdwijnen door kapotte media');
161 for (const m of alle) assert.equal(m.media_json, null);
162});
Note: See TracBrowser for help on using the repository browser.