| [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.
|
|---|
| 12 | import { test } from 'node:test';
|
|---|
| 13 | import assert from 'node:assert/strict';
|
|---|
| 14 | import http from 'http';
|
|---|
| 15 |
|
|---|
| 16 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 17 | process.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).
|
|---|
| 21 | const NU = Date.now();
|
|---|
| 22 | const uurGeleden = (u) => new Date(NU - u * 3600 * 1000).toISOString();
|
|---|
| 23 | let outboxItems = [];
|
|---|
| 24 | const 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 | });
|
|---|
| 35 | await new Promise((r) => ander.listen(0, '127.0.0.1', r));
|
|---|
| 36 | const anderHost = `127.0.0.1:${ander.address().port}`;
|
|---|
| 37 | process.env.AP_ALLOW_HOSTS = anderHost;
|
|---|
| 38 | test.after(() => ander.close());
|
|---|
| 39 |
|
|---|
| 40 | const dbMod = await import('../src/config/database.js');
|
|---|
| 41 | const db = dbMod.default;
|
|---|
| 42 | { const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
|
|---|
| 43 | const W = await import('../src/services/wake.js');
|
|---|
| 44 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 45 | const { setSetting } = await import('../src/services/SettingsService.js');
|
|---|
| 46 |
|
|---|
| 47 | // ── Herkennen ────────────────────────────────────────────────────
|
|---|
| 48 |
|
|---|
| 49 | test('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 |
|
|---|
| 56 | test('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 |
|
|---|
| 68 | test('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 |
|
|---|
| 84 | test('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 |
|
|---|
| 102 | test('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 |
|
|---|
| 115 | test('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 |
|
|---|
| 127 | test('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 |
|
|---|
| 134 | test('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 |
|
|---|
| 146 | test('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 | });
|
|---|