source: Klonkt/test/reactions-characterization.test.js@ 02fb7ce

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

Karakteriseringstests voor reacties (shaer-9e9 fase 1, stap 1)

Legt vast wat de code NU doet met "heb ik hierop gereageerd", voordat de drie
bronnen worden samengevoegd. Geen gedragswijziging: alleen tests, groen op de
huidige code.

Nodig omdat dit gebied stil faalt. Elke mark*-aanroep zit in een try/catch die
niets doet en een UPDATE die geen rij raakt is voor SQLite geen fout -- zo kon de
shaer:liked-bug maanden bestaan. Een refactor kan hier dus slagen, groen testen
en tóch state kwijtraken, tenzij het huidige gedrag eerst is vastgeschreven.

Vastgelegd per ingang: de tijdlijn-route schrijft en leest de kolom, de
interact-route schrijft en leest de tussentabel, en alleen het C2S-pad raakt
allebei. Plus twee dingen die makkelijk sneuvelen in een refactor: een vlag op
een post die niet in je tijdlijn staat verdwijnt geruisloos, en
upsertBoostedNote is geen vlag-zetter maar een INSERT die de post je tijdlijn in
trekt zodat de Cirkel hem kan tonen.

Drie tests heten expres "HUIDIG GEDRAG" en horen te GAAN FALEN zodra fase 1 één
setReaction oplevert. Dat is het doel, niet een regressie: zo wordt het
samenvoegen een zichtbare gebeurtenis in plaats van een stille. Dat staat ook in
de kop van het bestand, met de instructie ze dan bewust bij te werken.

De laatste test meet de scheefheid met dezelfde query als fase 0 in shaer-9e9,
zodat de refactor hem kan omdraaien naar === 0.

Suite 422/422.

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

  • Property mode set to 100644
File size: 8.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 bij het opruimen: de tests onder "de twee bronnen lopen uiteen" horen
12// te GAAN FALEN zodra fase 1 (één setReaction) klaar is. Dat is geen regressie
13// maar het doel; werk ze dan bewust bij in plaats van ze te laten verdwijnen.
14//
15// Wat hier NIET in kan: de routes zelf (die vragen HTTP + sessie) en het
16// C2S-pad voor Like/Announce (dat doet netwerk; zie de kop van
17// c2s-outbox.test.js). Dit dekt de servicelaag waar alle drie de ingangen op
18// uitkomen.
19//
20// Run: npm test
21
22import { test } from 'node:test';
23import assert from 'node:assert/strict';
24
25process.env.DATABASE_PATH = ':memory:';
26process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
27
28const dbMod = await import('../src/config/database.js');
29const db = dbMod.default;
30dbMod.initializeDatabase();
31const AP = await import('../src/services/ActivityPubService.js');
32
33db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
34 .run('u1', 'u1', 'u1@test', 'x', 'god');
35db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
36
37const uri = (n) => `https://r.test/ap/notes/${n}`;
38
39/** Een post van iemand anders in JOUW tijdlijn-cache. */
40function seedTimeline(id, author = 'https://r.test/users/anna') {
41 db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, created_at)
42 VALUES (?,?,?,?,?,?)`).run(id, 'me', author, 'Anna', '<p>x</p>', '2026-08-06 09:00:00');
43}
44
45// ── Ingang 1: de tijdlijn-route (posts.js ~1279-1304) ────────────────────
46// Schrijft de KOLOM en leest de kolom. De tussentabel blijft ongemoeid.
47
48test('tijdlijn-route: markLiked zet de kolom en getTimelineReaction leest hem', () => {
49 const u = uri('t1'); seedTimeline(u);
50 assert.deepEqual(AP.getTimelineReaction('me', u), { liked: false, boosted: false });
51 AP.markLiked('me', u);
52 assert.deepEqual(AP.getTimelineReaction('me', u), { liked: true, boosted: false });
53 AP.unmarkLiked('me', u);
54 assert.equal(AP.getTimelineReaction('me', u).liked, false);
55});
56
57test('tijdlijn-route: markBoosted zet de kolom en de post verschijnt in de Cirkel', () => {
58 const u = uri('t2'); seedTimeline(u);
59 assert.equal(AP.boostedCount('me'), 0);
60 AP.markBoosted('me', u);
61 assert.equal(AP.getTimelineReaction('me', u).boosted, true);
62 assert.equal(AP.boostedCount('me'), 1);
63 // De Cirkel is wat een gebruiker ziet: geboost = zichtbaar daar.
64 assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'geboost hoort in de Cirkel te staan');
65 AP.unmarkBoosted('me', u);
66 assert.equal(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), false);
67});
68
69test('tijdlijn-route: een vlag op een post die NIET in je tijdlijn staat gaat stil verloren', () => {
70 // Dit is het stille falen waar de refactor voor moet oppassen: geen fout,
71 // geen rij, geen spoor. Een UPDATE die niets raakt is voor SQLite in orde.
72 const u = uri('bestaat-niet');
73 AP.markLiked('me', u);
74 assert.deepEqual(AP.getTimelineReaction('me', u), { liked: false, boosted: false });
75});
76
77// ── Ingang 2: de interact-route (posts.js ~841-869) ──────────────────────
78// Schrijft de TUSSENTABEL en leest de tussentabel. De kolom blijft ongemoeid.
79
80test('interact-route: setMyReaction schrijft de tussentabel, ook zonder tijdlijnrij', () => {
81 const u = uri('i1'); // bewust NIET in ap_timeline
82 assert.deepEqual(AP.getMyReactions('me', u), { liked: false, boosted: false });
83 AP.setMyReaction('me', u, 'like', true);
84 assert.deepEqual(AP.getMyReactions('me', u), { liked: true, boosted: false });
85 AP.setMyReaction('me', u, 'boost', true);
86 assert.deepEqual(AP.getMyReactions('me', u), { liked: true, boosted: true });
87 AP.setMyReaction('me', u, 'like', false);
88 assert.deepEqual(AP.getMyReactions('me', u), { liked: false, boosted: true });
89});
90
91test('interact-route: tweemaal dezelfde reactie levert geen dubbele rij op', () => {
92 const u = uri('i2');
93 AP.setMyReaction('me', u, 'like', true);
94 AP.setMyReaction('me', u, 'like', true);
95 const n = db.prepare('SELECT COUNT(*) AS n FROM ap_my_reactions WHERE site_slug=? AND target_uri=?').get('me', u).n;
96 assert.equal(n, 1, 'de UNIQUE hoort het dubbel opslaan te voorkomen');
97});
98
99// ── De valkuil met naam: upsertBoostedNote is een INSERT ─────────────────
100
101test('upsertBoostedNote trekt een post die je NIET volgt je tijdlijn in', () => {
102 // Dit is geen vlag zetten. Zonder deze insert bestaat de rij niet, kan de
103 // vlag nergens landen, en verschijnt de boost nergens. Wie dit in de refactor
104 // vervangt door "gewoon markBoosted" boost iets wat daarna onvindbaar is.
105 const u = uri('b1');
106 assert.equal(db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE id=?').get(u).n, 0);
107 AP.upsertBoostedNote('me', {
108 object_uri: u, actor_uri: 'https://r.test/users/bo', actor_name: 'Bo',
109 content: '<p>geboost</p>', media: '[]',
110 });
111 assert.equal(db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE id=?').get(u).n, 1, 'de rij hoort aangemaakt te worden');
112 assert.equal(AP.getTimelineReaction('me', u).boosted, true, 'en meteen als geboost gemarkeerd');
113 assert.ok(AP.getCirkelPosts('me', 60, 0).some((p) => p.id === u), 'en dus zichtbaar in de Cirkel');
114});
115
116// ── De twee bronnen lopen uiteen ─────────────────────────────────────────
117// DEZE TESTS HOREN TE FALEN NA FASE 1. Ze leggen de huidige splitsing vast,
118// zodat het samenvoegen een zichtbare gebeurtenis is en geen stille.
119
120test('HUIDIG GEDRAG: de tijdlijn-route vult de tussentabel niet', () => {
121 const u = uri('d1'); seedTimeline(u);
122 AP.markLiked('me', u);
123 assert.equal(AP.getTimelineReaction('me', u).liked, true, 'de kolom staat aan');
124 assert.equal(AP.getMyReactions('me', u).liked, false,
125 'NA FASE 1 hoort dit true te zijn: pas deze test dan bewust aan');
126});
127
128test('HUIDIG GEDRAG: de interact-route vult de kolom niet', () => {
129 const u = uri('d2'); seedTimeline(u);
130 AP.setMyReaction('me', u, 'like', true);
131 assert.equal(AP.getMyReactions('me', u).liked, true, 'de tussentabel staat aan');
132 assert.equal(AP.getTimelineReaction('me', u).liked, false,
133 'NA FASE 1 hoort dit true te zijn: pas deze test dan bewust aan');
134});
135
136test('HUIDIG GEDRAG: alleen het C2S-pad schrijft allebei', () => {
137 // Wat ingestOutboxActivity sinds Klonkt 04aca12 doet, hier nagebootst zonder
138 // het netwerk: setMyReaction + markLiked. Dit is het enige pad dat de twee
139 // bronnen gelijk houdt, en precies daarom werkt un-liken vanuit een app wel.
140 const u = uri('d3'); seedTimeline(u);
141 AP.setMyReaction('me', u, 'like', true);
142 AP.markLiked('me', u);
143 assert.equal(AP.getMyReactions('me', u).liked, true);
144 assert.equal(AP.getTimelineReaction('me', u).liked, true);
145});
146
147// ── Wat de invariant-test van fase 1 straks moet bewaken ─────────────────
148
149test('meetbaar: hoeveel rijen hebben een vlag zonder tegenhanger', () => {
150 // Dezelfde query als fase 0 in shaer-9e9, hier als test zodat de refactor
151 // hem kan gebruiken: na fase 1 hoort dit getal 0 te zijn en te blijven.
152 const scheef = db.prepare(`
153 SELECT COUNT(*) AS n FROM ap_timeline t
154 WHERE (t.liked = 1 OR t.boosted = 1)
155 AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r
156 WHERE r.site_slug = t.slug AND r.target_uri = t.id)`).get().n;
157 assert.ok(scheef > 0, 'vandaag lopen ze uiteen; na fase 1 hoort deze assert omgedraaid te worden naar === 0');
158});
Note: See TracBrowser for help on using the repository browser.