/* RFC-0504: PBT test for the rfc-0504 migrator — verifies idempotency (f(f(x)) == f(x)) by running the migrator twice and checking the result is identical. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-0504: initial PBT test for rfc-0504 migrator idempotency. */ import { test, expect } from "vitest"; import fs from "node:fs/promises"; import path from "node:path"; import os from "node:os"; import { rfc0504Migrator } from "./rfc-0504.ts"; import type { SternsystemData, MigrationContext } from "./types.ts"; function makeCtx(): MigrationContext { return { systemId: "test-system", missionId: "test-mission", logger: { info: () => {} }, }; } function makeData(tmpDir: string): SternsystemData { return { rootPath: tmpDir, dataPaths: [], }; } const SAMPLE_ARTICLE = `--- title: "Test Article" articleType: grundlagenartikel status: published categoryId: test --- Content here. `; const SAMPLE_PROSE_WITH_H1 = `# Test Article ## Einleitung Some content. ## Kernfrage A question. `; const SAMPLE_PROSE_WITH_UNIQUE_H1 = `# Unique Heading ## Einleitung Some content. `; test("rfc-0504 migrator is idempotent — f(f(x)) == f(x)", async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc-0504-pbt-")); const data = makeData(tmpDir); const ctx = makeCtx(); try { // Set up article + prose files const articleDir = path.join(tmpDir, "src", "content", "surface", "articles", "de"); const proseDir = path.join(tmpDir, "src", "content", "prose", "de"); await fs.mkdir(articleDir, { recursive: true }); await fs.mkdir(proseDir, { recursive: true }); await fs.writeFile(path.join(articleDir, "test-article.md"), SAMPLE_ARTICLE, "utf-8"); await fs.writeFile( path.join(proseDir, "ratgeber-test-article.md"), SAMPLE_PROSE_WITH_H1, "utf-8", ); // First run await rfc0504Migrator.transform(data, ctx); const articleAfter1 = await fs.readFile(path.join(articleDir, "test-article.md"), "utf-8"); const proseAfter1 = await fs.readFile(path.join(proseDir, "ratgeber-test-article.md"), "utf-8"); // Second run await rfc0504Migrator.transform(data, ctx); const articleAfter2 = await fs.readFile(path.join(articleDir, "test-article.md"), "utf-8"); const proseAfter2 = await fs.readFile(path.join(proseDir, "ratgeber-test-article.md"), "utf-8"); expect(articleAfter1).toBe(articleAfter2); expect(proseAfter1).toBe(proseAfter2); } finally { await fs.rm(tmpDir, { recursive: true, force: true }); } }); test("rfc-0504 migrator adds articleSections and changelog fields", async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc-0504-pbt-")); const data = makeData(tmpDir); const ctx = makeCtx(); try { const articleDir = path.join(tmpDir, "src", "content", "surface", "articles", "de"); await fs.mkdir(articleDir, { recursive: true }); await fs.writeFile(path.join(articleDir, "test-article.md"), SAMPLE_ARTICLE, "utf-8"); await rfc0504Migrator.transform(data, ctx); const content = await fs.readFile(path.join(articleDir, "test-article.md"), "utf-8"); expect(content).toMatch(/articleSections:\s*\[\]/); expect(content).toMatch(/changelog:\s*\[\]/); } finally { await fs.rm(tmpDir, { recursive: true, force: true }); } }); test("rfc-0504 migrator strips H1 that duplicates article title", async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc-0504-pbt-")); const data = makeData(tmpDir); const ctx = makeCtx(); try { const articleDir = path.join(tmpDir, "src", "content", "surface", "articles", "de"); const proseDir = path.join(tmpDir, "src", "content", "prose", "de"); await fs.mkdir(articleDir, { recursive: true }); await fs.mkdir(proseDir, { recursive: true }); await fs.writeFile(path.join(articleDir, "test-article.md"), SAMPLE_ARTICLE, "utf-8"); await fs.writeFile( path.join(proseDir, "ratgeber-test-article.md"), SAMPLE_PROSE_WITH_H1, "utf-8", ); await rfc0504Migrator.transform(data, ctx); const prose = await fs.readFile(path.join(proseDir, "ratgeber-test-article.md"), "utf-8"); expect(prose).not.toMatch(/^#\s+Test Article/m); expect(prose).toMatch(/^## Einleitung/m); } finally { await fs.rm(tmpDir, { recursive: true, force: true }); } }); test("rfc-0504 migrator converts unique H1 to H2", async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc-0504-pbt-")); const data = makeData(tmpDir); const ctx = makeCtx(); try { const articleDir = path.join(tmpDir, "src", "content", "surface", "articles", "de"); const proseDir = path.join(tmpDir, "src", "content", "prose", "de"); await fs.mkdir(articleDir, { recursive: true }); await fs.mkdir(proseDir, { recursive: true }); await fs.writeFile(path.join(articleDir, "test-article.md"), SAMPLE_ARTICLE, "utf-8"); await fs.writeFile( path.join(proseDir, "ratgeber-test-article.md"), SAMPLE_PROSE_WITH_UNIQUE_H1, "utf-8", ); await rfc0504Migrator.transform(data, ctx); const prose = await fs.readFile(path.join(proseDir, "ratgeber-test-article.md"), "utf-8"); expect(prose).not.toMatch(/^#\s+Unique Heading/m); expect(prose).toMatch(/^## Unique Heading/m); } finally { await fs.rm(tmpDir, { recursive: true, force: true }); } }); test("rfc-0504 migrator removes H1 when duplicate H2 exists", async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc-0504-pbt-")); const data = makeData(tmpDir); const ctx = makeCtx(); try { const articleDir = path.join(tmpDir, "src", "content", "surface", "articles", "de"); const proseDir = path.join(tmpDir, "src", "content", "prose", "de"); await fs.mkdir(articleDir, { recursive: true }); await fs.mkdir(proseDir, { recursive: true }); await fs.writeFile(path.join(articleDir, "test-article.md"), SAMPLE_ARTICLE, "utf-8"); const proseWithDupH2 = `# Einleitung ## Einleitung Content. `; await fs.writeFile(path.join(proseDir, "ratgeber-test-article.md"), proseWithDupH2, "utf-8"); await rfc0504Migrator.transform(data, ctx); const prose = await fs.readFile(path.join(proseDir, "ratgeber-test-article.md"), "utf-8"); // H1 should be removed (not converted to H2) because H2 with same text exists const h1Matches = prose.match(/^#\s+Einleitung/gm); expect(h1Matches).toBeNull(); const h2Matches = prose.match(/^##\s+Einleitung/gm); expect(h2Matches).toHaveLength(1); } finally { await fs.rm(tmpDir, { recursive: true, force: true }); } }); test("rfc-0504 migrator does not touch H1 inside fenced code blocks", async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc-0504-pbt-")); const data = makeData(tmpDir); const ctx = makeCtx(); try { const articleDir = path.join(tmpDir, "src", "content", "surface", "articles", "de"); const proseDir = path.join(tmpDir, "src", "content", "prose", "de"); await fs.mkdir(articleDir, { recursive: true }); await fs.mkdir(proseDir, { recursive: true }); await fs.writeFile(path.join(articleDir, "test-article.md"), SAMPLE_ARTICLE, "utf-8"); const proseWithCodeBlock = `\`\`\`markdown # Not a real heading \`\`\` ## Einleitung Content. `; await fs.writeFile( path.join(proseDir, "ratgeber-test-article.md"), proseWithCodeBlock, "utf-8", ); await rfc0504Migrator.transform(data, ctx); const prose = await fs.readFile(path.join(proseDir, "ratgeber-test-article.md"), "utf-8"); expect(prose).toContain("# Not a real heading"); } finally { await fs.rm(tmpDir, { recursive: true, force: true }); } });