Changeset 5327324 in Klonkt for test


Ignore:
Timestamp:
08/03/2026 06:17:00 AM (5 weeks ago)
Author:
Bart <bart@…>
Branches:
main
Children:
69bd747
Parents:
04d5aeb
Message:

FEP-633c §4.2: een ward wordt geen guardian, ook niet stiekem

Klonkt weigerde een Offer van een bekende ward (a_ward_cannot_guard), maar
controleerde de kandidaat daarna nooit meer. Wie vrij was bij het aanbod en
daarna zelf geadopteerd werd, kwam er alsnog doorheen: de ward telde een
guardian wiens escalaties bij aflevering worden weggegooid (§4.1). De commit is
de onomkeerbare stap, dus daar moet het houden.

maybeCommit controleert nu de kandidaat vlak voor het schrijven. Drie
uitkomsten, en de derde is geen falen van de controle maar het niet kunnen
uitvoeren ervan: 'ok' commit, 'malformed' weigert luid, 'unverified' doet geen
van beide. Niet voiden bij onbereikbaar, want dan sloopt één hik een
meerpartijen-adoptie; niet committen ook niet, want dan leg je een guardian
vast die niemand gecontroleerd heeft. Het aanbod blijft staan.

De weigering is luid: het aanbod wordt void, niets wordt vastgelegd, en de
handelende partij stuurt een Reject (met shaer:notATeapot als reden). Een
Reject is wat §3 al kent, dus een server die nog nooit van §4 gehoord heeft
ruimt zijn kopie gewoon op.

Een lokale kandidaat wordt in onze eigen tabel opgezocht in plaats van bij
onszelf opgehaald. De co-location-guard in de tests ving dat ik daarnaast nog
een tweede lokale tak in een beslispad had gezet, voor het versturen van de
Reject; die is weg. De Reject vertrekt nu gewoon van wie er aan het handelen
was, zonder te vragen wie waar woont.

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

Location:
test
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • test/co-location.test.js

    r04d5aeb r5327324  
    214214  const allowed = {
    215215    existingGuardiansOf: 'reads our own guardian list instead of fetching our own actor doc',
     216    candidateFitness: '§4.2: same question, same source — is this candidate a ward? Our table, not a self-fetch',
    216217    applyCommitLocally: '§3.1.4: each instance writes the side of the commit it hosts',
    217218    endGuardianship: '§3.2: same, for the ward side of the Undo, after the fanout',
  • test/guardianship.test.js

    r04d5aeb r5327324  
    119119  assert.equal(r.status, 403);
    120120  assert.equal(r.error, 'a_ward_cannot_guard');
     121});
     122
     123test('a candidate adopted between Offer and Accept is refused at commit (§4.2)', async () => {
     124  // The case the §1 check above structurally cannot catch. Tess is free when
     125  // she offers, so the Offer is legitimate and accepted. Only afterwards does
     126  // she become a ward herself. An implementation that checks the candidate
     127  // only when the Offer arrives would commit her anyway, and Sam would be left
     128  // counting a guardian whose escalations get dropped (§4.1).
     129  // Fresh actors throughout: the suite shares one database, so adopting Tess
     130  // with an existing guardian would hand that guardian an extra ward and
     131  // quietly change the arithmetic of the emancipation tests further down.
     132  const tess = site('s10', 'tess');
     133  const sam = site('s11', 'sam');
     134  const ada = site('s12', 'ada');
     135  const [TESS, SAM, ADA] = [A('tess'), A('sam'), A('ada')];
     136
     137  // 1. Tess offers to guard Sam while she is still free of guardians.
     138  const off = await G.handleGuardianshipOutbox(tess, {
     139    type: 'Offer', object: { type: 'Relationship', subject: SAM, relationship: 'shaer:Guardian', object: TESS },
     140  });
     141  assert.equal(off.status, 202, 'a free candidate may offer');
     142  const id = off.id;
     143  assert.deepEqual(G.listGuardians('sam'), [], 'nothing committed until Sam accepts');
     144
     145  // 2. Before Sam answers, Tess is adopted: she is now a ward herself.
     146  const adopt = await G.handleGuardianshipOutbox(ada, {
     147    type: 'Offer', object: { type: 'Relationship', subject: TESS, relationship: 'shaer:Guardian', object: ADA },
     148  });
     149  await G.handleGuardianshipOutbox(tess, { type: 'Accept', object: adopt.id });
     150  assert.equal(G.listGuardians('tess').length, 1, 'Tess is a ward now');
     151
     152  // 3. Sam accepts. The tally is complete, so this WOULD commit.
     153  const done = await G.handleGuardianshipOutbox(sam, { type: 'Accept', object: id });
     154  assert.equal(done.committed, false, 'but a ward cannot serve as a guardian (§1)');
     155  assert.equal(done.refused, 'not_a_teapot');
     156
     157  // The refusal is loud, not a silent skip: nothing recorded, offer voided.
     158  assert.deepEqual(G.listGuardians('sam'), [], 'Sam gains no guardian');
     159  assert.deepEqual(G.listWards('tess').map((w) => w.other_uri), [], 'and Tess gains no ward');
     160  const stillPending = G.offersCollection(`${SAM}/queues/offers`, 'sam', SAM).orderedItems.filter((o) => o.id === id);
     161  assert.deepEqual(stillPending, [], 'the handshake is void, not left hanging');
    121162});
    122163
Note: See TracChangeset for help on using the changeset viewer.