source: Klonkt/test/reactions-characterization.test.js@ 7842ca1

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

Reactie-migratie draait bij boot, inclusief hersleutelen (shaer-9e9)

Een upgrade naar de fase-2-code was tot nu toe onveilig. Het schema migreert
vanzelf (ensureColumn bij boot), maar voor data was er niets: de backfill was een
handmatig script. Bij een vloot die met klonkt-update in een stap naar nieuwe code
gaat wordt dat vergeten, en het falen is stil -- een reactie die niemand meer
ziet geeft geen fout, en de gebruiker klikt gewoon opnieuw, met een tweede Like
de fediverse in als gevolg.

migrateReactions() draait nu bij boot, één keer per REACTIONS_MIGRATION_VERSION,
op dezelfde manier als selfHealTimeline. Twee stappen:

HERSLEUTELEN De oude interact-route bewaarde de URI waarmee je binnenkwam, en

de bookmarklet geeft window.location.href door -- dus de
permalink. Sinds canonicalReactionUri op de object-URI gezocht
wordt, zouden die rijen wees zijn. Dit gat zat NIET in mijn
eerste backfill-script; dat vulde alleen aan vanuit de kolommen
en zag deze rijen niet. Op dev viel het niet op omdat daar alles
al canoniek stond.

AANVULLEN Alles wat op oude code via de Krant is gegeven staat alleen in

ap_timeline.liked/boosted.

Volgorde binnen het hersleutelen: eerst de canonieke variant erbij, dan de
permalink weg. Een onderbreking levert dan hooguit een dubbele rij op en nooit
een verdwenen reactie. Bij hersleutelen reist created_at mee; bij aanvullen
bestaat die informatie niet en wordt het de migratiedatum.

Mislukt de migratie, dan wordt de versievlag niet gezet en probeert de volgende
boot het opnieuw. De kolommen blijven staan, dus de oude waarheid is intussen
niet weg.

Het losse script is nu een schil om diezelfde functie, met --dry-run en --force.
Twee implementaties van een migratie lopen uiteen, en dan repareert de ene wat de
andere niet ziet.

Droogloop tegen de dev-database: 0 te hersleutelen, 0 aan te vullen -- die is al
schoon, wat meteen de idempotentie bevestigt. Suite 438/438.

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

  • Property mode set to 100644
File size: 19.2 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 en de post verschijnt in de Cirkel', () => {
64 const u = uri('t2'); seedTimeline(u);
65 assert.equal(AP.boostedCount('me'), 0);
66 AP.markBoosted('me', u);
67 assert.equal(AP.getTimelineReaction('me', u).boosted, true);
68 assert.equal(AP.boostedCount('me'), 1);
69 // De Cirkel is wat een gebruiker ziet: geboost = zichtbaar daar.
70 assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'geboost hoort in de Cirkel te staan');
71 AP.unmarkBoosted('me', u);
72 assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false);
73});
74
75test('tijdlijn-route: een vlag op een post die NIET in je tijdlijn staat gaat stil verloren', () => {
76 // Dit is het stille falen waar de refactor voor moet oppassen: geen fout,
77 // geen rij, geen spoor. Een UPDATE die niets raakt is voor SQLite in orde.
78 const u = uri('bestaat-niet');
79 AP.markLiked('me', u);
80 assert.deepEqual(AP.getTimelineReaction('me', u), { liked: false, boosted: false });
81});
82
83// ── Ingang 2: de interact-route (posts.js ~841-869) ──────────────────────
84// Schrijft de TUSSENTABEL en leest de tussentabel. De kolom blijft ongemoeid.
85
86test('interact-route: setMyReaction schrijft de tussentabel, ook zonder tijdlijnrij', () => {
87 const u = uri('i1'); // bewust NIET in ap_timeline
88 assert.deepEqual(AP.getMyReactions('me', u), { liked: false, boosted: false });
89 AP.setMyReaction('me', u, 'like', true);
90 assert.deepEqual(AP.getMyReactions('me', u), { liked: true, boosted: false });
91 AP.setMyReaction('me', u, 'boost', true);
92 assert.deepEqual(AP.getMyReactions('me', u), { liked: true, boosted: true });
93 AP.setMyReaction('me', u, 'like', false);
94 assert.deepEqual(AP.getMyReactions('me', u), { liked: false, boosted: true });
95});
96
97test('interact-route: tweemaal dezelfde reactie levert geen dubbele rij op', () => {
98 const u = uri('i2');
99 AP.setMyReaction('me', u, 'like', true);
100 AP.setMyReaction('me', u, 'like', true);
101 const n = db.prepare('SELECT COUNT(*) AS n FROM ap_my_reactions WHERE site_slug=? AND target_uri=?').get('me', u).n;
102 assert.equal(n, 1, 'de UNIQUE hoort het dubbel opslaan te voorkomen');
103});
104
105// ── De valkuil met naam: upsertBoostedNote is een INSERT ─────────────────
106
107test('upsertBoostedNote trekt een post die je NIET volgt je tijdlijn in', () => {
108 // Dit is geen vlag zetten. Zonder deze insert bestaat de rij niet, kan de
109 // vlag nergens landen, en verschijnt de boost nergens. Wie dit in de refactor
110 // vervangt door "gewoon markBoosted" boost iets wat daarna onvindbaar is.
111 const u = uri('b1');
112 assert.equal(db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE id=?').get(u).n, 0);
113 AP.upsertBoostedNote('me', {
114 object_uri: u, actor_uri: 'https://r.test/users/bo', actor_name: 'Bo',
115 content: '<p>geboost</p>', media: '[]',
116 });
117 assert.equal(db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE id=?').get(u).n, 1, 'de rij hoort aangemaakt te worden');
118 assert.equal(AP.getTimelineReaction('me', u).boosted, true, 'en meteen als geboost gemarkeerd');
119 assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'en dus zichtbaar in de Cirkel');
120});
121
122// ── De twee bronnen lopen uiteen ─────────────────────────────────────────
123// De primitieven raken elkaar niet, en dat blijft na fase 1 zo: het
124// samenvoegen zit in setReaction. Deze drie leggen vast waarom die functie
125// moet bestaan, en waarom markLiked en broers uiteindelijk intern horen te
126// worden -- zolang ze los aanroepbaar zijn, kan een aanroeper de helft doen.
127
128test('HUIDIG GEDRAG: de tijdlijn-route vult de tussentabel niet', () => {
129 const u = uri('d1'); seedTimeline(u);
130 AP.markLiked('me', u);
131 assert.equal(AP.getTimelineReaction('me', u).liked, true, 'de kolom staat aan');
132 assert.equal(AP.getMyReactions('me', u).liked, false,
133 'de primitief raakt de tussentabel niet -- daarvoor is setReaction');
134});
135
136test('HUIDIG GEDRAG: de interact-route vult de kolom niet', () => {
137 const u = uri('d2'); seedTimeline(u);
138 AP.setMyReaction('me', u, 'like', true);
139 assert.equal(AP.getMyReactions('me', u).liked, true, 'de tussentabel staat aan');
140 assert.equal(AP.getTimelineReaction('me', u).liked, false,
141 'de primitief raakt de kolom niet -- daarvoor is setReaction');
142});
143
144test('HUIDIG GEDRAG: alleen het C2S-pad schrijft allebei', () => {
145 // Wat ingestOutboxActivity sinds Klonkt 04aca12 doet, hier nagebootst zonder
146 // het netwerk: setMyReaction + markLiked. Dit is het enige pad dat de twee
147 // bronnen gelijk houdt, en precies daarom werkt un-liken vanuit een app wel.
148 const u = uri('d3'); seedTimeline(u);
149 AP.setMyReaction('me', u, 'like', true);
150 AP.markLiked('me', u);
151 assert.equal(AP.getMyReactions('me', u).liked, true);
152 assert.equal(AP.getTimelineReaction('me', u).liked, true);
153});
154
155// ── De scheefheid, meetbaar (fase 0 uit shaer-9e9 als test) ─────────────
156
157test('meetbaar: hoeveel rijen hebben een vlag zonder tegenhanger', () => {
158 // Dezelfde query als fase 0 in shaer-9e9, hier als test zodat de refactor
159 // hem kan gebruiken: na fase 1 hoort dit getal 0 te zijn en te blijven.
160 const scheef = db.prepare(`
161 SELECT COUNT(*) AS n FROM ap_timeline t
162 WHERE (t.liked = 1 OR t.boosted = 1)
163 AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r
164 WHERE r.site_slug = t.slug AND r.target_uri = t.id)`).get().n;
165 assert.ok(scheef > 0, 'vandaag lopen ze uiteen; na fase 1 hoort deze assert omgedraaid te worden naar === 0');
166});
167
168// ── De invariant van fase 1: setReaction houdt de bronnen gelijk ─────────
169
170test('setReaction: een like landt in BEIDE bronnen', () => {
171 const u = uri('sr1'); seedTimeline(u);
172 AP.setReaction('me', u, 'like', true);
173 assert.equal(AP.getMyReactions('me', u).liked, true, 'tussentabel');
174 assert.equal(AP.getTimelineReaction('me', u).liked, true, 'afgeleide vlag');
175 AP.setReaction('me', u, 'like', false);
176 assert.equal(AP.getMyReactions('me', u).liked, false);
177 assert.equal(AP.getTimelineReaction('me', u).liked, false);
178});
179
180test('setReaction: een boost met note trekt de post je tijdlijn in EN vult beide', () => {
181 // De valkuil uit het plan: zonder de note bestaat de rij niet en landt de
182 // vlag nergens, dus zou de boost onvindbaar zijn.
183 const u = uri('sr2');
184 AP.setReaction('me', u, 'boost', true, {
185 note: { object_uri: u, actor_uri: 'https://r.test/users/bo', actor_name: 'Bo', content: '<p>x</p>', media: '[]' },
186 });
187 assert.equal(AP.getMyReactions('me', u).boosted, true, 'tussentabel');
188 assert.equal(AP.getTimelineReaction('me', u).boosted, true, 'afgeleide vlag');
189 assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'en zichtbaar in de Cirkel');
190 AP.setReaction('me', u, 'boost', false);
191 assert.equal(AP.getMyReactions('me', u).boosted, false);
192 assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false);
193});
194
195test('setReaction: flagUri bepaalt de sleutel voor BEIDE bronnen', () => {
196 // Vroeger kreeg de tussentabel de URI die de client stuurde en de vlag de
197 // opgeloste. Nu is het er één, anders is dezelfde like onvindbaar vanaf een
198 // pagina die de andere URI kent.
199 const gestuurd = 'https://r.test/@anna/123';
200 const opgelost = uri('sr3');
201 seedTimeline(opgelost);
202 AP.setReaction('me', gestuurd, 'like', true, { flagUri: opgelost });
203 assert.equal(AP.getReaction('me', opgelost).liked, true, 'onder de opgeloste uri');
204 assert.equal(AP.getTimelineReaction('me', opgelost).liked, true, 'en de vlag ook');
205 assert.equal(AP.getMyReactions('me', gestuurd).liked, false, 'niet meer onder de gestuurde');
206});
207
208test('setReaction: rommelige invoer doet niets in plaats van iets halfs', () => {
209 AP.setReaction('me', uri('sr4'), 'sterretje', true);
210 assert.equal(AP.getMyReactions('me', uri('sr4')).liked, false);
211 AP.setReaction('', uri('sr5'), 'like', true);
212 AP.setReaction('me', '', 'like', true);
213 assert.equal(AP.getMyReactions('me', uri('sr5')).liked, false);
214});
215
216// ── Fase 2: lezen komt uit de tussentabel ────────────────────────────────
217
218test('getReaction leest de tussentabel, niet de kolom', () => {
219 const u = uri('g1'); seedTimeline(u);
220 AP.setReaction('me', u, 'like', true);
221 assert.deepEqual(AP.getReaction('me', u), { liked: true, boosted: false });
222 AP.setReaction('me', u, 'boost', true);
223 assert.deepEqual(AP.getReaction('me', u), { liked: true, boosted: true });
224});
225
226test('WAAROM DE BACKFILL EERST MOET: een kale kolomvlag is onzichtbaar voor getReaction', () => {
227 // Dit is de reden dat scripts/backfill-reactions.mjs tussen fase 1 en 2 hoort.
228 // Een reactie van vóór fase 1 staat alleen in de kolom; zodra de lezers de
229 // tussentabel volgen is die stil verdwenen -- geen fout, geen spoor.
230 const u = uri('g2'); seedTimeline(u);
231 AP.markLiked('me', u); // zoals de oude tijdlijn-route
232 assert.equal(AP.getTimelineReaction('me', u).liked, true, 'de kolom staat aan');
233 assert.equal(AP.getReaction('me', u).liked, false, 'maar het nieuwe leespad ziet hem niet');
234 // Wat de backfill doet:
235 AP.setMyReaction('me', u, 'like', true);
236 assert.equal(AP.getReaction('me', u).liked, true, 'na aanvullen wel');
237});
238
239test('getReactionsFor haalt een hele pagina in één keer op', () => {
240 const a = uri('g3'), b = uri('g4'), c = uri('g5');
241 seedTimeline(a); seedTimeline(b);
242 AP.setReaction('me', a, 'like', true);
243 AP.setReaction('me', b, 'boost', true);
244 const m = AP.getReactionsFor('me', [a, b, c]);
245 assert.equal(m.get(a).liked, true);
246 assert.equal(m.get(b).boosted, true);
247 assert.equal(m.get(c), undefined, 'wie niets heeft komt niet in de map; de aanroeper valt terug op false');
248 // Dezelfde uitkomst als per stuk vragen, zodat de batch geen eigen waarheid wordt.
249 for (const u of [a, b]) assert.deepEqual(m.get(u), AP.getReaction('me', u));
250});
251
252test('getReactionsFor: lege of rommelige invoer geeft een lege map', () => {
253 assert.equal(AP.getReactionsFor('me', []).size, 0);
254 assert.equal(AP.getReactionsFor('me', null).size, 0);
255 assert.equal(AP.getReactionsFor('', [uri('g3')]).size, 0);
256});
257
258// ── Fase 3: het publieke oppervlak is versmald ───────────────────────────
259
260test('de primitieven zijn niet meer bereikbaar via het service-object', async () => {
261 // Routes doen `import ActivityPubService from ...` en werken dus met het
262 // default-object. Zolang markLiked daar in staat, kan een aanroeper de helft
263 // schrijven -- en dat is niet hypothetisch: precies zo bleef shaer:liked
264 // maandenlang false. De named exports blijven bestaan voor intern gebruik en
265 // voor deze tests.
266 const svc = (await import('../src/services/ActivityPubService.js')).default;
267 for (const naam of ['markLiked', 'unmarkLiked', 'markBoosted', 'unmarkBoosted',
268 'setMyReaction', 'getMyReactions', 'getTimelineReaction']) {
269 assert.equal(svc[naam], undefined, `${naam} hoort niet op het publieke oppervlak te staan`);
270 }
271 // Wat er WEL hoort te staan: het ene schrijfpad en het ene leespad.
272 for (const naam of ['setReaction', 'getReaction', 'getReactionsFor']) {
273 assert.equal(typeof svc[naam], 'function', `${naam} hoort er wel te zijn`);
274 }
275});
276
277// ── De permalink en de object-URI zijn dezelfde post ─────────────────────
278
279test('een like uit de Krant is zichtbaar op de interact-pagina (Robins melding)', () => {
280 // De Krant kent een post als AP-object-URI, de interact-pagina als permalink.
281 // Zochten die twee in verschillende sleutelruimtes, dan toonde een geboost en
282 // geliket bericht daar geen enkele highlight -- terwijl de reactie bestond.
283 const obj = uri('perm1');
284 const permalink = 'https://sound-fabrics.com/effortlesseffect';
285 db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, url, created_at)
286 VALUES (?,?,?,?,?,?,?)`)
287 .run(obj, 'me', 'https://r.test/users/anna', 'Anna', '<p>x</p>', permalink, '2026-08-06 09:00:00');
288
289 AP.setReaction('me', obj, 'like', true); // zoals de Krant het doet
290 AP.setReaction('me', obj, 'boost', true);
291 // en de interact-pagina vraagt het met de permalink:
292 assert.deepEqual(AP.getReaction('me', permalink), { liked: true, boosted: true });
293});
294
295test('andersom net zo: reageren via de permalink landt op de object-URI', () => {
296 const obj = uri('perm2');
297 const permalink = 'https://sound-fabrics.com/tweede';
298 db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, url, created_at)
299 VALUES (?,?,?,?,?,?,?)`)
300 .run(obj, 'me', 'https://r.test/users/bo', 'Bo', '<p>y</p>', permalink, '2026-08-06 09:00:00');
301
302 AP.setReaction('me', permalink, 'like', true); // zoals de interact-pagina het doet
303 assert.equal(AP.getReaction('me', obj).liked, true, 'onder de object-uri opgeslagen');
304 assert.equal(AP.getTimelineReaction('me', obj).liked, true, 'dus de vlag landt ook goed');
305 assert.equal(AP.getReaction('me', permalink).liked, true, 'en blijft vindbaar via de permalink');
306});
307
308test('een reactie op iets buiten je tijdlijn blijft gewoon werken', () => {
309 // Kennen we de post niet, dan is er niets te canoniseren en blijft de invoer
310 // de sleutel. Een like op een vreemde post mag daar niet op stuklopen.
311 const onbekend = 'https://elders.test/notes/xyz';
312 AP.setReaction('me', onbekend, 'like', true);
313 assert.equal(AP.getReaction('me', onbekend).liked, true);
314});
315
316// ── De migratie bij boot ─────────────────────────────────────────────────
317
318test('migrateReactions hersleutelt een permalink-rij naar de object-URI', () => {
319 // De oude interact-route bewaarde de URI waarmee je binnenkwam, en de
320 // bookmarklet geeft de permalink door. Zonder hersleutelen zijn die rijen
321 // wees zodra er op de object-URI gezocht wordt.
322 const obj = uri('mig1');
323 const permalink = 'https://sound-fabrics.com/oud-artikel';
324 db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, url, created_at)
325 VALUES (?,?,?,?,?,?,?)`)
326 .run(obj, 'me', 'https://r.test/users/anna', 'Anna', '<p>x</p>', permalink, '2026-08-06 09:00:00');
327 db.prepare("INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind, created_at) VALUES (?,?,?,?)")
328 .run('me', permalink, 'like', '2026-07-01 12:00:00');
329
330 const uitkomst = AP.migrateReactions({ force: true });
331 assert.ok(uitkomst.hersleuteld >= 1);
332 assert.equal(AP.getReaction('me', obj).liked, true, 'nu vindbaar op de object-uri');
333 const rij = db.prepare('SELECT created_at FROM ap_my_reactions WHERE site_slug=? AND target_uri=? AND kind=?').get('me', obj, 'like');
334 assert.equal(rij.created_at, '2026-07-01 12:00:00', 'bij hersleutelen blijft de oorspronkelijke datum staan');
335 const oud = db.prepare('SELECT COUNT(*) AS n FROM ap_my_reactions WHERE target_uri=?').get(permalink).n;
336 assert.equal(oud, 0, 'de permalink-rij is opgeruimd');
337});
338
339test('migrateReactions vult een kale kolomvlag aan', () => {
340 const u = uri('mig2'); seedTimeline(u);
341 AP.markLiked('me', u); // zoals de oude Krant-route
342 assert.equal(AP.getReaction('me', u).liked, false, 'vooraf onzichtbaar');
343 AP.migrateReactions({ force: true });
344 assert.equal(AP.getReaction('me', u).liked, true, 'daarna zichtbaar');
345});
346
347test('migrateReactions is idempotent en respecteert de versievlag', () => {
348 const eerste = AP.migrateReactions({ force: true });
349 const tweede = AP.migrateReactions({ force: true });
350 assert.equal(tweede.hersleuteld, 0, 'niets meer te hersleutelen');
351 assert.equal(tweede.aangevuld, 0, 'niets meer aan te vullen');
352 assert.ok(eerste.hersleuteld >= 0);
353 // Zonder force draait hij niet nog eens zodra de vlag staat.
354 db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?,?)').run('reactions_migration_version', '1');
355 assert.equal(AP.migrateReactions().overgeslagen, true);
356});
357
358test('migrateReactions --dry-run schrijft niets', () => {
359 const u = uri('mig3'); seedTimeline(u);
360 AP.markLiked('me', u);
361 const telling = AP.migrateReactions({ force: true, dryRun: true });
362 assert.ok(telling.aangevuld >= 1, 'hij ziet wel wat er te doen is');
363 assert.equal(AP.getReaction('me', u).liked, false, 'maar heeft niets geschreven');
364});
Note: See TracBrowser for help on using the repository browser.