'use strict'; /** * Property-based tests for frontmatter.cjs * * Module: gsd-core/bin/lib/frontmatter.cjs * Exported (pure): extractFrontmatter, reconstructFrontmatter, spliceFrontmatter * * Properties tested: * (a) extractFrontmatter never throws on ANY string input (including binary/unicode) * (b) extractFrontmatter always returns a plain object (not null, not array) * (c) round-trip: reconstructFrontmatter(extractFrontmatter(spliceFrontmatter(content, obj))) * preserves key-value pairs for simple flat string values * (d) spliceFrontmatter never throws on any string/object combination * (e) extractFrontmatter returns {} for content without a leading ---...--- block */ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const fc = require('./helpers/fast-check-setup.cjs'); const { extractFrontmatter, reconstructFrontmatter, spliceFrontmatter, } = require('../gsd-core/bin/lib/frontmatter.cjs'); // ─── Arbitraries ───────────────────────────────────────────────────────────── // Simple YAML key: alphanumeric + underscore, at least 1 char const yamlKey = fc.stringMatching(/^[a-z][a-z0-9_]{0,19}$/); // Simple YAML scalar value: printable ASCII without : ' " # newlines const yamlScalarValue = fc.stringMatching(/^[a-zA-Z0-9 ._/-]{1,40}$/); // ─── Tests ──────────────────────────────────────────────────────────────────── describe('frontmatter: extractFrontmatter properties', () => { // (a) Never throws on any string input test('property: extractFrontmatter never throws on arbitrary binary/unicode input', () => { fc.assert( fc.property( fc.oneof( fc.string({ unit: 'binary', maxLength: 300 }), fc.string({ unit: 'grapheme-composite', maxLength: 300 }), fc.constant(''), fc.constant('---\n---'), fc.constant('---\nkey: value\n---\n# body'), fc.string({ maxLength: 300 }) ), (input) => { assert.doesNotThrow( () => extractFrontmatter(input), `extractFrontmatter threw on input: ${JSON.stringify(input.slice(0, 50))}` ); } ) ); }); // (b) Always returns a plain object test('property: extractFrontmatter always returns a plain object', () => { fc.assert( fc.property( fc.oneof( fc.string({ unit: 'binary', maxLength: 200 }), fc.string({ unit: 'grapheme-composite', maxLength: 200 }), fc.string({ maxLength: 200 }) ), (input) => { const result = extractFrontmatter(input); assert.ok( typeof result === 'object' && result !== null && !Array.isArray(result), `extractFrontmatter must return plain object, got ${JSON.stringify(result)}` ); } ) ); }); // (e) Returns {} for content without leading --- block test('property: content without leading --- block returns empty object', () => { fc.assert( fc.property( fc.oneof( fc.string({ minLength: 0, maxLength: 200 }).filter((s) => !s.startsWith('---')), fc.constant('# Just a heading'), fc.constant('plain text content'), fc.constant('') ), (input) => { const result = extractFrontmatter(input); assert.deepEqual( result, {}, `Expected {} for non-frontmatter input, got ${JSON.stringify(result)}` ); } ) ); }); }); describe('frontmatter: reconstructFrontmatter properties', () => { test('property: reconstructFrontmatter never throws on plain objects with string values', () => { fc.assert( fc.property( fc.dictionary(yamlKey, yamlScalarValue, { maxKeys: 10 }), (obj) => { assert.doesNotThrow( () => reconstructFrontmatter(obj), `reconstructFrontmatter threw on ${JSON.stringify(obj)}` ); } ) ); }); test('property: reconstructFrontmatter output is a string', () => { fc.assert( fc.property( fc.dictionary(yamlKey, yamlScalarValue, { maxKeys: 8 }), (obj) => { const result = reconstructFrontmatter(obj); assert.ok(typeof result === 'string', `Expected string got ${typeof result}`); } ) ); }); test('property: reconstructFrontmatter on {} returns empty string', () => { assert.equal(reconstructFrontmatter({}), ''); }); }); describe('frontmatter: spliceFrontmatter properties', () => { // (d) Never throws on any combination test('property: spliceFrontmatter never throws on arbitrary content + object', () => { fc.assert( fc.property( fc.string({ maxLength: 300 }), fc.dictionary(yamlKey, yamlScalarValue, { maxKeys: 8 }), (content, obj) => { assert.doesNotThrow( () => spliceFrontmatter(content, obj), `spliceFrontmatter threw on content=${JSON.stringify(content.slice(0, 30))}` ); } ) ); }); test('property: spliceFrontmatter always returns a string', () => { fc.assert( fc.property( fc.string({ maxLength: 200 }), fc.dictionary(yamlKey, yamlScalarValue, { maxKeys: 5 }), (content, obj) => { const result = spliceFrontmatter(content, obj); assert.ok(typeof result === 'string', `Expected string got ${typeof result}`); } ) ); }); // (c) Round-trip: splice then extract preserves flat string keys test('property: splice then extract round-trip preserves flat string values', () => { fc.assert( fc.property( fc.string({ maxLength: 100 }), // existing document body // Only keys + simple values without colons/hashes that would confuse the minimal parser fc.dictionary( fc.stringMatching(/^[a-z][a-z0-9]{0,14}$/), fc.stringMatching(/^[a-zA-Z0-9]{1,30}$/), { minKeys: 1, maxKeys: 5 } ), (body, obj) => { const spliced = spliceFrontmatter(body, obj); const extracted = extractFrontmatter(spliced); for (const [key, value] of Object.entries(obj)) { if (typeof value === 'string' && value.length > 0) { assert.equal( extracted[key], value, `Round-trip failed for key=${key}: expected ${value} got ${extracted[key]}` ); } } } ) ); }); });