/** * Property-based tests using fast-check. * * Covers: * - Idempotent rendering: re-parsing an already-rendered literal produces same output. * - Counter monotone: named counters increase within a single parse call. * - getNestedValue / setNestedValue round-trip. * - PluginPriority strict ordering. */ import { describe, expect, it } from "@suaveplan/testing/runner"; import * as fc from "fast-check"; import { TemplateEngine } from "../src/engine/TemplateEngine.js"; import { createCounterPlugin } from "../src/plugins/dynamic/CounterPlugin.js"; import { createDotNotationPlugin } from "../src/plugins/variables/DotNotationPlugin.js"; import { PluginPriority } from "../src/types/index.js"; import { getNestedValue, setNestedValue } from "../src/utils/nested-value.js"; describe("idempotent literal rendering", () => { it("re-parsing a rendered literal string returns the same string", { timeout: 30000, }, () => { const e = new TemplateEngine().use(createDotNotationPlugin()); fc.assert( fc.property( // Strings with no { or } — guaranteed literals after render fc.string({ unit: fc.char().filter((c) => c !== "{" && c !== "}"), }), (literal) => { const first = e.parse(literal, {}); const second = e.parse(first, {}); expect(second).toBe(first); } ), { numRuns: 200 } ); }); }); describe("counter monotone within single parse", () => { it("counter values strictly increase left-to-right in one parse call", { timeout: 30000, }, () => { fc.assert( fc.property(fc.integer({ min: 2, max: 10 }), (count) => { const e = new TemplateEngine().use(createCounterPlugin()); // Build a template with `count` counter tokens const template = Array.from( { length: count }, () => "{_counter}" ).join(" "); const result = e.parse(template, {}); const values = result .split(" ") .map((v) => Number.parseInt(v, 10)); for (let i = 1; i < values.length; i++) { expect(values[i]).toBeGreaterThan(values[i - 1] as number); } }), { numRuns: 100 } ); }); }); describe("getNestedValue / setNestedValue round-trip", () => { it("reading back after set returns the same primitive", { timeout: 30000, }, () => { // Safe path: only lowercase letters and digits, no array notation const safePath = fc .array(fc.stringMatching(/^[a-z][a-z0-9]{0,5}$/), { minLength: 1, maxLength: 4, }) .map((parts) => parts.join(".")); fc.assert( fc.property( safePath, fc.oneof( fc.integer(), fc.string(), fc.boolean() // Note: null excluded because getNestedValue returns undefined for null values ), (path, value) => { const obj: Record = {}; setNestedValue(obj, path, value); const retrieved = getNestedValue(obj, path); expect(retrieved).toBe(value); } ), { numRuns: 300 } ); }); }); describe("PluginPriority strict ordering", () => { it("MINIMAL < LOW < MEDIUM < HIGH < CRITICAL", { timeout: 5000 }, () => { expect(PluginPriority.MINIMAL).toBeLessThan(PluginPriority.LOW); expect(PluginPriority.LOW).toBeLessThan(PluginPriority.MEDIUM); expect(PluginPriority.MEDIUM).toBeLessThan(PluginPriority.HIGH); expect(PluginPriority.HIGH).toBeLessThan(PluginPriority.CRITICAL); }); });