// SignerPresence (S1) — leak-free single-signer election from connection presence. Pure logic + injectable
// clock, so this runs anywhere (node/bare) with no swarm. Proves: lowest-key election, grace blip-tolerance,
// cascade failover, self-election, and — structurally — that it can never write to the append-only log.
// Run: `node test/signer-presence.test.mjs`
import { SignerPresence } from '../lib/signer-presence.js'
const assert = (c, m) => { if (!c) throw new Error('FAIL: ' + m); console.log(' ok -', m) }
let NOW = 1000
const clock = () => NOW
const A = 'aaaa', B = 'bbbb', C = 'cccc'
// 1. lowest-key co-owner is the signer
{
NOW = 1000
const sp = new SignerPresence({ self: B, graceMs: 15000, clock })
sp.markPresent(A)
assert(sp.currentSigner() === A, 'lowest-key co-owner (A) is elected signer')
assert(sp.holds() === false, 'B (higher key) does not hold while A is present')
}
// 2 & 3. holder departs → grace keeps it → after grace, failover
{
NOW = 1000
const sp = new SignerPresence({ self: B, graceMs: 15000, clock })
sp.markPresent(A)
NOW = 5000; sp.markAbsent(A)
NOW = 5000 + 14999
assert(sp.currentSigner() === A, 'within grace, departed holder A stays elected (no premature failover)')
assert(sp.holds() === false, 'B still does not hold within grace')
NOW = 5000 + 15001
assert(sp.currentSigner() === B, 'after grace elapses, failover to B')
assert(sp.holds() === true, 'B now holds')
}
// 4. blip (depart + return within grace) does NOT thrash the signer
{
NOW = 1000
const sp = new SignerPresence({ self: B, graceMs: 15000, clock })
sp.markPresent(A)
NOW = 5000; sp.markAbsent(A)
NOW = 12000; sp.markPresent(A) // returned within grace
NOW = 5000 + 20000 // well past the original grace window
assert(sp.currentSigner() === A, 'a blip (return within grace) does NOT fail the signer over')
assert(sp.holds() === false, 'B never took over on the blip')
}
// 5. self is lowest → self holds
{
NOW = 1000
const sp = new SignerPresence({ self: A, graceMs: 15000, clock })
sp.markPresent(B)
assert(sp.holds() === true, 'self=A (lowest) holds')
}
// 6. all lower co-owners gone + grace → self holds
{
NOW = 1000
const sp = new SignerPresence({ self: B, graceMs: 15000, clock })
sp.markPresent(A)
NOW = 2000; sp.markAbsent(A)
NOW = 2000 + 15001
assert(sp.holds() === true, 'after the only lower co-owner departs+grace, self holds')
assert(sp.currentSigner() === B, 'self is the sole effective signer')
}
// 7. three co-owners: cascade failover A→B→C
{
NOW = 1000
const sp = new SignerPresence({ self: C, graceMs: 10000, clock })
sp.markPresent(A); sp.markPresent(B)
assert(sp.currentSigner() === A, 'A (lowest of three) signs')
NOW = 2000; sp.markAbsent(A); NOW = 2000 + 10001
assert(sp.currentSigner() === B, 'A gone+grace → B signs')
assert(sp.holds() === false, 'C still not holding while B present')
NOW = 30000; sp.markAbsent(B); NOW = 30000 + 10001
assert(sp.holds() === true, 'B gone+grace → C (self) holds')
}
// 8. structurally leak-free: no statebase, no append path
{
const sp = new SignerPresence({ self: A })
assert(sp.sb === undefined && typeof sp.set !== 'function' && typeof sp.append !== 'function',
'SignerPresence has NO statebase/append path — zero log writes by construction')
}
console.log('\nSIGNER-PRESENCE OK: lowest-key election, grace blip-tolerance, cascade failover, zero log writes')