Files
suaveplan/packages/foundation/data/templates/tests/property.test.ts
2026-05-27 23:47:53 +01:00

108 lines
4.0 KiB
TypeScript

/**
* 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<string, unknown> = {};
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);
});
});