source: Klonkt/test/interaction-reactions.test.js@ 3b43e4c

main
Last change on this file since 3b43e4c was 279ca0b, checked in by roboburr <roboburr@…>, 5 weeks ago

De derde reactiebron ook naar ap_my_reactions (shaer-ipb)

ap_interactions.acted_like/acted_boost -- wat JIJ deed met een reactie onder je
eigen post -- was de derde bron van 'heb ik hierop gereageerd'. shaer-9e9 trok de
andere twee samen en liet deze staan omdat hij op het interactie-rij-id
gesleuteld was en de tussentabel op de object-URI.

De betekeniskeuze die de bead eerst wilde beantwoorden: dezelfde note, geliket
als comment en als post, wordt EEN feit. Er gaat immers ook maar een Like de
fediverse in; twee knoppen die los van elkaar aan konden staan voor hetzelfde
object was eerder een bug dan een feature. Mechanisch blijkt dat al veilig:
markLiked en markBoosted zijn kale UPDATEs op ap_timeline die nul rijen raken als
de URI daar niet staat, en upsertBoostedNote draait alleen met opts.note mee.

Volgorde zoals bij shaer-9e9, en om dezelfde reden -- een leespad dat naar de
tussentabel wijst terwijl de rijen nog in de kolommen staan laat reacties STIL
verdwijnen:

schrijven fedi-react roept setReaction aan; acted_* loopt nog als afgeleide

mee, hetzelfde vangnet dat ap_timeline.liked na shaer-9e9 is. Ook de
toggle leest nu uit de tussentabel: las de knop de ene bron en de
richting de andere, dan stuurt een divergentie een Undo voor iets dat
nooit verstuurd is.

migreren REACTIONS_MIGRATION_VERSION 1 -> 2, derde stap erbij. Een bump laat

alle stappen opnieuw lopen; ze zijn alle drie idempotent. Rijen
zonder object_uri vallen af, en dat verliest per constructie niets:
fedi-react eist een object_uri voordat acted_* gezet kan worden.

lezen getInteractions doet een batch-lookup langs dezelfde sleutel als de

schrijfkant, dus geen N+1 in een drukke thread.

Het backfill-script zou anders vals alarm slaan: een reactie op een COMMENT hoort
geen tijdlijnrij te hebben, dus die telt niet meer als wees. Er is een controle
bij voor acted_* zonder tegenhanger.

Gemeten op een kopie van dev: scheefActed 1 -> 0, tussentabel 19 -> 20, wees
onveranderd. Suite 472/472. Gecontroleerd dat de tests bijten: laat het leespad
weer uit de kolom lezen en vier van de zes vallen om.

De karakteriseringstest zette de versievlag hard op 1 en zou bij elke bump
omvallen zonder dat er iets stuk is; die toetst nu het gedrag (ouder = opnieuw,
gelijk of hoger = overslaan) in plaats van het getal.

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

  • Property mode set to 100644
File size: 5.0 KB
Line 
1// shaer-ipb: ap_interactions.acted_* was de DERDE bron van "heb ik hierop
2// gereageerd", naast ap_timeline.liked/boosted en ap_my_reactions. shaer-9e9
3// trok de eerste twee samen en liet deze staan, omdat hij op het interactie-rij-
4// id gesleuteld was en de tussentabel op de object-URI.
5//
6// Nu is ook deze bron de tussentabel. Wat hier bewaakt wordt:
7// - de migratie neemt bestaande acted_*-rijen mee
8// - getInteractions leest uit de tussentabel, niet meer uit de kolom
9// - dezelfde note die je als comment EN als post kent, is EEN feit
10//
11// Run: npm test
12
13import { test } from 'node:test';
14import assert from 'node:assert/strict';
15
16process.env.DATABASE_PATH = ':memory:';
17process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
18
19const dbMod = await import('../src/config/database.js');
20const db = dbMod.default;
21dbMod.initializeDatabase();
22const AP = await import('../src/services/ActivityPubService.js');
23
24db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
25 .run('u1', 'u1', 'u1@test', 'x', 'god');
26db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
27db.prepare('INSERT INTO posts (id, site_id, slug, author_id, title, content, status) VALUES (?,?,?,?,?,?,?)')
28 .run('p1', 's1', 'mijn-post', 'u1', 'Mijn post', '<p>x</p>', 'published');
29
30const site = { slug: 'me', title: 'Me' };
31const COMMENT = 'https://203.0.113.60/notes/comment-1';
32const COMMENT2 = 'https://203.0.113.60/notes/comment-2';
33
34function comment(uri, extra = {}) {
35 db.prepare(`INSERT INTO ap_interactions (kind, post_id, object_uri, actor_uri, actor_name, actor_handle, content, acted_like, acted_boost)
36 VALUES ('reply', 'p1', ?, ?, 'Anna', '@anna@203.0.113.60', '<p>hoi</p>', ?, ?)`)
37 .run(uri, 'https://203.0.113.60/users/anna', extra.like ? 1 : 0, extra.boost ? 1 : 0);
38 return db.prepare('SELECT id FROM ap_interactions WHERE object_uri = ?').get(uri).id;
39}
40
41const knop = (uri) => {
42 const plat = [];
43 const loop = (ns) => ns.forEach((n) => { plat.push(n); loop(n.children || []); });
44 loop(AP.getInteractions('p1', 'https://klonkt.test', site).thread);
45 const n = plat.find((x) => x.noteId === uri);
46 assert.ok(n, `node ${uri} hoort in de thread te staan`);
47 return { like: n.acted_like, boost: n.acted_boost };
48};
49
50test('de migratie neemt acted_* mee naar de tussentabel', () => {
51 comment(COMMENT, { like: true });
52 const uit = AP.migrateReactions({ force: true });
53 assert.equal(uit.reacties, 1, 'één reactie hoort overgenomen te worden');
54 assert.deepEqual(AP.getReaction('me', COMMENT), { liked: true, boosted: false });
55});
56
57test('en is idempotent: nog een keer draaien voegt niets toe', () => {
58 const uit = AP.migrateReactions({ force: true });
59 assert.equal(uit.reacties, 0);
60});
61
62test('de knopstand komt uit de tussentabel, niet uit de kolom', () => {
63 // Zet de kolom en de tussentabel EXPLICIET tegenover elkaar. Zou de view nog
64 // uit de kolom lezen, dan zou hier true uit komen.
65 db.prepare('UPDATE ap_interactions SET acted_boost = 1 WHERE object_uri = ?').run(COMMENT);
66 assert.equal(knop(COMMENT).boost, false, 'de kolom liegt en wordt genegeerd');
67 AP.setReaction('me', COMMENT, 'boost', true);
68 assert.equal(knop(COMMENT).boost, true);
69});
70
71test('een reactie geven en intrekken laat niets achter', () => {
72 comment(COMMENT2);
73 assert.deepEqual(knop(COMMENT2), { like: false, boost: false });
74 AP.setReaction('me', COMMENT2, 'like', true);
75 assert.equal(knop(COMMENT2).like, true);
76 AP.setReaction('me', COMMENT2, 'like', false);
77 assert.equal(knop(COMMENT2).like, false);
78});
79
80test('dezelfde note als comment en als post is EEN feit', () => {
81 // De betekeniswijziging van deze bead, expliciet vastgelegd: like je een note
82 // in je Krant, dan staat de knop onder diezelfde note als comment ook aan.
83 // Twee knoppen die los van elkaar aan konden staan voor hetzelfde object was
84 // eerder een bug dan een feature -- er gaat immers één Like de fediverse in.
85 const BEIDE = 'https://203.0.113.60/notes/overlap';
86 comment(BEIDE);
87 db.prepare(`INSERT INTO ap_timeline (id, slug, author_uri, author_name, content, created_at)
88 VALUES (?,?,?,?,?,?)`).run(BEIDE, 'me', 'https://203.0.113.60/users/anna', 'Anna', '<p>x</p>', '2026-08-06 09:00:00');
89 AP.setReaction('me', BEIDE, 'like', true);
90 assert.equal(knop(BEIDE).like, true, 'de like uit de Krant hoort onder de comment te staan');
91});
92
93test('een reactie zonder object_uri blokkeert de migratie niet', () => {
94 // Een like of announce draagt geen object_uri, en fedi-react eist er een --
95 // zulke rijen kunnen dus nooit acted_* dragen. De migratie moet ze overslaan
96 // in plaats van erover te struikelen. De kolom is NOT NULL, dus zo'n rij draagt
97 // een lege string; zo staat hij ook op beta.
98 db.prepare(`INSERT INTO ap_interactions (kind, post_id, object_uri, actor_uri, actor_name, acted_like)
99 VALUES ('like', 'p1', '', ?, 'Bob', 1)`).run('https://203.0.113.60/users/bob');
100 const uit = AP.migrateReactions({ force: true });
101 assert.equal(uit.reacties, 0, 'niets over te nemen, en geen fout');
102});
Note: See TracBrowser for help on using the repository browser.