| … | … | @@ -0,0 +1,42 @@ |
| 1 | // attachPresence (S1 wiring) — proves the presence handshake over a REAL encrypted connection (a |
| 2 | // SecretStream pair = exactly a hyperswarm connection). Two peers exchange member keys, both elect the |
| 3 | // lowest-key signer, and the survivor fails over (after grace) when the holder disconnects — appending |
| 4 | // nothing to any log. Run: `node test/signer-presence-channel.test.mjs` |
| 5 | import test from 'brittle' |
| 6 | import SecretStream from '@hyperswarm/secret-stream' |
| 7 | import { SignerPresence } from '../lib/signer-presence.js' |
| 8 | import { attachPresence } from '../lib/signer-presence-channel.js' |
| 9 | |
| 10 | const A = 'aaaa000000000000000000000000000000000000000000000000000000000000' |
| 11 | const B = 'bbbb000000000000000000000000000000000000000000000000000000000000' |
| 12 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) |
| 13 | const waitFor = async (fn, timeout = 3000) => { const end = Date.now() + timeout; while (Date.now() < end) { if (fn()) return true; await sleep(20) } return false } |
| 14 | |
| 15 | test('presence handshake over a real connection: elect lowest-key signer, fail over on disconnect', async (t) => { |
| 16 | const grace = 400 |
| 17 | const spA = new SignerPresence({ self: A, graceMs: grace }) |
| 18 | const spB = new SignerPresence({ self: B, graceMs: grace }) |
| 19 | |
| 20 | const a = new SecretStream(true) |
| 21 | const b = new SecretStream(false) |
| 22 | a.rawStream.pipe(b.rawStream).pipe(a.rawStream) // cross-pipe = a connected pair |
| 23 | |
| 24 | const detachA = attachPresence(a, { selfKey: A, presence: spA }) |
| 25 | const detachB = attachPresence(b, { selfKey: B, presence: spB }) |
| 26 | t.teardown(() => { detachA(); detachB(); try { a.destroy() } catch {} try { b.destroy() } catch {} }) |
| 27 | |
| 28 | // member keys exchanged over the wire → both compute the same signer (A, lowest) |
| 29 | t.ok(await waitFor(() => spB.effective().includes(A) && spA.effective().includes(B)), 'peers exchanged member keys over the connection') |
| 30 | t.is(spA.currentSigner(), A, 'A (lowest key) is elected signer') |
| 31 | t.is(spB.currentSigner(), A, 'B independently agrees A is the signer') |
| 32 | t.ok(spA.holds(), 'A holds the signer role') |
| 33 | t.absent(spB.holds(), 'B does not hold while A is present') |
| 34 | |
| 35 | // A disconnects → B's connection closes → B fails over, but only AFTER the grace window |
| 36 | a.destroy() |
| 37 | await sleep(150) // close fires + A marked absent, but 150ms < grace(400) → still in grace |
| 38 | t.absent(spB.holds(), 'immediately after disconnect B stays in grace (no premature/thrashing failover)') |
| 39 | await sleep(grace) // now past grace |
| 40 | t.ok(spB.holds(), 'after grace, B fails over to signer') |
| 41 | t.is(spB.currentSigner(), B, 'B is now the sole signer') |
| 42 | }) |