/** * Property-based tests using fast-check. */ import { describe, expect, it } from "@suaveplan/testing/runner"; import * as fc from "fast-check"; import { IdempotencyGuard } from "../src/guard/idempotency-guard.js"; import { compositeKey, fingerprintPayload, generateIdempotencyKey, } from "../src/key/key-generator.js"; import { MemoryIdempotencyStore } from "../src/store/memory-store.js"; import { KeyVersioning } from "../src/versioning/key-versioning.js"; describe("Property: compositeKey", () => { it("is deterministic for equal inputs", { timeout: 30000 }, () => { fc.assert( fc.property( fc.array(fc.string({ minLength: 1 }), { minLength: 1, maxLength: 5, }), (parts) => { const k1 = compositeKey(...parts); const k2 = compositeKey(...parts); expect(k1).toBe(k2); } ), { numRuns: 200 } ); }); it("produces a hex string for any non-empty input", { timeout: 30000, }, () => { fc.assert( fc.property( fc.array(fc.string({ minLength: 1 }), { minLength: 1, maxLength: 4, }), (parts) => { const k = compositeKey(...parts); expect(k).toMatch(/^[0-9a-f]+$/); } ), { numRuns: 200 } ); }); }); describe("Property: fingerprintPayload", () => { it("produces same hash for JSON-equivalent objects (key-order independent)", { timeout: 30000, }, () => { fc.assert( fc.property( fc.dictionary( fc.string({ minLength: 1, maxLength: 10 }), fc.oneof(fc.integer(), fc.string(), fc.boolean()) ), (obj) => { // Reconstruct with reversed key order const reversed = Object.fromEntries( Object.entries(obj).reverse() ); const fp1 = fingerprintPayload(obj); const fp2 = fingerprintPayload(reversed); expect(fp1).toBe(fp2); } ), { numRuns: 200 } ); }); it("produces different hashes for different values", { timeout: 30000, }, () => { // Not 100% true for collisions but holds for random non-equal primitives fc.assert( fc.property( fc.integer({ min: 0, max: 1_000_000 }), fc.integer({ min: 1_000_001, max: 2_000_000 }), (a, b) => { expect(fingerprintPayload(a)).not.toBe( fingerprintPayload(b) ); } ), { numRuns: 100 } ); }); }); describe("Property: generateIdempotencyKey", () => { it("produces non-empty strings", { timeout: 30000 }, () => { fc.assert( fc.property(fc.constant(undefined), () => { const k = generateIdempotencyKey(); expect(k.length).toBeGreaterThan(0); }), { numRuns: 100 } ); }); }); describe("Property: IdempotencyGuard never executes fn twice for completed key", { timeout: 30000, }, () => { it("fn called at most once across N concurrent calls", async () => { await fc.assert( fc.asyncProperty( fc.integer({ min: 2, max: 10 }), async (concurrency) => { const store = new MemoryIdempotencyStore(); const guard = new IdempotencyGuard({ store }); let execCount = 0; const fn = async () => { execCount++; return "result"; }; // Sequential calls (concurrency not fully simulated in single-thread) for (let i = 0; i < concurrency; i++) { await guard.execute("prop-key", fn); } expect(execCount).toBe(1); } ), { numRuns: 20 } ); }); }); describe("Property: KeyVersioning never exceeds maxVersions", { timeout: 30000, }, () => { it("history length stays within maxVersions", () => { fc.assert( fc.property( fc.integer({ min: 1, max: 20 }), fc.integer({ min: 1, max: 50 }), (maxVersions, insertCount) => { const store = new MemoryIdempotencyStore(); const versioning = new KeyVersioning({ store, maxVersions, }); for (let i = 0; i < insertCount; i++) { versioning.addVersionEntry("k", { version: i + 1, fingerprint: `fp${i}`, status: "completed", createdAt: Date.now(), }); } const history = versioning.getHistory("k"); expect(history.length).toBeLessThanOrEqual(maxVersions); } ), { numRuns: 100 } ); }); });