source: Klonkt/test/wake.test.js@ 7ccdced

main
Last change on this file since 7ccdced was 31288dc, checked in by Robin <roboburr@…>, 30 hours ago

Wakker worden: aankondigen en inhalen na echte afwezigheid

De wakker-helft van shaer-xzab (xzab.4 en .5), zoals Bart hem ontwierp: één
routine met twee helften, en wakker worden herkend aan een sprong in de
wandklok tussen twee ticks of aan het opstarten na lang uit -- niet aan elke
herstart, want npm herstart Klonkt bij elke opgeslagen wijziging.

services/wake.js. Het laatste teken van leven staat elke minuut in
app_settings (last_alive_at), zodat het een herstart overleeft. Meer dan een
half uur stilte is een terugkomst; dan:

.5 AANKONDIGEN: per site een actor-Update naar de gedeelde inboxes van de
volgers (deliverActorUpdate). Uit xzab.6: een binnenkomende POST is wat
Mastodon, Akkoma en Misskey weer naar ons laat bezorgen, en wat bij Pleroma
de controle van onze / start. Een half uur en niet langer, omdat Pleroma ons
al na één uitgeputte bezorging afschrijft.

.4 INHALEN: de outboxes van wie je volgt (alleen geaccepteerd), ondertekend,
vanaf het wegvallen. backfillFromOutbox kreeg daarvoor since; een post
zonder datum telt dan niet mee. Niet verder terug dan een week, hoogstens 40
posts per account en 200 accounts per site, na elkaar met een adempauze,
zodat een week weg geen storm wordt.

Eerst aankondigen, dan inhalen: dan bezorgen de anderen alweer terwijl wij
nog lezen. Een site die niet lukt, houdt de rest niet op.

test/wake.test.js: negen toetsen, het inhalen tegen een echte outbox op
127.0.0.1. Elf mutaties apart gedraaid, elk gevangen. Volledige suite 1371
groen.

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

  • Property mode set to 100644
File size: 7.3 KB
RevLine 
[31288dc]1// Wakker worden (shaer-xzab.4 en .5): een Klonkt die niet altijd aan staat,
2// kondigt zich bij terugkomst aan en haalt in wat hij miste.
3//
4// Wat hier bewaakt wordt:
5// - wakker worden herkennen: na AWAY_MS stilte, bij het opstarten of als
6// sprong tussen twee ticks -- en NIET bij een gewone herstart;
7// - eerst aankondigen (een actor-Update per site), dan inhalen;
8// - inhalen alleen bij geaccepteerde volgrelaties, vanaf het wegvallen, met
9// een venster van een week en een maximum per account;
10// - backfillFromOutbox met `since` neemt alleen wat daarna kwam, tegen een
11// echte outbox op 127.0.0.1.
12import { test } from 'node:test';
13import assert from 'node:assert/strict';
14import http from 'http';
15
16process.env.DATABASE_PATH = ':memory:';
17process.env.PUBLIC_BASE_URL = 'https://test.example';
18
19// Een andere server met een actor en een outbox, vóór het laden van de modules
20// (AP_ALLOW_HOSTS wordt bij het laden gelezen).
21const NU = Date.now();
22const uurGeleden = (u) => new Date(NU - u * 3600 * 1000).toISOString();
23let outboxItems = [];
24const ander = http.createServer((req, res) => {
25 const host = `http://${req.headers.host}`;
26 res.setHeader('Content-Type', 'application/activity+json');
27 if (req.url === '/users/anna') {
28 return res.end(JSON.stringify({ id: `${host}/users/anna`, type: 'Person', preferredUsername: 'anna', inbox: `${host}/users/anna/inbox`, outbox: `${host}/users/anna/outbox` }));
29 }
30 if (req.url === '/users/anna/outbox') {
31 return res.end(JSON.stringify({ type: 'OrderedCollection', orderedItems: outboxItems }));
32 }
33 res.statusCode = 404; res.end();
34});
35await new Promise((r) => ander.listen(0, '127.0.0.1', r));
36const anderHost = `127.0.0.1:${ander.address().port}`;
37process.env.AP_ALLOW_HOSTS = anderHost;
38test.after(() => ander.close());
39
40const dbMod = await import('../src/config/database.js');
41const db = dbMod.default;
42{ const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
43const W = await import('../src/services/wake.js');
44const AP = await import('../src/services/ActivityPubService.js');
45const { setSetting } = await import('../src/services/SettingsService.js');
46
47// ── Herkennen ────────────────────────────────────────────────────
48
49test('een herstart is geen terugkomst, een half uur stilte wel', () => {
50 assert.equal(W.absence(null, NU), null, 'een eerste start is geen terugkomst');
51 assert.equal(W.absence(NU - 10_000, NU), null, 'npm herstart na een opgeslagen wijziging');
52 assert.equal(W.absence(NU - W.AWAY_MS + 1000, NU), null);
53 assert.deepEqual(W.absence(NU - W.AWAY_MS, NU), { since: NU - W.AWAY_MS, awayMs: W.AWAY_MS });
54});
55
56test('een sprong tussen twee ticks is wakker worden, gewone ticks niet', () => {
57 W._resetForTests();
58 const gezien = [];
59 const wek = (since, now) => gezien.push({ since, now });
60 W.tick(NU, wek);
61 W.tick(NU + 60_000, wek);
62 assert.deepEqual(gezien, [], 'elke minuut een tik: niets aan de hand');
63 W.tick(NU + 60_000 + 45 * 60_000, wek); // deksel dicht, 45 minuten
64 assert.deepEqual(gezien, [{ since: NU + 60_000, now: NU + 60_000 + 45 * 60_000 }], 'weg sinds de vorige tik');
65 W._resetForTests();
66});
67
68test('bij het opstarten telt hoe lang we uit stonden, en dat overleeft de herstart', () => {
69 const gezien = [];
70 setSetting('last_alive_at', String(Date.now() - 2 * 3600 * 1000));
71 W._resetForTests();
72 W.startWakeWatch((since) => gezien.push(since));
73 W._resetForTests();
74 assert.equal(gezien.length, 1, 'twee uur uit: wakker worden');
75
76 gezien.length = 0;
77 W.startWakeWatch((since) => gezien.push(since)); // meteen weer: het teken is net gezet
78 W._resetForTests();
79 assert.equal(gezien.length, 0, 'en een herstart vlak daarna niet');
80});
81
82// ── De routine ───────────────────────────────────────────────────
83
84test('eerst aankondigen, dan inhalen, vanaf het wegvallen', async () => {
85 const log = [];
86 const sinds = NU - 3 * 3600 * 1000;
87 const r = await W.runWake({ since: sinds, now: NU }, {
88 sites: () => [{ slug: 'een' }, { slug: 'twee' }],
89 deliverActorUpdate: async (s) => { log.push(`update:${s.slug}`); },
90 following: (slug) => (slug === 'een' ? ['https://a/u/1', 'https://b/u/2'] : []),
91 backfill: async (slug, actor, limit, opts) => { log.push(`inhalen:${slug}:${actor}:${limit}:${opts.since}`); return 2; },
92 pause: async () => {},
93 });
94 assert.deepEqual(log, [
95 'update:een', 'update:twee',
96 `inhalen:een:https://a/u/1:${W.CATCHUP_PER_ACCOUNT}:${sinds}`,
97 `inhalen:een:https://b/u/2:${W.CATCHUP_PER_ACCOUNT}:${sinds}`,
98 ]);
99 assert.deepEqual(r, { announced: 2, fetched: 2, added: 4 });
100});
101
102test('een week weg wordt geen storm: venster en maximum', async () => {
103 assert.equal(W.catchupSince(NU - 30 * 24 * 3600 * 1000, NU), NU - W.CATCHUP_MAX_WINDOW_MS);
104 let gevraagd = 0;
105 await W.runWake({ since: NU - 1, now: NU }, {
106 sites: () => [{ slug: 'veel' }],
107 deliverActorUpdate: async () => {},
108 following: () => Array.from({ length: 500 }, (_, i) => `https://x/u/${i}`),
109 backfill: async () => { gevraagd++; return 0; },
110 pause: async () => {},
111 });
112 assert.equal(gevraagd, W.CATCHUP_MAX_ACCOUNTS);
113});
114
115test('een site die niet lukt, houdt de rest niet op', async () => {
116 const log = [];
117 await W.runWake({ since: NU - 3600_000, now: NU }, {
118 sites: () => [{ slug: 'stuk' }, { slug: 'goed' }],
119 deliverActorUpdate: async (s) => { if (s.slug === 'stuk') throw new Error('kapot'); log.push(s.slug); },
120 following: () => [],
121 backfill: async () => 0,
122 pause: async () => {},
123 });
124 assert.deepEqual(log, ['goed']);
125});
126
127test('alleen geaccepteerde volgrelaties worden ingehaald', () => {
128 db.prepare("INSERT INTO ap_following (slug, actor_uri, status) VALUES ('kind', 'https://a/u/ja', 'accepted'), ('kind', 'https://a/u/wacht', 'pending')").run();
129 assert.deepEqual(W.acceptedFollowing('kind'), ['https://a/u/ja']);
130});
131
132// ── backfillFromOutbox met since ─────────────────────────────────
133
134test('inhalen neemt alleen wat na het wegvallen kwam', async () => {
135 const anna = `http://${anderHost}/users/anna`;
136 const note = (id, published) => ({ type: 'Create', object: { id: `http://${anderHost}/notes/${id}`, type: 'Note', attributedTo: anna, content: `<p>${id}</p>`, ...(published ? { published } : {}) } });
137 outboxItems = [note('vers', uurGeleden(1)), note('oud', uurGeleden(72)), note('zonder-datum', null)];
138 const stil = console.log; console.log = () => {};
139 let added;
140 try { added = await AP.backfillFromOutbox('kind', anna, 40, { since: NU - 2 * 3600 * 1000 }); } finally { console.log = stil; }
141 assert.equal(added, 1);
142 const ids = db.prepare("SELECT id FROM ap_timeline WHERE slug = 'kind'").all().map((r) => r.id);
143 assert.deepEqual(ids, [`http://${anderHost}/notes/vers`]);
144});
145
146test('zonder since gaat inhalen als vroeger', async () => {
147 const anna = `http://${anderHost}/users/anna`;
148 outboxItems = [{ type: 'Note', id: `http://${anderHost}/notes/x1`, attributedTo: anna, content: 'x', published: uurGeleden(500) }];
149 const stil = console.log; console.log = () => {};
150 try { assert.equal(await AP.backfillFromOutbox('ander', anna), 1); } finally { console.log = stil; }
151});
Note: See TracBrowser for help on using the repository browser.