source: Klonkt/test/thread-crawl.test.js@ 1d5ffc0

main
Last change on this file since 1d5ffc0 was dc41bef, checked in by roboburr <roboburr@…>, 2 months ago

feat(fediverse): fetch remote reply threads (cached, stale-while-revalidate)

A local post's "From the fediverse" section only had replies that were delivered
to us; replies-to-replies living on other servers were missed. A background crawl
now pulls the AS2 replies collections of the replies we have and caches any new
ones in ap_interactions. It never runs in a page request — the view renders from
cache and a stale post triggers a background refresh for the next view. Bounded
(depth 3 / 30 fetches), polite (serial), PULL only, respects blocks + SSRF guard.

  • src/services/ActivityPubService.js — crawlThread() (bounded BFS over reply collections), collectReplyItems() (paged AS2 collection reader), and the maybeCrawlThread() stale-while-revalidate entry (per-post TTL + in-flight lock, timestamp in app_settings).
  • src/routes/posts.js — the post view fires maybeCrawlThread(post.id) after rendering interactions (non-blocking, gated on apEnabled).
  • test/thread-crawl.test.js — entry exists, no-seed no-op, TTL gate.

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

  • Property mode set to 100644
File size: 1.5 KB
Line 
1// Remote thread crawl — the stale-while-revalidate guard. The BFS itself is network-bound
2// (verified live), so here we only cover that the entry point exists, is safe with no cached
3// replies (no seeds → no crawl), and is TTL-gated. In-memory SQLite. Run: npm test
4import { test } from 'node:test';
5import assert from 'node:assert/strict';
6
7process.env.DATABASE_PATH = ':memory:';
8process.env.PUBLIC_BASE_URL = 'https://test.example';
9
10const dbMod = await import('../src/config/database.js');
11const db = dbMod.default;
12dbMod.initializeDatabase();
13const AP = (await import('../src/services/ActivityPubService.js')).default;
14
15test('maybeCrawlThread is exported and safe with no cached replies', () => {
16 assert.equal(typeof AP.maybeCrawlThread, 'function');
17 // No ap_interactions rows for this post → the crawl finds no seeds and does nothing.
18 AP.maybeCrawlThread('no-replies-post');
19 assert.ok(true, 'did not throw');
20});
21
22test('a second call within the TTL is gated (timestamp recorded)', () => {
23 AP.maybeCrawlThread('ttl-post');
24 const row = db.prepare('SELECT value FROM app_settings WHERE key = ?').get('thread_crawl:ttl-post');
25 assert.ok(row && Number(row.value) > 0, 'crawl timestamp recorded');
26 // Immediately calling again must not reset/refire (still gated by the fresh timestamp).
27 const before = row.value;
28 AP.maybeCrawlThread('ttl-post');
29 const after = db.prepare('SELECT value FROM app_settings WHERE key = ?').get('thread_crawl:ttl-post').value;
30 assert.equal(after, before, 'timestamp unchanged within TTL');
31});
Note: See TracBrowser for help on using the repository browser.