import { afterEach, describe, expect, it, mock } from "bun:test"; import { promises as fs } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; mock.module("@raycast/api", () => ({ environment: { supportPath: tmpdir() } })); const project = await import("../src/lib/velum-project"); const revision = await import("../src/lib/project-revision"); const roots: string[] = []; afterEach(async () => { await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true }))); }); describe("Nachbearbeitung von Projektständen", () => { it("gibt eine Person frei und führt zwei andere nachträglich zusammen", async () => { const root = await fs.mkdtemp(join(tmpdir(), "velum-revision-")); roots.push(root); const source = join(root, "Quelle"); const support = join(root, "Raycast"); await fs.mkdir(source); const saved = await project.createRun(source, support); saved.run.mapping = { PERSON_01: { type: "PERSON", original: "Falsch Positiv" }, PERSON_02: { type: "PERSON", original: "Franz Fuchs" }, PERSON_03: { type: "PERSON", original: "Hr. Fuchs" }, PERSON_04: { type: "PERSON", original: "Franz F." }, }; saved.run.files = [{ source: "Bericht.txt", output: "PERSON_01-PERSON_03.txt", kind: "text" }]; await fs.writeFile(join(saved.directory, "files", "PERSON_01-PERSON_03.txt"), "PERSON_01 traf PERSON_02 und PERSON_03. PERSON_04 blieb."); await project.saveRun(saved.directory, saved.run); const result = await revision.reviseProjectRun({ previous: saved.run, previousDirectory: saved.directory, support, release: new Set(["PERSON_01"]), merges: { PERSON_03: "PERSON_02", PERSON_04: "PERSON_02" }, rewriteDocument: async () => { throw new Error("Unerwartetes Office-Dokument"); }, }); expect(result.run.files[0].output).toBe("Falsch Positiv-PERSON_02.txt"); expect(await fs.readFile(join(result.directory, "files", result.run.files[0].output), "utf8")) .toBe("Falsch Positiv traf PERSON_02 und PERSON_02. PERSON_02 blieb."); expect(result.run.mapping.PERSON_03.merged_into).toBe("PERSON_02"); expect(result.run.mapping.PERSON_04.merged_into).toBe("PERSON_02"); expect(result.run.mapping.PERSON_01).toBeUndefined(); expect(await project.loadProjectWhitelist(source, support)) .toEqual([{ type: "PERSON", original: "Falsch Positiv" }]); expect(await fs.readFile(join(saved.directory, "files", "PERSON_01-PERSON_03.txt"), "utf8")) .toContain("PERSON_03"); }); it("gibt eine konsolidierte Person samt Varianten frei", () => { const plan = revision.planProjectRevision({ PERSON_01: { type: "PERSON", original: "Franz Fuchs" }, PERSON_02: { type: "PERSON", original: "Franz F.", merged_into: "PERSON_01" }, PERSON_03: { type: "PERSON", original: "Hr. Fuchs", merged_into: "PERSON_01" }, }, new Set(["PERSON_01"]), {}); expect(plan.released).toEqual(["PERSON_01", "PERSON_02", "PERSON_03"]); expect(plan.whitelist.map((item) => item.original)).toEqual(["Franz Fuchs", "Franz F.", "Hr. Fuchs"]); expect(plan.mapping).toEqual({}); }); it("ersetzt einen vorhandenen Projektordner nur, wenn er noch dem letzten Stand entspricht", async () => { const root = await fs.mkdtemp(join(tmpdir(), "velum-revision-place-")); roots.push(root); const source = join(root, "Quelle"); const support = join(root, "Raycast"); await fs.mkdir(source); const saved = await project.createRun(source, support); saved.run.mapping = { PERSON_01: { type: "PERSON", original: "Falscher Treffer" } }; saved.run.files = [{ source: "Notiz.txt", output: "PERSON_01.txt", kind: "text" }]; saved.run.inPlace = true; await fs.writeFile(join(saved.directory, "files", "PERSON_01.txt"), "PERSON_01"); await fs.writeFile(join(source, "PERSON_01.txt"), "PERSON_01"); await project.saveRun(saved.directory, saved.run); await fs.writeFile(join(source, "PERSON_01.txt"), "PERSON_01 mit lokaler Änderung"); await expect(revision.reviseProjectRun({ previous: saved.run, previousDirectory: saved.directory, release: new Set(["PERSON_01"]), merges: {}, support, rewriteDocument: async () => { throw new Error("Unerwartetes Office-Dokument"); }, })).rejects.toThrow("seit dem letzten Stand geändert"); expect((await project.listRuns(source, support)).length).toBe(1); expect(await fs.readFile(join(source, "PERSON_01.txt"), "utf8")) .toBe("PERSON_01 mit lokaler Änderung"); await fs.writeFile(join(source, "PERSON_01.txt"), "PERSON_01"); const result = await revision.reviseProjectRun({ previous: saved.run, previousDirectory: saved.directory, release: new Set(["PERSON_01"]), merges: {}, support, rewriteDocument: async () => { throw new Error("Unerwartetes Office-Dokument"); }, }); expect(await fs.readFile(join(source, "Falscher Treffer.txt"), "utf8")).toBe("Falscher Treffer"); expect(await fs.readFile(join(result.directory, "backup", "PERSON_01.txt"), "utf8")).toBe("PERSON_01"); }); it("übergibt Office-Dateien mit einer gezielten Ersetzung an die Dokumentfunktion", async () => { const root = await fs.mkdtemp(join(tmpdir(), "velum-revision-doc-")); roots.push(root); const source = join(root, "Quelle"); const support = join(root, "Raycast"); await fs.mkdir(source); const saved = await project.createRun(source, support); saved.run.mapping = { PERSON_01: { type: "PERSON", original: "Falscher Treffer" }, PERSON_02: { type: "PERSON", original: "Echte Person" }, }; saved.run.files = [{ source: "Bericht.docx", output: "Bericht.docx", kind: "document" }]; await fs.writeFile(join(saved.directory, "files", "Bericht.docx"), "synthetic office bytes"); await project.saveRun(saved.directory, saved.run); let called = false; const result = await revision.reviseProjectRun({ previous: saved.run, previousDirectory: saved.directory, release: new Set(["PERSON_01"]), merges: {}, support, rewriteDocument: async (filename, data, mapping) => { called = true; expect(filename).toBe("Bericht.docx"); expect(data.toString()).toBe("synthetic office bytes"); expect(Object.keys(mapping)).toEqual(["PERSON_01"]); return Buffer.from("changed office bytes"); }, }); expect(called).toBe(true); expect(await fs.readFile(join(result.directory, "files", "Bericht.docx"), "utf8")) .toBe("changed office bytes"); }); it("schützt längere importierte Platzhalter beim Freigeben eines Präfixes", async () => { const root = await fs.mkdtemp(join(tmpdir(), "velum-revision-prefix-")); roots.push(root); const source = join(root, "Quelle"); const support = join(root, "Raycast"); await fs.mkdir(source); const saved = await project.createRun(source, support); saved.run.origin = "imported"; saved.run.mapping = { BEGRIFF_100001: { type: "BEGRIFF", original: "Alpha" }, BEGRIFF_1000012: { type: "BEGRIFF", original: "Beta" }, }; saved.run.matchModes = { BEGRIFF_100001: "anywhere", BEGRIFF_1000012: "anywhere" }; saved.run.files = [{ source: "Code.txt", output: "BEGRIFF_1000012.txt", kind: "text" }]; await fs.writeFile(join(saved.directory, "files", "BEGRIFF_1000012.txt"), "xBEGRIFF_1000012 und xBEGRIFF_100001"); await project.saveRun(saved.directory, saved.run); const result = await revision.reviseProjectRun({ previous: saved.run, previousDirectory: saved.directory, release: new Set(["BEGRIFF_100001"]), merges: {}, support, rewriteDocument: async () => { throw new Error("Unerwartetes Office-Dokument"); }, }); expect(result.run.origin).toBe("imported"); expect(result.run.files[0].output).toBe("BEGRIFF_1000012.txt"); expect(await fs.readFile(join(result.directory, "files", result.run.files[0].output), "utf8")) .toBe("xBEGRIFF_1000012 und xAlpha"); }); });