/* RFC-0483: PBT test for the rfc-0483 content migrator — verifies idempotency (f(f(x)) == f(x)) over the reference migration + stopgap removal. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-0483: initial PBT test for content migrator. */ import { test, expect } from "vitest"; import fc from "fast-check"; import fs from "node:fs/promises"; import path from "node:path"; import os from "node:os"; import { rfc0483Migrator } from "./rfc-0483.ts"; import type { SternsystemData, MigrationContext } from "./types.ts"; const ctx: MigrationContext = { systemId: "test", missionId: "test-mission", logger: { info: () => {} }, }; const SAMPLE_REFERENCES = [ "{business.legal.companyName}", "{business.legal.owner.fullName}", "{business.legal.owner.address.street}", "{business.legal.owner.address.streetNumber}", "{business.legal.owner.address.zip}", "{business.legal.owner.address.city}", "{business.contact.email}", "{business.contact.supportEmail}", "{business.offer.price.monthly}", "{business.offer.price.yearly}", "{business.offer.price.setup}", "{business.offer.price.monthlyAmount}", "{business.offer.price.yearlyAmount}", "{business.offer.price.setupAmount}", "{business.offer.price.moduleVisibilityAmount}", "{business.offer.price.moduleBookingAmount}", "{business.offer.price.moduleTrustAmount}", "{business.offer.price.moduleMultilangAmount}", "{business.offer.price.moduleAutomationAmount}", "{business.offer.guarantees.delivery.label}", "{business.offer.guarantees.delivery.detail}", "{business.offer.guarantees.uptime.label}", "{business.offer.guarantees.uptime.detail}", "{business.offer.guarantees.smallChanges.label}", "{business.offer.guarantees.smallChanges.detail}", "{business.offer.guarantees.response.label}", "{business.offer.guarantees.response.detail}", "{business.offer.guarantees.dataPackage.label}", "{business.offer.guarantees.dataPackage.detail}", "{business.offer.capacity.display.label}", "{business.offer.capacity.display.rangeLabel}", "{business.offer.capacity.display.unknownAvailabilityLabel}", "{business.offer.growthModules.visibility.label}", "{business.offer.growthModules.visibility.price}", "{business.offer.growthModules.booking.label}", "{business.offer.growthModules.booking.price}", "{business.offer.growthModules.trust.label}", "{business.offer.growthModules.trust.price}", "{business.offer.growthModules.multilingual.label}", "{business.offer.growthModules.multilingual.price}", "{business.offer.growthModules.automation.label}", "{business.offer.growthModules.automation.price}", "{business.offer.changePrice}", "{business.offer.hourlyRate}", "{business.offer.billingDay}", "{business.legal.tax.taxNumber}", "{business.legal.tax.vatIdOrSmallBusinessNote}", "{business.web.domains.primary}", "{business.meta.agbEffectiveDate}", "{business.meta.agbNextReviewDate}", "{business.meta.datenschutzCreationDate}", "{business.meta.impressumLastUpdateDate}", "{business.meta.barrierefreiheitCreationDate}", "{business.meta.barrierefreiheitLastReviewDate}", "{business.meta.widerrufCreationDate}", "{business.meta.widerrufFormCreationDate}", "{business.platform-comparison.display.pageText}", "{business.platform-comparison.display.disclosure}", "{business.services.websiteDevelopment.backupRetentionDays}", "{business.external-services.chatbotPlatform}", ]; async function withTempWorkpiece( setup: (dir: string) => Promise, fn: (dir: string) => Promise, ): Promise { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc-0483-pbt-")); try { await setup(dir); await fn(dir); } finally { await fs.rm(dir, { recursive: true, force: true }); } } async function createBaseWorkpiece(dir: string): Promise { const contentDir = path.join(dir, "src", "content"); await fs.mkdir(contentDir, { recursive: true }); await fs.writeFile( path.join(contentDir, "system.md"), `---\ni18n:\n default: de\n supported:\n de:\n name: Deutsch\n---\n`, ); await fs.writeFile( path.join(dir, "src", "content.config.ts"), `// GENERATED. Do not change this line unless the file contains project specific changes.\nimport { defineCollection } from "astro:content";\nimport { fsDataCollectionLoader } from "@warpgogol/werkstatt-site/content-source";\nimport { pbpCollections } from "@warpgogol/werkstatt-site/pbp/astro";\nimport { toDataEntryId } from "@warpgogol/werkstatt-shared/content";\n\nconst business = defineCollection({\n loader: fsDataCollectionLoader({\n base: "src/content/business",\n generateId: (entry) => toDataEntryId(entry),\n }),\n schema: z.object({}).catchall(z.any()),\n});\n\nexport const collections = {\n ...pbpCollections,\n business,\n};\n`, ); } async function createLegacyBusinessFiles(dir: string): Promise { const businessDir = path.join(dir, "src", "content", "business", "de"); await fs.mkdir(businessDir, { recursive: true }); await fs.writeFile( path.join(businessDir, "offer.md"), `---\nprice:\n monthly: "70"\n yearly: "700"\n setup: "200"\nguarantees:\n delivery:\n label: "Test"\n detail: "Test detail"\ncapacity:\n display:\n label: "Available"\n rangeLabel: "1-3"\n unknownAvailabilityLabel: "Unknown"\ngrowthModules:\n visibility:\n label: "Visibility"\n price: "30"\nchangePrice: "15"\nhourlyRate: "80"\nbillingDay: "1"\n---\n`, ); await fs.writeFile( path.join(businessDir, "legal.md"), `---\ncompanyName: "Test GmbH"\nowner:\n fullName: "Test Person"\n address:\n street: "Test St"\n streetNumber: "1"\n zip: "12345"\n city: "Test City"\ntax:\n taxNumber: "123/456"\n vatIdOrSmallBusinessNote: "Small business"\n---\n`, ); await fs.writeFile( path.join(businessDir, "contact.md"), `---\nemail: "test@test.com"\nsupportEmail: "support@test.com"\n---\n`, ); await fs.writeFile( path.join(businessDir, "web.md"), `---\ndomains:\n primary: "test.com"\n---\n`, ); await fs.writeFile( path.join(businessDir, "meta.md"), `---\nagbEffectiveDate: "2026-01-01"\ndatenschutzCreationDate: "2026-01-01"\nimpressumLastUpdateDate: "2026-01-01"\nwiderrufCreationDate: "2026-01-01"\nwiderrufFormCreationDate: "2026-01-01"\nagbNextReviewDate: "2027-01-01"\nbarrierefreiheitCreationDate: "2026-01-01"\nbarrierefreiheitLastReviewDate: "2026-01-01"\n---\n`, ); await fs.writeFile( path.join(businessDir, "external-services.md"), `---\nchatbotPlatform: "TestPlatform"\n---\n`, ); await fs.writeFile( path.join(businessDir, "company.md"), `---\nplatformComparison:\n display:\n pageText: "Test comparison"\n disclosure: "Test disclosure"\nservices:\n websiteDevelopment:\n backupRetentionDays: "30"\n---\n`, ); } async function createUkPbpEntities(dir: string): Promise { const ukDir = path.join(dir, "src", "content", "business-profile", "uk"); const subdirs = [ "contact", "organization", "places", "web", "offerings", "catalog", "catalog/entries", "policies", "documents", "products", "trust/claims", "trust/disclosures", "trust/evidence", ]; for (const sub of subdirs) { await fs.mkdir(path.join(ukDir, sub), { recursive: true }); } await fs.writeFile( path.join(ukDir, "organization", "legal-identity.md"), `---\nschema: pbp/legal-identity@1\nid: https://test.com/id/legal-identity\ntype: legal-identity\nstatus: draft\nname: Test\nlegalName: Test\nresponsiblePerson:\n name: "Test Person"\ngovernance:\n authorityRef: https://test.com/id/business\n effectiveFrom: "2026-01-01"\n---\n`, ); await fs.writeFile( path.join(ukDir, "contact", "general-email.md"), `---\nschema: pbp/contact-point@1\nid: https://test.com/id/contact/general-email\ntype: contact-point\nstatus: published\nname: "Contact"\nchannel: email\nvalue: test@test.com\ngovernance:\n authorityRef: https://test.com/id/business\n effectiveFrom: "2026-01-01"\n---\n`, ); await fs.writeFile( path.join(ukDir, "places", "backnang.md"), `---\nschema: pbp/place@1\nid: https://test.com/id/places/backnang\ntype: place\nstatus: draft\nname: "Test Place"\naddress:\n street: Test St\n streetNumber: "1"\n postalCode: "12345"\n locality: Test City\n countryCode: DE\ngovernance:\n authorityRef: https://test.com/id/business\n effectiveFrom: "2026-01-01"\n---\n`, ); await fs.writeFile( path.join(ukDir, "web", "primary.md"), `---\nschema: pbp/web-presence@1\nid: https://test.com/id/web/primary\ntype: web-presence\nstatus: published\nname: "Test Web"\ncanonicalUrl: https://test.com/\ngovernance:\n authorityRef: https://test.com/id/business\n effectiveFrom: "2026-01-01"\n---\n`, ); await fs.writeFile( path.join(ukDir, "offerings", "digital-foundation.md"), `---\nschema: pbp/offering@1\nid: https://test.com/id/offerings/digital-foundation\ntype: offering\nstatus: published\nname: "Test Offering"\ngovernance:\n authorityRef: https://test.com/id/business\n effectiveFrom: "2026-01-01"\n---\n`, ); for (const doc of ["terms", "privacy", "imprint", "legal-notice"]) { await fs.writeFile( path.join(ukDir, "documents", `${doc}.md`), `---\nschema: pbp/public-document@1\nid: https://test.com/id/documents/${doc}\ntype: public-document\nstatus: published\nname: "Test ${doc}"\nkind: ${doc}\ngovernance:\n authorityRef: https://test.com/id/business\n effectiveFrom: "2026-01-01"\n---\n`, ); } await fs.writeFile( path.join(ukDir, "business.md"), `---\nschema: pbp/business@1\nid: https://test.com/id/business\ntype: business\nstatus: published\nname: Test\ngovernance:\n authorityRef: https://test.com/id/business\n effectiveFrom: "2026-01-01"\n---\n`, ); } async function createOldFormatBpFiles(dir: string): Promise { const deDir = path.join(dir, "src", "content", "business-profile", "de"); await fs.mkdir(deDir, { recursive: true }); await fs.writeFile(path.join(deDir, "company.md"), `---\nid: Test\nname: Test\n---\n`); await fs.writeFile(path.join(deDir, "contact.md"), `---\nemail: test@test.com\n---\n`); await fs.writeFile(path.join(deDir, "location.md"), `---\ncity:\n name: Test\n---\n`); await fs.writeFile(path.join(deDir, "web.md"), `---\ndomains:\n primary: test.com\n---\n`); } async function createPagesWithReferences(dir: string, references: string[]): Promise { const pagesDir = path.join(dir, "src", "content", "pages", "de"); await fs.mkdir(pagesDir, { recursive: true }); const body = references.map((r) => `- ${r}`).join("\n"); await fs.writeFile( path.join(pagesDir, "test-page.md"), `---\nkind: page\npageId: test-page\ncosmicStar: star-alpha\nblocks: []\n---\n${body}\n`, ); } const refSubsetArbitrary = fc.array(fc.constantFrom(...SAMPLE_REFERENCES), { minLength: 1, maxLength: 60, }); test("rfc-0483 migrator is idempotent: f(f(x)) == f(x) for random reference subsets", async () => { await fc.asyncProperty(refSubsetArbitrary, async (refs) => { await withTempWorkpiece( async (dir) => { await createBaseWorkpiece(dir); await createLegacyBusinessFiles(dir); await createUkPbpEntities(dir); await createOldFormatBpFiles(dir); await createPagesWithReferences(dir, refs); }, async (dir) => { const data: SternsystemData = { rootPath: dir, dataPaths: [] }; const once = await rfc0483Migrator.transform(data, ctx); const pagePath = path.join(dir, "src", "content", "pages", "de", "test-page.md"); const content1 = await fs.readFile(pagePath, "utf8"); expect(content1).not.toContain("{business."); expect(content1).toContain("{business-profile."); const businessDir = path.join(dir, "src", "content", "business"); let businessExists = false; try { await fs.access(businessDir); businessExists = true; } catch { businessExists = false; } expect(businessExists).toBe(false); for (const oldFile of ["company.md", "contact.md", "location.md", "web.md"]) { const oldPath = path.join(dir, "src", "content", "business-profile", "de", oldFile); let oldExists = false; try { await fs.access(oldPath); oldExists = true; } catch { oldExists = false; } expect(oldExists).toBe(false); } const twice = await rfc0483Migrator.transform(once, ctx); const content2 = await fs.readFile(pagePath, "utf8"); expect(content2).toEqual(content1); expect(twice).toEqual(once); }, ); }); }); test("rfc-0483 migrator is a no-op on already-migrated content", async () => { await withTempWorkpiece( async (dir) => { await createBaseWorkpiece(dir); await createUkPbpEntities(dir); const pagesDir = path.join(dir, "src", "content", "pages", "de"); await fs.mkdir(pagesDir, { recursive: true }); await fs.writeFile( path.join(pagesDir, "test-page.md"), `---\nkind: page\npageId: test-page\ncosmicStar: star-alpha\nblocks: []\n---\n- {business-profile.contact/general-email.value}\n`, ); }, async (dir) => { const data: SternsystemData = { rootPath: dir, dataPaths: [] }; const result = await rfc0483Migrator.transform(data, ctx); expect(result).toEqual(data); }, ); });