source: Klonkt/test/sent-notes.test.js@ 6a99668

main
Last change on this file since 6a99668 was 6a99668, checked in by Robin <roboburr@…>, 6 weeks ago

Reply op een ander gefixt: gesigneerd resolven, eigen replies terug in de feed, duplicaat idempotent

Robins waarneming (30-7) legde de keten bloot: reply op jezelf lukt,
reply op een ander geeft 502. Drie oorzaken, drie fixes.

Een: resolveRemoteNote haalde de parent-note ANONIEM op. Een publieke
note (je eigen post) geeft dat, maar een friends-only note (de
Shaer-standaard!) weigert een anonieme GET terecht. Het reply-, like-
en boost-pad over C2S resolven nu gesigneerd als de eigen actor
(asSlug), zodat de andere server ziet wie er vraagt en serveert wat de
vriendschap verdient. Zelfde principe als friends-get-the-history, nu
ook aan de vraagkant.

Twee: je eigen verzonden replies (ap_outbox) werden nergens over C2S
geserveerd. Je reply bestond overal behalve in je eigen app: dus je
probeerde het opnieuw. De C2S inbox-read krijgt een derde leg:
getSentNotes bouwt ze via buildReplyNote (inReplyTo, Mention-tags,
attachments, friends-adressering), met de eigen shaer:author erop en de
leidende mention gestript zoals de DM-leg dat doet.

Drie: die herhaalpoging liep in de duplicate-guard, die zonder id
antwoordde; de ingest maakte daar 502 reply_failed van. Een duplicaat
is nu idempotent succes met het BESTAANDE id.

Changed files:
src/services/ActivityPubService.js

  • resolveRemoteNote(url, {asSlug}): gesigneerde fetches (note, actor en de ancestor-keten); reply/like/boost/undo geven site.slug mee
  • getSentNotes(base, site): eigen ap_outbox-rijen als AS2 Notes
  • duplicate-guard geeft het bestaande id terug

src/routes/activitypub.js

  • C2S inbox-read: sent-leg naast timeline en messages, zelfde vorm

New file:
test/sent-notes.test.js

  • sent reply komt terug als threadbare Note (parent in to, Mention, media absoluut, ISO-published); duplicaat = zelfde id

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

  • Property mode set to 100644
File size: 2.9 KB
Line 
1// Your own sent replies must come BACK over C2S (Robins melding, 30-7):
2// a reply that stores fine but never shows in the app reads as "replyen
3// werkt niet", gets retried, and the retry hit the duplicate guard which
4// answered without an id — which the ingest then called 502 reply_failed.
5// Two guarantees here: getSentNotes serves the reply as an AS2 Note the
6// app can thread, and a duplicate is idempotent success with the same id.
7import { test } from 'node:test';
8import assert from 'node:assert/strict';
9
10process.env.DATABASE_PATH = ':memory:';
11process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
12
13const dbMod = await import('../src/config/database.js');
14const db = dbMod.default;
15dbMod.initializeDatabase();
16const AP = await import('../src/services/ActivityPubService.js');
17
18db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
19 .run('u1', 'robin', 'r@test', 'x', 'god');
20db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
21const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get('me');
22const parent = {
23 id: 1, post_id: '', actor_uri: 'https://unresolvable.invalid/u/ness',
24 actor_url: 'https://unresolvable.invalid/@ness', actor_handle: '@ness@unresolvable.invalid',
25 object_uri: 'https://unresolvable.invalid/notes/9',
26};
27
28test('a sent reply comes back as a threadable AS2 Note', async () => {
29 const r = await AP.deliverReply(site, {
30 postId: '', postSlug: null, parent, text: 'test', visibility: 'friends',
31 attachments: [{ url: '/media/reply-media/foto.jpg', mediaType: 'image/jpeg', name: 'kiek' }],
32 });
33 assert.ok(r && r.id, 'the reply stored');
34
35 const notes = AP.getSentNotes('https://klonkt.test', site);
36 const note = notes.find((n) => n.id.endsWith(r.id));
37 assert.ok(note, 'getSentNotes serves it');
38 assert.equal(note.inReplyTo, parent.object_uri, 'threads under the parent');
39 assert.ok(note.to.includes(parent.actor_uri), 'addressed to the parent author: the app finds the counterpart');
40 assert.ok((note.tag || []).some((t) => t.type === 'Mention' && t.href === parent.actor_uri), 'and the Mention tag agrees');
41 const att = (note.attachment || []).find((a) => a.url.endsWith('foto.jpg'));
42 assert.ok(att && att.type === 'Image', 'the media rides along, absolute');
43 assert.match(String(note.published), /T.*Z$/, 'published is ISO, so the merged feed sorts');
44});
45
46test('a duplicate reply is idempotent success with the SAME id, not a 502', async () => {
47 const first = await AP.deliverReply(site, { postId: '', postSlug: null, parent, text: 'nogmaals', visibility: 'friends' });
48 assert.ok(first && first.id);
49 const again = await AP.deliverReply(site, { postId: '', postSlug: null, parent, text: 'nogmaals', visibility: 'friends' });
50 assert.ok(again && again.duplicate, 'recognised as a duplicate');
51 assert.equal(again.id, first.id, 'and it answers with the existing id, so the ingest 201s');
52});
Note: See TracBrowser for help on using the repository browser.