// ── The audience ceiling lattice ─────────────────────────────────────────── // // ENGAGEMENT.md §5.1a / G24. These are the tests for the security property the // whole rule model rests on: **composition may narrow, never widen**, and a // ceiling is about WHICH people rather than how many. // // The case worth naming is `staff` vs `owner`. Under the flat total order the // plan's wording invites — self < owner < staff < members < authenticated < // everyone — a trigger ceilinged at `staff` also permits `owner`, so a rule // could mail `uo.cheat.detected` to the player it detected. That is the bug this // file exists to keep out, so it is asserted explicitly rather than left implied // by the shape of the table. const { test } = require('node:test') const assert = require('node:assert/strict') const ceilings = require('../src/modules/ceilings') test('the seven ceilings are the vocabulary, and nothing else is', () => { assert.deepEqual( [...ceilings.CEILINGS].sort(), ['admin', 'authenticated', 'everyone', 'members', 'owner', 'staff', 'subscribers'], ) for (const id of ceilings.CEILINGS) assert.ok(ceilings.LABELS[id], `${id} has an operator label`) assert.equal(ceilings.isCeiling('nobody'), false) assert.equal(ceilings.isCeiling(undefined), false) }) test('everyone permits every ceiling; every ceiling permits itself', () => { for (const id of ceilings.CEILINGS) { assert.equal(ceilings.permits('everyone', id), true, `everyone permits ${id}`) assert.equal(ceilings.permits(id, id), true, `${id} permits itself`) } }) test('authenticated permits every branch and admin beneath staff, but not everyone', () => { for (const below of ['subscribers', 'members', 'staff', 'owner', 'admin']) { assert.equal(ceilings.permits('authenticated', below), true) } assert.equal(ceilings.permits('authenticated', 'everyone'), false) }) // The one that a flat ordering gets wrong. test('a staff ceiling does NOT permit owner — fewer people is not less exposure', () => { assert.equal(ceilings.permits('staff', 'owner'), false) assert.equal(ceilings.permits('owner', 'staff'), false) // …and the same for every other pair of leaves, so the property is the tree's // and not a special case someone wrote for cheat detection. const leaves = ['subscribers', 'members', 'staff', 'owner'] for (const a of leaves) { for (const b of leaves) { if (a === b) continue assert.equal(ceilings.permits(a, b), false, `${a} must not permit ${b}`) } } }) // ── `admin`, added in Phase 11 ───────────────────────────────────────────── // // The one genuine refinement in the tree: every admin is staff, which is the // containment no other pair has. These assert that it is a NARROWING and not a // second way to widen — the failure this file exists to keep out, in its newest // possible costume. test('staff permits admin and admin does not permit staff — the one true refinement', () => { assert.equal(ceilings.permits('staff', 'admin'), true) assert.equal(ceilings.permits('admin', 'staff'), false) assert.equal(ceilings.meet('staff', 'admin'), 'admin') assert.equal(ceilings.meet('admin', 'staff'), 'admin') }) test('admin is incomparable with every branch that is not staff', () => { for (const other of ['subscribers', 'members', 'owner']) { assert.equal(ceilings.permits('admin', other), false, `admin must not permit ${other}`) assert.equal(ceilings.permits(other, 'admin'), false, `${other} must not permit admin`) assert.equal(ceilings.meet('admin', other), null, `admin ∧ ${other} has no bound`) } }) test('an admin-ceilinged trigger refuses a staff audience', () => { // The acceptance criterion in as many words: a rule cannot give an // admin-ceiling trigger a `staff` audience. `permits` is what both the save // check and the send-time re-check call. assert.equal(ceilings.permits('admin', 'staff'), false) // …and the narrowing direction is allowed, which is what makes the node useful // rather than merely restrictive. assert.equal(ceilings.permits('staff', 'admin'), true) }) test('the role ceilings are a table, so a new one cannot be forgotten', () => { // `visibleTo` used to ask `ceiling !== 'staff'`. That spelling was correct // while `staff` was the only role-gated value and would have silently published // every admin-ceilinged id to every player's preferences screen the day `admin` // arrived. The table is what makes that impossible to get wrong quietly. assert.deepEqual(Object.keys(ceilings.ROLE_CEILINGS).sort(), ['admin', 'staff']) for (const id of Object.keys(ceilings.ROLE_CEILINGS)) { assert.ok(ceilings.isRoleCeiling(id), `${id} is a role ceiling`) assert.ok(ceilings.ROLE_CEILINGS[id].roles.length, `${id} names at least one role`) } assert.equal(ceilings.isRoleCeiling('subscribers'), false) }) test('reachableBy gates the role ceilings and lets everything else through', () => { assert.equal(ceilings.reachableBy('admin', 'admin'), true) assert.equal(ceilings.reachableBy('admin', 'editor'), false) assert.equal(ceilings.reachableBy('admin', 'moderator'), false) assert.equal(ceilings.reachableBy('admin', 'user'), false) assert.equal(ceilings.reachableBy('staff', 'editor'), true) assert.equal(ceilings.reachableBy('staff', 'user'), false) // Fails closed on a missing viewer, which is how a signed-out catalog read // reaches it. assert.equal(ceilings.reachableBy('staff', undefined), false) assert.equal(ceilings.reachableBy('admin', undefined), false) // Everything that is not role-gated is visible to anyone, including the `null` // a stream-only catalog item carries. for (const open of ['everyone', 'authenticated', 'subscribers', 'members', 'owner']) { assert.equal(ceilings.reachableBy(open, 'user'), true, `${open} is not role-gated`) } assert.equal(ceilings.reachableBy(null, 'user'), true) }) test('an unknown ceiling is permitted by nothing, on either side', () => { assert.equal(ceilings.permits('everyone', 'god'), false) assert.equal(ceilings.permits('god', 'owner'), false) assert.equal(ceilings.permits('everyone', undefined), false) }) test('A OR B takes the NARROWER of the two ceilings, not the wider', () => { assert.equal(ceilings.meet('everyone', 'staff'), 'staff') assert.equal(ceilings.meet('staff', 'everyone'), 'staff') assert.equal(ceilings.meet('authenticated', 'members'), 'members') assert.equal(ceilings.meet('members', 'members'), 'members') }) test('incomparable ceilings have no meet — the save is refused, not guessed', () => { assert.equal(ceilings.meet('staff', 'members'), null) assert.equal(ceilings.meet('admin', 'owner'), null) assert.equal(ceilings.meet('owner', 'subscribers'), null) assert.equal(ceilings.meet('staff', 'nonsense'), null) }) test('meetAll folds, short-circuits to null, and has no opinion about an empty list', () => { assert.equal(ceilings.meetAll(['everyone', 'authenticated', 'members']), 'members') // members ∧ staff is undefined, so the whole composition is. assert.equal(ceilings.meetAll(['everyone', 'members', 'staff']), null) assert.equal(ceilings.meetAll(['owner']), 'owner') // Not 'everyone': an empty composition states no bound, and defaulting it to // the top would make "no audiences selected" the widest possible rule. assert.equal(ceilings.meetAll([]), null) assert.equal(ceilings.meetAll(null), null) })