| … | … | @@ -0,0 +1,42 @@ |
| 1 | // normalizeMembers — the roster shape rule, at the boundary that consumes it. |
| 2 | // |
| 3 | // WHY THIS IS A TEST AND NOT A COMMENT. `members` is written to config.json in three shapes: |
| 4 | // [{key,role}], an array of hex strings (what coownerJoin writes), and a "hex,hex" comma-string (what |
| 5 | // wj62 carries in production). Downstream, projectMembers (atproto-room/lib/state-base.js) reads |
| 6 | // `m.key` — so the two string shapes yield undefined for every entry, are skipped, and the roster |
| 7 | // arrives EMPTY. An empty roster is not a loud failure: the room quietly refuses to project it |
| 8 | // ("empty roster would wipe N existing member(s)"), so a device stays unlisted while its own config |
| 9 | // plainly names members. That is a whole evening of debugging, and it has now happened three times in |
| 10 | // this codebase (coowner.js documents its own instance). |
| 11 | // |
| 12 | // So bootSwarmDevice normalises what it is HANDED rather than trusting each caller to have used |
| 13 | // loadConfig. These assertions pin that rule. |
| 14 | // |
| 15 | // Run: bare test/roster-normalize.test.mjs |
| 16 | import { normalizeMembers } from '../lib/config.js' |
| 17 | |
| 18 | const assert = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log(' ok -', m) } |
| 19 | const A = 'a'.repeat(64) |
| 20 | const B = 'b'.repeat(64) |
| 21 | const keys = (r) => (r || []).map((m) => m.key) |
| 22 | |
| 23 | console.log('the three shapes all reach one form') |
| 24 | assert(JSON.stringify(normalizeMembers([{ key: A, role: 1 }])) === JSON.stringify([{ key: A, role: 1 }]), |
| 25 | 'already-normalised input is returned unchanged — idempotent, so every fleet node is unaffected') |
| 26 | assert(JSON.stringify(keys(normalizeMembers([A, B]))) === JSON.stringify([A, B]), |
| 27 | 'ARRAY OF HEX STRINGS — what coownerJoin writes, and what silently emptied the phone roster') |
| 28 | assert(JSON.stringify(keys(normalizeMembers(A + ',' + B))) === JSON.stringify([A, B]), |
| 29 | 'COMMA-STRING — the shape wj62 carries in production') |
| 30 | assert(normalizeMembers([{ key: A }])[0].role === 1, 'role defaults to 1 (all devices role-equal)') |
| 31 | assert(normalizeMembers([A.toUpperCase()])[0].key === A, 'normalised to lower case, so key comparisons hold') |
| 32 | assert(normalizeMembers([' ' + A + ' '])[0].key === A, 'entries are trimmed') |
| 33 | |
| 34 | console.log('absence stays absence — never an empty roster') |
| 35 | // null (not []) matters: bootSwarmDevice falls back to [{key:self,role:1}] on a falsy roster, and an |
| 36 | // empty ARRAY would instead project "no members", which is the wipe the room has to defend against. |
| 37 | assert(normalizeMembers(null) === null, 'null in, null out') |
| 38 | assert(normalizeMembers([]) === null, 'an empty array yields null, NOT an empty roster') |
| 39 | assert(normalizeMembers('') === null, 'an empty string yields null') |
| 40 | assert(normalizeMembers(['', ' ']) === null, 'entries that are all blank yield null, not [{key:""}]') |
| 41 | |
| 42 | console.log('\nall roster-normalize tests passed') |