import { describe, expect, it } from "@suaveplan/testing/runner"; import fc from "fast-check"; import { parseArgs } from "../src/layers/cli-layer/cli-layer.js"; import { coerceValue } from "../src/layers/env-layer/env-layer.js"; import type { ConfigData } from "../src/types/common.js"; import { getByPath, maskSensitive, setByPath } from "../src/utils/mask.js"; import { deepMerge, mergeLayers } from "../src/utils/merge.js"; /** * Arbitrarily generates a plain JSON-serialisable config object with no * undefined values (required for associativity property). */ const plainObjectArb = fc.dictionary( fc .string({ minLength: 1, maxLength: 10 }) .filter((s) => /^[a-z]+$/i.test(s)), fc.oneof( fc.integer(), fc.string({ maxLength: 20 }), fc.boolean(), fc.constant(null) ), { minKeys: 0, maxKeys: 5 } ) as fc.Arbitrary; /** * Valid dot-path: 1–4 alphanum segments. */ const validPathArb = fc .array( fc .string({ minLength: 1, maxLength: 8 }) .filter((s) => /^[a-zA-Z0-9]+$/.test(s)), { minLength: 1, maxLength: 4 } ) .map((parts) => parts.join(".")); describe("deepMerge associativity (no undefined values)", () => { it("merge(merge(a,b),c) === merge(a,merge(b,c))", { timeout: 30000, }, () => { fc.assert( fc.property( plainObjectArb, plainObjectArb, plainObjectArb, (a, b, c) => { const left = deepMerge(deepMerge(a, b), c); const right = deepMerge(a, deepMerge(b, c)); expect(left).toEqual(right); } ), { numRuns: 200 } ); }); }); describe("mergeLayers priority ordering", () => { it("result equals manually sorted-then-merged layers", { timeout: 30000, }, () => { const layerArb = fc .record({ priority: fc.integer({ min: 0, max: 100 }), data: plainObjectArb, }) .map((l) => ({ name: `layer-${l.priority}`, ...l })); fc.assert( fc.property( fc.array(layerArb, { minLength: 2, maxLength: 8 }), (layers) => { const resultFromMergeLayers = mergeLayers( [...layers].sort((a, b) => a.priority - b.priority) ); const sortedAndFolded = layers .slice() .sort((a, b) => a.priority - b.priority) .reduce( (acc, l) => deepMerge(acc, l.data), {} ); expect(resultFromMergeLayers).toEqual(sortedAndFolded); } ), { numRuns: 200 } ); }); }); describe("setByPath / getByPath round-trip", () => { it("getByPath(setByPath({}, path, v), path) === v", { timeout: 30000, }, () => { const leafArb = fc.oneof( fc.integer(), fc.string({ maxLength: 20 }), fc.boolean(), fc.constant(null) ); fc.assert( fc.property(validPathArb, leafArb, (path, value) => { const obj: ConfigData = {}; setByPath(obj, path, value); expect(getByPath(obj, path)).toStrictEqual(value); }), { numRuns: 300 } ); }); }); describe("maskSensitive idempotency", () => { it("applying mask twice equals applying once", { timeout: 30000 }, () => { const pathsArb = fc.array(validPathArb, { minLength: 0, maxLength: 4 }); fc.assert( fc.property(plainObjectArb, pathsArb, (data, paths) => { const once = maskSensitive(data, paths); const twice = maskSensitive(once, paths); expect(twice).toEqual(once); }), { numRuns: 200 } ); }); }); describe("coerceValue stability", () => { it("coercing the string representation of a non-zero safe integer yields the same number", { timeout: 30000, }, () => { // "0" is in FALSY_STRINGS and maps to false, not 0 — excluded here. // "1" is in TRUTHY_STRINGS and maps to true — also excluded. fc.assert( fc.property(fc.integer({ min: 2, max: 1_000_000 }), (n) => { const coerced = coerceValue(String(n)); expect(coerced).toBe(n); }), { numRuns: 200 } ); }); it("coercing the string representation of a negative integer (not -1) yields the same number", { timeout: 30000, }, () => { fc.assert( fc.property(fc.integer({ min: -1_000_000, max: -2 }), (n) => { const coerced = coerceValue(String(n)); expect(coerced).toBe(n); }), { numRuns: 200 } ); }); it("coercing the string representation of a boolean yields the same boolean", { timeout: 30000, }, () => { fc.assert( fc.property(fc.boolean(), (b) => { const coerced = coerceValue(String(b)); expect(coerced).toBe(b); }), { numRuns: 100 } ); }); it("coercing JSON.stringify of an object yields a deeply equal object", { timeout: 30000, }, () => { fc.assert( fc.property( fc.dictionary( fc .string({ minLength: 1, maxLength: 8 }) .filter((s) => /^[a-z]+$/.test(s)), fc.oneof( fc.integer(), fc.string({ maxLength: 10 }), fc.boolean() ), { minKeys: 1, maxKeys: 4 } ), (obj) => { const coerced = coerceValue(JSON.stringify(obj)); expect(coerced).toEqual(obj); } ), { numRuns: 200 } ); }); }); describe("parseArgs completeness", () => { it("every --key value pair appears in output when key is in mapping", { timeout: 30000, }, () => { const keyArb = fc .string({ minLength: 2, maxLength: 10 }) .filter((s) => /^[a-z][a-z0-9-]*$/.test(s)); const valueArb = fc .string({ minLength: 1, maxLength: 20 }) .filter((s) => !s.startsWith("-")); const pairsArb = fc.array(fc.tuple(keyArb, valueArb), { minLength: 1, maxLength: 6, }); fc.assert( fc.property(pairsArb, (pairs) => { // Deduplicate keys (last wins in args, but first in mapping) const seen = new Set(); const unique: [string, string][] = []; for (const [k, v] of pairs) { if (!seen.has(k)) { seen.add(k); unique.push([k, v]); } } const mapping: Record = {}; for (const [k] of unique) { mapping[k] = `config.${k}`; } const args: string[] = []; for (const [k, v] of unique) { args.push(`--${k}`, v); } const result = parseArgs(args, mapping); for (const [k, v] of unique) { expect(result[k]).toBe(v); } }), { numRuns: 200 } ); }); });