Release Random Tools 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 12:28:20 +02:00
parent e4aa85a503
commit 4f61d001ab
30 changed files with 1179 additions and 85 deletions
+128
View File
@@ -20,6 +20,17 @@ import {
randomDates,
} from "../../src/random/draws";
import { randomSource } from "../../src/random/source";
import {
commitmentForReveal,
createCeremonyReveal,
finalizeCeremony,
parseCeremonyDocument,
} from "../../src/random/ceremony";
import { parseSeededRecipe, runSeededRecipe } from "../../src/random/recipes";
import {
parseWeightedItems,
weightedSampleWithoutReplacement,
} from "../../src/random/weighted";
describe("random generators", () => {
it("repeats deterministic recipes", () =>
@@ -208,3 +219,120 @@ describe("local draws", () => {
expect(() => randomCoordinates(source, 1, 11)).toThrow(/010/u);
});
});
describe("weighted draws and recipes", () => {
it("parses quoted CSV and samples entries without replacement", () => {
const items = parseWeightedItems('"Alpha, Inc.",1\nBeta,10\nGamma,2');
const first = weightedSampleWithoutReplacement(
randomSource("deterministic", "weighted"),
items,
3,
);
const second = weightedSampleWithoutReplacement(
randomSource("deterministic", "weighted"),
items,
3,
);
expect(first).toEqual(second);
expect(new Set(first.map((item) => item.inputIndex)).size).toBe(3);
expect(items[0]?.value).toBe("Alpha, Inc.");
});
it("rejects invalid weights before drawing", () => {
expect(() => parseWeightedItems("Alpha,0")).toThrow(/between/u);
expect(() =>
weightedSampleWithoutReplacement(
randomSource("deterministic", "weighted"),
[{ value: "Alpha", weight: Number.POSITIVE_INFINITY }],
1,
),
).toThrow(/invalid/u);
});
it("validates and exactly replays a versioned seeded recipe", () => {
const source = JSON.stringify({
schemaVersion: 1,
algorithm: "weighted-sample-v1",
seed: "recipe-seed",
parameters: {
count: 2,
items: [
{ value: "one", weight: 1 },
{ value: "two", weight: 4 },
{ value: "three", weight: 2 },
],
},
});
const recipe = parseSeededRecipe(source);
expect(runSeededRecipe(recipe)).toEqual(runSeededRecipe(recipe));
expect(runSeededRecipe(recipe).output).toHaveLength(2);
});
});
describe("commitreveal ceremonies", () => {
it("verifies commitments and derives an order-independent final seed", async () => {
const alice = await createCeremonyReveal("draw-1", "Alice");
const bob = await createCeremonyReveal("draw-1", "Bob");
const first = await finalizeCeremony({
schemaVersion: 1,
ceremonyId: "draw-1",
participants: [alice, bob],
});
const second = await finalizeCeremony({
schemaVersion: 1,
ceremonyId: "draw-1",
participants: [bob, alice],
});
expect(first.valid).toBe(true);
expect(first.seed).toMatch(/^[A-Za-z0-9_-]{43}$/u);
expect(second.seed).toBe(first.seed);
});
it("rejects a reveal changed after commitment", async () => {
const entry = await createCeremonyReveal("draw-2", "Alice");
const other = await createCeremonyReveal("draw-2", "Other");
const result = await finalizeCeremony({
schemaVersion: 1,
ceremonyId: "draw-2",
participants: [{ ...entry, nonce: other.nonce }],
});
expect(result).toMatchObject({ valid: false, verified: 0 });
expect(result.errors.join(" ")).toMatch(/does not match/u);
});
it("parses a bounded document and reproduces its commitment", async () => {
const entry = await createCeremonyReveal("draw-3", "Alice");
const parsed = parseCeremonyDocument(
JSON.stringify({
schemaVersion: 1,
ceremonyId: "draw-3",
participants: [entry],
}),
);
await expect(
commitmentForReveal("draw-3", "Alice", parsed.participants[0]!.nonce),
).resolves.toMatchObject({ commitment: entry.commitment });
});
it("retains per-reveal ceremony IDs and rejects malformed commitments", async () => {
const entry = await createCeremonyReveal("draw-4", "Alice");
const parsed = parseCeremonyDocument(
JSON.stringify({
schemaVersion: 1,
ceremonyId: "draw-4",
participants: [{ ...entry, ceremonyId: "another-draw" }],
}),
);
await expect(finalizeCeremony(parsed)).resolves.toMatchObject({
valid: false,
verified: 0,
});
await expect(
finalizeCeremony({
schemaVersion: 1,
ceremonyId: "draw-4",
participants: [{ ...entry, commitment: "not-base64url" }],
}),
).resolves.toMatchObject({ valid: false, verified: 0 });
});
});