source: Klonkt/test/gated-settings.test.js@ 65abc85

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

Gated settings federeren: guardians beslissen samen, ook van een andere server

Ik had de knop alleen voor het co-located geval gebouwd, en dat is precies het
uitzonderingsgeval. In de echte opstelling staat de ward op de ene server en zijn
drie guardians op twee andere: er was dus nergens een knop. Dat botst met onze
eigen regel dat co-locatie een optimalisatie is en nooit de aanname.

Nu volgens FEP-633c 5.6 (deze week aan de spec toegevoegd): een guardian stelt
een wijziging voor met een Offer van een shaer:GatedSetting aan de server van de
WARD; de andere guardians antwoorden met Accept/Reject; de server van de ward
telt en handhaaft, want die serveert de feed. Co-locatie neemt dezelfde weg: ook
daar wordt voorgesteld en geteld, anders zou een guardian naast de deur meer te
zeggen hebben dan een op afstand.

De tally is een 3.5-beslissing en staat als pure functie apart: gesnapshotte set,
strikte meerderheid, venster van een dag. Omkeerbaar, dus race naar de drempel in
BEIDE richtingen (settelt ook zodra een meerderheid onhaalbaar is) en faalt dicht
op de deadline. Een Reject is een stem voor de andere waarde, geen schouderophalen.

New file:
src/services/guardianship/gated.js

  • tallyGatedSetting (puur), thresholdFor, featureColumn (onbekende features geweigerd i.p.v. geraden), recordGatedVote, en de Offer-vorm

test/gated-settings.test.js

  • 9 tests: drempel, vroeg settelen in beide richtingen, dicht op de deadline, vreemden tellen niet mee, geen guardians = niets toegekend, van gedachten veranderen vervangt je stem, en een onbekende feature raakt geen kolom

Changed files:
src/config/database.js

  • ap_gated_offers + ap_gated_votes

src/services/guardianship/handshake.js

  • inbox: Offer(shaer:GatedSetting) en Accept/Reject erop, met de stem van de voorsteller meegeteld (one-step-clausule)

src/services/guardianship/index.js

  • gated geexporteerd

src/routes/guardian.js

  • de knop stuurt een voorstel, lokaal en remote langs dezelfde weg

src/assets/js/guardian.js

  • knop bij ELKE ward, ook remote; toont 'wacht op de andere guardians'

src/services/i18n.js

  • embeds_propose / embeds_waiting in nl, en, de

remarks: 228 tests groen (was 219).

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

  • Property mode set to 100644
File size: 2.9 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});
Note: See TracBrowser for help on using the repository browser.