profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
anonymize.test.ts 4,191 bytes
1 import { describe, expect, it } from 'vitest';
2 import {
3 anonymizePeers,
4 hashSeed,
5 labelForIndex,
6 makeRng,
7 shuffle,
8 } from './anonymize';
9
10 describe('makeRng', () => {
11 it('is deterministic for a given seed', () => {
12 const a = makeRng(42);
13 const b = makeRng(42);
14 const seqA = [a(), a(), a(), a()];
15 const seqB = [b(), b(), b(), b()];
16 expect(seqA).toEqual(seqB);
17 });
18
19 it('produces values in [0, 1)', () => {
20 const rng = makeRng(7);
21 for (let i = 0; i < 1000; i++) {
22 const v = rng();
23 expect(v).toBeGreaterThanOrEqual(0);
24 expect(v).toBeLessThan(1);
25 }
26 });
27
28 it('produces different sequences for different seeds', () => {
29 expect(makeRng(1)()).not.toEqual(makeRng(2)());
30 });
31 });
32
33 describe('hashSeed', () => {
34 it('is stable and order-sensitive', () => {
35 expect(hashSeed('debate', 1, 'critique', 'p0')).toBe(hashSeed('debate', 1, 'critique', 'p0'));
36 expect(hashSeed('debate', 1, 'critique', 'p0')).not.toBe(hashSeed('debate', 1, 'critique', 'p1'));
37 expect(hashSeed('a', 'b')).not.toBe(hashSeed('b', 'a'));
38 });
39
40 it('returns an unsigned 32-bit integer', () => {
41 const h = hashSeed('anything', 99);
42 expect(Number.isInteger(h)).toBe(true);
43 expect(h).toBeGreaterThanOrEqual(0);
44 expect(h).toBeLessThanOrEqual(0xffffffff);
45 });
46 });
47
48 describe('shuffle', () => {
49 const rng = () => 0.5; // fixed generator
50
51 it('does not mutate the input', () => {
52 const input = [1, 2, 3, 4];
53 const copy = [...input];
54 shuffle(input, makeRng(1));
55 expect(input).toEqual(copy);
56 });
57
58 it('returns a permutation (same multiset)', () => {
59 const input = ['a', 'b', 'c', 'd', 'e'];
60 const out = shuffle(input, makeRng(123));
61 expect([...out].sort()).toEqual([...input].sort());
62 expect(out).toHaveLength(input.length);
63 });
64
65 it('is deterministic for a given rng seed', () => {
66 const input = [1, 2, 3, 4, 5, 6];
67 expect(shuffle(input, makeRng(9))).toEqual(shuffle(input, makeRng(9)));
68 });
69
70 it('handles empty and singleton arrays', () => {
71 expect(shuffle([], rng)).toEqual([]);
72 expect(shuffle([42], rng)).toEqual([42]);
73 });
74 });
75
76 describe('labelForIndex', () => {
77 it('maps 0..25 to A..Z', () => {
78 expect(labelForIndex(0)).toBe('A');
79 expect(labelForIndex(1)).toBe('B');
80 expect(labelForIndex(25)).toBe('Z');
81 });
82
83 it('continues to AA, AB for larger councils', () => {
84 expect(labelForIndex(26)).toBe('AA');
85 expect(labelForIndex(27)).toBe('AB');
86 });
87 });
88
89 describe('anonymizePeers', () => {
90 const peers = [
91 { participantId: 'p0', content: 'answer zero' },
92 { participantId: 'p1', content: 'answer one' },
93 { participantId: 'p2', content: 'answer two' },
94 ];
95
96 it('assigns unique sequential labels and a consistent labelMap', () => {
97 const { peers: out, labelMap } = anonymizePeers(peers, hashSeed('d', 1, 'p0'));
98 const labels = out.map((p) => p.label);
99 expect(new Set(labels).size).toBe(labels.length);
100 expect(labels).toEqual(['A', 'B', 'C']);
101 for (const p of out) {
102 expect(labelMap[p.label]).toBe(p.participantId);
103 }
104 });
105
106 it('preserves every participant exactly once (no dropping or duplication)', () => {
107 const { peers: out } = anonymizePeers(peers, 555);
108 expect(out.map((p) => p.participantId).sort()).toEqual(['p0', 'p1', 'p2']);
109 });
110
111 it('is deterministic for a given seed', () => {
112 const seed = hashSeed('debate', 1, 'critique', 'reviewerA');
113 const a1 = anonymizePeers(peers, seed).peers.map((p) => p.participantId);
114 const a2 = anonymizePeers(peers, seed).peers.map((p) => p.participantId);
115 expect(a1).toEqual(a2);
116 });
117
118 it('produces more than one ordering across reviewers (order is not shared)', () => {
119 // The point of per-reviewer shuffling is that different seeds generally
120 // yield different orderings. Sample many seeds and require real variation.
121 const orderings = new Set<string>();
122 for (let i = 0; i < 50; i++) {
123 const seed = hashSeed('debate', 1, 'critique', `reviewer-${i}`);
124 orderings.add(anonymizePeers(peers, seed).peers.map((p) => p.participantId).join(','));
125 }
126 expect(orderings.size).toBeGreaterThan(1);
127 });
128 });
129