From 8e928bc69f38bb24bb9ef6ad61f1740cca3319c5 Mon Sep 17 00:00:00 2001 From: muena Date: Thu, 24 Sep 2026 19:25:06 +0200 Subject: [PATCH] fix: migrate placeholders after encoded path separators --- tests/migration.test.ts | 33 ++++++++++++++++++++++++++++++++ tools/migrate-ki-datenschutz.mjs | 5 ++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/migration.test.ts b/tests/migration.test.ts index 975a97c..899e009 100644 --- a/tests/migration.test.ts +++ b/tests/migration.test.ts @@ -61,3 +61,36 @@ it("migriert einen künstlichen Altbestand verlustarm ohne den Quellordner zu ä expect(restorePath(file.output, run.mapping, run.matchModes)).toBe("Müller GmbH.txt"); expect(await fs.readFile(join(project, legacyName), "utf8")).toBe(legacyContent); }); + +it("ersetzt Platzhalter hinter URL-kodierten Pfadtrennern im Dateinamen", async () => { + const root = await fs.mkdtemp(join(tmpdir(), "velum-encoded-name-test-")); + roots.push(root); + const project = join(root, "Projekt"); + const targetSupport = join(root, "VelumSupport"); + const manifestFile = join(root, "OldSupport", "projects", "manifest.json"); + const previewFile = join(root, "Vorschau.md"); + await fs.mkdir(join(project, "KUNDE_A"), { recursive: true }); + await fs.mkdir(join(root, "OldSupport", "projects"), { recursive: true }); + const source = "KUNDE_A/%2fkunde_a%2fzc.asddls"; + await fs.writeFile(join(project, source), "Technischer Inhalt"); + await fs.writeFile(manifestFile, JSON.stringify({ + version: 1, projectPath: project, + projectTerms: [{ id: "t1", term: "Alpha GmbH", category: "Kunde", enabled: true, aliases: [] }], + variantTable: [ + { termId: "t1", termVariant: "Alpha GmbH", placeholderVariant: "KUNDE_A", style: "typed", boundary: "strict" }, + { termId: "t1", termVariant: "alpha_gmbh", placeholderVariant: "kunde_a", style: "snake", boundary: "strict" }, + ], + })); + const preview = await migrate({ project, manifestFile, targetSupport, previewFile }); + expect(preview.summary.replacements).toBe(2); + const report = await fs.readFile(previewFile, "utf8"); + expect(report).toContain('"KUNDE_A/%2fkunde_a%2fzc.asddls"'); + expect(report).toContain('"KUNDE_100001/%2fKUNDE_100002%2fzc.asddls"'); + expect(report).not.toContain("Alpha GmbH"); + + const result = await migrate({ project, manifestFile, targetSupport, apply: true }); + const run = JSON.parse(await fs.readFile(join(result.runDir!, "run.json"), "utf8")); + expect(restorePath(run.files[0].output, run.mapping, run.matchModes)) + .toBe("Alpha GmbH/%2falpha_gmbh%2fzc.asddls"); + expect(await fs.readFile(join(project, source), "utf8")).toBe("Technischer Inhalt"); +}); diff --git a/tools/migrate-ki-datenschutz.mjs b/tools/migrate-ki-datenschutz.mjs index 53b633b..da6640b 100644 --- a/tools/migrate-ki-datenschutz.mjs +++ b/tools/migrate-ki-datenschutz.mjs @@ -11,6 +11,9 @@ const excluded = new Set([".git", "node_modules", "dist", "build", "out", ".next ".venv", "venv", "__pycache__", "target", "vendor", "coverage", ".cache", ".turbo", ".idea", ".vscode", "Pods", "DerivedData"]); const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +// In URL-kodierten Dateinamen steht nach %2f unmittelbar ein "f" vor dem Platzhalter. +// Das ist ein Pfadtrenner, kein angrenzender Buchstabe des Bezeichners. +const strictPrefix = "(?:(? /[^\p{L}\p{N}]/u.test(value); const shape = (value) => !/\p{L}/u.test(value) ? "mixed" : value === value.toUpperCase() ? "upper" : value === value.toLowerCase() ? "lower" : "mixed"; @@ -52,7 +55,7 @@ export function createConverter(manifest, globalTerms = []) { })).sort((a, b) => b.needle.length - a.needle.length || a.needle.localeCompare(b.needle)); const lookup = new Map(needles.map((item) => [item.needle, item])); const pattern = needles.length ? new RegExp(needles.map((item) => item.strict - ? `(?