@@ -57,7 +57,7 @@ test("runs the local draw catalogue with recipe metadata", async ({ page }) => {
|
||||
await page
|
||||
.getByRole("textbox", { name: "Seed", exact: true })
|
||||
.fill("draw-catalogue");
|
||||
await page.getByRole("tab", { name: "Draws" }).click();
|
||||
await page.getByRole("button", { name: "Draws" }).click();
|
||||
|
||||
await page.getByLabel("Result count").fill("7");
|
||||
await page.getByRole("button", { name: "Generate draw" }).click();
|
||||
@@ -115,7 +115,7 @@ test("identifies normalized custom passphrase inputs without announcing output",
|
||||
await page
|
||||
.getByRole("textbox", { name: "Seed", exact: true })
|
||||
.fill("word-list-identity");
|
||||
await page.getByRole("tab", { name: "Passphrases" }).click();
|
||||
await page.getByRole("button", { name: "Passphrases" }).click();
|
||||
await page
|
||||
.getByLabel(/Optional custom word list/u)
|
||||
.fill(" alpha \n\nbeta\n gamma ");
|
||||
@@ -139,6 +139,48 @@ test("identifies normalized custom passphrase inputs without announcing output",
|
||||
expect(external).toEqual([]);
|
||||
});
|
||||
|
||||
test("runs weighted draws and a local commit–reveal ceremony", async ({
|
||||
page,
|
||||
}) => {
|
||||
const external = await localOnly(page);
|
||||
await page.goto("/deep/nested/rand/");
|
||||
await page.getByLabel("Random source").selectOption("deterministic");
|
||||
await page
|
||||
.getByRole("textbox", { name: "Seed", exact: true })
|
||||
.fill("weighted");
|
||||
await page.getByRole("button", { name: "Lists" }).click();
|
||||
await page.getByLabel("Sampling model").selectOption("weighted");
|
||||
await page
|
||||
.getByRole("textbox", { name: /CSV rows/u })
|
||||
.fill('"Alpha, Inc",10\nBeta,2\nGamma,1');
|
||||
await page.getByLabel(/Sample count/u).fill("2");
|
||||
await page.getByRole("button", { name: "Draw weighted sample" }).click();
|
||||
const weighted = (await page.locator(".result > pre").textContent())
|
||||
?.trim()
|
||||
.split("\n");
|
||||
expect(weighted).toHaveLength(2);
|
||||
expect(
|
||||
weighted?.every((value) => ["Alpha, Inc", "Beta", "Gamma"].includes(value)),
|
||||
).toBe(true);
|
||||
expect(new Set(weighted).size).toBe(2);
|
||||
|
||||
await page.getByRole("button", { name: "Commit–reveal" }).click();
|
||||
await page.getByRole("button", { name: /Generate commitment/u }).click();
|
||||
await expect(page.getByLabel("Public commitment")).toContainText(
|
||||
/"commitment": "[A-Za-z0-9_-]{43}"/u,
|
||||
);
|
||||
await expect(page.getByLabel("Private reveal")).toContainText(
|
||||
/"nonce": "[A-Za-z0-9_-]{43}"/u,
|
||||
);
|
||||
await page
|
||||
.getByRole("button", { name: "Use this reveal in verifier" })
|
||||
.click();
|
||||
await page.getByRole("button", { name: /Verify reveals/u }).click();
|
||||
await expect(page.getByText("1 reveals verified")).toBeVisible();
|
||||
await expect(page.getByText(/Final Base64url seed:/u)).toBeVisible();
|
||||
expect(external).toEqual([]);
|
||||
});
|
||||
|
||||
test("serves the release identity and hardened headers", async ({
|
||||
request,
|
||||
}) => {
|
||||
@@ -157,7 +199,7 @@ test("serves the release identity and hardened headers", async ({
|
||||
const manifest = await request.get("/deep/nested/rand/toolbox-app.json");
|
||||
await expect(manifest.json()).resolves.toMatchObject({
|
||||
id: "de.add-ideas.rand-tools",
|
||||
version: "0.1.1",
|
||||
version: "0.2.0",
|
||||
entry: "./",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("keeps the primary workspace inside a narrow viewport", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/deep/nested/rand/");
|
||||
await expect(page.locator("main").first()).toBeVisible();
|
||||
await expect(
|
||||
page.locator("main .loading, main .workbench-loading"),
|
||||
).toHaveCount(0);
|
||||
|
||||
const widths = await page.evaluate(() => ({
|
||||
content: document.documentElement.scrollWidth,
|
||||
viewport: document.documentElement.clientWidth,
|
||||
}));
|
||||
expect(widths.viewport).toBeLessThanOrEqual(430);
|
||||
expect(widths.content).toBeLessThanOrEqual(widths.viewport + 1);
|
||||
});
|
||||
@@ -13,7 +13,7 @@ describe("Random Tools", () => {
|
||||
await screen.findByRole("heading", { name: "Random Tools" }),
|
||||
).toBeVisible();
|
||||
expect(await screen.findByText("No network requests")).toBeVisible();
|
||||
expect(screen.getByRole("tab", { name: "Draws" })).toBeVisible();
|
||||
expect(screen.getByRole("button", { name: "Draws" })).toBeVisible();
|
||||
expect(screen.queryByText(/RANDOM\.ORG/iu)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(/0–10/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("commit–reveal 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 });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user