/* Unit tests for generated marker.pbt covering its documented public contract and regression-sensitive edge cases. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. Fixtures stay inline and minimal so each case states its own setup. ADR-0019: initial PBT tests for stripGeneratedMarker — 4 properties. */ // @ai-invariant fixtures must not couple to internals beyond the public surface under test import { test, expect } from "vitest"; import fc from "fast-check"; import { GENERATED_MARKER, hasGeneratedMarker, stripGeneratedMarker, buildGeneratedHeader, } from "../generated-marker.ts"; // --------------------------------------------------------------------------- // Arbitraries — generate valid-ish HTML with tags, comments, and text // --------------------------------------------------------------------------- const STRUCTURAL_TAGS = [ "div", "span", "p", "main", "section", "article", "header", "footer", "ul", "li", ] as const; const textSegment = fc .string({ minLength: 0, maxLength: 50 }) .filter( (s) => !s.includes("") && !s.includes(GENERATED_MARKER) && !s.includes("<") && !s.includes(">"), ); const commentWithoutMarker = fc.tuple(textSegment).map(([text]) => ``); const generatedHeaderHtml = fc.constant( buildGeneratedHeader({ filePath: "public/index.md", ownerCommand: "page.markdown.generate" }), ); const generatedHeaderCss = fc.constant( buildGeneratedHeader({ filePath: "src/styles/biome.generated.css", ownerCommand: "biome.css.generate", }), ); const balancedTag = fc .tuple(fc.constantFrom(...STRUCTURAL_TAGS), textSegment) .map(([tag, text]) => `<${tag}>${text}`); const htmlSegment = fc.oneof(textSegment, commentWithoutMarker, balancedTag); const htmlWithMarkerComment = fc .tuple( fc.array(htmlSegment, { minLength: 0, maxLength: 8 }), generatedHeaderHtml, fc.array(htmlSegment, { minLength: 0, maxLength: 3 }), ) .map(([before, header, after]) => [...before, header, ...after].join("\n")); const htmlWithMultipleComments = fc .tuple( fc.array(commentWithoutMarker, { minLength: 1, maxLength: 3 }), generatedHeaderHtml, fc.array(commentWithoutMarker, { minLength: 1, maxLength: 3 }), fc.array(balancedTag, { minLength: 1, maxLength: 3 }), ) .map(([beforeComments, header, afterComments, tags]) => [...beforeComments, header, ...afterComments, ...tags].join("\n"), ); // --------------------------------------------------------------------------- // Property 1: Tag balance preservation // --------------------------------------------------------------------------- function countTagOccurrences(html: string, tag: string): number { const openRe = new RegExp(`<${tag}\\b[^>]*>`, "gi"); const closeRe = new RegExp(``, "gi"); return (html.match(openRe) ?? []).length + (html.match(closeRe) ?? []).length; } test("PBT: stripGeneratedMarker preserves tag balance — structural tags in input remain in output", () => { fc.assert( fc.property(htmlWithMarkerComment, (input) => { const { content: output } = stripGeneratedMarker(input); for (const tag of STRUCTURAL_TAGS) { const inputCount = countTagOccurrences(input, tag); const outputCount = countTagOccurrences(output, tag); expect(outputCount).toBe(inputCount); } }), ); }); // --------------------------------------------------------------------------- // Property 2: Comment isolation // --------------------------------------------------------------------------- test("PBT: stripGeneratedMarker preserves non-marker comments and non-comment content", () => { fc.assert( fc.property(htmlWithMultipleComments, (input) => { const { content: output } = stripGeneratedMarker(input); // Marker is removed expect(hasGeneratedMarker(output)).toBe(false); // All non-marker comments' text content is preserved const commentRe = //g; let match: RegExpExecArray | null; const inputComments: string[] = []; while ((match = commentRe.exec(input)) !== null) { const inner = match[1].trim(); if (!inner.includes(GENERATED_MARKER)) { inputComments.push(inner); } } for (const commentText of inputComments) { expect(output).toContain(commentText); } // All balanced tags from input are preserved in output const tagRe = /<(div|span|p|main|section|article|header|footer|ul|li)\b[^>]*>/gi; const inputTags: string[] = []; let tagMatch: RegExpExecArray | null; while ((tagMatch = tagRe.exec(input)) !== null) { inputTags.push(tagMatch[0]); } for (const tag of inputTags) { expect(output).toContain(tag); } }), ); }); // --------------------------------------------------------------------------- // Property 3: Idempotency // --------------------------------------------------------------------------- test("PBT: stripGeneratedMarker is idempotent — strip(strip(x)) === strip(x)", () => { fc.assert( fc.property(htmlWithMarkerComment, (input) => { const once = stripGeneratedMarker(input); const twice = stripGeneratedMarker(once.content); expect(twice.content).toBe(once.content); expect(twice.changed).toBe(false); }), ); }); // Also test idempotency with CSS comment markers test("PBT: stripGeneratedMarker is idempotent with CSS block comments", () => { fc.assert( fc.property( fc .tuple(fc.array(textSegment, { minLength: 0, maxLength: 3 }), generatedHeaderCss) .map(([segments, header]) => [...segments, header, ...segments].join("\n")), (input) => { const once = stripGeneratedMarker(input); const twice = stripGeneratedMarker(once.content); expect(twice.content).toBe(once.content); }, ), ); }); // --------------------------------------------------------------------------- // Property 4: No content creation // --------------------------------------------------------------------------- test("PBT: stripGeneratedMarker does not create HTML tags not present in input", () => { fc.assert( fc.property(htmlWithMarkerComment, (input) => { const { content: output } = stripGeneratedMarker(input); // Extract all tag names from input and output const tagRe = /<([a-zA-Z][a-zA-Z0-9]*)\b/g; const inputTagNames = new Set(); let m: RegExpExecArray | null; while ((m = tagRe.exec(input)) !== null) { inputTagNames.add(m[1].toLowerCase()); } const outputTagNames = new Set(); while ((m = tagRe.exec(output)) !== null) { outputTagNames.add(m[1].toLowerCase()); } // Every tag in output must exist in input for (const tag of outputTagNames) { expect(inputTagNames.has(tag)).toBe(true); } }), ); });