source: Klonkt/test/ap-track-context.test.js

main
Last change on this file was abcf51a, checked in by Robin <roboburr@…>, 4 weeks ago

Een track wijst naar de post die hem uitbrengt

Robins waarneming: Shaer toont sinds fb22f78 losse, niet-werkende
track-kaarten. Dat is mijn regressie -- ik zette Create(Audio) in de outbox
voor Funkwhale, en Shaer bouwt zijn HomeBase-feed uit precies die outbox.
noteFrom() daar kijkt niet naar type, dus een Audio wordt een C2SNote met lege
content, en onze url is een Link-array die as? String niet vangt.

Robins voorstel was beter dan wegfilteren: koppel ze aan de hostende post en
filter DAAROP. Die relatie bestond al, maar alleen in posts.content --
[[track:id]], [[playlist:id]], [[album:naam]] -- en nergens op de draad.

Nu wel, met twee velden die allebei al bestonden:

context AS2-KERN, "de context waarbinnen dit object bestaat". Wijst naar

de Note van de post. Funkwhale kijkt er niet naar, dus puur
additief.

url[0] de text/html-link naar de postpagina, vooraan zoals Funkwhale zijn

trackpagina zet. Dit veld lieten we bewust leeg omdat Klonkt geen
trackpagina heeft -- maar de post IS waar je het kunt horen.

Daarmee wordt de filterregel in Shaer principieel in plaats van bot: verberg
een Audio waarvan de context al in je feed staat. Een track die in GEEN post
zit houdt geen context en blijft zichtbaar -- dat is de enige plek waar je hem
zou zien.

Rangorde als een track op meerdere manieren is ingesloten: rechtstreeks boven
playlist boven albumnaam, en bij gelijke rang de nieuwste post. Specifieker
gaat dus voor recenter; daar staat een test op. Een concept telt niet als host.

EEN ZOEKOPDRACHT VOOR ALLES, niet per track: drie LIKE-scans maal
tweehonderd nummers is zeshonderd scans, en de collectie loopt over elke open
track. trackHostPosts levert een map, en die gaat als optie mee door de
collectie, de playlist en de outbox.

8 tests. context erbij in de AS2-allowlist -- het is een kernterm.

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

  • Property mode set to 100644
File size: 5.6 KB
Line 
1// Een track wijst naar de post die hem uitbrengt (shaer-0nh).
2//
3// Aanleiding: sinds Create(Audio) in de outbox staat (fb22f78) bouwt Shaer zijn
4// HomeBase-feed uit diezelfde outbox, en maakte het van elke Audio een lege
5// kaart -- een Audio heeft geen `content` en onze `url` is een Link-array.
6//
7// De relatie stond alleen in posts.content ([[track:]], [[playlist:]],
8// [[album:]]) en nergens op de draad. Nu wel: AS2-kern `context` wijst naar de
9// Note van de post, en de text/html-link vooraan wijst naar de pagina. Een
10// lezer die de post al heeft kan het nummer daarmee overslaan in plaats van er
11// een dode kaart van te maken.
12import { test } from 'node:test';
13import assert from 'node:assert/strict';
14
15process.env.DATABASE_PATH = ':memory:';
16process.env.PUBLIC_BASE_URL = 'https://test.example';
17
18const dbMod = await import('../src/config/database.js');
19const db = dbMod.default;
20dbMod.initializeDatabase();
21const AP = (await import('../src/services/ActivityPubService.js')).default;
22
23const BASE = 'https://test.example';
24db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
25 .run('u1', 'u1', 'u1@t', 'x', 'god');
26db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public, is_primary) VALUES (?,?,?,?,1,1)')
27 .run('s1', 'band', 'De Band', 'u1');
28
29const insM = db.prepare('INSERT INTO media (id, site_id, filename, storage_path, mime_type, size) VALUES (?,?,?,?,?,1000)');
30const insT = db.prepare('INSERT INTO audio_tracks (id, site_id, title, duration, media_id, album, fedi_open) VALUES (?,?,?,?,?,?,1)');
31for (const [id, titel, album] of [['t-los', 'Losse track', null], ['t-pl', 'Via playlist', null],
32 ['t-alb', 'Via album', 'De Plaat'], ['t-wees', 'Nergens ingesloten', null]]) {
33 insM.run('m-' + id, 's1', id + '.mp3', 'audio/' + id + '.mp3', 'audio/mpeg');
34 insT.run(id, 's1', titel, 60, 'm-' + id, album);
35}
36db.prepare("INSERT INTO playlists (id, site_id, title, kind) VALUES ('plaat','s1','De Plaat','album')").run();
37db.prepare("INSERT INTO playlist_tracks (playlist_id, track_id, position) VALUES ('plaat','t-pl',1)").run();
38
39const insP = db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at)
40 VALUES (?,?,?,?,?,?,'published',?)`);
41insP.run('p-los', 's1', 'u1', 'losse-post', 'Losse post', 'Hier: [[track:t-los]]', '2026-01-01T00:00:00Z');
42insP.run('p-pl', 's1', 'u1', 'playlist-post', 'Playlist-post', 'Nieuw album [[playlist:plaat]]', '2026-02-01T00:00:00Z');
43insP.run('p-alb', 's1', 'u1', 'album-post', 'Album-post', 'Luister [[album:De Plaat]]', '2026-03-01T00:00:00Z');
44
45const site = () => db.prepare("SELECT * FROM sites WHERE id = 's1'").get();
46const audioVoor = (id) => {
47 const r = AP.siteOpenTracks('s1').find((x) => x.id === id);
48 return AP.buildTrackAudio(BASE, site(), r);
49};
50
51test('rechtstreeks ingesloten: context wijst naar de Note van die post', () => {
52 const a = audioVoor('t-los');
53 assert.equal(a.context, 'https://test.example/ap/notes/p-los');
54 assert.deepEqual(a.url[0], { type: 'Link', href: 'https://test.example/losse-post', mediaType: 'text/html' },
55 'de post staat VOORAAN, zoals Funkwhale zijn trackpagina zet');
56 assert.equal(a.url[1].mediaType, 'audio/mpeg', 'het bestand komt daarna');
57});
58
59test('via een playlist gevonden', () => {
60 assert.equal(audioVoor('t-pl').context, 'https://test.example/ap/notes/p-pl');
61});
62
63test('via een albumnaam gevonden', () => {
64 assert.equal(audioVoor('t-alb').context, 'https://test.example/ap/notes/p-alb');
65});
66
67test('een track die nergens is ingesloten krijgt GEEN context', () => {
68 const a = audioVoor('t-wees');
69 assert.equal(a.context, undefined, 'niets verzinnen als er geen post is');
70 assert.equal(a.url.length, 1, 'dan ook geen text/html-link');
71 assert.equal(a.url[0].mediaType, 'audio/mpeg');
72});
73
74test('rechtstreeks wint van playlist wint van album', () => {
75 // t-pl zit in playlist "plaat"; die playlist is ook een album met dezelfde
76 // naam. Sluit een NIEUWERE post hem rechtstreeks in, dan wint die -- de
77 // specifiekste insluiting, niet de laatste.
78 db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status, published_at)
79 VALUES ('p-direct','s1','u1','direct-post','Direct','Deze: [[track:t-pl]]','published','2025-01-01T00:00:00Z')`).run();
80 assert.equal(audioVoor('t-pl').context, 'https://test.example/ap/notes/p-direct',
81 'ouder maar specifieker gaat voor');
82 db.prepare("DELETE FROM posts WHERE id = 'p-direct'").run();
83});
84
85test('een concept telt niet als host', () => {
86 db.prepare(`INSERT INTO posts (id, site_id, author_id, slug, title, content, status)
87 VALUES ('p-concept','s1','u1','concept','Concept','[[track:t-wees]]','draft')`).run();
88 assert.equal(audioVoor('t-wees').context, undefined, 'alleen gepubliceerde posts hosten iets');
89 db.prepare("DELETE FROM posts WHERE id = 'p-concept'").run();
90});
91
92test('de collectie doet EEN zoekopdracht en levert dezelfde koppelingen', () => {
93 const col = AP.buildTrackCollection(BASE, site(), AP.siteOpenTracks('s1'));
94 const perNaam = Object.fromEntries(col.orderedItems.map((a) => [a.name, a.context]));
95 assert.equal(perNaam['Losse track'], 'https://test.example/ap/notes/p-los');
96 assert.equal(perNaam['Via playlist'], 'https://test.example/ap/notes/p-pl');
97 assert.equal(perNaam['Nergens ingesloten'], undefined);
98});
99
100test('en de outbox draagt de context ook', () => {
101 const ob = AP.buildOutbox(BASE, site(), [], AP.siteOpenTracks('s1'));
102 const c = ob.orderedItems.find((x) => (x.object || {}).name === 'Losse track');
103 assert.equal(c.object.context, 'https://test.example/ap/notes/p-los');
104});
Note: See TracBrowser for help on using the repository browser.