| 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 |
|
|---|
| 8 | import { test } from 'node:test';
|
|---|
| 9 | import assert from 'node:assert/strict';
|
|---|
| 10 |
|
|---|
| 11 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 12 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 13 |
|
|---|
| 14 | const dbMod = await import('../src/config/database.js');
|
|---|
| 15 | dbMod.initializeDatabase();
|
|---|
| 16 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 17 |
|
|---|
| 18 | const { threadKey, groupConversations } = AP;
|
|---|
| 19 |
|
|---|
| 20 | // Aflopend gesorteerd, zoals getMessages ze aanlevert.
|
|---|
| 21 | const ts = (n) => new Date(Date.UTC(2026, 0, 1, 12, n)).toISOString();
|
|---|
| 22 |
|
|---|
| 23 | test('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 |
|
|---|
| 31 | test('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 |
|
|---|
| 39 | test('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 |
|
|---|
| 61 | test('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 |
|
|---|
| 72 | test('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 |
|
|---|
| 83 | test('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 |
|
|---|
| 96 | test('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 |
|
|---|
| 105 | test('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 |
|
|---|
| 115 | test('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 |
|
|---|
| 129 | test('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 | });
|
|---|