import { describe, expect, it } from "vitest"; import { normalizeWordList, passphrase, passphraseWithWordList, randomIdentifiers, randomIntegers, randomString, rollDice, ulid, uuidV4, uuidV7, } from "../../src/random/generators"; import { coinFlips, dealCards, decimalFractions, integerSequence, randomCoordinates, randomDates, } from "../../src/random/draws"; import { randomSource } from "../../src/random/source"; describe("random generators", () => { it("repeats deterministic recipes", () => expect( randomIntegers(randomSource("deterministic", "seed"), 10, 1, 6), ).toEqual(randomIntegers(randomSource("deterministic", "seed"), 10, 1, 6))); it("produces RFC-shaped UUIDs", () => { const source = randomSource("deterministic", "id"); expect(uuidV4(source)).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u, ); expect(uuidV7(source, 0)).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u, ); }); it("creates a 26-character ULID", () => expect(ulid(randomSource("deterministic", "id"), 0)).toMatch( /^[0-9A-HJKMNP-TV-Z]{26}$/u, )); it("bounds identifier batches before allocating their output array", () => { const source = randomSource("deterministic", "identifier-bounds"); expect(() => randomIdentifiers(source, 0, "uuid4", 0)).toThrow( /safe integer from 1–100,000/u, ); expect(() => randomIdentifiers(source, 100_001, "uuid7", 0)).toThrow( /safe integer from 1–100,000/u, ); expect(() => randomIdentifiers(source, Number.NaN, "ulid", 0)).toThrow( /safe integer from 1–100,000/u, ); expect(randomIdentifiers(source, 2, "uuid4", 0)).toHaveLength(2); }); it("rejects an inclusive MAX_SAFE_INTEGER bound before adding one", () => { const source = randomSource("deterministic", "integer-boundary"); expect(() => randomIntegers( source, 1, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, ), ).toThrow(/less than Number\.MAX_SAFE_INTEGER/u); expect( randomIntegers( source, 1, Number.MAX_SAFE_INTEGER - 1, Number.MAX_SAFE_INTEGER - 1, ), ).toEqual([Number.MAX_SAFE_INTEGER - 1]); }); it("rejects biased duplicate alphabets", () => expect(() => randomString(randomSource("deterministic", "x"), 3, "aab"), ).toThrow(/duplicate/u)); it("bounds dice and reports the modifier", () => expect( rollDice(randomSource("deterministic", "dice"), "4d6+2"), ).toMatchObject({ expression: "4d6+2", modifier: 2 })); it("reports only the uniform-list entropy model", () => expect( passphrase(randomSource("deterministic", "words"), 4, [ "one", "two", "three", "four", ]).entropy, ).toBe(8)); it("normalizes a custom word list once and reports its eligible count", () => { const words = normalizeWordList(" one \r\n\r\ntwo\n three "); expect(words).toMatchObject({ words: ["one", "two", "three"], canonical: "one\ntwo\nthree", normalization: "trim-lines-drop-empty-preserve-order-v1", }); expect( passphraseWithWordList( randomSource("deterministic", "normalized-words"), 4, words, ), ).toMatchObject({ listSize: 3, entropy: 4 * Math.log2(3) }); }); it("bounds alphabet and word-list allocation before generation", () => { const source = randomSource("deterministic", "bounds"); expect(() => randomString(source, 1, "ab".repeat(70_000))).toThrow( /131,072/u, ); expect(() => passphrase(source, 1, ["x".repeat(10_001), "y"])).toThrow( /10,000/u, ); }); }); describe("local draws", () => { it("reproduces coin flips and reports only binary outcomes", () => { const first = coinFlips(randomSource("deterministic", "coins"), 50); const second = coinFlips(randomSource("deterministic", "coins"), 50); expect(first).toEqual(second); expect(first.every((value) => value === "Heads" || value === "Tails")).toBe( true, ); }); it("deals every card from a standard deck without replacement", () => { const cards = dealCards(randomSource("deterministic", "cards"), 1, 52); expect(cards).toHaveLength(52); expect(new Set(cards).size).toBe(52); expect( cards.every((card) => /^(?:A|[2-9]|10|J|Q|K)[♠♥♦♣]$/u.test(card)), ).toBe(true); }); it("shuffles an inclusive integer range exactly once", () => { const values = integerSequence( randomSource("deterministic", "sequence"), -2, 3, ); expect(values).toHaveLength(6); expect([...values].sort((a, b) => a - b)).toEqual([-2, -1, 0, 1, 2, 3]); }); it("filters dates by weekday and can sample without replacement", () => { const values = randomDates( randomSource("deterministic", "dates"), 4, "2024-02-01", "2024-02-29", { weekdays: [1], withoutReplacement: true }, ); expect(values).toHaveLength(4); expect(new Set(values).size).toBe(4); expect( values.every((value) => new Date(`${value}T00:00:00Z`).getUTCDay() === 1), ).toBe(true); expect(() => randomDates( randomSource("deterministic", "bad-date"), 1, "2023-02-29", "2023-03-01", ), ).toThrow(/valid Gregorian/u); }); it("generates exact-length decimal fractions with no binary formatting", () => { const values = decimalFractions( randomSource("deterministic", "fractions"), 4, 12, ); expect(values).toHaveLength(4); expect(values.every((value) => /^0\.\d{12}$/u.test(value))).toBe(true); }); it("generates reproducible coordinates inside geographic bounds", () => { const first = randomCoordinates( randomSource("deterministic", "coordinates"), 100, 6, ); const second = randomCoordinates( randomSource("deterministic", "coordinates"), 100, 6, ); expect(first).toEqual(second); expect( first.every( ({ latitude, longitude }) => latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180, ), ).toBe(true); }); it("rejects draw requests beyond their allocation bounds", () => { const source = randomSource("deterministic", "bounds"); expect(() => coinFlips(source, 100_001)).toThrow(/100,000/u); expect(() => dealCards(source, 1, 53)).toThrow(/1–52/u); expect(() => integerSequence(source, 0, 100_000)).toThrow(/100,000/u); expect(() => decimalFractions(source, 100_000, 64)).toThrow(/4,000,000/u); expect(() => randomCoordinates(source, 1, 11)).toThrow(/0–10/u); }); });