source: Klonkt/test/gated-settings.test.js@ 70677e96

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

De knop deed niets: AP.deliverTo bestond niet

Op Propose link previews drukken gaf geen zichtbaar resultaat. Oorzaak: de route
riep AP.deliverTo aan, maar die naam bestond alleen als sleutel in het
deps-object dat we aan de guardianship-module doorgeven, niet als export. Dus een
TypeError, een 500 uit Express, en de knop die zichzelf netjes weer inschakelde.
Van buiten af: er gebeurt niks.

Mijn fout zat in de aanname: ik zag deliverTo in een grep-treffer staan en nam
aan dat het een export was. Een grep-treffer is geen export. Er staat nu een test
die alle AP-helpers controleert die de guardian-route aanroept, zodat deze klasse
fout niet nog eens stil kan blijven.

Changed files:
src/services/ActivityPubService.js

  • deliverToActor geexporteerd (stond alleen in de deps)

src/routes/guardian.js

  • gebruikt AP.deliverToActor

test/gated-settings.test.js

  • test dat elke AP-helper die de route aanroept ook echt bestaat
  • test op de Offer-vorm (ward, feature, value, geadresseerd aan de ward)

remarks: 230 tests groen.

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

  • Property mode set to 100644
File size: 4.2 KB
Line 
1// FEP-633c §5.6 + §3.5: a gated setting is decided by the guardians together,
2// by threshold within a window, and it has to work across servers.
3import { test } from 'node:test';
4import assert from 'node:assert/strict';
5process.env.DATABASE_PATH = ':memory:';
6process.env.PUBLIC_BASE_URL = 'https://test.example';
7const dbMod = await import('../src/config/database.js');
8dbMod.initializeDatabase();
9const { tallyGatedSetting, thresholdFor, featureColumn } = await import('../src/services/guardianship/gated.js');
10
11const G3 = ['https://a/g1', 'https://b/g2', 'https://c/g3']; // three, on three servers
12const vote = (uri, value) => ({ guardian_uri: uri, value });
13
14test('the threshold is a strict majority', () => {
15 assert.equal(thresholdFor(1), 1);
16 assert.equal(thresholdFor(2), 2);
17 assert.equal(thresholdFor(3), 2);
18 assert.equal(thresholdFor(4), 3);
19});
20
21test('it settles the moment the majority is there, without waiting for the rest', () => {
22 const r = tallyGatedSetting([vote(G3[0], true), vote(G3[1], true)], G3, 1000);
23 assert.deepEqual(r, { state: 'settled', value: true });
24});
25
26test('one guardian alone does not decide for the others', () => {
27 const r = tallyGatedSetting([vote(G3[0], true)], G3, 1000);
28 assert.equal(r.state, 'open', 'a single voice is not the guardians as a group');
29});
30
31test('it also settles early when the majority has become unreachable', () => {
32 const r = tallyGatedSetting([vote(G3[0], false), vote(G3[1], false)], G3, 1000);
33 assert.deepEqual(r, { state: 'settled', value: false }, 'two against is itself a majority');
34});
35
36test('an undecided decision fails closed at the deadline', () => {
37 const open = tallyGatedSetting([vote(G3[0], true)], G3, 1000);
38 assert.equal(open.state, 'open');
39 const late = tallyGatedSetting([vote(G3[0], true)], G3, 25 * 60 * 60 * 1000);
40 assert.equal(late.state, 'expired', 'silence is an answer once the window closes');
41});
42
43test('answers from outside the snapshotted set are ignored', () => {
44 const r = tallyGatedSetting([vote(G3[0], true), vote('https://x/stranger', true)], G3, 1000);
45 assert.equal(r.state, 'open', 'a stranger cannot make up the majority');
46});
47
48test('a ward with no guardians has nobody to decide, so nothing is granted', () => {
49 assert.equal(tallyGatedSetting([vote('https://a/g1', true)], [], 1000).state, 'expired');
50});
51
52test('a guardian changing its mind replaces its own answer, it does not add one', () => {
53 // The store keys on (slug, feature, guardian), so the tally sees one per guardian.
54 const r = tallyGatedSetting([vote(G3[0], true), vote(G3[0], false), vote(G3[1], false)], G3, 1000);
55 assert.deepEqual(r, { state: 'settled', value: false });
56});
57
58test('unknown features are refused, never guessed onto a column', () => {
59 assert.equal(featureColumn('shaer:externalEmbeds'), 'external_embeds');
60 assert.equal(featureColumn('shaer:somethingElse'), null);
61 assert.equal(featureColumn('external_embeds = 1; DROP TABLE sites'), null);
62});
63
64// Guard against the mistake that made the button do nothing: the route called
65// AP.deliverTo, which existed only as a key in the guardianship deps object and
66// not as an export. It threw a TypeError, Express answered 500, and the button
67// silently reset. A grep hit is not an export.
68test('the guardian route can reach every ActivityPub helper it calls', async () => {
69 const AP = (await import('../src/services/ActivityPubService.js')).default;
70 for (const fn of ['actorId', 'deliverToActor', 'followActor', 'backfillFromOutbox', 'getTimeline']) {
71 assert.equal(typeof AP[fn], 'function', `AP.${fn} must be exported, the guardian route calls it`);
72 }
73});
74
75test('a gated-setting Offer carries ward, feature and value', async () => {
76 const { buildGatedOffer, parseGatedSetting } = await import('../src/services/guardianship/gated.js');
77 const o = buildGatedOffer('https://a/gated/1', 'https://a/g1', 'https://b/ward', 'shaer:externalEmbeds', true);
78 assert.equal(o.type, 'Offer');
79 assert.deepEqual(o.to, ['https://b/ward'], 'addressed to the ward server, which tallies');
80 const parsed = parseGatedSetting(o.object);
81 assert.deepEqual(parsed, { ward: 'https://b/ward', feature: 'shaer:externalEmbeds', value: true });
82 assert.equal(parseGatedSetting({ type: 'Relationship' }), null, 'a different Offer is not ours');
83});
Note: See TracBrowser for help on using the repository browser.