source: Klonkt/test/reactions-characterization.test.js@ 25f2e64

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

Weergave gelijkgetrokken: Krant en Cirkel lezen de tussentabel (shaer-9e9)

CORRECTIE: ik meldde fase 2 als klaar, en dat was te vroeg. De Krant TEKENDE
zijn like- en boostknoppen nog uit ap_timeline.liked/boosted (tl-item.ejs leest
p.liked en p.boosted), terwijl de toggle al uit getReaction besliste. Tekenen en
beslissen leunden dus op verschillende bronnen. Ze waren het eens zolang de
migratie ze gelijk hield -- synchronisatie, geen ontwerp.

getTimeline leidt liked/boosted nu af uit de tussentabel, met dezelfde
batch-query als de C2S-tijdlijn: één extra query per pagina, geen N+1. Bewust in
JS en niet als join, want de rijen komen met SELECT * en dan zouden twee
kolommen liked heten -- welke dan wint hangt van de driver af.

getCirkelPosts kon niet in JS: die filtert in de WHERE. Daar is het een LEFT JOIN
op ap_my_reactions geworden, met (rb.target_uri IS NOT NULL) AS boosted zodat de
view ongewijzigd blijft. De UNIQUE(site_slug, target_uri, kind) garandeert
hoogstens één match, dus de join kan geen rijen verdubbelen.

boostedCount telt nu de tussentabel, nog steeds met een join op ap_timeline zodat
de betekenis gelijk blijft: geboost EN in je tijdlijn.

Twee karakteriseringstests gingen hierdoor stuk, en dat is precies waarvoor ze er
staan: ze legden vast dat een kale kolomvlag een post de Cirkel in trok. Dat is
nu niet meer waar. Bewust bijgewerkt met de reden erbij, niet weggehaald.
upsertBoostedNote blijft verantwoordelijk voor de RIJ -- zonder die insert heeft
een boost van iemand die je niet volgt geen post om bij te horen.

Daarmee leest niets in de weergave nog uit de kolommen. Ze worden alleen nog
geschreven (vangnet) en gelezen door de migratie. Dat maakt de drop een losse,
terugdraaibare beslissing in plaats van een verweven.

Geen breaking change: additief en intern, dus kandidaat voor de volgende MINOR.

Suite 441/441.

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

  • Property mode set to 100644
File size: 22.0 KB
Line 
1// KARAKTERISERING van "heb ik hierop gereageerd", vóór de opschoning van
2// shaer-9e9. Deze tests beschrijven wat de code NU doet, niet wat ze zou moeten
3// doen. Ze staan hier om de aanstaande refactor hoorbaar te maken.
4//
5// Waarom dat nodig is: dit gebied faalt stil. Elke mark*-aanroep zit in een
6// try/catch die niets doet, en een UPDATE die geen rij raakt is geen fout. Zo
7// kon de shaer:liked-bug (Klonkt 04aca12) maanden bestaan. Een refactor kan hier
8// dus slagen, groen testen en tóch state kwijtraken -- tenzij het huidige gedrag
9// eerst is vastgeschreven.
10//
11// LET OP: de tests onder "de twee bronnen lopen uiteen" roepen de PRIMITIEVEN
12// aan (markLiked, setMyReaction). Die blijven bewust gescheiden -- het
13// samenvoegen zit in setReaction, dus in de aanroepers, niet in de primitieven.
14// Deze tests blijven daarom groen na fase 1 en beschrijven dan nog steeds iets
15// waars: wie markLiked los aanroept, raakt de tussentabel niet. Dat is precies
16// waarom setReaction bestaat en waarom die primitieven op termijn intern moeten
17// worden.
18//
19// De invariant die fase 1 wél bewaakt staat onderaan, bij setReaction.
20//
21// Wat hier NIET in kan: de routes zelf (die vragen HTTP + sessie) en het
22// C2S-pad voor Like/Announce (dat doet netwerk; zie de kop van
23// c2s-outbox.test.js). Dit dekt de servicelaag waar alle drie de ingangen op
24// uitkomen.
25//
26// Run: npm test
27
28import { test } from 'node:test';
29import assert from 'node:assert/strict';
30
31process.env.DATABASE_PATH = ':memory:';
32process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
33
34const dbMod = await import('../src/config/database.js');
35const db = dbMod.default;
36dbMod.initializeDatabase();
37const AP = await import('../src/services/ActivityPubService.js');
38
39db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
40 .run('u1', 'u1', 'u1@test', 'x', 'god');
41db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
42
43const uri = (n) => `https://r.test/ap/notes/${n}`;
44
45/** Een post van iemand anders in JOUW tijdlijn-cache. */
46function seedTimeline(id, author = 'https://r.test/users/anna') {
47 db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, created_at)
48 VALUES (?,?,?,?,?,?)`).run(id, 'me', author, 'Anna', '<p>x</p>', '2026-08-06 09:00:00');
49}
50
51// ── Ingang 1: de tijdlijn-route (posts.js ~1279-1304) ────────────────────
52// Schrijft de KOLOM en leest de kolom. De tussentabel blijft ongemoeid.
53
54test('tijdlijn-route: markLiked zet de kolom en getTimelineReaction leest hem', () => {
55 const u = uri('t1'); seedTimeline(u);
56 assert.deepEqual(AP.getTimelineReaction('me', u), { liked: false, boosted: false });
57 AP.markLiked('me', u);
58 assert.deepEqual(AP.getTimelineReaction('me', u), { liked: true, boosted: false });
59 AP.unmarkLiked('me', u);
60 assert.equal(AP.getTimelineReaction('me', u).liked, false);
61});
62
63test('tijdlijn-route: markBoosted zet de kolom, maar de Cirkel volgt de tussentabel', () => {
64 // BIJGEWERKT toen de weergave werd gelijkgetrokken. Vroeger trok een kale
65 // kolomvlag de post de Cirkel in; nu selecteert de Cirkel op de tussentabel,
66 // dus de primitief alleen is niet meer genoeg. Dat is het doel: tekenen en
67 // beslissen leunen op dezelfde bron.
68 const u = uri('t2'); seedTimeline(u);
69 AP.markBoosted('me', u);
70 assert.equal(AP.getTimelineReaction('me', u).boosted, true, 'de kolom staat aan');
71 assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false,
72 'maar dat brengt hem niet in de Cirkel');
73 // Via het echte schrijfpad wel.
74 AP.setReaction('me', u, 'boost', true);
75 assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'via setReaction wel');
76 AP.setReaction('me', u, 'boost', false);
77 assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false);
78});
79
80test('tijdlijn-route: een vlag op een post die NIET in je tijdlijn staat gaat stil verloren', () => {
81 // Dit is het stille falen waar de refactor voor moet oppassen: geen fout,
82 // geen rij, geen spoor. Een UPDATE die niets raakt is voor SQLite in orde.
83 const u = uri('bestaat-niet');
84 AP.markLiked('me', u);
85 assert.deepEqual(AP.getTimelineReaction('me', u), { liked: false, boosted: false });
86});
87
88// ── Ingang 2: de interact-route (posts.js ~841-869) ──────────────────────
89// Schrijft de TUSSENTABEL en leest de tussentabel. De kolom blijft ongemoeid.
90
91test('interact-route: setMyReaction schrijft de tussentabel, ook zonder tijdlijnrij', () => {
92 const u = uri('i1'); // bewust NIET in ap_timeline
93 assert.deepEqual(AP.getMyReactions('me', u), { liked: false, boosted: false });
94 AP.setMyReaction('me', u, 'like', true);
95 assert.deepEqual(AP.getMyReactions('me', u), { liked: true, boosted: false });
96 AP.setMyReaction('me', u, 'boost', true);
97 assert.deepEqual(AP.getMyReactions('me', u), { liked: true, boosted: true });
98 AP.setMyReaction('me', u, 'like', false);
99 assert.deepEqual(AP.getMyReactions('me', u), { liked: false, boosted: true });
100});
101
102test('interact-route: tweemaal dezelfde reactie levert geen dubbele rij op', () => {
103 const u = uri('i2');
104 AP.setMyReaction('me', u, 'like', true);
105 AP.setMyReaction('me', u, 'like', true);
106 const n = db.prepare('SELECT COUNT(*) AS n FROM ap_my_reactions WHERE site_slug=? AND target_uri=?').get('me', u).n;
107 assert.equal(n, 1, 'de UNIQUE hoort het dubbel opslaan te voorkomen');
108});
109
110// ── De valkuil met naam: upsertBoostedNote is een INSERT ─────────────────
111
112test('upsertBoostedNote trekt een post die je NIET volgt je tijdlijn in', () => {
113 // Dit is geen vlag zetten. Zonder deze insert bestaat de rij niet, kan de
114 // vlag nergens landen, en verschijnt de boost nergens. Wie dit in de refactor
115 // vervangt door "gewoon markBoosted" boost iets wat daarna onvindbaar is.
116 const u = uri('b1');
117 assert.equal(db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE id=?').get(u).n, 0);
118 AP.upsertBoostedNote('me', {
119 object_uri: u, actor_uri: 'https://r.test/users/bo', actor_name: 'Bo',
120 content: '<p>geboost</p>', media: '[]',
121 });
122 assert.equal(db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE id=?').get(u).n, 1, 'de rij hoort aangemaakt te worden');
123 assert.equal(AP.getTimelineReaction('me', u).boosted, true, 'en meteen als geboost gemarkeerd');
124 // BIJGEWERKT: de Cirkel volgt sinds het gelijktrekken van de weergave de
125 // tussentabel, en die vult alleen setReaction. upsertBoostedNote blijft
126 // verantwoordelijk voor de RIJ -- zonder die insert heeft de reactie nergens
127 // een post om bij te horen.
128 assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false, 'de rij alleen is niet genoeg');
129 AP.setMyReaction('me', u, 'boost', true);
130 assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'met de tussentabel erbij wel');
131});
132
133// ── De twee bronnen lopen uiteen ─────────────────────────────────────────
134// De primitieven raken elkaar niet, en dat blijft na fase 1 zo: het
135// samenvoegen zit in setReaction. Deze drie leggen vast waarom die functie
136// moet bestaan, en waarom markLiked en broers uiteindelijk intern horen te
137// worden -- zolang ze los aanroepbaar zijn, kan een aanroeper de helft doen.
138
139test('HUIDIG GEDRAG: de tijdlijn-route vult de tussentabel niet', () => {
140 const u = uri('d1'); seedTimeline(u);
141 AP.markLiked('me', u);
142 assert.equal(AP.getTimelineReaction('me', u).liked, true, 'de kolom staat aan');
143 assert.equal(AP.getMyReactions('me', u).liked, false,
144 'de primitief raakt de tussentabel niet -- daarvoor is setReaction');
145});
146
147test('HUIDIG GEDRAG: de interact-route vult de kolom niet', () => {
148 const u = uri('d2'); seedTimeline(u);
149 AP.setMyReaction('me', u, 'like', true);
150 assert.equal(AP.getMyReactions('me', u).liked, true, 'de tussentabel staat aan');
151 assert.equal(AP.getTimelineReaction('me', u).liked, false,
152 'de primitief raakt de kolom niet -- daarvoor is setReaction');
153});
154
155test('HUIDIG GEDRAG: alleen het C2S-pad schrijft allebei', () => {
156 // Wat ingestOutboxActivity sinds Klonkt 04aca12 doet, hier nagebootst zonder
157 // het netwerk: setMyReaction + markLiked. Dit is het enige pad dat de twee
158 // bronnen gelijk houdt, en precies daarom werkt un-liken vanuit een app wel.
159 const u = uri('d3'); seedTimeline(u);
160 AP.setMyReaction('me', u, 'like', true);
161 AP.markLiked('me', u);
162 assert.equal(AP.getMyReactions('me', u).liked, true);
163 assert.equal(AP.getTimelineReaction('me', u).liked, true);
164});
165
166// ── De scheefheid, meetbaar (fase 0 uit shaer-9e9 als test) ─────────────
167
168test('meetbaar: hoeveel rijen hebben een vlag zonder tegenhanger', () => {
169 // Dezelfde query als fase 0 in shaer-9e9, hier als test zodat de refactor
170 // hem kan gebruiken: na fase 1 hoort dit getal 0 te zijn en te blijven.
171 const scheef = db.prepare(`
172 SELECT COUNT(*) AS n FROM ap_timeline t
173 WHERE (t.liked = 1 OR t.boosted = 1)
174 AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r
175 WHERE r.site_slug = t.slug AND r.target_uri = t.id)`).get().n;
176 assert.ok(scheef > 0, 'vandaag lopen ze uiteen; na fase 1 hoort deze assert omgedraaid te worden naar === 0');
177});
178
179// ── De invariant van fase 1: setReaction houdt de bronnen gelijk ─────────
180
181test('setReaction: een like landt in BEIDE bronnen', () => {
182 const u = uri('sr1'); seedTimeline(u);
183 AP.setReaction('me', u, 'like', true);
184 assert.equal(AP.getMyReactions('me', u).liked, true, 'tussentabel');
185 assert.equal(AP.getTimelineReaction('me', u).liked, true, 'afgeleide vlag');
186 AP.setReaction('me', u, 'like', false);
187 assert.equal(AP.getMyReactions('me', u).liked, false);
188 assert.equal(AP.getTimelineReaction('me', u).liked, false);
189});
190
191test('setReaction: een boost met note trekt de post je tijdlijn in EN vult beide', () => {
192 // De valkuil uit het plan: zonder de note bestaat de rij niet en landt de
193 // vlag nergens, dus zou de boost onvindbaar zijn.
194 const u = uri('sr2');
195 AP.setReaction('me', u, 'boost', true, {
196 note: { object_uri: u, actor_uri: 'https://r.test/users/bo', actor_name: 'Bo', content: '<p>x</p>', media: '[]' },
197 });
198 assert.equal(AP.getMyReactions('me', u).boosted, true, 'tussentabel');
199 assert.equal(AP.getTimelineReaction('me', u).boosted, true, 'afgeleide vlag');
200 assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'en zichtbaar in de Cirkel');
201 AP.setReaction('me', u, 'boost', false);
202 assert.equal(AP.getMyReactions('me', u).boosted, false);
203 assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false);
204});
205
206test('setReaction: flagUri bepaalt de sleutel voor BEIDE bronnen', () => {
207 // Vroeger kreeg de tussentabel de URI die de client stuurde en de vlag de
208 // opgeloste. Nu is het er één, anders is dezelfde like onvindbaar vanaf een
209 // pagina die de andere URI kent.
210 const gestuurd = 'https://r.test/@anna/123';
211 const opgelost = uri('sr3');
212 seedTimeline(opgelost);
213 AP.setReaction('me', gestuurd, 'like', true, { flagUri: opgelost });
214 assert.equal(AP.getReaction('me', opgelost).liked, true, 'onder de opgeloste uri');
215 assert.equal(AP.getTimelineReaction('me', opgelost).liked, true, 'en de vlag ook');
216 assert.equal(AP.getMyReactions('me', gestuurd).liked, false, 'niet meer onder de gestuurde');
217});
218
219test('setReaction: rommelige invoer doet niets in plaats van iets halfs', () => {
220 AP.setReaction('me', uri('sr4'), 'sterretje', true);
221 assert.equal(AP.getMyReactions('me', uri('sr4')).liked, false);
222 AP.setReaction('', uri('sr5'), 'like', true);
223 AP.setReaction('me', '', 'like', true);
224 assert.equal(AP.getMyReactions('me', uri('sr5')).liked, false);
225});
226
227// ── Fase 2: lezen komt uit de tussentabel ────────────────────────────────
228
229test('getReaction leest de tussentabel, niet de kolom', () => {
230 const u = uri('g1'); seedTimeline(u);
231 AP.setReaction('me', u, 'like', true);
232 assert.deepEqual(AP.getReaction('me', u), { liked: true, boosted: false });
233 AP.setReaction('me', u, 'boost', true);
234 assert.deepEqual(AP.getReaction('me', u), { liked: true, boosted: true });
235});
236
237test('WAAROM DE BACKFILL EERST MOET: een kale kolomvlag is onzichtbaar voor getReaction', () => {
238 // Dit is de reden dat scripts/backfill-reactions.mjs tussen fase 1 en 2 hoort.
239 // Een reactie van vóór fase 1 staat alleen in de kolom; zodra de lezers de
240 // tussentabel volgen is die stil verdwenen -- geen fout, geen spoor.
241 const u = uri('g2'); seedTimeline(u);
242 AP.markLiked('me', u); // zoals de oude tijdlijn-route
243 assert.equal(AP.getTimelineReaction('me', u).liked, true, 'de kolom staat aan');
244 assert.equal(AP.getReaction('me', u).liked, false, 'maar het nieuwe leespad ziet hem niet');
245 // Wat de backfill doet:
246 AP.setMyReaction('me', u, 'like', true);
247 assert.equal(AP.getReaction('me', u).liked, true, 'na aanvullen wel');
248});
249
250test('getReactionsFor haalt een hele pagina in één keer op', () => {
251 const a = uri('g3'), b = uri('g4'), c = uri('g5');
252 seedTimeline(a); seedTimeline(b);
253 AP.setReaction('me', a, 'like', true);
254 AP.setReaction('me', b, 'boost', true);
255 const m = AP.getReactionsFor('me', [a, b, c]);
256 assert.equal(m.get(a).liked, true);
257 assert.equal(m.get(b).boosted, true);
258 assert.equal(m.get(c), undefined, 'wie niets heeft komt niet in de map; de aanroeper valt terug op false');
259 // Dezelfde uitkomst als per stuk vragen, zodat de batch geen eigen waarheid wordt.
260 for (const u of [a, b]) assert.deepEqual(m.get(u), AP.getReaction('me', u));
261});
262
263test('getReactionsFor: lege of rommelige invoer geeft een lege map', () => {
264 assert.equal(AP.getReactionsFor('me', []).size, 0);
265 assert.equal(AP.getReactionsFor('me', null).size, 0);
266 assert.equal(AP.getReactionsFor('', [uri('g3')]).size, 0);
267});
268
269// ── Fase 3: het publieke oppervlak is versmald ───────────────────────────
270
271test('de primitieven zijn niet meer bereikbaar via het service-object', async () => {
272 // Routes doen `import ActivityPubService from ...` en werken dus met het
273 // default-object. Zolang markLiked daar in staat, kan een aanroeper de helft
274 // schrijven -- en dat is niet hypothetisch: precies zo bleef shaer:liked
275 // maandenlang false. De named exports blijven bestaan voor intern gebruik en
276 // voor deze tests.
277 const svc = (await import('../src/services/ActivityPubService.js')).default;
278 for (const naam of ['markLiked', 'unmarkLiked', 'markBoosted', 'unmarkBoosted',
279 'setMyReaction', 'getMyReactions', 'getTimelineReaction']) {
280 assert.equal(svc[naam], undefined, `${naam} hoort niet op het publieke oppervlak te staan`);
281 }
282 // Wat er WEL hoort te staan: het ene schrijfpad en het ene leespad.
283 for (const naam of ['setReaction', 'getReaction', 'getReactionsFor']) {
284 assert.equal(typeof svc[naam], 'function', `${naam} hoort er wel te zijn`);
285 }
286});
287
288// ── De permalink en de object-URI zijn dezelfde post ─────────────────────
289
290test('een like uit de Krant is zichtbaar op de interact-pagina (Robins melding)', () => {
291 // De Krant kent een post als AP-object-URI, de interact-pagina als permalink.
292 // Zochten die twee in verschillende sleutelruimtes, dan toonde een geboost en
293 // geliket bericht daar geen enkele highlight -- terwijl de reactie bestond.
294 const obj = uri('perm1');
295 const permalink = 'https://sound-fabrics.com/effortlesseffect';
296 db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, url, created_at)
297 VALUES (?,?,?,?,?,?,?)`)
298 .run(obj, 'me', 'https://r.test/users/anna', 'Anna', '<p>x</p>', permalink, '2026-08-06 09:00:00');
299
300 AP.setReaction('me', obj, 'like', true); // zoals de Krant het doet
301 AP.setReaction('me', obj, 'boost', true);
302 // en de interact-pagina vraagt het met de permalink:
303 assert.deepEqual(AP.getReaction('me', permalink), { liked: true, boosted: true });
304});
305
306test('andersom net zo: reageren via de permalink landt op de object-URI', () => {
307 const obj = uri('perm2');
308 const permalink = 'https://sound-fabrics.com/tweede';
309 db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, url, created_at)
310 VALUES (?,?,?,?,?,?,?)`)
311 .run(obj, 'me', 'https://r.test/users/bo', 'Bo', '<p>y</p>', permalink, '2026-08-06 09:00:00');
312
313 AP.setReaction('me', permalink, 'like', true); // zoals de interact-pagina het doet
314 assert.equal(AP.getReaction('me', obj).liked, true, 'onder de object-uri opgeslagen');
315 assert.equal(AP.getTimelineReaction('me', obj).liked, true, 'dus de vlag landt ook goed');
316 assert.equal(AP.getReaction('me', permalink).liked, true, 'en blijft vindbaar via de permalink');
317});
318
319test('een reactie op iets buiten je tijdlijn blijft gewoon werken', () => {
320 // Kennen we de post niet, dan is er niets te canoniseren en blijft de invoer
321 // de sleutel. Een like op een vreemde post mag daar niet op stuklopen.
322 const onbekend = 'https://elders.test/notes/xyz';
323 AP.setReaction('me', onbekend, 'like', true);
324 assert.equal(AP.getReaction('me', onbekend).liked, true);
325});
326
327// ── De migratie bij boot ─────────────────────────────────────────────────
328
329test('migrateReactions hersleutelt een permalink-rij naar de object-URI', () => {
330 // De oude interact-route bewaarde de URI waarmee je binnenkwam, en de
331 // bookmarklet geeft de permalink door. Zonder hersleutelen zijn die rijen
332 // wees zodra er op de object-URI gezocht wordt.
333 const obj = uri('mig1');
334 const permalink = 'https://sound-fabrics.com/oud-artikel';
335 db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, url, created_at)
336 VALUES (?,?,?,?,?,?,?)`)
337 .run(obj, 'me', 'https://r.test/users/anna', 'Anna', '<p>x</p>', permalink, '2026-08-06 09:00:00');
338 db.prepare("INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind, created_at) VALUES (?,?,?,?)")
339 .run('me', permalink, 'like', '2026-07-01 12:00:00');
340
341 const uitkomst = AP.migrateReactions({ force: true });
342 assert.ok(uitkomst.hersleuteld >= 1);
343 assert.equal(AP.getReaction('me', obj).liked, true, 'nu vindbaar op de object-uri');
344 const rij = db.prepare('SELECT created_at FROM ap_my_reactions WHERE site_slug=? AND target_uri=? AND kind=?').get('me', obj, 'like');
345 assert.equal(rij.created_at, '2026-07-01 12:00:00', 'bij hersleutelen blijft de oorspronkelijke datum staan');
346 const oud = db.prepare('SELECT COUNT(*) AS n FROM ap_my_reactions WHERE target_uri=?').get(permalink).n;
347 assert.equal(oud, 0, 'de permalink-rij is opgeruimd');
348});
349
350test('migrateReactions vult een kale kolomvlag aan', () => {
351 const u = uri('mig2'); seedTimeline(u);
352 AP.markLiked('me', u); // zoals de oude Krant-route
353 assert.equal(AP.getReaction('me', u).liked, false, 'vooraf onzichtbaar');
354 AP.migrateReactions({ force: true });
355 assert.equal(AP.getReaction('me', u).liked, true, 'daarna zichtbaar');
356});
357
358test('migrateReactions is idempotent en respecteert de versievlag', () => {
359 const eerste = AP.migrateReactions({ force: true });
360 const tweede = AP.migrateReactions({ force: true });
361 assert.equal(tweede.hersleuteld, 0, 'niets meer te hersleutelen');
362 assert.equal(tweede.aangevuld, 0, 'niets meer aan te vullen');
363 assert.ok(eerste.hersleuteld >= 0);
364 // Zonder force draait hij niet nog eens zodra de vlag staat.
365 db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?,?)').run('reactions_migration_version', '1');
366 assert.equal(AP.migrateReactions().overgeslagen, true);
367});
368
369test('migrateReactions --dry-run schrijft niets', () => {
370 const u = uri('mig3'); seedTimeline(u);
371 AP.markLiked('me', u);
372 const telling = AP.migrateReactions({ force: true, dryRun: true });
373 assert.ok(telling.aangevuld >= 1, 'hij ziet wel wat er te doen is');
374 assert.equal(AP.getReaction('me', u).liked, false, 'maar heeft niets geschreven');
375});
376
377// ── De weergave leest dezelfde bron als de knop ──────────────────────────
378
379test('getTimeline tekent liked/boosted uit de tussentabel, niet uit de kolom', () => {
380 // De Krant tekende zijn knoppen op ap_timeline.liked/boosted terwijl de
381 // toggle al uit getReaction besliste. Tekenen en beslissen leunden dus op
382 // verschillende bronnen -- ze waren het alleen eens zolang iets ze gelijk
383 // hield. Hier wordt de kolom expres tegengesteld gezet.
384 const u = uri('tl1'); seedTimeline(u);
385 AP.setReaction('me', u, 'like', true);
386 db.prepare('UPDATE ap_timeline SET liked = 0, boosted = 1 WHERE slug = ? AND id = ?').run('me', u);
387
388 const rij = AP.getTimeline('me', 50, 0).find((r) => r.id === u);
389 assert.equal(rij.liked, true, 'de tussentabel zegt geliket, dus de knop hoort aan');
390 assert.equal(rij.boosted, false, 'en de kolom die iets anders beweert telt niet');
391});
392
393test('de Cirkel selecteert op de tussentabel', () => {
394 const u = uri('tl2'); seedTimeline(u);
395 // Kolom aan, tussentabel leeg: hoort NIET in de Cirkel te staan.
396 db.prepare('UPDATE ap_timeline SET boosted = 1 WHERE slug = ? AND id = ?').run('me', u);
397 assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false,
398 'een kale kolomvlag trekt niets meer de Cirkel in');
399 // En andersom: tussentabel aan, kolom uit.
400 db.prepare('UPDATE ap_timeline SET boosted = 0 WHERE slug = ? AND id = ?').run('me', u);
401 AP.setMyReaction('me', u, 'boost', true);
402 const post = AP.getCirkelPosts('me', 60, 0).find((p) => p.id === u);
403 assert.ok(post, 'de tussentabel bepaalt wat er in de Cirkel staat');
404 assert.ok(post.boosted, 'en het veld dat de view tekent komt daar ook vandaan');
405});
406
407test('boostedCount telt de tussentabel, en alleen wat in je tijdlijn staat', () => {
408 const inTl = uri('tl3'), buiten = 'https://elders.test/notes/qqq';
409 seedTimeline(inTl);
410 const voor = AP.boostedCount('me');
411 AP.setMyReaction('me', inTl, 'boost', true);
412 AP.setMyReaction('me', buiten, 'boost', true);
413 assert.equal(AP.boostedCount('me'), voor + 1, 'alleen de post die in je tijdlijn staat telt mee');
414});
Note: See TracBrowser for help on using the repository browser.