Changeset 780a7c6 in Klonkt for test/guardianship.test.js
- Timestamp:
- 07/24/2026 07:44:15 PM (7 weeks ago)
- Branches:
- main
- Children:
- b5924eb
- Parents:
- c26cc18
- File:
-
- 1 edited
-
test/guardianship.test.js (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
test/guardianship.test.js
rc26cc18 r780a7c6 1 // The guardianship module (FEP-633c) : relations, actor props, the adoption2 // handshake and the dashboard queues. Pins the module's public surface so the3 // Shaer clients' contract stays stable.1 // The guardianship module (FEP-633c) — the multi-party handshake (§3). 2 // Everyone lives on one in-memory instance here, so the handshake copies all 3 // converge locally; that also exercises the "multiple local parties" routing. 4 4 import { test } from 'node:test'; 5 5 import assert from 'node:assert/strict'; … … 14 14 const G = await import('../src/services/guardianship/index.js'); 15 15 16 function site(id, slug) { 17 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run(id, slug, slug, 'u1', id === 's1' ? 1 : 0); 18 return db.prepare('SELECT * FROM sites WHERE id = ?').get(id); 19 } 16 20 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god'); 17 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s1', 'parent', 'Parent', 'u1', 1); 18 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s2', 'kid', 'Kid', 'u1', 0); 19 const parent = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1'); 20 const kid = db.prepare('SELECT * FROM sites WHERE id = ?').get('s2'); 21 const ME = 'https://test.example/ap/users/parent'; 22 const KID = 'https://test.example/ap/users/kid'; 21 const parent = site('s1', 'parent'); // first guardian-candidate 22 const kid = site('s2', 'kid'); // ward 23 const gran = site('s3', 'gran'); // second guardian-candidate (co-approver later) 24 const A = (slug) => `https://test.example/ap/users/${slug}`; 25 const [ME, KID, GRAN] = [A('parent'), A('kid'), A('gran')]; 23 26 24 // No network in tests: the handshake delivers via this stub.25 const sent = []; 27 // No network: the handshake delivers by feeding each activity straight into the 28 // inbound handler of every addressed local party (what real S2S would do). 26 29 G.wireHandshake({ 27 selfId: (slug) => `https://test.example/ap/users/${slug}`, 28 deliverTo: async (site, uri, activity) => { sent.push({ from: site.slug, to: uri, activity }); return true; }, 29 deriveHandle: (uri) => '@' + String(uri).split('/').pop() + '@test.example', 30 selfId: A, 31 localSlug: (uri) => (uri.startsWith('https://test.example/ap/users/') ? uri.split('/').pop() : null), 32 deriveHandle: (uri) => '@' + uri.split('/').pop() + '@test.example', 33 fetchActor: async () => null, 34 deliverTo: async (fromSite, toUri, activity) => { 35 const slug = toUri.split('/').pop(); 36 const s = db.prepare('SELECT * FROM sites WHERE slug = ?').get(slug); 37 if (s) await G.handleGuardianshipInbox(s, activity); 38 return { delivered: true }; 39 }, 30 40 onEvent: null, 31 41 }); 32 42 33 test('actor doc advertises shaer:queues (and blocked stays)', () => { 34 const actor = AP.buildActor('https://test.example', parent); 35 assert.equal(actor.blocked, `${ME}/blocked`); 36 assert.deepEqual(actor['shaer:queues'], { 37 offers: `${ME}/queues/offers`, 38 follows: `${ME}/queues/follows`, 39 wards: `${ME}/queues/wards`, 43 const offerIdFrom = (r) => r.id; 44 45 test('first guardian: candidate offers, ward accepts, candidate completes', async () => { 46 const off = await G.handleGuardianshipOutbox(parent, { 47 type: 'Offer', object: { type: 'Relationship', subject: KID, relationship: 'shaer:Guardian', object: ME }, 40 48 }); 41 assert.equal(actor['shaer:isGuardian'], undefined); // no wards yet 49 assert.equal(off.status, 202); 50 const id = offerIdFrom(off); 51 52 // The kid sees the offer and it needs its accept. 53 const kidQ = G.offersCollection(`${KID}/queues/offers`, 'kid', KID).orderedItems; 54 assert.equal(kidQ.length, 1); 55 assert.equal(kidQ[0]['shaer:needsMyAccept'], true); 56 assert.equal(kidQ[0]['shaer:iAmCandidate'], false); 57 58 // Not committed on a lone candidate — the ward has not accepted. 59 assert.deepEqual(G.listGuardians('kid'), []); 60 61 // The kid accepts (C2S from the kid's own Klonkt). Not committed yet: the 62 // candidate must still agree to serve (§3.1.2). 63 await G.handleGuardianshipOutbox(kid, { type: 'Accept', object: id }); 64 assert.deepEqual(G.listGuardians('kid'), []); 65 const parentQ = G.offersCollection(`${ME}/queues/offers`, 'parent', ME).orderedItems; 66 assert.equal(parentQ[0]['shaer:iAmCandidate'], true); 67 assert.equal(parentQ[0]['shaer:needsMyAccept'], true); // candidate has not accepted 68 69 // The candidate accepts → tally complete → commit everywhere. 70 const done = await G.handleGuardianshipOutbox(parent, { type: 'Accept', object: id }); 71 assert.equal(done.committed, true); 72 assert.deepEqual(G.listGuardians('kid').map((g) => g.other_uri), [ME]); 73 assert.deepEqual(G.listWards('parent').map((w) => w.other_uri), [KID]); 74 75 // The ward actor now names its guardian; parent reads as guardian (§2). 76 assert.deepEqual(AP.buildActor('https://test.example', kid)['shaer:guardians'], [ME]); 77 assert.equal(AP.buildActor('https://test.example', parent)['shaer:isGuardian'], true); 78 // §1 mutual exclusion: the ward is not also a guardian. 79 assert.equal(AP.buildActor('https://test.example', kid)['shaer:isGuardian'], undefined); 42 80 }); 43 81 44 test(' C2S Offer from the candidate records + delivers (FEP-633c 3)', async () => {45 const r = await G.handleGuardianshipOutbox(parent, {46 type: 'Offer',47 object: { type: 'Relationship', subject: KID, relationship: 'shaer:Guardian', object: ME},82 test('second guardian needs the EXISTING guardian to co-accept (§3.1.2)', async () => { 83 // Gran offers to also guard the kid (who already has parent). 84 const off = await G.handleGuardianshipOutbox(gran, { 85 type: 'Offer', object: { type: 'Relationship', subject: KID, relationship: 'shaer:Guardian', object: GRAN }, 48 86 }); 49 assert.equal(r.status, 202); 50 assert.equal(sent.length, 1); 51 assert.equal(sent[0].to, KID); 52 assert.equal(sent[0].activity.type, 'Offer'); 53 const wards = G.listWards('parent'); 54 assert.equal(wards.length, 1); 55 assert.equal(wards[0].status, 'offered'); 56 // The guardian-to-be now reads as guardian; the actor doc follows. 57 const actor = AP.buildActor('https://test.example', parent); 58 assert.equal(actor['shaer:isGuardian'], true); 87 const id = offerIdFrom(off); 88 // The existing guardian (parent) is a party and must accept. 89 const parentQ = G.offersCollection(`${ME}/queues/offers`, 'parent', ME).orderedItems.find((o) => o.id === id); 90 assert.ok(parentQ, 'parent sees the co-guardianship offer'); 91 assert.deepEqual(parentQ['shaer:existingGuardians'], [ME]); 92 93 // Kid accepts, then gran (candidate) accepts — still NOT committed, because 94 // the existing guardian (parent) has not co-accepted (§3.1.2). 95 await G.handleGuardianshipOutbox(kid, { type: 'Accept', object: id }); 96 const early = await G.handleGuardianshipOutbox(gran, { type: 'Accept', object: id }); 97 assert.equal(early.committed, false); 98 assert.equal(G.listGuardians('kid').length, 1, 'still just the first guardian'); 99 100 // The existing guardian co-accepts → tally complete → commit. 101 await G.handleGuardianshipOutbox(parent, { type: 'Accept', object: id }); 102 assert.deepEqual(G.listGuardians('kid').map((g) => g.other_uri).sort(), [GRAN, ME].sort()); 59 103 }); 60 104 61 test(' only the candidate may offer', async () => {62 const r = await G.handleGuardianshipOutbox(parent, {63 type: 'Offer',64 object: { type: 'Relationship', subject: KID, relationship: 'shaer:Guardian', object: 'https://elders.test/u/x'},105 test('a single Reject from a required party voids the offer (§3.2)', async () => { 106 // parent offers to guard gran (who is free). 107 const off = await G.handleGuardianshipOutbox(parent, { 108 type: 'Offer', object: { type: 'Relationship', subject: GRAN, relationship: 'shaer:Guardian', object: ME }, 65 109 }); 66 assert.equal(r.status, 403); 110 const id = offerIdFrom(off); 111 await G.handleGuardianshipOutbox(gran, { type: 'Reject', object: id }); 112 const q = G.offersCollection(`${ME}/queues/offers`, 'parent', ME).orderedItems.find((o) => o.id === id); 113 assert.equal(q, undefined, 'voided offer leaves the queue'); 114 assert.equal(G.listWards('parent').some((w) => w.other_uri === GRAN), false); 67 115 }); 68 116 69 test('inbound Offer parks in the ward queue; C2S Accept commits both ends', async () => { 70 // The kid's side receives the offer S2S. 71 const offerId = sent[0].activity.id; 72 const consumed = await G.handleGuardianshipInbox(kid, { 73 id: offerId, type: 'Offer', actor: ME, 74 object: { type: 'Relationship', subject: KID, relationship: 'shaer:Guardian', object: ME }, 75 }); 76 assert.equal(consumed, true); 77 assert.equal(G.listOffers('kid').length, 1); 78 79 // The kid's offers queue carries the daemon-contract helper fields the 80 // Shaer clients render their accept button from. 81 const kidQ = G.offersCollection(`${KID}/queues/offers`, 'kid', KID); 82 assert.equal(kidQ.totalItems, 1); 83 assert.equal(kidQ.orderedItems[0]['shaer:needsMyAccept'], true); 84 assert.equal(kidQ.orderedItems[0]['shaer:iAmCandidate'], false); 85 assert.equal(kidQ.orderedItems[0]['shaer:ward'], KID); 86 assert.equal(kidQ.orderedItems[0]['shaer:candidate'], ME); 87 88 // The kid accepts over C2S; the answer travels to the guardian. 89 const r = await G.handleGuardianshipOutbox(kid, { type: 'Accept', object: offerId }); 90 assert.equal(r.status, 202); 91 assert.deepEqual(G.listGuardians('kid').map((g) => g.other_uri), [ME]); 92 93 // The guardian's side hears the Accept S2S and commits. 94 const ok = await G.handleGuardianshipInbox(parent, { type: 'Accept', actor: KID, object: offerId }); 95 assert.equal(ok, true); 96 const wards = G.listWards('parent').filter((w) => w.status === 'accepted'); 97 assert.deepEqual(wards.map((w) => w.other_uri), [KID]); 98 99 // The ward's actor doc now names its guardian (FEP-633c 2.1). 100 const actor = AP.buildActor('https://test.example', kid); 101 assert.deepEqual(actor['shaer:guardians'], [ME]); 102 }); 103 104 test('queues serve the daemon contract shapes', () => { 105 const wardsQ = G.wardsCollection(`${ME}/queues/wards`, 'parent'); 106 assert.equal(wardsQ.type, 'OrderedCollection'); 107 assert.equal(wardsQ.totalItems, 1); 108 assert.equal(wardsQ.orderedItems[0].id, KID); 109 const followsQ = G.followsCollection(`${ME}/queues/follows`); 110 assert.deepEqual(followsQ.orderedItems, []); 111 const offersQ = G.offersCollection(`${ME}/queues/offers`, 'parent', ME); 112 assert.equal(offersQ.type, 'OrderedCollection'); // empty again after the accept 113 assert.equal(offersQ.totalItems, 0); 114 }); 115 116 test('a ward cannot become a guardian (FEP-633c 1)', async () => { 117 test('a ward cannot become a guardian (§1)', async () => { 117 118 const r = await G.handleGuardianshipOutbox(kid, { 118 type: 'Offer', 119 object: { type: 'Relationship', subject: 'https://other.test/u/y', relationship: 'shaer:Guardian', object: KID }, 119 type: 'Offer', object: { type: 'Relationship', subject: A('someone'), relationship: 'shaer:Guardian', object: KID }, 120 120 }); 121 121 assert.equal(r.status, 403); … … 123 123 }); 124 124 125 test('only the candidate may offer (§3.1 fixed initiator)', async () => { 126 const r = await G.handleGuardianshipOutbox(parent, { 127 type: 'Offer', object: { type: 'Relationship', subject: A('newkid'), relationship: 'shaer:Guardian', object: GRAN }, 128 }); 129 assert.equal(r.status, 403); 130 assert.equal(r.error, 'only_the_candidate_offers'); 131 }); 132 125 133 test('helpRequest props only ride direct notes', () => { 126 assert.deepEqual(G.helpRequestProps({ visibility: 'direct', help_request: 1 }), { 'shaer:helpRequest': true });127 assert.deepEqual(G.helpRequestProps({ visibility: 'public', help_request: 1 }), {});128 134 assert.equal(G.isHelpRequest({ 'shaer:helpRequest': true }), true); 129 135 assert.equal(G.isHelpRequest({}), false);
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)