import { describe, it } from "@suaveplan/testing/runner"; import * as fc from "fast-check"; import { bucketUser, normaliseWeights, selectVariant, } from "../src/bucketing/bucketing.js"; import { evaluateTargeting } from "../src/targeting/targeting.js"; describe("property: bucketUser", () => { it("always returns [0,1) for arbitrary inputs", { timeout: 30000 }, () => { fc.assert( fc.property( fc.string(), fc.string(), fc.integer({ min: -1_000_000, max: 1_000_000 }), (userId, experimentId, seed) => { const b = bucketUser(userId, experimentId, seed); return b >= 0 && b < 1; } ), { numRuns: 1000 } ); }); it("is deterministic for the same inputs", { timeout: 30000 }, () => { fc.assert( fc.property(fc.string(), fc.string(), (userId, experimentId) => { const a = bucketUser(userId, experimentId); const b = bucketUser(userId, experimentId); return a === b; }), { numRuns: 1000 } ); }); }); describe("property: normaliseWeights + selectVariant", () => { const positiveVariantArb = fc.record({ id: fc.string({ minLength: 1 }), name: fc.string({ minLength: 1 }), weight: fc.float({ min: Math.fround(0.001), max: Math.fround(1000), noNaN: true, }), }); it("selected variant always belongs to input", { timeout: 30000 }, () => { fc.assert( fc.property( fc.array(positiveVariantArb, { minLength: 1, maxLength: 10 }), fc.float({ min: Math.fround(0), max: Math.fround(0.9999), noNaN: true, }), (rawVariants, bucket) => { const normalised = normaliseWeights(rawVariants); if (normalised.length === 0) return true; // skip degenerate const selected = selectVariant(normalised, bucket); return rawVariants.some((v) => v.id === selected.id); } ), { numRuns: 500 } ); }); it("last normalised weight is always exactly 1.0", { timeout: 30000, }, () => { fc.assert( fc.property( fc.array(positiveVariantArb, { minLength: 1, maxLength: 20 }), (rawVariants) => { const normalised = normaliseWeights(rawVariants); if (normalised.length === 0) return true; return ( normalised[normalised.length - 1]?.normalizedWeight === 1.0 ); } ), { numRuns: 500 } ); }); }); describe("property: evaluateTargeting", () => { it("empty rules always returns true", { timeout: 30000 }, () => { fc.assert( fc.property( fc.dictionary( fc.string(), fc.oneof(fc.string(), fc.integer(), fc.boolean()) ), (attrs) => { return evaluateTargeting([], attrs) === true; } ), { numRuns: 500 } ); }); it("eq rule is reflexive for present attributes", { timeout: 30000, }, () => { fc.assert( fc.property( fc.string({ minLength: 1 }), fc.oneof(fc.string(), fc.integer()), (key, value) => { const attrs = { [key]: value }; const rule = { attribute: key, operator: "eq" as const, value, }; return evaluateTargeting([rule], attrs) === true; } ), { numRuns: 500 } ); }); });