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"; import type { RunFile } from "../src/lib/velum-project"; mock.module("@raycast/api", () => ({ environment: { supportPath: tmpdir() } })); const project = await import("../src/lib/velum-project"); const roots: string[] = []; afterEach(async () => { await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true }))); }); describe("Velum-Projektstände", () => { it("speichert Projektregeln und Bearbeitungsstände getrennt vom Quellordner", async () => { const root = await fs.mkdtemp(join(tmpdir(), "velum-project-test-")); roots.push(root); const source = join(root, "Quelle"); const support = join(root, "Raycast"); await fs.mkdir(source); await fs.writeFile(join(source, "Notiz.txt"), "Max Mustermann"); const rules = project.parseProjectRules("PERSON: Max Mustermann\nKundenSuite"); await project.saveProjectRules(source, rules, support); expect(await project.loadProjectRules(source, support)).toEqual(rules); await project.saveProjectWhitelist(source, [ { type: "PERSON", original: "Falscher Treffer" }, { type: "PERSON", original: "falscher treffer" }, ], support); expect(await project.loadProjectWhitelist(source, support)).toEqual([ { type: "PERSON", original: "falscher treffer" }, ]); expect(await fs.readFile(join(source, "Notiz.txt"), "utf8")).toBe("Max Mustermann"); const created = await project.createRun(source, support); created.run.mapping = { PERSON_01: { original: "Max Mustermann", type: "PERSON" } }; created.run.files.push({ source: "Notiz.txt", output: "PERSON_01.txt", kind: "text" }); await project.saveRun(created.directory, created.run); expect((await project.listRuns(source, support))[0].run.files[0].output).toBe("PERSON_01.txt"); expect(await fs.readFile(join(source, "Notiz.txt"), "utf8")).toBe("Max Mustermann"); }); it("ordnet erneute Läufe demselben Projektpfad zu und erkennt ältere Pfadschreibweisen", async () => { const root = await fs.mkdtemp(join(tmpdir(), "velum-project-identity-test-")); roots.push(root); const source = join(root, "Quelle"); const support = join(root, "Raycast"); await fs.mkdir(source); const first = await project.createRun(`${source}/`, support); first.run.projectPath = `${source}/`; await project.saveRun(first.directory, first.run); const second = await project.createRun(join(source, "."), support); await project.saveRun(second.directory, second.run); expect(project.projectDataRoot(source, support)).toBe(project.projectDataRoot(`${source}/`, support)); expect(first.run.id).not.toBe(second.run.id); expect((await project.listRuns(source, support)).map(({ run }) => run.id).sort()) .toEqual([first.run.id, second.run.id].sort()); }); it("ersetzt nur bestätigte Vorkommen und stellt Dateinamen wieder her", () => { const hits = [ { id: "a", start: 0, end: 3, placeholder: "PERSON_01" }, { id: "b", start: 8, end: 11, placeholder: "PERSON_01" }, ]; expect(project.replaceSpans("Max und Max", hits, new Set(["a"]))).toBe("PERSON_01 und Max"); expect(project.restorePath("PERSON_01_Bericht.txt", { PERSON_01: { original: "Max Mustermann", type: "PERSON" }, })).toBe("Max Mustermann_Bericht.txt"); expect(() => project.safeRelativePath("../geheim.txt")).toThrow(); }); it("übernimmt API-Trefferpositionen auch nach einem Emoji korrekt", () => { const text = "😀 Max"; const spans = project.toUtf16Offsets(text, [{ id: "max", start: 2, end: 5, placeholder: "PERSON_01" }]); expect(project.replaceSpans(text, spans, new Set(["max"]))).toBe("😀 PERSON_01"); }); it("ersetzt ausgewählte Projektdateien vor Ort und sichert die Originale außerhalb", async () => { const root = await fs.mkdtemp(join(tmpdir(), "velum-in-place-test-")); roots.push(root); const source = join(root, "Quelle"); const support = join(root, "Raycast"); await fs.mkdir(source); await fs.writeFile(join(source, "Bericht.txt"), "Max Mustermann"); await fs.chmod(join(source, "Bericht.txt"), 0o640); await fs.writeFile(join(source, "Unberührt.txt"), "bleibt"); const created = await project.createRun(source, support); await fs.writeFile(join(created.directory, "files", "PERSON_01.txt"), "PERSON_01"); const files: RunFile[] = [{ source: "Bericht.txt", output: "PERSON_01.txt", kind: "text" }]; await project.replaceProjectFilesInPlace(source, created.directory, files); expect(await fs.readFile(join(source, "PERSON_01.txt"), "utf8")).toBe("PERSON_01"); expect((await fs.stat(join(source, "PERSON_01.txt"))).mode & 0o777).toBe(0o640); expect(await fs.readFile(join(created.directory, "backup", "Bericht.txt"), "utf8")).toBe("Max Mustermann"); expect((await fs.stat(join(created.directory, "backup", "Bericht.txt"))).mode & 0o777).toBe(0o600); expect(await fs.readFile(join(source, "Unberührt.txt"), "utf8")).toBe("bleibt"); await expect(fs.access(join(source, "Bericht.txt"))).rejects.toMatchObject({ code: "ENOENT" }); }); it("verhindert Namenskollisionen vor dem Ersetzen", async () => { const root = await fs.mkdtemp(join(tmpdir(), "velum-in-place-collision-test-")); roots.push(root); const source = join(root, "Quelle"); const support = join(root, "Raycast"); await fs.mkdir(source); await fs.writeFile(join(source, "Bericht.txt"), "Original"); await fs.writeFile(join(source, "PERSON_01.txt"), "Andere Datei"); const created = await project.createRun(source, support); await fs.writeFile(join(created.directory, "files", "PERSON_01.txt"), "Pseudonym"); await expect(project.replaceProjectFilesInPlace(source, created.directory, [{ source: "Bericht.txt", output: "PERSON_01.txt", kind: "text" }])).rejects.toThrow("Zielname bereits vorhanden"); expect(await fs.readFile(join(source, "Bericht.txt"), "utf8")).toBe("Original"); expect(await fs.readFile(join(source, "PERSON_01.txt"), "utf8")).toBe("Andere Datei"); }); it("erkennt unveränderte Ausgaben für einen erneuten Lauf desselben Ordners", async () => { const root = await fs.mkdtemp(join(tmpdir(), "velum-repeat-test-")); roots.push(root); const source = join(root, "Quelle"); const support = join(root, "Raycast"); await fs.mkdir(source); const created = await project.createRun(source, support); const filename = "PERSON_01.txt"; await fs.writeFile(join(created.directory, "files", filename), "PERSON_01"); const current = join(source, filename); await fs.writeFile(current, "PERSON_01"); const hash = project.digest(await fs.readFile(current)); expect(await project.matchesPreviousOutput(created.directory, filename, hash)).toBe(true); await fs.writeFile(current, "PERSON_01 und neuer Text"); expect(await project.matchesPreviousOutput(created.directory, filename, project.digest(await fs.readFile(current)))).toBe(false); }); });