source: Klonkt/test/guardian-event-log.test.js@ 7e53594

main
Last change on this file since 7e53594 was 7e53594, checked in by Bart <bart@…>, 4 weeks ago

shaer:log: het logboek staat naast de wachtrijen, niet erin

De apps konden er niet bij. Het logboek zat achter /guardian/api/events, een
PWA-route, dus een ward kon niet lezen waarom haar eigen aanbod geweigerd was --
en dat is precies de helft die §4.2 eist.

Eigen sleutel op de actor, GET /ap/users/:slug/log, dezelfde eigenaar-only
bearer als de wachtrijen. Bewust niet onder shaer:queues: alles daar wacht op
een antwoord en dit is wat er al besloten is. Geschiedenis onderbrengen bij een
woord dat "wachtend" betekent maakt van twee dingen één, en dat is de fout die
deze module vandaag drie keer heeft opgeruimd.

type is 'shaer:Event' en geen AS2-werkwoord. De meeste soorten zijn er geen: een
lapse-stem of een opgepakte hulpvraag is geen Accept, en het zo noemen zou
netter lezen dan het is.

Clients vinden hem via het actor-document, niet via een pad dat ze zelf
verzinnen (shaer-qa0). De AS2-test viel netjes om tot shaer:log was aangemeld --
tweede keer vandaag dat die het vangt.

De daemon volgt (shaer-6d9): zolang die geen shaer:log serveert, liegt een
client die tegen beide praat over een van de twee.

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

  • Property mode set to 100644
File size: 5.7 KB
Line 
1// FEP-633c §4.2 — de reden mag niet verdampen.
2//
3// Guardianship-events waren vluchtig: onGuardianshipEvent wekte de long-poll,
4// keek in een tabel van zeven soorten of er een push bij hoorde, en liet de
5// rest vallen. Elf van de achttien soorten verdwenen daarmee spoorloos, met hun
6// inhoud erbij. Een weigering droeg `reason: 'not_a_teapot'` tot precies daar en
7// niet verder, terwijl §4.2 eist dat de ward en zijn guardians die reden TE
8// HOREN krijgen -- en niet dat ze het afleiden uit een aanbod dat opeens weg is.
9//
10// Vastleggen en melden zijn nu twee dingen. Deze test bewaakt het eerste; welke
11// gebeurtenis een mens wakker maakt blijft een aparte beslissing.
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
23db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
24 .run('u1', 'u1', 'u1@test', 'x', 'god');
25db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)')
26 .run('s1', 'kid', 'kid', 'u1');
27
28test('een gebeurtenis die geen push oplevert wordt tóch onthouden', () => {
29 // offer_rejected staat niet in de meldingstabel: niemand wordt er wakker van,
30 // en dat was precies de reden dat hij ook nergens terechtkwam.
31 const push = AP.onGuardianshipEvent('kid', {
32 kind: 'offer_rejected', offer: 'o1',
33 reason: 'not_a_teapot', candidate: 'https://elders.example/users/tess',
34 });
35 assert.equal(push, null, 'geen melding, zoals bedoeld');
36
37 const log = AP.listGuardianEvents('kid');
38 assert.equal(log.length, 1, 'maar wel vastgelegd');
39 assert.equal(log[0].kind, 'offer_rejected');
40 assert.equal(log[0].reason, 'not_a_teapot', 'MET de reden: daar staat dit logboek voor');
41 assert.equal(log[0].candidate, 'https://elders.example/users/tess', 'en om wie het ging');
42});
43
44test('nieuwste eerst, want geschiedenis leest van achteren', () => {
45 AP.onGuardianshipEvent('kid', { kind: 'committed', ward: 'https://test.example/ap/users/kid' });
46 const log = AP.listGuardianEvents('kid');
47 assert.equal(log[0].kind, 'committed');
48 assert.equal(log[1].kind, 'offer_rejected');
49});
50
51test('het logboek van een ander account blijft van hem', () => {
52 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,0)')
53 .run('s2', 'mum', 'mum', 'u1');
54 assert.deepEqual(AP.listGuardianEvents('mum'), [],
55 'wat er over een kind is besloten is niet van iedereen');
56});
57
58test('het loopt niet vol', () => {
59 // Een logboek dat oneindig groeit wordt een logboek dat niemand opent, en
60 // hier ligt het in dezelfde database als alles wat wél om aandacht vraagt.
61 for (let i = 0; i < AP.GUARDIAN_EVENT_KEEP + 25; i++) {
62 AP.onGuardianshipEvent('mum', { kind: 'setting', n: i });
63 }
64 const n = db.prepare('SELECT COUNT(*) AS n FROM ap_guardian_events WHERE slug = ?').get('mum').n;
65 assert.equal(n, AP.GUARDIAN_EVENT_KEEP, 'afgekapt op de bewaargrens');
66 // En het zijn de JONGSTE die blijven: afkappen aan de verkeerde kant zou het
67 // logboek precies onbruikbaar maken op het moment dat er iets gebeurt.
68 assert.equal(AP.listGuardianEvents('mum', 1)[0].n, AP.GUARDIAN_EVENT_KEEP + 24);
69});
70
71test('een kapotte gebeurtenis breekt de gebeurtenis zelf niet', () => {
72 // Het logboek is bijzaak. Zou een schrijffout hier de commit of de weigering
73 // meesleuren, dan is de cure erger dan de kwaal.
74 assert.doesNotThrow(() => AP.onGuardianshipEvent('kid', { kind: 'setting', zelf: { a: null } }));
75 assert.doesNotThrow(() => AP.onGuardianshipEvent('kid', {}));
76 assert.doesNotThrow(() => AP.onGuardianshipEvent(null, { kind: 'setting' }));
77});
78
79// ── De collectie die de apps lezen (shaer:log) ────────────────────────────
80// Het logboek staat NAAST shaer:queues. Alles daarin wacht op een antwoord; dit
81// is wat er al besloten is. Een aparte sleutel dus, en een aparte vorm.
82
83test('de actor wijst het logboek aan, los van de wachtrijen', async () => {
84 const G = await import('../src/services/guardianship/index.js');
85 const props = G.guardianshipActorProps('https://test.example/ap/users/kid', 'kid');
86 assert.equal(props['shaer:log'], 'https://test.example/ap/users/kid/log');
87 assert.equal(props['shaer:queues'].log, undefined,
88 'geen geschiedenis onder een woord dat "wachtend" betekent');
89});
90
91test('elk item draagt zijn soort en zijn reden mee', async () => {
92 const G = await import('../src/services/guardianship/index.js');
93 const c = G.logCollection('https://test.example/ap/users/kid/log', 'kid',
94 (s) => AP.listGuardianEvents(s, 50));
95 assert.equal(c.type, 'OrderedCollection');
96 const weigering = c.orderedItems.find((i) => i['shaer:kind'] === 'offer_rejected');
97 assert.ok(weigering, 'de weigering staat erin');
98 assert.equal(weigering['shaer:reason'], 'not_a_teapot',
99 'MET de reden: daarvoor bestaat dit ding');
100 assert.equal(weigering['shaer:candidate'], 'https://elders.example/users/tess');
101 assert.ok(weigering.published, 'en wanneer');
102 // Geen AS2-werkwoord: een lapse-stem of een opgepakte hulpvraag is geen
103 // Accept, en het zo noemen zou netter lezen dan het is.
104 assert.equal(weigering.type, 'shaer:Event');
105});
106
107test('een leeg logboek is een lege collectie, geen fout', async () => {
108 const G = await import('../src/services/guardianship/index.js');
109 const c = G.logCollection('https://test.example/ap/users/niemand/log', 'niemand',
110 (s) => AP.listGuardianEvents(s, 50));
111 assert.equal(c.totalItems, 0);
112 assert.deepEqual(c.orderedItems, []);
113});
Note: See TracBrowser for help on using the repository browser.