|
| 1 | +import { describe, expect, test } from "bun:test"; |
| 2 | +import { hashToSlot, getVariant } from "./experiment"; |
| 3 | +import type { Experiment } from "../assets/data/experiments"; |
| 4 | + |
| 5 | +describe("hashToSlot", () => { |
| 6 | + test("returns a number between 0 and 99", () => { |
| 7 | + for (let id = 0; id < 1000; id++) { |
| 8 | + const slot = hashToSlot(id, "test-experiment"); |
| 9 | + expect(slot).toBeGreaterThanOrEqual(0); |
| 10 | + expect(slot).toBeLessThan(100); |
| 11 | + } |
| 12 | + }); |
| 13 | + |
| 14 | + test("is deterministic for the same inputs", () => { |
| 15 | + const a = hashToSlot(42, "hero-cta"); |
| 16 | + const b = hashToSlot(42, "hero-cta"); |
| 17 | + expect(a).toBe(b); |
| 18 | + }); |
| 19 | + |
| 20 | + test("different experiment names produce different slots for same user", () => { |
| 21 | + expect(hashToSlot(12345, "experiment-a")).toBe(83); |
| 22 | + expect(hashToSlot(12345, "experiment-b")).toBe(82); |
| 23 | + }); |
| 24 | + |
| 25 | + test("different user IDs produce different slots for same experiment", () => { |
| 26 | + const slot1 = hashToSlot(1, "hero-cta"); |
| 27 | + const slot2 = hashToSlot(2, "hero-cta"); |
| 28 | + expect(slot1).not.toBe(slot2); |
| 29 | + }); |
| 30 | +}); |
| 31 | + |
| 32 | +describe("getVariant", () => { |
| 33 | + const fiftyFifty: Experiment = { |
| 34 | + name: "test", |
| 35 | + variants: [ |
| 36 | + { name: "control", weight: 50 }, |
| 37 | + { name: "variant-b", weight: 50 }, |
| 38 | + ], |
| 39 | + enabled: true, |
| 40 | + }; |
| 41 | + |
| 42 | + test("always returns a valid variant name", () => { |
| 43 | + const validNames = fiftyFifty.variants.map((v) => v.name); |
| 44 | + for (let id = 0; id < 1000; id++) { |
| 45 | + const variant = getVariant(fiftyFifty, id); |
| 46 | + expect(validNames).toContain(variant); |
| 47 | + } |
| 48 | + }); |
| 49 | + |
| 50 | + test("roughly 50/50 distribution over many IDs", () => { |
| 51 | + const counts: Record<string, number> = { control: 0, "variant-b": 0 }; |
| 52 | + const total = 10000; |
| 53 | + for (let id = 0; id < total; id++) { |
| 54 | + counts[getVariant(fiftyFifty, id)]++; |
| 55 | + } |
| 56 | + // Allow 10% tolerance |
| 57 | + expect(counts.control).toBeGreaterThan(total * 0.4); |
| 58 | + expect(counts.control).toBeLessThan(total * 0.6); |
| 59 | + expect(counts["variant-b"]).toBeGreaterThan(total * 0.4); |
| 60 | + expect(counts["variant-b"]).toBeLessThan(total * 0.6); |
| 61 | + }); |
| 62 | + |
| 63 | + test("respects unequal weights", () => { |
| 64 | + const experiment: Experiment = { |
| 65 | + name: "weighted", |
| 66 | + variants: [ |
| 67 | + { name: "a", weight: 80 }, |
| 68 | + { name: "b", weight: 20 }, |
| 69 | + ], |
| 70 | + enabled: true, |
| 71 | + }; |
| 72 | + const counts: Record<string, number> = { a: 0, b: 0 }; |
| 73 | + const total = 10000; |
| 74 | + for (let id = 0; id < total; id++) { |
| 75 | + counts[getVariant(experiment, id)]++; |
| 76 | + } |
| 77 | + // "a" should get ~80% (allow 10% tolerance) |
| 78 | + expect(counts.a).toBeGreaterThan(total * 0.7); |
| 79 | + expect(counts.a).toBeLessThan(total * 0.9); |
| 80 | + }); |
| 81 | + |
| 82 | + test("is deterministic", () => { |
| 83 | + const v1 = getVariant(fiftyFifty, 42); |
| 84 | + const v2 = getVariant(fiftyFifty, 42); |
| 85 | + expect(v1).toBe(v2); |
| 86 | + }); |
| 87 | +}); |
0 commit comments