// STATIC GUARD: no `.sort(` introduced in shared/turn.js outside the ONE // allowed function (sortParticipantsByInitiative, used by startEncounter to // freeze the list once). Slot-not-sort design (docs/INITIATIVE_ORDERING.md): // mutations = insert/move, never wholesale re-sort. A stray `.sort(` after // start destroys drag tie-break order. // // This test errs the moment someone adds `.sort(` anywhere but the allowlist. // Maintenance: add a function to ALLOWED only if it runs at startEncounter // (one-time freeze), NOT in add/update/reorder/nextTurn paths. 'use strict'; const fs = require('fs'); const path = require('path'); const SRC = fs.readFileSync(path.join(__dirname, '..', 'turn.js'), 'utf8'); // Functions permitted to call .sort(. Listed so intent is explicit + reviewed. const ALLOWED_SORT_FUNCTIONS = new Set([ 'sortParticipantsByInitiative', ]); // Collect top-level function bodies by brace counting. // Matches `function NAME(` decls AND `const NAME = ... =>` arrow fns. // Returns [{ name, body }]. function collectBody(src, fromIdx) { let i = fromIdx; while (i < src.length && src[i] !== '{') i++; let depth = 0; const start = i; for (; i < src.length; i++) { if (src[i] === '{') depth++; else if (src[i] === '}') { depth--; if (depth === 0) break; } } return src.slice(start, i + 1); } function extractFunctions(src) { const out = []; const fnRe = /\bfunction\s+([A-Za-z0-9_$]+)\s*\(/g; const arrowRe = /(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*[^=;]*?=>/g; let m; while ((m = fnRe.exec(src)) !== null) { out.push({ name: m[1], body: collectBody(src, m.index + m[0].length) }); } while ((m = arrowRe.exec(src)) !== null) { out.push({ name: m[1], body: collectBody(src, m.index + m[0].length) }); } return out; } describe('STATIC: no .sort( outside allowlist (slot-not-sort design)', () => { test('every .sort( call lives in an allowed function', () => { const fns = extractFunctions(SRC); const offenders = []; for (const { name, body } of fns) { if (!ALLOWED_SORT_FUNCTIONS.has(name) && /\.sort\(/.test(body)) { offenders.push(name); } } expect(offenders).toEqual([]); }); test('allowed sort function is declared (no silent allowlist drift)', () => { const declared = new Set(extractFunctions(SRC).map(f => f.name)); for (const name of ALLOWED_SORT_FUNCTIONS) { expect(declared.has(name)).toBe(true); } }); });